// Engine-independent art contracts. Decode the actual PNG bytes independently; // check geometry/masks, not just filenames, magic bytes or color diversity. import test from 'node:test'; import assert from 'node:assert/strict'; import * as fs from 'node:fs/promises'; import { realpath } from 'node:fs'; import { promisify } from 'node:util'; import os from 'node:os'; import path from 'node:path'; import { inflateSync } from 'node:zlib'; import { createHash } from 'node:crypto'; import { BOTANY_ART_MANIFEST, generateAssets, png, artContactSheetPNG } from '../scripts/assets.mjs'; import { PALETTE, PLANT_NAMES, SEASONAL_PLANT_NAMES, WORLD_NAMES, ITEM_NAMES, ANIMAL_NAMES, BIOME_WORLD_NAMES, SPECIES_NAMES, ROCK_NAMES, SPECIES_SEASONAL_NAMES, UNDERSTORY_NAMES, DORMANT_UNDERSTORY_NAMES, BOTANY_CATALOGUE, BOTANY_ART_NAMES, BOTANY_SPRITE_NAMES, EVERGREEN_NAMES, worldArt, contactSheet, animalOBJ, animalBounds } from '../scripts/world-art.mjs'; const digest = data => createHash('sha256').update(data).digest('hex'); const pixel = (image,x,y) => [...image.data.subarray((y*image.width+x)*4,(y*image.width+x+1)*4)]; const pixels = image => Array.from({length:image.width*image.height},(_,i)=>pixel(image,i%image.width,Math.floor(i/image.width))); const tile = (art,name) => art.get(`hearth_${name}.png`); function crc32(bytes) { let crc=0xffffffff; for(const b of bytes) { crc^=b; for(let i=0;i<8;i++) crc=(crc>>>1)^((crc&1)?0xedb88320:0); } return (crc^0xffffffff)>>>0; } function decodePNG(bytes) { assert.deepEqual([...bytes.subarray(0,8)],[137,80,78,71,13,10,26,10]); let offset=8, width, height; const chunks=[], compressed=[]; while(offset { const art=worldArt(); const requiredWorld='stone soil grass_top grass_side sand log_top log_side leaves planks wall roof thatch foundation frame stairs door window vent storage pantry reeds berry_bush berry_bush_bare sapling grain_patch crop_1 crop_2 crop_3 crop_4 clay_bank iron_ore_vein water fire fire_lit torch sticks loose_stone bedroll drying_rack fish_trap weir_stone meadow_grass wildflowers fern litter fungi deadwood tree_seedling young_tree mature_tree dung leaves_spring leaves_autumn leaves_winter meadow_grass_dormant fern_dormant wildflowers_dormant spring_greens spring_greens_dormant autumn_mushrooms autumn_mushrooms_dormant'.split(' '); const requiredItems='berries grain grain_seed raw_food cooked_food fish_bait raw_fish cooked_fish fish_drying_charge dried_fish iron_ore iron_ingot charcoal charcoal_charge smelt_charge clay clay_jar clay_jar_charge dried_food dried_food_charge cloak vessel water_raw water_clean firestarter iron_axe spring_greens autumn_mushrooms'.split(' '); const species=['oak','birch','pine','willow','alder','beech','aspen','rowan','hazel','spruce', 'crab_apple','wild_cherry','field_maple'], rocks=['granite','basalt','limestone','slate','sandstone']; const understory=['clover','nettle','heather','bluebell','sedge','bulrush','moss','lichen']; const biome=[...rocks,...rocks.map(n=>`${n}_bricks`), ...'forest_floor podzol alluvium peat alpine_turf gravel scree snow quartz_vein copper_ore_vein coal_ore_vein'.split(' '), ...species.flatMap(s=>['log_top','log_side','leaves','planks'].map(n=>`${s}_${n}`))]; const seasonal=species.filter(s=>s!=='pine' && s!=='spruce').flatMap(s=>['spring','autumn','winter'].map(n=>`${s}_leaves_${n}`)); const dormant=understory.slice(0,6).map(n=>`${n}_dormant`); assert.deepEqual(SPECIES_NAMES,species); assert.deepEqual(ROCK_NAMES,rocks); assert.deepEqual(BIOME_WORLD_NAMES,biome); assert.equal(biome.length,73); assert.deepEqual(UNDERSTORY_NAMES,understory); assert.deepEqual(DORMANT_UNDERSTORY_NAMES,dormant); assert.deepEqual(SPECIES_SEASONAL_NAMES,seasonal); assert.deepEqual([...WORLD_NAMES].sort(),[...requiredWorld,...biome,...seasonal,...understory,...dormant].sort()); assert.deepEqual([...ITEM_NAMES].sort(),[...requiredItems,'quartz','copper_ore','coal','copper_ingot','copper_charge','copper_pick'].sort()); const names=[...WORLD_NAMES.map(n=>`hearth_${n}.png`),...ITEM_NAMES.map(n=>`hearth_item_${n}.png`), 'hearth_hotbar.png','hearth_selected.png','hearth_crosshair.png',...ANIMAL_NAMES.map(n=>`hearth_${n}.png`), ...BOTANY_SPRITE_NAMES.map(n=>`hearth_${n}.png`)]; assert.equal(new Set(names).size,names.length); assert.deepEqual([...art.keys()],names); assert.equal(art.size,421); assert.equal(WORLD_NAMES.length,181); assert.equal(ITEM_NAMES.length,34); for(const [name,image] of art) { assert.match(name,/^hearth_[a-z0-9_]+\.png$/); assert.equal(image.width,32); assert.equal(image.height,32); assert.equal(image.data.length,4096); assert.ok(image.data.some(n=>n>0)); } }); test('all 100 exact catalogue IDs and Latin identities match the actual Lua records, with two manifest states each',async()=> { const source=await fs.readFile(new URL('../mods/hearth/botany.lua',import.meta.url),'utf8'); const rows=[...source.matchAll(/^\s*add\("([a-z_]+)","[^"]+","([^"]+)"/gm)].map(([,id,latin])=>({id,latin})); assert.equal(rows.length,100); assert.equal(new Set(rows.map(r=>r.id)).size,100); assert.equal(new Set(rows.map(r=>r.latin)).size,100); assert.deepEqual(BOTANY_ART_NAMES,rows.map(r=>r.id)); assert.deepEqual(BOTANY_CATALOGUE.map(({id,latin})=>({id,latin})),rows); const groups=['trees','shrubs','grasses','herbs','ferns_mosses','fungi','aquatic','lichens']; const counts=[13,14,10,22,11,10,10,10]; let offset=0; for(const [i,g] of groups.entries()) { assert.deepEqual(BOTANY_CATALOGUE.slice(offset,offset+counts[i]).map(r=>r.group),Array(counts[i]).fill(g)); offset+=counts[i]; } assert.deepEqual(BOTANY_ART_NAMES.slice(0,13),SPECIES_NAMES); assert.equal(BOTANY_ART_MANIFEST.length,200); assert.deepEqual(BOTANY_ART_MANIFEST.map(r=>r.filename),BOTANY_SPRITE_NAMES.map(n=>`hearth_${n}.png`)); for(const r of BOTANY_ART_MANIFEST) { assert.equal(r.botanicalLatin,rows.find(row=>row.id===r.id).latin); assert.match(r.morphology,/[a-z].*[; ]/); assert.equal(r.license,'MIT'); assert.deepEqual([r.width,r.height],[32,32]); assert.ok(['active','dormant'].includes(r.state)); } }); test('100 recognizable botanical cutouts have unique silhouettes, crisp padded alpha, and 100 distinct bloom-free inactive drawings',()=> { const art=worldArt(), activeMasks=new Set(), dormantMasks=new Set(), allBytes=new Set(); for(const id of BOTANY_ART_NAMES) { const active=tile(art,`botany_${id}`), dormant=tile(art,`botany_${id}_dormant`); const mask=image=>Buffer.from(pixels(image).map(p=>p[3])); activeMasks.add(digest(mask(active))); dormantMasks.add(digest(mask(dormant))); assert.notDeepEqual(mask(active),mask(dormant),`${id}: changed growth form, not just RGB`); for(const [state,image] of [['active',active],['dormant',dormant]]) { const ps=pixels(image), count=ps.filter(p=>p[3]).length; assert.ok(count>=15 && count<800,`${id} ${state}: cutout, not a square (${count})`); assert.ok(ps.every(p=>p[3]===0 || p[3]===255)); for(let j=0;j<32;j++) for(const [x,y] of [[j,0],[j,31],[0,j],[31,j]]) assert.equal(pixel(image,x,y)[3],0); const colors=new Set(ps.filter(p=>p[3]).map(p=>p.slice(0,3).join())); assert.ok(colors.size>=2 && colors.size<=14,`${id} ${state}: small botanical palette`); allBytes.add(digest(image.data)); if(state==='dormant') assert.ok(ps.every(p=>!p[3] || !(p[1]>p[0] && p[1]>p[2])),`${id}: dry inactive state`); } } assert.equal(activeMasks.size,100,'no two taxa masquerade as the same recoloured silhouette'); assert.equal(dormantMasks.size,100,'each inactive species retains its own structure'); assert.equal(allBytes.size,200); }); test('botanical diagnostic landmarks distinguish flowers, pinnate ferns, moss cushions, fungi and true floating lily pads',()=> { const art=worldArt(), image=id=>tile(art,`botany_${id}`), has=(id,color)=>pixels(image(id)).some(p=>p[3] && p.slice(0,3).join()===PALETTE[color].join()); for(const id of ['oak','birch','pine','willow','alder','beech','aspen','rowan','hazel','spruce']) assert.ok(pixels(image(id)).some((p,i)=>p[3] && Math.floor(i/32)<10),'tree fieldguide specimen has diagnostic upper growth'); assert.ok(has('bluebell','mauve')); assert.ok(has('foxglove','wine')); assert.ok(has('cowslip','gold')); assert.ok(has('fly_agaric','cream') && has('fly_agaric','rust')); assert.ok(has('shaggy_inkcap','ink')); assert.ok(has('turkey_tail','earth')); assert.ok(has('polypody','ochre')); assert.ok(has('haircap_moss','copper')); for(const id of ['yellow_waterlily','white_waterlily']) { const pad=image(id); assert.equal(pixel(pad,5,23)[3],255,'broad floating pad, not horns'); assert.equal(pixel(pad,10,26)[3],0,'radial basal notch in the pad'); assert.equal(pixel(pad,1,1)[3],0,'no painted water square'); } assert.ok(has('white_waterlily','cream') && has('yellow_waterlily','gold')); assert.ok(has('sunburst_lichen','copper')); assert.ok(has('lichen','barkDark')); assert.ok(pixels(image('hornwort')).filter(p=>p[3]).length>100,'rootless forked submerged whorls'); }); test('approved leaf PNG bytes survive the catalogue extension without rewriting the existing leaf designs',()=> { const hashes={ leaves:'0fdd70c561c7a9be4c7a2d6cae08dfc7b3467cf767d2ee84c9aa17fe4e52d1c4', oak_leaves:'af2dfa295be79d2bc714aa57ae10c55f688d053c9b02cbbcf5bc84c8385dc582', birch_leaves:'3ee8bedf16d9f7caae1b1b86d3160ac1057f935f77e89b6721c25fb108fcfdc2', pine_leaves:'f38ab9e868e7b954edf94e0d8673e57d8ffffbf2dd2e7699aa43c5abb1b8fb22', willow_leaves:'f9ac6a8d21976e42ff680d6eaf08b339b88f22b6ac1d8e6846eaa278be6caf73', alder_leaves:'8fc4d476ddb52ddafa9dc662b5ba9e4bb509e9104da0f51f33e153697c3eb08d', }, art=worldArt(); for(const [name,hash] of Object.entries(hashes)) { const image=tile(art,name); assert.equal(digest(png(32,(x,y)=>pixel(image,x,y))),hash,name); } }); test('twenty additions retain exact contract identities and diagnostic flowers, climbing structures, seedheads and fern forks',()=> { const additions='crab_apple wild_cherry field_maple honeysuckle ivy gorse broom dandelion daisy red_clover birdsfoot_trefoil ribwort_plantain selfheal knapweed oxeye_daisy field_scabious teasel cow_parsley garlic_mustard holly_fern'.split(' '); const art=worldArt(),has=(id,color)=>pixels(tile(art,`botany_${id}`)).some(p=>p[3]&&p.slice(0,3).join()===PALETTE[color].join()); assert.equal(additions.length,20); for(const id of additions) { assert.ok(BOTANY_ART_NAMES.includes(id)); assert.equal(BOTANY_ART_MANIFEST.filter(r=>r.id===id).length,2); const active=tile(art,`botany_${id}`),dry=tile(art,`botany_${id}_dormant`); assert.notDeepEqual(active.data,dry.data); const upper=pixels(active).filter((p,i)=>p[3]&&Math.floor(i/32)<18).length; assert.ok(upper>8,`${id}: diagnostic upper specimen, not only a generic basal tuft`); } for(const [id,colors] of Object.entries({honeysuckle:['cream','gold','rose'],ivy:['ochre','barkLight'], gorse:['gold','ochre'],broom:['gold','leafLight'],dandelion:['gold','ochre'],daisy:['cream','gold'], red_clover:['wine','mauve'],birdsfoot_trefoil:['gold','ochre'],ribwort_plantain:['cream','bark'], selfheal:['wine','mauve'],knapweed:['wine','rose'],oxeye_daisy:['cream','gold'], field_scabious:['mauve','rose'],teasel:['wood','mauve'],cow_parsley:['cream','sage'], garlic_mustard:['cream','straw'],holly_fern:['ochre','leafLight']})) for(const color of colors) assert.ok(has(id,color),`${id}: ${color} diagnostic landmark`); // Ivy's climbing support/rootlets remain through dormancy; honey tubes are // narrow at the mouth, unlike a daisy's radial head or a teasel's seed cone. for(const y of [8,12,16,20,24]) assert.ok([12,13,14].some(x=>pixel(tile(art,'botany_ivy'),x,y)[3]===255)); assert.ok(pixels(tile(art,'botany_garlic_mustard_dormant')).filter(p=>p[3]).length>50); }); test('every tile is deterministic original clustered palette art, not independent speckle noise or a recolored square',()=> { const first=worldArt(), second=worldArt(); const allowed=new Set(Object.values(PALETTE).map(c=>c.join(','))), unique=new Set(); for(const [name,image] of first) { assert.deepEqual(image.data,second.get(name).data); unique.add(digest(image.data)); const colors=new Set(); for(const p of pixels(image)) if(p[3]) { assert.ok(allowed.has(p.slice(0,3).join(',')),`${name}: explicit palette only`); colors.add(p.slice(0,3).join(',')); } else assert.deepEqual(p,[0,0,0,0],`${name}: no hidden RGB in cutout texels`); assert.ok(colors.size>=2 && colors.size<=14,`${name}: restrained per-tile palette`); } assert.equal(unique.size,first.size,'no identical stand-in art'); // Color-independent boundary masks prove different material geometries, rather // than the same noisy grid with swapped RGB values. const structures=new Set(); for(const name of ['stone','soil','grass_top','sand','log_top','log_side','planks','wall','roof','clay_bank','water']) { const image=tile(first,name), edges=[]; let equal=0, comparisons=0; for(let y=0;y<31;y++) for(let x=0;x<31;x++) { for(const [dx,dy] of [[1,0],[0,1]]) { const same=pixel(image,x,y).join()===pixel(image,x+dx,y+dy).join(); edges.push(same?0:1); equal+=Number(same); comparisons++; } } assert.ok(equal/comparisons>.48,`${name}: contiguous hand-authored clusters, not hash noise`); structures.add(digest(Buffer.from(edges))); } assert.equal(structures.size,11); }); test('approved leaf pixels are preserved; natural ground has no masonry grout, broad stripes or outlined blade symbols',()=> { const art=worldArt(); assert.equal(digest(tile(art,'leaves').data),'6ca3e11b7e8e98f46220db92a4dbe729515835c3ae39c76ac3003fa5494f3f4c'); const rows=image=>Array.from({length:32},(_,y)=>Array.from({length:32},(_,x)=>pixel(image,x,y).join())); for(const name of ['stone',...ROCK_NAMES]) { const image=tile(art,name); assert.ok(pixels(image).every(p=>p[3]===255)); assert.ok(!pixels(image).some(p=>p.slice(0,3).join()===PALETTE.outline.join()),`${name}: no mortar outlines`); assert.ok(rows(image).filter(row=>new Set(row).size===1).length<4,`${name}: interrupted mineral microfacets`); if(name!=='stone') { const brick=tile(art,`${name}_bricks`); assert.equal(new Set(rows(brick)[11]).size,1,'crafted masonry has an explicit bed joint'); assert.notDeepEqual(image.data,brick.data); } } const soil=tile(art,'soil'), turf=tile(art,'grass_top'); for(const name of ['loamShade','loamGlow']) { const color=PALETTE[name].join(); assert.ok(rows(soil).every(row=>row.filter(p=>p.startsWith(color+',')).length<24),'soil lenses never form broad solid strata'); } const greens=new Set(['turf','turfShade','turfGlow','leaf','sage'].map(c=>PALETTE[c].join())); assert.ok(pixels(turf).every(p=>p[3]===255 && greens.has(p.slice(0,3).join())),'dense unoutlined natural turf only'); for(const [x0,y0] of [[0,0],[16,0],[0,16],[16,16]]) { const colors=new Set(); for(let y=y0;y=4,'mosaic is distributed across every quadrant, not isolated giant blades'); } const side=tile(art,'grass_side'), humus=PALETTE.humus.join(); assert.ok(Array.from({length:10},(_,i)=>i+5).some(y=>rows(side)[y].some(p=>p.startsWith(humus+','))),'roots extend below the turf cap'); assert.deepEqual(pixel(side,16,31),pixel(soil,16,31),'mineral soil continues below organic transition'); }); test('understory and species leaves have botanical identities, leafless winters and non-blooming dormant silhouettes',()=> { const art=worldArt(), opaque=name=>pixels(tile(art,name)).filter(p=>p[3]), has=(name,color)=>opaque(name).some(p=>p.slice(0,3).join()===PALETTE[color].join()); for(const name of DORMANT_UNDERSTORY_NAMES) { const active=name.replace(/_dormant$/,''); assert.ok(opaque(name).length!(p[1]>p[0] && p[1]>p[2])),`${name}: spent straw/bark, not green`); for(const color of ['mauve','bud','rose','wine','gold']) assert.ok(!has(name,color),`${name}: no flowers`); } assert.ok(has('clover','cream')); assert.ok(has('bluebell','mauve')); assert.ok(has('heather','bud')); assert.ok(has('nettle','leafLight')); assert.ok(has('bulrush','bark')); assert.ok(!art.has('hearth_moss_dormant.png') && !art.has('hearth_lichen_dormant.png')); for(const s of SPECIES_NAMES.filter(s=>!EVERGREEN_NAMES.includes(s))) { const winter=`${s}_leaves_winter`; assert.ok(opaque(winter).length!(p[1]>p[0] && p[1]>p[2]))); assert.ok(has(`${s}_leaves_spring`,'bud')); assert.ok(has(`${s}_leaves_autumn`,'wine')); } for(const s of EVERGREEN_NAMES) for(const season of ['spring','autumn','winter']) assert.ok(!art.has(`hearth_${s}_leaves_${season}.png`)); for(const name of ['quartz_vein','copper_ore_vein','coal_ore_vein']) assert.ok(pixels(tile(art,name)).every(p=>p[3]===255),'complete opaque host rock, not an overlay'); assert.ok(has('copper_ore_vein','verdigris')); assert.ok(has('item_copper_ore','verdigris')); assert.ok(has('item_copper_ingot','copperGlow')); assert.ok(has('item_copper_pick','copperGlow')); assert.notDeepEqual(tile(art,'item_copper_charge').data,tile(art,'item_copper_ore').data); }); test('actual content craftitems and explicit material texture references have generated art, not compatibility swatches',async()=> { const text=await fs.readFile(new URL('../mods/hearth/content.lua',import.meta.url),'utf8'), art=worldArt(); for(const match of text.matchAll(/api\.register_craftitem\("hearth:([a-z0-9_]+)"/g)) assert.ok(art.has(`hearth_item_${match[1]}.png`),`craftitem ${match[1]} has an authored icon`); for(const match of text.matchAll(/item_image\("([a-z0-9_]+)"\)/g)) assert.ok(art.has(`hearth_item_${match[1]}.png`),`explicit icon ${match[1]}`); const biomes=await fs.readFile(new URL('../mods/hearth/biomes.lua',import.meta.url),'utf8'); for(const name of ROCK_NAMES) assert.ok(biomes.includes(`"${name}"`) && art.has(`hearth_${name}_bricks.png`)); for(const name of ['quartz','copper_ore','coal']) assert.ok(art.has(`hearth_item_${name}.png`)); }); test('plant and item masks have empty borders/corners, crisp alpha and recognizable non-square negative space',()=> { const art=worldArt(), masks=new Set(); for(const name of [...PLANT_NAMES,...ITEM_NAMES.map(n=>`item_${n}`)]) { const image=tile(art,name), ps=pixels(image), opaque=ps.filter(p=>p[3]===255).length; assert.ok(ps.every(p=>p[3]===0 || p[3]===255),name); assert.ok(opaque>=15 && opaque<800,`${name}: silhouette occupancy ${opaque}`); assert.equal(pixel(image,0,0)[3],0,name); assert.equal(pixel(image,31,31)[3],0,name); const mask=Buffer.from(ps.map(p=>p[3])); masks.add(digest(mask)); if(name.startsWith('item_')) { for(let i=0;i<32;i++) for(const [x,y] of [[i,0],[0,i],[31,i],[i,31]]) assert.equal(pixel(image,x,y)[3],0,`${name}: inventory padding`); } } assert.ok(masks.size>=32,'different plant/item silhouettes, not generic masked squares'); const bush=tile(art,'berry_bush'), bare=tile(art,'berry_bush_bare'); assert.deepEqual(pixels(bush).map(p=>p[3]),pixels(bare).map(p=>p[3]),'berries change fruit, not foliage silhouette'); assert.notDeepEqual(bush.data,bare.data); const growth=[1,2,3,4].map(n=>pixels(tile(art,`crop_${n}`)).filter(p=>p[3]).length); assert.ok(growth[0] { const art=worldArt(); assert.equal(pixel(tile(art,'log_top'),0,0).slice(0,3).join(),PALETTE.barkDark.join()); const cut=tile(art,'log_top'); for(const [dx,dy] of [[1,0],[0,1],[-1,0],[0,-1]]) { const ray=Array.from({length:13},(_,r)=>pixel(cut,14+dx*r,16+dy*r).join()); const changes=ray.slice(1).filter((color,i)=>color!==ray[i]).length; assert.ok(changes>=5,'multiple growth rings on every radius, not an arbitrary pair of unequal pixels'); } assert.deepEqual(pixel(tile(art,'planks'),0,0).slice(0,3),PALETTE.barkLight,'subdued plank joints'); assert.deepEqual(pixel(tile(art,'door'),25,17).slice(0,3),PALETTE.gold,'door latch'); for(const [a,b] of [['stone','iron_ore_vein'],['fire','fire_lit'],['grass_top','grass_side'], ['storage','pantry'],['item_water_raw','item_water_clean'],['item_raw_fish','item_dried_fish']]) assert.notDeepEqual(tile(art,a).data,tile(art,b).data,`${a} vs ${b}`); const window=tile(art,'window'), alphas=new Set(pixels(window).map(p=>p[3])); assert.deepEqual([...alphas].sort((a,b)=>a-b),[0,95,255]); assert.equal(pixel(window,0,0)[3],0); assert.equal(pixel(window,5,5)[3],95); }); test('seasonal botanical silhouettes show sage buds, copper/wine autumn, bare winter twigs and bloom-free dormancy',()=> { const art=worldArt(), opaque=name=>pixels(tile(art,name)).filter(p=>p[3]); const has=(name,color)=>opaque(name).some(p=>p.slice(0,3).join()===PALETTE[color].join()); const green=p=>p[1]>p[0] && p[1]>p[2]; for(const name of SEASONAL_PLANT_NAMES) assert.ok(art.has(`hearth_${name}.png`)); for(const color of ['springDark','springLeaf','springLight','bud']) assert.ok(has('leaves_spring',color)); const buds=opaque('leaves_spring').filter(p=>p.slice(0,3).join()===PALETTE.bud.join()).length; assert.ok(buds>=6 && buds<30,'buds are a subtle accent, not a pink canopy'); for(const color of ['copper','ochre','wine']) assert.ok(has('leaves_autumn',color)); for(const name of ['leaves_autumn','leaves_winter','meadow_grass_dormant','fern_dormant', 'wildflowers_dormant','spring_greens_dormant','autumn_mushrooms_dormant']) { assert.ok(opaque(name).every(p=>!green(p)),`${name}: no living green`); for(const color of ['rose','mauve','gold','bud']) assert.ok(!has(name,color),`${name}: no bloom`); } const mask=name=>Buffer.from(pixels(tile(art,name)).map(p=>p[3])); assert.ok(opaque('leaves_winter').length { const art=worldArt(), water=tile(art,'water'); for(let i=0;i<32;i++) { assert.deepEqual(pixel(water,0,i),pixel(water,31,i)); assert.deepEqual(pixel(water,i,0),pixel(water,i,31)); } const base=PALETTE.water.join(), ripples=pixels(water).filter(p=>p.slice(0,3).join()!==base).length; assert.ok(ripples>30 && ripples<100); assert.ok(pixels(water).every(p=>p[3]===255)); for(const name of ['sand','clay_bank']) for(let y=0;y<32;y++) assert.deepEqual(pixel(tile(art,name),0,y),pixel(tile(art,name),31,y),`${name}: continuous strata`); // Exposed grass must keep a soil body and a foliage cap on every side. const side=tile(art,'grass_side'); assert.ok([PALETTE.turfShade,PALETTE.turf,PALETTE.turfGlow].some(c=>c.join()===pixel(side,16,0).slice(0,3).join())); assert.ok(pixel(side,16,31)[0]>pixel(side,16,31)[1]); }); function parseOBJ(text) { const vertices=[], uv=[], faces=[], groups=new Map(); let current; for(const line of text.trim().split('\n')) { const [kind,...parts]=line.split(' '); if(kind==='g') { current={vertices:[],faces:[]}; groups.set(parts[0],current); } if(kind==='v') { const v=parts.map(Number); vertices.push(v); current.vertices.push(v); } if(kind==='vt') uv.push(parts.map(Number)); if(kind==='f') { const face=parts.map(p=>p.split('/').map(Number)); faces.push(face); current.faces.push(face); } assert.ok(['#','g','v','vt','f'].includes(kind),'no material/path loading directives'); } return {vertices,uv,faces,groups}; } test('wildlife meshes have outward nondegenerate indexed triangles, semantic UVs, bounded counts and stable node bounds',()=> { const unique=new Set(); for(const name of ANIMAL_NAMES) { const text=animalOBJ(name); assert.equal(text,animalOBJ(name)); unique.add(text); const {vertices,uv,faces,groups}=parseOBJ(text), bounds=animalBounds(name); assert.ok(vertices.length>=100 && vertices.length<=300); assert.ok(faces.length>=150 && faces.length<=450); assert.ok(vertices.flat().every(Number.isFinite)); assert.equal(uv.length,vertices.length/2); assert.ok(uv.flat().every(n=>Number.isFinite(n) && n>0 && n<1)); assert.equal(Math.min(...vertices.map(v=>v[1])),0,'feet are exactly at ground'); for(let i=0;i<3;i++) { assert.ok(Math.abs(Math.min(...vertices.map(v=>v[i]))/10-bounds.min[i])<1e-6); assert.ok(Math.abs(Math.max(...vertices.map(v=>v[i]))/10-bounds.max[i])<1e-6); assert.ok(bounds.max[i]-bounds.min[i]<2,'less than two nodes on every axis'); } assert.deepEqual(bounds.visualSize,{x:1,y:1}); const referenced=new Set(); for(const group of groups.values()) { const center=[0,1,2].map(i=>group.vertices.reduce((s,v)=>s+v[i],0)/group.vertices.length); for(const face of group.faces) { assert.equal(face.length,3); assert.equal(new Set(face.map(p=>p[0])).size,3); for(const [v,t] of face) { assert.ok(v>=1 && v<=vertices.length && t>=1 && t<=uv.length); referenced.add(v); } const [a,b,c]=face.map(([id])=>vertices[id-1]), u=b.map((n,i)=>n-a[i]), v=c.map((n,i)=>n-a[i]); const normal=[u[1]*v[2]-u[2]*v[1],u[2]*v[0]-u[0]*v[2],u[0]*v[1]-u[1]*v[0]]; assert.ok(Math.hypot(...normal)>1e-4,'nonzero triangle area'); assert.ok(normal.reduce((sum,n,i)=>sum+n*(a[i]-center[i]),0)>0,'outward face winding'); // All triangle UVs stay inside the same 8px semantic atlas patch. const cells=face.map(([,id])=>uv[id-1].map(n=>Math.floor(n*4)).join()); assert.equal(new Set(cells).size,1,'no UV crossing into another material'); } } assert.equal(referenced.size,vertices.length); } assert.equal(unique.size,3); for(const bad of ['../hare','toString','constructor','wolf']) { assert.throws(()=>animalOBJ(bad),/Unknown animal/); assert.throws(()=>animalBounds(bad),/Unknown animal/); } }); test('species-specific silhouettes retain hare long ears, fox long bushy tail, deer hooves/antlers and leg clearance',()=> { const hare=parseOBJ(animalOBJ('hare')), fox=parseOBJ(animalOBJ('fox')), deer=parseOBJ(animalOBJ('deer')); const extent=(group,axis)=>Math.max(...group.vertices.map(v=>v[axis]))-Math.min(...group.vertices.map(v=>v[axis])); assert.ok(extent(hare.groups.get('ear_left'),1)>3*extent(hare.groups.get('ear_left'),0)); assert.ok(extent(fox.groups.get('tail'),2)>3.5); assert.ok(extent(fox.groups.get('tail'),0)>2*extent(fox.groups.get('leg_1_1.9'),0)); assert.ok(deer.groups.has('antler_1') && deer.groups.has('point_-1') && deer.groups.has('hoof_1_2.4')); assert.ok(Math.min(...deer.groups.get('body').vertices.map(v=>v[1]))>=4); assert.ok(animalBounds('deer').max[1]>animalBounds('hare').max[1]*1.6); }); test('PNG writer supports rectangular contact sheets, validates bounded dimensions/RGBA, and independently decodes exact atlas cells',()=> { const art=worldArt(), sheet=contactSheet(art), pngBytes=artContactSheetPNG(art), decoded=decodePNG(pngBytes); assert.equal(sheet.width,512); assert.equal(sheet.height,Math.ceil(art.size/16)*32); assert.equal(decoded.width,sheet.width); assert.equal(decoded.height,sheet.height); assert.deepEqual(decoded.data,sheet.data); const ordered=[...art.entries()].sort(([a],[b])=>ab?1:0); ordered.forEach(([,image],i)=> { for(const [x,y] of [[0,0],[15,15],[31,31]]) { const p=pixel(image,x,y), output=pixel(decoded,(i%16)*32+x,Math.floor(i/16)*32+y); assert.equal(output[3],255); if(p[3]===255) assert.deepEqual(output,p,'opaque art copied without scaling/filtering'); } }); // Every seasonal tile is included, not merely present in the named manifest. for(const name of SEASONAL_PLANT_NAMES) { const index=ordered.findIndex(([key])=>key===`hearth_${name}.png`), image=tile(art,name); for(let y=0;y<32;y++) for(let x=0;x<32;x++) if(pixel(image,x,y)[3]===255) assert.deepEqual(pixel(decoded,index%16*32+x,Math.floor(index/16)*32+y),pixel(image,x,y)); } assert.deepEqual(pngBytes,artContactSheetPNG()); const small=decodePNG(png(3,()=>[1,2,3,4],5)); assert.equal(small.width,3); assert.equal(small.height,5); for(const [w,h] of [[0,1],[1,-1],[1.5,2],[Infinity,1],[2049,1],[2048,2048]]) assert.throws(()=>png(w,()=>[0,0,0,0],h),/dimensions/); for(const color of [[1,2,3],[-1,0,0,255],[256,0,0,255],[0,0,NaN,255],[0,0,0,.5]]) assert.throws(()=>png(1,()=>color),/RGBA/); for(const images of [new Map(),new Map([['bad',{width:32,height:32,data:new Uint8Array(4)}]]), new Map(Array.from({length:513},(_,i)=>[i,art.values().next().value])),{},null]) assert.throws(()=>contactSheet(images),/1\.\.512 RGBA 32px tiles/); assert.equal(contactSheet(new Map(Array.from({length:512},(_,i)=>[String(i),art.values().next().value]))).height,1024); assert.deepEqual(contactSheet(new Map([...art].reverse())),sheet,'filename sorting makes the review legend stable'); }); test('generateAssets bundles every named art/model and contact sheet from a minimal fixture source with guarded output paths',async t=> { const root=await fs.mkdtemp(path.join(await promisify(realpath)(os.tmpdir()),'hearth-world-art-')); t.after(()=>fs.rm(root,{recursive:true,force:true})); const source=path.join(root,'source'), destination=path.join(root,'output'); await fs.mkdir(path.join(source,'mods/hearth'),{recursive:true}); await fs.writeFile(path.join(source,'mods/hearth/init.lua'),'-- fixture: no named asset references or source binaries\n'); await generateAssets(destination,source); const textures=path.join(destination,'mods/hearth/textures'), art=worldArt(); for(const [name,image] of art) { const decoded=decodePNG(await fs.readFile(path.join(textures,name))); assert.equal(decoded.width,32); assert.equal(decoded.height,32); assert.deepEqual(decoded.data,image.data,name); } assert.deepEqual(await fs.readFile(path.join(textures,'hearth_art_contact_sheet.png')),artContactSheetPNG()); for(const name of ANIMAL_NAMES) assert.equal(await fs.readFile(path.join(destination,`mods/hearth/models/hearth_${name}.obj`),'utf8'),animalOBJ(name)); // New named assets use the existing safePath writer, including direct calls. const outside=path.join(root,'outside'); await fs.mkdir(outside); await fs.writeFile(path.join(outside,'sentinel'),'unchanged'); const linked=path.join(root,'linked'), models=path.join(linked,'mods/hearth/models'); await fs.mkdir(path.dirname(models),{recursive:true}); await fs.symlink(outside,models,process.platform==='win32'?'junction':'dir'); await assert.rejects(generateAssets(linked,source),/symbolic link|junction/); assert.deepEqual(await fs.readdir(outside),['sentinel']); assert.equal(await fs.readFile(path.join(outside,'sentinel'),'utf8'),'unchanged'); });