Back to Outbreak: Soycoon City
SOURCE / PINNED RELEASE

Made of little things.

Outbreak: Soycoon City

Release
db7750b1d009…
Author-recorded commit
3ee3f5fd27a6…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-b87c04571a9

Archive hash verified: dbebdc772b142a1c…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

scripts/pack-room-renders.mjs
// Convert reviewed local generation outputs into the game's small linked JPEGs.
// This does not generate images or contact a service. Pass an untracked JSON array
// of {name, path, prompt}; each name is room-id + '-' + camera-id.
import {readFile,writeFile,readdir,copyFile,mkdir,unlink} from 'node:fs/promises';
import {createHash} from 'node:crypto';
import {execFileSync} from 'node:child_process';
import {gameModule} from './game-module.mjs';
const input=process.argv[2];if(!input)throw new Error('Use: node scripts/pack-room-renders.mjs <reviewed-shot-index.json>');
const shots=JSON.parse(await readFile(input,'utf8'));
const {parseStage,signature}=await gameModule('../src/world.ts');
const level=parseStage(await readFile('levels/demo.json','utf8'));
const names=level.rooms.flatMap(r=>r.cameras.map(c=>`${r.id}-${c.id}`));
if(shots.length!==names.length||new Set(shots.map(s=>s.name)).size!==names.length||shots.some(s=>!names.includes(s.name)||!s.path||!s.prompt))throw new Error('Provide exactly one reviewed render and prompt for every demo camera.');
for(const s of shots){await readFile(s.path);await readFile(`.napplet-space/rerender/${s.name}-schema.png`);}
await mkdir('src/art',{recursive:true});await mkdir('docs/schema-shots',{recursive:true});
const hash=b=>createHash('sha256').update(b).digest('hex');const records=[];level.assets=[];
for(const r of level.rooms)for(const c of r.cameras){
  const name=`${r.id}-${c.id}`,shot=shots.find(s=>s.name===name),path=`src/art/${name}.jpg`;
  execFileSync('ffmpeg',['-v','error','-y','-i',shot.path,'-vf','scale=640:480:flags=lanczos','-frames:v','1','-q:v','3','-pix_fmt','yuvj420p','-map_metadata','-1',path]);
  const bytes=await readFile(path),asset={id:`scpd-${name}`,path,sha256:hash(bytes),mime:'image/jpeg',size:bytes.length};level.assets.push(asset);
  c.background={asset:asset.id,signature:signature(r,c)};delete c.backdrop;delete c.artSignature;
  const schema=`docs/schema-shots/${name}-schema.png`,prompt=`docs/schema-shots/${name}-prompt.txt`;
  await copyFile(`.napplet-space/rerender/${name}-schema.png`,schema);await writeFile(prompt,shot.prompt+'\n');
  records.push({room:r.id,camera:c.id,image:path,width:640,height:480,size:bytes.length,sha256:asset.sha256,schema,schemaSha256:hash(await readFile(schema)),prompt,sourceImageSha256:hash(await readFile(shot.path))});
}
await writeFile('levels/demo.json',JSON.stringify(level,null,2)+'\n');
await writeFile('docs/room-render-manifest.json',JSON.stringify({schema:'soyindent.room-renders',version:1,generationDate:'2026-10-06',generator:'Built-in image_gen; schema-guided individual renders',delivery:'640x480 JPEG, ffmpeg q=3, no high-resolution masters in the repository',shots:records},null,2)+'\n');
// Remove superseded renders only after every new view was converted and linked.
const keep=new Set(records.map(s=>s.image));
for(const dir of ['src/art','src/art-v2']){let files;try{files=await readdir(dir);}catch{continue;}for(const file of files){const path=`${dir}/${file}`;if(!keep.has(path)&&/\.(png|jpe?g|webp)$/i.test(file))await unlink(path);}}
console.log(`Packed ${records.length} reviewed room views, ${records.reduce((n,s)=>n+s.size,0).toLocaleString()} JPEG bytes; removed superseded backgrounds.`);