This commit is contained in:
2026-11-05 09:57:11 -05:00
parent d34388bf2c
commit 89e392cc2e
81 changed files with 12410 additions and 1135 deletions
+13 -13
View File
@@ -14,23 +14,23 @@ const pixel = (im,tile,x,y)=> {
};
const alpha = im=>im.data.reduce((n,value,i)=>n+(i%4===3 && value===255 ? 1 : 0),0);
test('exact public option IDs and 170 deterministic filenames include every Cartesian combination',()=> {
test('exact public option IDs and deterministic filenames include every Cartesian combination',()=> {
assert.deepEqual(O,{
bodies:['compact','balanced','broad'],skin:['porcelain','sand','honey','olive','umber','ebony'],
faces:['calm','angular','soft','weathered'],eyes:['hazel','brown','green','blue','grey'],
hairStyles:['cropped','sidepart','wavy','braids','long','shaved','none'],
bodies:['compact','lean','balanced','broad','sturdy'],skin:['porcelain','sand','honey','olive','umber','ebony'],
faces:['calm','angular','soft','weathered','lined'],eyes:['hazel','brown','green','blue','grey'],
hairStyles:['cropped','sidepart','wavy','braids','long','shaved','none','curly','topknot','undercut'],
hairColors:['ash','flax','chestnut','auburn','black','silver'],
facialHair:['none','stubble','moustache','short_beard','full_beard'],
garments:['tunic','workshirt','jerkin','coat'],clothColors:['sage','teal','slate','mauve','ochre','rust','linen','charcoal'],
facialHair:['none','stubble','moustache','short_beard','full_beard','goatee','braided_beard'],
garments:['tunic','workshirt','jerkin','coat','hooded_cloak','travel_coat','wool_mantle'],clothColors:['sage','teal','slate','mauve','ochre','rust','linen','charcoal'],
trousers:['earth','slate','moss','charcoal'],boots:['walnut','black','tan'],
accents:['none','scarf','apron','sash','vest'],details:['none','freckles','scar','freckles_scar'],
accents:['none','scarf','apron','sash','vest','satchel','tool_belt','pauldrons','hood'],details:['none','freckles','scar','freckles_scar','birthmark'],
});
const specs=[['skin',O.skin],['face',O.faces],['eyes',O.eyes],['hair',O.hairStyles,O.hairColors],
['beard',O.facialHair,O.hairColors],['garment',O.garments,O.clothColors],['trousers',O.trousers],
['boots',O.boots],['accent',O.accents,O.clothColors],['detail',O.details]];
const expected=specs.flatMap(([kind,ids,colors])=>ids.flatMap(id=>(colors||[null]).map(color=>
`hearth_char_${kind}_${id}${color ? '_'+color : ''}.png`)));
assert.equal(M.length,170); assert.equal(new Set(expected).size,170);
assert.equal(M.length,expected.length); assert.equal(new Set(expected).size,expected.length);
assert.deepEqual(M.map(r=>r.filename),expected);
assert.ok(Object.isFrozen(O) && Object.values(O).every(Object.isFrozen) && M.every(Object.isFrozen));
assert.deepEqual([...characterArt().keys()],expected);
@@ -59,7 +59,7 @@ test('all layers are native 64 RGBA, crisp alpha, no hidden RGB; only named none
test('overlays paint only their semantic UV tiles, never held metal, wood, wrap or unused atlas space',()=> {
const allowed={face:['face'],eyes:['face'],hair:['face','hair','side','back','sleeve'],
beard:['face','side'],garment:['tunic','back','sleeve'],trousers:['trousers'],boots:['boot'],
accent:['tunic','back','sleeve'],detail:['face']};
accent:['tunic','back','sleeve','face','hair','side'],detail:['face']};
for (const r of M.filter(r=>r.kind!=='skin')) {
const im=characterLayer(r.filename), tiles=new Set(allowed[r.kind].map(t=>ATLAS[t].join(',')));
for (let y=0;y<64;y++) for (let x=0;x<64;x++) if (im.data[(y*64+x)*4+3])
@@ -116,10 +116,10 @@ test('layer names and order match Luanti composition; garments cannot erase long
assert.deepEqual(names,[
'hearth_char_skin_sand.png','hearth_char_face_calm.png','hearth_char_eyes_hazel.png',
'hearth_char_garment_coat_rust.png','hearth_char_trousers_earth.png','hearth_char_boots_walnut.png',
'hearth_char_accent_scarf_sage.png','hearth_char_hair_long_chestnut.png',
'hearth_char_beard_none_chestnut.png','hearth_char_detail_none.png']);
'hearth_char_hair_long_chestnut.png','hearth_char_beard_none_chestnut.png',
'hearth_char_accent_scarf_sage.png','hearth_char_detail_none.png']);
assert.equal(characterTexture({hair:'long',garment:'coat',cloth:'rust'}),names.join('^'));
const im=compositeCharacter({hair:'long',garment:'coat',cloth:'rust'}), hair=characterLayer(names[7]);
const im=compositeCharacter({hair:'long',garment:'coat',cloth:'rust'}), hair=characterLayer(names[6]);
assert.deepEqual(pixel(im,'back',3,2),pixel(hair,'back',3,2));
assert.deepEqual(pixel(im,'sleeve',0,1),pixel(hair,'sleeve',0,1));
assert.equal(alpha(im),4096);
@@ -150,7 +150,7 @@ test('clothing has tailored collars, rolled cuffs, leather seams, plackets, boot
assert.notDeepEqual(pixel(garment('jerkin'),'tunic',1,3),pixel(garment('jerkin'),'tunic',2,3),'leather panel seam');
assert.notDeepEqual(pixel(garment('coat'),'tunic',3,3),pixel(garment('coat'),'tunic',2,3),'coat opening');
const masks=O.accents.map(style=>characterLayer(`hearth_char_accent_${style}_sage.png`).data.filter((_,i)=>i%4===3));
assert.equal(new Set(masks.map(bytes=>createHash('sha256').update(bytes).digest('hex'))).size,5);
assert.equal(new Set(masks.map(bytes=>createHash('sha256').update(bytes).digest('hex'))).size,O.accents.length);
for (const id of O.boots) assert.deepEqual(pixel(characterLayer(`hearth_char_boots_${id}.png`),'boot',0,7),[30,28,26,255]);
});