// Pure asset checks only. No engine, external art, network or live-world writes. import test from 'node:test'; import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; import { ATLAS } from '../scripts/media.mjs'; import { CHARACTER_OPTIONS as O, CHARACTER_ART_MANIFEST as M, DEFAULT_CHARACTER, CHARACTER_PORTRAITS, characterArt, characterLayer, characterLayers, characterTexture, characterMesh, compositeCharacter, portraitSheet } from '../scripts/character-art.mjs'; const hash = image=>createHash('sha256').update(image.data).digest('hex'); const pixel = (im,tile,x,y)=> { const [tx,ty]=ATLAS[tile], i=((ty*8+y)*im.width+tx*8+x)*4; return [...im.data.subarray(i,i+4)]; }; 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',()=> { 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'], 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'], trousers:['earth','slate','moss','charcoal'],boots:['walnut','black','tan'], accents:['none','scarf','apron','sash','vest'],details:['none','freckles','scar','freckles_scar'], }); 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.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); for (const r of M) { assert.match(r.filename,/^hearth_char_[a-z_]+\.png$/); assert.equal(r.width,64); assert.equal(r.height,64); assert.equal(r.license,'MIT'); assert.match(r.author,/original/); } }); test('all layers are native 64 RGBA, crisp alpha, no hidden RGB; only named none layers are empty',()=> { for (const r of M) { const im=characterLayer(r.filename); assert.equal(im.width,64); assert.equal(im.height,64); assert.equal(im.data.length,16384); assert.equal(hash(im),hash(characterLayer(r.filename))); for (let i=0;ir.empty).length,21); }); 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']}; 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]) assert.ok(tiles.has([Math.floor(x/8),Math.floor(y/8)].join(',')),r.filename); } }); test('all six skins differ meaningfully on every exposed tile, not clothes or tools',()=> { const layers=O.skin.map(id=>characterLayer(`hearth_char_skin_${id}.png`)); for (let i=1;in!==b[c])) { changed++; delta+=a.reduce((n,v,c)=>n+Math.abs(v-b[c]),0); } } assert.equal(changed,256,'four exposed 8px head/hand tiles'); assert.ok(delta/changed>50); for (const tile of ['face','hair','side','skin']) assert.notDeepEqual(pixel(layers[i],tile,3,4),pixel(layers[i-1],tile,3,4)); for (const tile of ['tunic','boot','trousers','metal','wood','wrap']) assert.deepEqual(pixel(layers[i],tile,3,4),pixel(layers[0],tile,3,4)); } }); test('all nonempty style/color layers are distinct; every appearance axis changes the composed character',()=> { const seen=new Set(); for (const r of M.filter(r=>!r.empty)) { const key=hash(characterLayer(r.filename)); assert.ok(!seen.has(key),r.filename); seen.add(key); } const axes={skin:O.skin,face:O.faces,eyes:O.eyes,hair:O.hairStyles,hairColor:O.hairColors, beard:O.facialHair,garment:O.garments,cloth:O.clothColors,trousers:O.trousers,boots:O.boots, accent:O.accents,accentColor:O.clothColors,detail:O.details}; for (const [key,values] of Object.entries(axes)) assert.equal(new Set(values.map(id=>hash(compositeCharacter({[key]:id})))).size,values.length,key); }); test('face linework is skin-neutral, eyes keep visible 8px landmarks, and no-hair scalp really uses chosen skin',()=> { for (const id of O.faces) { const im=characterLayer(`hearth_char_face_${id}.png`); assert.deepEqual(pixel(im,'face',3,4),[0,0,0,0]); assert.ok(alpha(im)>=5 && alpha(im)<=12,'linework, not a painted skin mask'); assert.equal(pixel(im,'face',2,3)[3],0,'iris reserved'); } for (const id of O.eyes) { const im=characterLayer(`hearth_char_eyes_${id}.png`); assert.equal(alpha(im),2); assert.equal(pixel(im,'face',2,3)[3],255); assert.equal(pixel(im,'face',5,3)[3],255); } for (const skin of O.skin) { const im=compositeCharacter({skin,hair:'none'}), base=characterLayer(`hearth_char_skin_${skin}.png`); for (const tile of ['hair','side','skin']) assert.deepEqual(pixel(im,tile,4,4),pixel(base,tile,4,4)); } }); test('layer names and order match Luanti composition; garments cannot erase long shoulder hair',()=> { const names=characterLayers({hair:'long',garment:'coat',cloth:'rust'}); 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']); assert.equal(characterTexture({hair:'long',garment:'coat',cloth:'rust'}),names.join('^')); const im=compositeCharacter({hair:'long',garment:'coat',cloth:'rust'}), hair=characterLayer(names[7]); 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); const independent=new Uint8Array(16384); for (const name of names) { const data=characterLayer(name).data; for (let i=0;i { for (const skin of O.skin) { const base=characterLayer(`hearth_char_skin_${skin}.png`), combined=compositeCharacter({skin}); for (const [tile,rgb] of [['metal',[177,195,191]],['wood',[146,100,59]],['wrap',[201,169,116]]]) for (let y=0;y<8;y++) for (let x=0;x<8;x++) { const [tx,ty]=ATLAS[tile], grain=(((tx*8+x)*13+(ty*8+y)*7)%5)-2; const expected=[...rgb.map(n=>n+grain),255]; assert.deepEqual(pixel(base,tile,x,y),expected); assert.deepEqual(pixel(combined,tile,x,y),expected); } } }); test('clothing has tailored collars, rolled cuffs, leather seams, plackets, boot soles and accent masks',()=> { const garment=id=>characterLayer(`hearth_char_garment_${id}_teal.png`); assert.notDeepEqual(pixel(garment('tunic'),'tunic',2,0),pixel(garment('tunic'),'tunic',2,2),'collar'); assert.notDeepEqual(pixel(garment('workshirt'),'sleeve',3,5),pixel(garment('workshirt'),'sleeve',3,4),'rolled cuff'); 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); for (const id of O.boots) assert.deepEqual(pixel(characterLayer(`hearth_char_boots_${id}.png`),'boot',0,7),[30,28,26,255]); }); test('eight contact-sheet cells are assembled clothed portraits sampled from actual composited atlases',()=> { const sheet=portraitSheet(); assert.equal(sheet.width,512); assert.equal(sheet.height,64); assert.equal(CHARACTER_PORTRAITS.length,8); assert.equal(alpha(sheet),512*64); assert.equal(hash(sheet),hash(portraitSheet())); const cells=new Set(); CHARACTER_PORTRAITS.forEach((p,index)=> { const im=compositeCharacter(p), cell=[]; for (let y=0;y<64;y++) for (let x=0;x<64;x++) { const offset=(y*512+index*64+x)*4; cell.push(...sheet.data.subarray(offset,offset+4)); } cells.add(createHash('sha256').update(new Uint8Array(cell)).digest('hex')); const sample=(x,y)=>[...sheet.data.subarray((y*512+index*64+x)*4,(y*512+index*64+x+1)*4)]; assert.deepEqual(sample(28,12),pixel(im,'face',2,4)); assert.deepEqual(sample(32,32),pixel(im,'tunic',4,4)); assert.deepEqual(sample(0,0),[30,34,32,255]); }); assert.equal(cells.size,8); }); test('pure renderers return fresh buffers and allowlisted choices reject traversal and prototype keys',()=> { const first=compositeCharacter(), digest=hash(first); first.data.fill(0); assert.equal(hash(compositeCharacter()),digest); const layer=characterLayer(M[0].filename), original=hash(layer); layer.data.fill(0); assert.equal(hash(characterLayer(M[0].filename)),original); assert.equal(characterMesh(),'hearth_person.b3d'); assert.equal(characterMesh('compact'),'hearth_person_compact.b3d'); assert.equal(characterMesh('broad'),'hearth_person_broad.b3d'); for (const bad of ['../sand','toString','constructor','__proto__','',null,{},0]) { assert.throws(()=>characterLayer(bad)); assert.throws(()=>characterMesh(bad)); assert.throws(()=>compositeCharacter({skin:bad})); } for (const bad of [null,[],1,'sand',{unknown:'sand'},JSON.parse('{"__proto__":{}}'), Object.create({skin:'sand'}),{skin:undefined},{[Symbol('skin')]:'sand'}]) assert.throws(()=>characterLayers(bad)); assert.deepEqual(DEFAULT_CHARACTER,{body:'balanced',skin:'sand',face:'calm',eyes:'hazel',hair:'sidepart', hairColor:'chestnut',beard:'none',garment:'tunic',cloth:'teal',trousers:'earth',boots:'walnut', accent:'scarf',accentColor:'sage',detail:'none'}); });