Back to Squad Table
SOURCE / PINNED RELEASE

Made of little things.

Squad Table

Release
d36201f8ed4f…
Author-recorded commit
7b9bb681d6ba…
License
LICENSE
Author’s source reference
nostr://npub1sj2vketepqw9v6fmge5fdzuj7atj9sv6qv2jss05n2z6rl64lcfqumdsnt/wss%3A%2F%2Fgit.napplet.soy%2F/n-c043b0c0f1d

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

tests/layout.mjs
// Layout capture: hotseat table at the required frame sizes. Saves PNGs to $SHOTS.
// Run: soyli build && SHOTS=$PWD/.napplet-space/shots soyli multiplayer tests/layout.mjs
const OUT = process.env.SHOTS;
async function sizeFrame(page, w, h) {
  await page.setViewportSize({ width: Math.max(w + 40, 600), height: Math.max(h + 300, 700) });
  await page.evaluate(([w, h]) => {
    const f = document.querySelector('iframe');
    f.style.cssText += `;width:${w}px !important;height:${h}px !important;min-height:0 !important;max-width:none !important;flex:none !important;`;
    f.scrollIntoView();
  }, [w, h]);
  await new Promise(r => setTimeout(r, 400));
}
async function shotFrame(p, name) {
  await p.page.locator('iframe').screenshot({ path: `${OUT}/${name}.png` });
}
export default async ({ players, check }) => {
  const p = players[0];
  const f = p.frame;
  await sizeFrame(p.page, 900, 600);
  await shotFrame(p, 'lobby-900');
  await f.getByRole('button', { name: 'Hotseat on this device', exact: true }).click();
  await f.locator('svg.board [data-hex]').first().waitFor();
  const dims = await f.evaluate(() => [innerWidth, innerHeight]);
  check(`frame 900x600 inner=${dims}`, dims[0] === 900);
  await shotFrame(p, 'table-900');
  // select K6 via data-hex stack
  await f.locator('svg.board [data-hex="K6"]').click();
  await new Promise(r => setTimeout(r, 300));
  await f.locator('#zoomIn').click(); await f.locator('#zoomIn').click(); await f.locator('#zoomIn').click();
  await f.locator('svg.board [data-hex="K6"]').scrollIntoViewIfNeeded().catch(()=>{});
  await shotFrame(p, 'table-900-zoom');
  await f.locator('#zoomFit').click();
  await f.locator('#rollDR').click();
  await f.locator('#losButton').click();
  await f.locator('svg.board [data-hex="N6"]').click();
  await new Promise(r => setTimeout(r, 300));
  await shotFrame(p, 'table-900-dice');
  for (const [w, h] of [[320, 560], [200, 160], [390, 844], [844, 390], [1400, 900]]) {
    await sizeFrame(p.page, w, h);
    await f.locator('#zoomFit').click({ timeout: 2000 }).catch(() => {});
    const d = await f.evaluate(() => [innerWidth, innerHeight]);
    check(`frame ${w}x${h} inner=${d}`, d[0] === w && d[1] === h);
    await shotFrame(p, `table-${w}x${h}`);
  }
  await sizeFrame(p.page, 320, 560);
  await f.locator('#panelButton').click();
  await f.getByRole('tab', { name: 'Actions' }).click();
  await shotFrame(p, 'actions-320');
  await f.getByRole('tab', { name: 'Counters' }).click();
  await shotFrame(p, 'tray-320');
  await f.getByRole('tab', { name: 'Stack' }).click();
  await shotFrame(p, 'stack-320');
  await f.locator('#panelButton').click();
  await f.locator('#modeCancel').click().catch(()=>{});
  await shotFrame(p, 'strip-320');
};