Back to Powder Tool V600Billion
SOURCE / PINNED RELEASE

Made of little things.

Powder Tool V600Billion

Release
142767edcab8…
Author-recorded commit
6d92971effd0…
License
LICENSE
Author’s source reference
nostr://npub1fllw8kw0thjj55wds0uugcnp5kej2nfxd36eruq39d56wwz8r44q5q78wj/wss%3A%2F%2Fgit.napplet.soy%2F/powder-toy

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

source/scripts/scene-lab.mjs
/**
 * Try out a scene while writing it: run a Lua scene file in the built napplet
 * (dist/index.html, through scripts/harness.mjs) and take pictures of it.
 *
 *   node scripts/scene-lab.mjs src/scenes/01-reactor.lua [--at 1,3,6] [--probe "<lua returning text>"]
 *
 * The file is read fresh each run, so the page need not be rebuilt for it.
 * Pictures land in qa/lab-<name>-<seconds>s.png; after each one the particle
 * count and the probe's answer are printed.
 */
import { readFileSync, mkdirSync } from 'node:fs';
import { basename } from 'node:path';
import { parseArgs } from 'node:util';

import { QA_DIR, startHarness } from './harness.mjs';

const { values, positionals } = parseArgs({
  allowPositionals: true,
  options: { at: { type: 'string', default: '1,3,6' }, probe: { type: 'string' }, width: { type: 'string', default: '1280' } },
});
if (!positionals.length) throw new Error('usage: node scripts/scene-lab.mjs <scene.lua> [--at 1,3,6] [--probe "<lua>"]');
mkdirSync(QA_DIR, { recursive: true });

const harness = await startHarness({ viewport: { width: Number(values.width), height: Math.round(Number(values.width) * 0.62) } });
try {
  await harness.waitForGame();
  await harness.frame().waitForFunction(() => window.powderUi, null, { timeout: 10000 });
  for (const file of positionals) {
    const source = readFileSync(file, 'utf8');
    const name = basename(file, '.lua').replace(/^\d+-/, '');
    const view = Number(/^--\s*view:\s*(\d+)/m.exec(source)?.[1] ?? 4);
    const started = Date.now();
    const error = await harness.frame().evaluate(({ source, view }) => {
      try {
        window.powderUi.loadScene({ id: 'lab', title: 'Lab', icon: '', hint: '', view, source });
        return '';
      } catch (e) { return String(e?.message ?? e); }
    }, { source, view });
    if (error) console.log(`${name}: ${error}`);
    for (const seconds of values.at.split(',').map(Number)) {
      const wait = started + seconds * 1000 - Date.now();
      if (wait > 0) await harness.tab.waitForTimeout(wait);
      const path = `${QA_DIR}lab-${name}-${seconds}s.png`;
      await harness.frame().locator('#viewport').screenshot({ path });
      const facts = await harness.frame().evaluate((probe) => {
        const lua = window.powderUi.lua;
        const parts = lua('return sim.partCount()');
        let answer = '';
        if (probe) { try { answer = lua(probe); } catch (e) { answer = `probe failed: ${e.message}`; } }
        return { parts, answer };
      }, values.probe ?? '');
      console.log(`${name} ${seconds}s: ${facts.parts} particles${facts.answer ? `, ${facts.answer}` : ''} -> ${path}`);
    }
  }
  const toasts = await harness.frame().evaluate(() => document.getElementById('toasts').textContent);
  if (toasts) console.log(`notices: ${toasts}`);
  if (harness.errors.length) console.log(`page errors: ${harness.errors.join(' | ')}`);
} finally {
  await harness.close();
}