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 path from 'node:path'; import os from 'node:os'; import { inflateSync } from 'node:zlib'; import { FLORA_NAMES, TREE_SEASONS, LEGACY_MESH_SPECIES, SPECIES_MESH_NAMES, SPECIES_ATLAS_NAMES, floraOBJ, floraBounds, treeAtlas } from '../scripts/flora-art.mjs'; import { PALETTE, SPECIES_NAMES, EVERGREEN_NAMES, worldArt } from '../scripts/world-art.mjs'; import { BOTANY_ART_MANIFEST, generateAssets, png } from '../scripts/assets.mjs'; import { packageGame, verifyPackage } from '../scripts/package.mjs'; import { CHARACTER_ART_MANIFEST } from '../scripts/character-art.mjs'; import { FAUNA_ART_MANIFEST, FAUNA_NAMES, FAUNA_ANIMATIONS, faunaB3D, faunaTexture, faunaPortraitSheet } from '../scripts/fauna-art.mjs'; const cross=(a,b)=>[a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]]; const sub=(a,b)=>a.map((v,i)=>v-b[i]); const dot=(a,b)=>a.reduce((s,n,i)=>s+n*b[i],0); function parse(text) { const v=[],vt=[],vn=[],groups=[]; let group; for(const line of text.trim().split('\n')) { const [kind,...p]=line.split(' '); assert.ok(['#','s','v','vt','vn','g','f'].includes(kind),'no external material references'); if(kind==='v') v.push(p.map(Number)); if(kind==='vt') vt.push(p.map(Number)); if(kind==='vn') vn.push(p.map(Number)); if(kind==='g') { group={name:p[0],faces:[]}; groups.push(group); } if(kind==='f') group.faces.push(p.map(t=>t.split('/').map(Number))); } return {v,vt,vn,groups}; } test('all original deterministic, closed volumetric meshes have finite indexed triangles, outward normals and inset material UVs',()=> { const distinct=new Set(); for(const name of FLORA_NAMES) { const text=floraOBJ(name); assert.equal(text,floraOBJ(name)); distinct.add(text); const {v,vt,vn,groups}=parse(text); const species=SPECIES_MESH_NAMES.includes(name), triangles=groups.flatMap(g=>g.faces).length; assert.ok(v.length>50 && v.length<=(species?640:300),`${name}: bounded per-species topology`); assert.ok(triangles<=(species?1200:600),`${name}: explicit triangle budget`); for(const values of [v,vt,vn]) assert.ok(values.flat().every(Number.isFinite)); const referenced=new Set(); for(const group of groups) { const ids=[...new Set(group.faces.flat().map(p=>p[0]))]; const center=[0,1,2].map(i=>ids.reduce((s,id)=>s+v[id-1][i],0)/ids.length); const edges=new Map(); let volume=0; for(const f of group.faces) { assert.equal(f.length,3); assert.equal(new Set(f.map(p=>p[0])).size,3); for(const [a,t,n] of f) { assert.ok(a>=1 && a<=v.length && t>=1 && t<=vt.length && n>=1 && n<=vn.length); referenced.add(a); const [u,w]=vt[t-1]; assert.ok(w>=.5/32 && w<=31.5/32); assert.ok(group.name.startsWith('leaf') || group.name.startsWith('foliage') ? u>=32.5/64 && u<=63.5/64 : u>=.5/64 && u<=31.5/64); } const [a,b,c]=f.map(p=>v[p[0]-1]), normal=cross(sub(b,a),sub(c,a)); assert.ok(Math.hypot(...normal)>1e-5); assert.ok(dot(normal,sub(a,center))>0,'outward winding'); assert.ok(dot(normal,vn[f[0][2]-1])/Math.hypot(...normal)>.99999); volume+=dot(a,cross(b,c))/6; for(let i=0;i<3;i++) { const a=f[i][0], b=f[(i+1)%3][0], key=[Math.min(a,b),Math.max(a,b)].join(); const entry=edges.get(key)||[0,0]; entry[0]++; entry[1]+=a.001,'solid 3D volume, not a billboard'); for(const edge of edges.values()) assert.deepEqual(edge,[2,0],'closed oriented manifold per lobe/branch'); } assert.equal(referenced.size,v.length); const bound=floraBounds(name); for(let i=0;i<3;i++) { assert.equal(Math.min(...v.map(p=>p[i]))/10,bound.min[i]); assert.equal(Math.max(...v.map(p=>p[i]))/10,bound.max[i]); } assert.equal(bound.min[1],-.5,'ground is node bottom'); assert.ok(bound.max[0]!==-bound.min[0] || bound.max[2]!==-bound.min[2],'authored asymmetry'); } assert.equal(distinct.size,FLORA_NAMES.length); for(const bad of ['../young_tree','constructor','oak',null]) { assert.throws(()=>floraOBJ(bad),/Unknown flora/); assert.throws(()=>floraBounds(bad),/Unknown flora/); } }); test('winter trees are forked wood-only sculptures, with fewer faces and every original trunk/bough preserved exactly',()=> { const geometry=(mesh,group)=>group.faces.map(face=>face.map(([v,t,n])=>[mesh.v[v-1],mesh.vt[t-1],mesh.vn[n-1]])); for(const name of ['young_tree','mature_tree']) { const summer=parse(floraOBJ(name)), winter=parse(floraOBJ(`${name}_winter`)); assert.ok(winter.groups.every(g=>!/^(leaf|foliage)/.test(g.name)),'no foliage geometry'); assert.ok(winter.groups.flatMap(g=>g.faces).lengthg.faces).length); const woody=summer.groups.filter(g=>!/^(leaf|foliage)/.test(g.name)); for(const group of woody) { const retained=winter.groups.find(g=>g.name===group.name); assert.ok(retained); assert.deepEqual(geometry(winter,retained),geometry(summer,group),group.name); } const forks=winter.groups.filter(g=>g.name.startsWith('winter_fork_')); assert.equal(forks.length,woody.length-1,'one lateral fork per bough'); for(const group of forks) { const points=geometry(winter,group).flat().map(p=>p[0]); assert.ok(Math.max(...points.map(p=>p[0]))-Math.min(...points.map(p=>p[0]))>1,'lateral silhouette'); } const a=floraBounds(name), b=floraBounds(`${name}_winter`); assert.equal(b.min[1],-.5); assert.ok(b.max[1]> (name==='mature_tree'?2: .9)); for(let i=0;i<3;i++) assert.ok(b.min[i]>=a.min[i] && b.max[i]<=a.max[i],'within existing clearance envelope'); assert.ok(winter.vt.every(([u])=>u<=31.5/64),'all faces sample bark only'); } assert.ok(!FLORA_NAMES.includes('tree_seedling_winter'),'runtime reuses seedling mesh'); }); test('tree heights, footprints and distinct overlapping foliage lobes respect gameplay scale',()=> { for(const [name,maxHeight,x,z] of [['tree_seedling',.7,.3,.3],['young_tree',2.2,.65,.65], ['mature_tree',3.5,1.1,1.1],['deadwood',.5,.45,.9]]) { const {min,max}=floraBounds(name); assert.ok(max[1]-min[1]<=maxHeight+1e-6,name); assert.ok(Math.max(Math.abs(min[0]),Math.abs(max[0]))<=x,name); assert.ok(Math.max(Math.abs(min[2]),Math.abs(max[2]))<=z,name); } assert.ok(floraBounds('young_tree').max[1]>=1.3); assert.equal(floraBounds('mature_tree').max[1],3); for(const [name,count] of [['tree_seedling',2],['young_tree',4],['mature_tree',5]]) { const {groups}=parse(floraOBJ(name)); assert.equal(groups.filter(g=>/^(leaf|foliage)/.test(g.name)).length,count); assert.ok(groups.some(g=>/^(stem|trunk)$/.test(g.name))); } }); test('single bark/leaf atlas is deterministic 64x32 straight RGBA with solid canopy underpaint and exact encoded pixels',()=> { const image=treeAtlas(); assert.deepEqual(image,treeAtlas()); assert.deepEqual([image.width,image.height,image.data.length],[64,32,8192]); assert.ok(image.data.filter((_,i)=>i%4===3).every(a=>a===255),'opaque lobes do not expose torn faces'); assert.notDeepEqual(image.data.slice(0,128),image.data.slice(128,256)); const bytes=png(64,(x,y)=>image.data.subarray((y*64+x)*4,(y*64+x+1)*4),32); assert.equal(bytes[25],6,'PNG RGBA color type'); const compressed=[]; for(let i=8;itreeAtlas(new Map()),/requires/); }); test('all four seasonal atlases preserve byte-exact bark and opaque 64x32 materials with season-specific leaf underpaint',()=> { const art=worldArt(), summer=treeAtlas(art), unique=new Set(); assert.deepEqual(TREE_SEASONS,['summer','spring','autumn','winter']); for(const season of TREE_SEASONS) { const image=treeAtlas(art,season), leaves=art.get(`hearth_leaves${season==='summer'?'':`_${season}`}.png`); assert.deepEqual(image,treeAtlas(art,season)); assert.deepEqual([image.width,image.height,image.data.length],[64,32,8192]); unique.add(Buffer.from(image.data).toString('hex')); for(let y=0;y<32;y++) { assert.deepEqual(image.data.slice(y*256,y*256+128),summer.data.slice(y*256,y*256+128),'unchanged bark'); for(let x=0;x<32;x++) { const from=(y*32+x)*4, to=(y*64+x+32)*4; const color=[...image.data.slice(to,to+4)]; assert.equal(color[3],255); if(leaves.data[from+3]===255) assert.deepEqual(color,[...leaves.data.slice(from,from+4)]); else assert.deepEqual(color,[...PALETTE[{summer:'leafDark',spring:'springDark',autumn:'autumnDark',winter:'barkDark'}[season]],255]); } } } assert.equal(unique.size,TREE_SEASONS.length); for(const bad of ['Summer','../spring','constructor',null]) assert.throws(()=>treeAtlas(art,bad),/Unknown tree season/); for(const season of TREE_SEASONS) assert.throws(()=>treeAtlas(new Map(),season),/requires/); }); test('deprecated 23-species-mesh inventory stays frozen; all thirteen cube species have 46 compatibility atlases',()=> { const species=['oak','birch','pine','willow','alder']; const meshes=species.flatMap(s=>[`${s}_seedling`,`${s}_young`,`${s}_mature`, ...(s==='pine'?[]:[`${s}_young_winter`,`${s}_mature_winter`])]); const atlases=SPECIES_NAMES.flatMap(s=>[`${s}_atlas`,...(EVERGREEN_NAMES.includes(s)?[]:['spring','autumn','winter'].map(n=>`${s}_atlas_${n}`))]); assert.deepEqual(LEGACY_MESH_SPECIES,species); assert.deepEqual(SPECIES_MESH_NAMES,meshes); assert.deepEqual(SPECIES_ATLAS_NAMES,atlases); assert.equal(meshes.length,23); assert.equal(atlases.length,46); assert.equal(FLORA_NAMES.length,29); for(const s of SPECIES_NAMES.slice(5)) for(const stage of ['seedling','young','mature','young_winter','mature_winter']) assert.throws(()=>floraOBJ(`${s}_${stage}`),/Unknown flora/,'new species must not grow unused irregular meshes'); for(const s of species) assert.ok(!FLORA_NAMES.includes(`${s}_deadwood`)); for(const bad of ['pine_mature_winter','oak_seedling_winter','oak_mature_spring','../oak_mature']) assert.throws(()=>floraOBJ(bad),/Unknown flora/); }); const points=(mesh,group)=>[...new Set(group.faces.flat().map(p=>p[0]))].sort((a,b)=>a-b).map(i=>mesh.v[i-1]); const extents=ps=>[0,1,2].map(i=>Math.max(...ps.map(p=>p[i]))-Math.min(...ps.map(p=>p[i]))); test('species architectures convey broad oak, airy slim birch, tapered pine whorls, drooping willow and rounded alder within clearance',()=> { const meshes=Object.fromEntries(LEGACY_MESH_SPECIES.map(s=>[s,parse(floraOBJ(`${s}_mature`))])); for(const s of LEGACY_MESH_SPECIES) for(const stage of ['seedling','young','mature']) { const {min,max}=floraBounds(`${s}_${stage}`), half=stage==='mature'?1.5:stage==='young'?.75:.3; assert.equal(min[1],-.5); assert.ok(max[1]-min[1]<=(stage==='mature'?5:stage==='young'?2.5:1)); for(const i of [0,2]) assert.ok(min[i]>=-half && max[i]<=half,`${s}_${stage}: footprint`); const mesh=parse(floraOBJ(`${s}_${stage}`)); assert.ok(mesh.groups.some(g=>/^(stem|trunk)$/.test(g.name))); assert.ok(mesh.groups.filter(g=>/^(leaf|foliage)/.test(g.name)).length>=2); } const oak=extents(meshes.oak.v), birch=extents(meshes.birch.v), alder=extents(meshes.alder.v); assert.ok(oak[0]>birch[0]*1.5,'spreading oak versus slim birch'); assert.ok(birch[1]/birch[0]>2.5,'birch is tall and narrow'); assert.ok(oak[0]>alder[0] && alder[1]extents(points(meshes[s],meshes[s].groups.find(g=>g.name==='trunk')))[0]; assert.ok(trunkWidth('birch')g.name===`foliage_tier_${i}_0`), upper=pine.groups.find(g=>g.name===`foliage_tier_${i+1}_0`); assert.ok(extents(points(pine,lower))[0]>extents(points(pine,upper))[0],'evergreen whorls taper upward'); } assert.ok(pine.groups.some(g=>g.name==='foliage_leader')); const willow=meshes.willow, curtains=willow.groups.filter(g=>g.name.startsWith('foliage_curtain_')); assert.equal(curtains.length,6); for(const g of curtains) { const e=extents(points(willow,g)); assert.ok(e[1]>e[0]*3 && e[1]>e[2]*3,'long pendant leaf curtains'); } const bough=willow.groups.find(g=>g.name==='pendant_0'), ps=points(willow,bough); const a=ps.slice(0,8).reduce((n,p)=>n+p[1],0)/8, b=ps.slice(8).reduce((n,p)=>n+p[1],0)/8; assert.ok(a>b+10,'willow terminal wood droops, not only its painted leaves'); }); test('deciduous winter species preserve all woody parts, omit every crown and sample bark only; pine stays evergreen',()=> { const geometry=(m,g)=>g.faces.map(f=>f.map(([v,t,n])=>[m.v[v-1],m.vt[t-1],m.vn[n-1]])); for(const s of LEGACY_MESH_SPECIES.filter(s=>s!=='pine')) for(const stage of ['young','mature']) { const leafy=parse(floraOBJ(`${s}_${stage}`)), bare=parse(floraOBJ(`${s}_${stage}_winter`)); const woody=leafy.groups.filter(g=>!g.name.startsWith('foliage')); assert.deepEqual(bare.groups.map(g=>g.name),woody.map(g=>g.name),'all forks and pendants retained'); for(const g of woody) assert.deepEqual(geometry(bare,bare.groups.find(b=>b.name===g.name)),geometry(leafy,g)); assert.ok(bare.vt.every(([u])=>u<=31.5/64)); assert.ok(bare.groups.flatMap(g=>g.faces).lengthg.faces).length); const a=floraBounds(`${s}_${stage}`), b=floraBounds(`${s}_${stage}_winter`); for(let i=0;i<3;i++) assert.ok(b.min[i]>=a.min[i] && b.max[i]<=a.max[i]); } assert.ok(parse(floraOBJ('pine_mature')).groups.some(g=>g.name.startsWith('foliage'))); assert.throws(()=>treeAtlas(worldArt(),'winter','pine'),/evergreen/); }); test('all species atlases use exact own bark and own seasonal leaves, opaque underpaint and half-texel inset UVs',()=> { const art=worldArt(), distinct=new Set(); for(const s of SPECIES_NAMES) { const summer=treeAtlas(art,'summer',s), seasons=EVERGREEN_NAMES.includes(s)?['summer']:TREE_SEASONS; for(const season of seasons) { const image=treeAtlas(art,season,s), bark=art.get(`hearth_${s}_log_side.png`), leaf=art.get(`hearth_${s}_leaves${season==='summer'?'':`_${season}`}.png`); assert.deepEqual(image,treeAtlas(art,season,s)); assert.deepEqual([image.width,image.height,image.data.length],[64,32,8192]); distinct.add(Buffer.from(image.data).toString('hex')); for(let y=0;y<32;y++) for(let x=0;x<32;x++) { const from=(y*32+x)*4, left=(y*64+x)*4, right=left+128; assert.deepEqual(image.data.slice(left,left+4),bark.data.slice(from,from+4)); assert.deepEqual(image.data.slice(left,left+4),summer.data.slice(left,left+4)); assert.equal(image.data[right+3],255); if(leaf.data[from+3]) assert.deepEqual(image.data.slice(right,right+4),leaf.data.slice(from,from+4)); else { const color=season==='summer'?{oak:'leafDark',birch:'leaf',pine:'pineShade',willow:'willowShade',alder:'leafDark', beech:'leafDark',aspen:'leafDark',rowan:'leafDark',hazel:'leafDark',spruce:'pineShade', crab_apple:'leafDark',wild_cherry:'leafDark',field_maple:'leafDark'}[s]: {spring:'springDark',autumn:'autumnDark',winter:'barkDark'}[season]; assert.deepEqual([...image.data.slice(right,right+4)],[...PALETTE[color],255]); } } } } assert.equal(distinct.size,SPECIES_ATLAS_NAMES.length); for(const s of EVERGREEN_NAMES) for(const season of ['spring','autumn','winter']) assert.throws(()=>treeAtlas(art,season,s),/evergreen/); for(const bad of ['constructor','../oak','maple','']) assert.throws(()=>treeAtlas(art,'summer',bad),/Unknown tree species/); }); test('minimal fixture generation and integrity-verified package include every seasonal texture, atlas and model; linked outputs refused',async t=> { const root=await fs.mkdtemp(path.join(await promisify(realpath)(os.tmpdir()),'hearth-flora-')); t.after(()=>fs.rm(root,{recursive:true,force:true})); const source=path.join(root,'source'), out=path.join(root,'out'); await fs.mkdir(path.join(source,'mods/hearth'),{recursive:true}); for(const name of ['init','model','runtime','content','encounters']) await fs.writeFile(path.join(source,`mods/hearth/${name}.lua`),'-- minimal fixture\n'); await generateAssets(out,source); async function check(destination) { for(const name of FLORA_NAMES) assert.equal(await fs.readFile(path.join(destination,`mods/hearth/models/hearth_${name}.obj`),'utf8'),floraOBJ(name)); const art=worldArt(); for(const season of TREE_SEASONS) { const atlas=treeAtlas(art,season), expected=png(64,(x,y)=>atlas.data.subarray((y*64+x)*4,(y*64+x+1)*4),32); assert.deepEqual(await fs.readFile(path.join(destination,`mods/hearth/textures/hearth_tree_atlas${season==='summer'?'':`_${season}`}.png`)),expected); } for(const name of SPECIES_ATLAS_NAMES) { const [,s,season='summer']=name.match(/^([a-z]+(?:_[a-z]+)*)_atlas(?:_(spring|autumn|winter))?$/), atlas=treeAtlas(art,season,s); const expected=png(64,(x,y)=>atlas.data.subarray((y*64+x)*4,(y*64+x+1)*4),32); assert.deepEqual(await fs.readFile(path.join(destination,`mods/hearth/textures/hearth_${name}.png`)),expected); } for(const [name,image] of art) assert.deepEqual(await fs.readFile(path.join(destination,'mods/hearth/textures',name)), png(32,(x,y)=>image.data.subarray((y*32+x)*4,(y*32+x+1)*4))); const expectedTextures=[...art.keys(),...TREE_SEASONS.map(s=>`hearth_tree_atlas${s==='summer'?'':`_${s}`}.png`), ...SPECIES_ATLAS_NAMES.map(n=>`hearth_${n}.png`), ...['knife','axe','pick','spear'].map(n=>`hearth_icon_${n}.png`), ...CHARACTER_ART_MANIFEST.map(entry=>entry.filename), ...FAUNA_ART_MANIFEST.map(entry=>entry.texture),'hearth_fauna_contact_sheet.png', 'hearth_character_contact_sheet.png', 'hearth_person.png','crosshair.png','hearth_art_contact_sheet.png','hearth_c7aa83.png','hearth_947853.png']; assert.equal(expectedTextures.length,675,'complete strict inventory: world + atlases + characters + fauna + two swatches'); assert.deepEqual(JSON.parse(await fs.readFile(path.join(destination,'botany-art.json'),'utf8')), {format:1,images:BOTANY_ART_MANIFEST},'all 200 botanical image identities are packaged'); assert.deepEqual(JSON.parse(await fs.readFile(path.join(destination,'fauna-art.json'),'utf8')), {format:1,animations:FAUNA_ANIMATIONS,animals:FAUNA_ART_MANIFEST, portraitSheet:{filename:'hearth_fauna_contact_sheet.png',width:512,height:768,frame:9,columns:4,cellSize:128,order:FAUNA_NAMES}}); for(const entry of FAUNA_ART_MANIFEST) { assert.deepEqual(await fs.readFile(path.join(destination,'mods/hearth/models',entry.mesh)),faunaB3D(entry.id)); const image=faunaTexture(entry.id); assert.deepEqual(await fs.readFile(path.join(destination,'mods/hearth/textures',entry.texture)), png(64,(x,y)=>image.data.subarray((y*64+x)*4,(y*64+x+1)*4))); } const portraits=faunaPortraitSheet(); assert.deepEqual(await fs.readFile(path.join(destination,'mods/hearth/textures/hearth_fauna_contact_sheet.png')), png(portraits.width,(x,y)=>portraits.data.subarray((y*portraits.width+x)*4,(y*portraits.width+x+1)*4),portraits.height)); assert.deepEqual((await fs.readdir(path.join(destination,'mods/hearth/textures'))).sort(),expectedTextures.sort()); const expectedModels=[...FLORA_NAMES,...['hare','fox','deer','knife','axe','pick','spear','person']] .map(n=>`hearth_${n}.obj`).concat('hearth_person.b3d','hearth_person_compact.b3d','hearth_person_broad.b3d', ...FAUNA_ART_MANIFEST.map(entry=>entry.mesh)); assert.equal(expectedModels.length,63,'37 OBJ files plus three player builds plus 23 fauna rigs'); assert.deepEqual((await fs.readdir(path.join(destination,'mods/hearth/models'))).sort(),expectedModels.sort()); } await check(out); for(const name of ['game.conf','minetest.conf','settingtypes.txt','LICENSE.txt']) await fs.writeFile(path.join(source,name),'fixture\n'); await fs.writeFile(path.join(source,'release.json'),JSON.stringify({format:1,gameId:'last_hearth',version:'0.4.0-m3', engine:{name:'Luanti',version:'5.18.0'},node:'>=22',packageFormat:1,backupFormat:1})); await fs.mkdir(path.join(source,'tests/fixtures'),{recursive:true}); await fs.writeFile(path.join(source,'tests/fixtures/scenarios.json'),'{}'); const packaged=path.join(root,'package'); await packageGame(packaged,{source}); await check(packaged); await verifyPackage(packaged,{requireBundled:false}); const outside=path.join(root,'outside'); await fs.mkdir(outside); await fs.rm(path.join(out,'mods/hearth/models'),{recursive:true}); await fs.symlink(outside,path.join(out,'mods/hearth/models'),process.platform==='win32'?'junction':'dir'); await assert.rejects(generateAssets(out,source),/link|junction|unsafe/i); assert.deepEqual(await fs.readdir(outside),[]); }); test('read-only cube diversity and biome contracts cover thirteen exact species; review layout is documented',async()=> { const source=await fs.readFile(new URL('../mods/hearth/diversity.lua',import.meta.url),'utf8'); const species=source.match(/species_order=\{([^}]+)\}/)[1].match(/"[a-z_]+"/g).map(s=>s.slice(1,-1)); assert.deepEqual(species,SPECIES_NAMES); assert.match(source,/drawtype=woody and "normal" or "plantlike"/); assert.match(source,/tiles=woody and \{"hearth_"\.\.s\.\."_log_top.png","hearth_"\.\.s\.\."_log_top.png","hearth_"\.\.s\.\."_log_side.png"\}/); assert.ok(!source.includes('.obj') && !source.includes('_atlas'),'no runtime dependency on deprecated irregular tree meshes'); assert.match(source,/evergreen=s=="pine" or s=="spruce"/); assert.match(source,/plant~="moss" and plant~="lichen"/,'persistent groundcover has no dormant alias'); const biomes=await fs.readFile(new URL('../mods/hearth/biomes.lua',import.meta.url),'utf8'); const biomeSpecies=biomes.match(/species_order\s*=\s*\{([^}]+)\}/)[1].match(/"[a-z_]+"/g).map(s=>s.slice(1,-1)); assert.deepEqual(biomeSpecies,SPECIES_NAMES); const art=worldArt(); for(const s of species) for(const face of ['log_top','log_side','leaves','planks']) assert.ok(art.has(`hearth_${s}_${face}.png`)); const docs=await fs.readFile(new URL('../docs/art-direction.md',import.meta.url),'utf8'); const current=docs.slice(0,docs.indexOf('
')); assert.match(current,/512×864/); assert.match(current,/1–512/); assert.match(current,/16 columns/); assert.match(current,/ceil\(count\/16\)\*32/); assert.match(current,/421/); assert.match(current,/200/); assert.match(current,/botanicalLatin/); for(const s of SPECIES_NAMES) assert.ok(current.includes(s)); }); test('actual seasonal content uses exact generated stems, explicit cube tiles and separate patch/craftitem naming',async()=> { const text=await fs.readFile(new URL('../mods/hearth/content.lua',import.meta.url),'utf8'); const loop=text.slice(text.indexOf('for _, plant in ipairs({'),text.indexOf('-- Cosmetic migration')); assert.match(loop,/drawtype = woody and "normal" or "plantlike"/); assert.match(loop,/visual_scale = woody and 1 or scale/); assert.match(loop,/walkable = woody/); assert.match(loop,/buildable_to = not woody/); assert.match(loop,/tiles = woody and \{world_image\("log_top"\),world_image\("log_top"\),world_image\("log_side"\)\}/); assert.ok(!loop.includes('.obj'),'retained OBJ exports are not the runtime tree contract'); assert.match(text,/def\.tiles = def\.tiles or/,'registrar preserves explicit mesh tiles without override_item'); for(const name of ['meadow_grass','wildflowers','fern']) assert.ok(loop.includes(`{"${name}"`)); const seasons=await fs.readFile(new URL('../mods/hearth/seasons.lua',import.meta.url),'utf8'); const docs=await fs.readFile(new URL('../docs/ecosystems.md',import.meta.url),'utf8'); const art=worldArt(); for(const stem of ['meadow_grass_dormant','fern_dormant','wildflowers_dormant', 'spring_greens','spring_greens_dormant','autumn_mushrooms','autumn_mushrooms_dormant', 'item_spring_greens','item_autumn_mushrooms']) { assert.ok(art.has(`hearth_${stem}.png`)); assert.ok(docs.includes(`hearth_${stem}.png`)); } for(const kind of ['tree_seedling','young_tree','mature_tree','leaves']) for(const season of ['spring','autumn','winter']) { const mesh=kind==='leaves'?null:`${kind}${season==='winter' && kind!=='tree_seedling'?'_winter':''}`; if(mesh) assert.ok(FLORA_NAMES.includes(mesh)); if(kind==='leaves') assert.ok(art.has(`hearth_leaves_${season}.png`)); else assert.ok(TREE_SEASONS.includes(season)); } // Seasonal runtime cube-copy semantics are tested by the seasons owner; this // suite verifies available bytes, not a stale assertion requiring OBJ usage. for(const kind of ['spring_greens','autumn_mushrooms']) { assert.ok(text.includes(`{"${kind}",`)); assert.ok(seasons.includes(`variant("${kind}_patch",`)); assert.ok(text.includes(`api.register_craftitem("hearth:${kind}",`)); assert.ok(!art.has(`hearth_${kind}_patch.png`),'patch node name is not a texture stem'); } });