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.

tests/editor.mjs
import { createRequire } from 'node:module';
import { realpath, mkdir } from 'node:fs/promises';
import { execFileSync } from 'node:child_process';
import assert from 'node:assert/strict';
const url = process.env.SOYINDENT_PREVIEW_URL;
if (!url) throw new Error('Start soyli dev, then run SOYINDENT_PREVIEW_URL=<printed URL> pnpm test:editor');
const require = createRequire(await realpath(new URL('../node_modules/@napplet/conformance-cli/package.json', import.meta.url)));
const { chromium } = require('playwright');
const profile = JSON.parse(execFileSync('soyli', ['browser', 'path', '--json'], { encoding: 'utf8' }));
const executablePath = profile.executables.find(e => e.name === 'chromium-headless-shell' && e.installed)?.path;
const browser = await chromium.launch({ executablePath, headless: true, args: ['--enable-webgl', '--use-gl=angle', '--use-angle=swiftshader'] });
const page = await browser.newPage({ viewport: { width: 1400, height: 1100 } });
const errors = []; page.on('pageerror', e => errors.push(e.message));
await mkdir('.napplet-space/testing', { recursive: true });
try {
  await page.goto(url); const f = page.frameLocator('iframe'); await f.locator('#scene canvas').waitFor();
  await f.locator('#editorTab').click(); await f.locator('summary').filter({hasText:'Map JSON'}).click();
  const readMap = async () => { await f.locator('#showJson').click(); return JSON.parse(await f.locator('#mapJson').inputValue()); };
  const initial = await readMap(); assert.equal(initial.rooms.length,6);
  const apply = async stage => { await f.locator('#mapJson').fill(JSON.stringify(stage)); await f.locator('#applyJson').click(); };
  const at = async (canvas, point, map = false) => {
    await canvas.scrollIntoViewIfNeeded(); const b = await canvas.boundingBox();
    const d = await canvas.evaluate(c=>({...c.dataset})), s = Number(d.scale);
    return map ? {x:b.x+b.width/2+(point.x-Number(d.centerX))*s,y:b.y+b.height/2+(point.z-Number(d.centerZ))*s} : {x:b.x+Number(d.offsetX)+point.x*s,y:b.y+Number(d.offsetZ)+point.z*s};
  };
  const drag = async (canvas,a,b,map=false) => { const p=await at(canvas,a,map), q=await at(canvas,b,map); await page.mouse.move(p.x,p.y); await page.mouse.down(); await page.mouse.move(q.x,q.y,{steps:8}); await page.mouse.up(); };
  const click = async (canvas,p,map=false) => { const a=await at(canvas,p,map); await page.mouse.click(a.x,a.y); };
  await f.locator('[data-camera-preset=frog]').click(); assert.equal(await f.locator('#camY').inputValue(),'0.4'); assert.equal(await f.locator('#camFov').inputValue(),'65');
  const plan = f.locator('#plan'); await f.locator('[data-tool=camera]').click();
  await drag(plan,{x:-4.6,z:5.8},{x:-3.6,z:4.8});
  await drag(plan,{x:0,z:-.8},{x:.75,z:-1.3});
  let current=await readMap(); assert(Math.abs(current.rooms[0].cameras[0].position[0]+3.5)<.26); assert.equal(current.rooms[0].cameras[0].target[0],.75);
  assert.equal(current.rooms[0].doors[0].z,2.9);
  await f.locator('#cameraHeight').fill('0.6'); await f.locator('#cameraHeight').dispatchEvent('input'); assert.equal(await f.locator('#camY').inputValue(),'0.6');
  await f.locator('[data-tool=dress]').click(); await f.locator('#dressingKind').selectOption('papers'); await click(plan,{x:1.5,z:2.5});
  current=await readMap(); assert.equal(current.rooms[0].dressing.length,initial.rooms[0].dressing.length+1);
  await f.locator('#deleteSelection').click(); assert.equal((await readMap()).rooms[0].dressing.length,initial.rooms[0].dressing.length);
  console.log('PASS camera/target dragging, frog preset, live height and editable dressing');
  await apply(initial);const safetyIndex=initial.rooms[0].cameras.findIndex(c=>c.fallback);await f.locator('#cameraList button').nth(safetyIndex).click();
  const safetyHeight=initial.rooms[0].cameras[safetyIndex].position[1];assert.equal(Number(await f.locator('#cameraHeight').inputValue()),safetyHeight);
  await f.locator('#cameraName').fill('Recovery shot');await f.locator('#cameraName').dispatchEvent('change');assert.equal((await readMap()).rooms[0].cameras[safetyIndex].position[1],safetyHeight);
  console.log('PASS recovery height survives unrelated camera-field edits');
  await apply(initial); await f.locator('#precinctMapTab').click(); const mapCanvas=f.locator('#mapCanvas');
  await drag(mapCanvas,{x:0,z:0},{x:.5,z:.5},true); current=await readMap(); assert.equal(current.rooms[0].origin.x,.5); assert.equal(current.rooms[0].origin.z,.5);
  const wall = (r,side,offset) => ({x:r.origin.x+(side==='west'?-r.w/2:side==='east'?r.w/2:offset),z:r.origin.z+(side==='north'?-r.d/2:side==='south'?r.d/2:offset)});
  const evidence=current.rooms[0], lab=current.rooms.find(r=>r.id==='forensics');
  await f.locator('#mapConnect').click();
  await click(mapCanvas,wall(evidence,'south',1),true); await click(mapCanvas,wall(lab,'west',.75),true);
  await drag(mapCanvas,wall(evidence,'south',3.25),wall(lab,'west',2.5),true);
  current=await readMap(); assert.equal(current.rooms[0].doors.filter(d=>d.side==='south').length,3);
  const link=current.rooms[0].doors.find(d=>d.to==='forensics'&&d.x===1); assert(link);
  const partner=current.rooms.find(r=>r.id==='forensics').doors.find(d=>d.id===link.targetDoor); assert.equal(partner.targetDoor,link.id);
  await f.locator('#mapMove').click(); await drag(mapCanvas,wall(evidence,'south',1),wall(evidence,'south',.5),true);
  current=await readMap(); assert.equal(current.rooms[0].doors.find(d=>d.id===link.id).x,.5);
  await f.locator('#removeLink').click(); current=await readMap(); assert(!current.rooms[0].doors.some(d=>d.id===link.id)); assert(!current.rooms.find(r=>r.id==='forensics').doors.some(d=>d.id===partner.id));
  console.log('PASS graphical room drag, tap/drag connections, several doors on one wall, endpoint drag and paired deletion');
  await f.locator('#mapFit').click(); await page.locator('iframe').screenshot({path:'.napplet-space/testing/precinct-map.png'});
  await apply(initial);
  for(const [w,h] of [[320,560],[900,600],[1600,900]]) {
    await page.locator('iframe').evaluate((e,[w,h])=>Object.assign(e.style,{width:`${w}px`,height:`${h}px`,minHeight:'0',flex:'none'}),[w,h]);
    await f.locator('#precinctMapTab').click(); await f.locator('#mapFit').click();
    assert((await f.locator('body').evaluate(()=>document.body.scrollWidth))<=w);
    await f.locator('#roomPlanTab').click(); assert((await f.locator('body').evaluate(()=>document.body.scrollWidth))<=w);
    console.log(`PASS room-plan/map editor frame ${w}×${h}`);
  }
  const touch = await browser.newContext({viewport:{width:390,height:844},hasTouch:true,isMobile:true,deviceScaleFactor:1});
  try {
    const tp = await touch.newPage(); tp.on('pageerror',e=>errors.push(e.message)); await tp.goto(url);
    const tf = tp.frameLocator('iframe'); await tf.locator('#scene canvas').waitFor(); await tf.locator('#editorTab').tap();
    await tf.locator('#precinctMapTab').tap(); const tc=tf.locator('#mapCanvas'); await tf.locator('#mapFit').tap();
    const xy = async (point,canvas=tc,graph=true) => {
      await canvas.scrollIntoViewIfNeeded(); const b=await canvas.boundingBox(), d=await canvas.evaluate(c=>({...c.dataset})),s=Number(d.scale);
      return graph ? {x:b.x+b.width/2+(point.x-Number(d.centerX))*s,y:b.y+b.height/2+(point.z-Number(d.centerZ))*s} : {x:b.x+Number(d.offsetX)+point.x*s,y:b.y+Number(d.offsetZ)+point.z*s};
    };
    const cdp=await touch.newCDPSession(tp);
    const td = async (a,b,cancel=false,canvas=tc,graph=true) => {
      const p=await xy(a,canvas,graph),q=await xy(b,canvas,graph);
      await cdp.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[{...p,id:1}]});
      for(let i=1;i<=8;i++) await cdp.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{x:p.x+(q.x-p.x)*i/8,y:p.y+(q.y-p.y)*i/8,id:1}]});
      await cdp.send('Input.dispatchTouchEvent',{type:cancel?'touchCancel':'touchEnd',touchPoints:[]});
    };
    const tm = async () => {
      const details=tf.locator('details').filter({has:tf.locator('#mapJson')});
      if(!(await details.getAttribute('open')) && !(await details.evaluate(d=>d.open))) await details.locator('summary').tap();
      await tf.locator('#showJson').tap(); return JSON.parse(await tf.locator('#mapJson').inputValue());
    };
    await td({x:0,z:0},{x:1,z:.5}); let m=await tm(); assert.deepEqual(m.rooms[0].origin,{x:1,z:.5});
    await td({x:1,z:.5},{x:2,z:1.5},true); m=await tm(); assert.deepEqual(m.rooms[0].origin,{x:1,z:.5});
    await tf.locator('#mapConnect').tap();
    const a=await xy(wall(m.rooms[0],'south',1)); await tp.touchscreen.tap(a.x,a.y);
    const b=await xy(wall(m.rooms.find(r=>r.id==='forensics'),'west',.75)); await tp.touchscreen.tap(b.x,b.y);
    m=await tm(); assert.equal(m.rooms[0].doors.length,3); assert.equal(m.rooms.find(r=>r.id==='forensics').doors.length,2);
    await tf.locator('#roomPlanTab').tap(); await tf.locator('[data-camera-preset=frog]').tap();
    await tf.locator('[data-tool=camera]').tap(); await td({x:-4.6,z:5.8},{x:-3.6,z:4.8},false,tf.locator('#plan'),false);
    m=await tm(); assert(Math.abs(m.rooms[0].cameras[0].position[0]+3.5)<.26);
    console.log('PASS native touch room drag, cancellation, paired wall taps and frog-camera placement');
  } finally {await touch.close();}
  assert.deepEqual(errors,[]);
} catch (error) {
  await page.screenshot({path:'.napplet-space/testing/editor-failure.png'});
  console.log(await page.frameLocator('iframe').locator('#deleteSelection').evaluate(e=>{const result=[];while(e){result.push({id:e.id,class:e.className,hidden:e.hidden,display:getComputedStyle(e).display,rect:e.getBoundingClientRect().toJSON()});e=e.parentElement;}return result;}));
  throw error;
} finally {await browser.close();}