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-thumbs.mjs
/**
 * The scene gallery's pictures: each scene runs in the built napplet
 * (dist/index.html, through scripts/harness.mjs) for as long as its header
 * says (`-- thumb: <seconds>`, default 6), optionally after one last line of
 * Lua (`-- snap: <lua>`, e.g. a lightning bolt), and its simulation is saved
 * half size, without the game's information in the corners, as
 * src/scenes/thumbs/<id>.webp. Build first; the pictures go into
 * the next build.
 *
 *   node scripts/scene-thumbs.mjs [scene ids...]
 */
import { readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { fileURLToPath } from 'node:url';

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

const SCENES = fileURLToPath(new URL('../src/scenes/', import.meta.url));
const THUMBS = `${SCENES}thumbs/`;
mkdirSync(THUMBS, { recursive: true });

const wanted = new Set(process.argv.slice(2));
const files = readdirSync(SCENES).filter((name) => name.endsWith('.lua')).sort();
const harness = await startHarness({ viewport: { width: 1280, height: 800 } });
try {
  await harness.waitForGame();
  await harness.frame().waitForFunction(() => window.powderUi, null, { timeout: 10000 });
  for (const file of files) {
    const id = file.replace(/^\d+-/, '').replace(/\.lua$/, '');
    if (wanted.size && !wanted.has(id)) continue;
    const source = readFileSync(`${SCENES}${file}`, 'utf8');
    const header = (key) => new RegExp(`^--\\s*${key}:\\s*(.*)$`, 'm').exec(source)?.[1]?.trim();
    const scene = await harness.frame().evaluate((sceneId) => window.powderUi.scenes.find((s) => s.id === sceneId), id);
    if (!scene) throw new Error(`${id} is not in the built page; npm run build first`);
    await harness.frame().evaluate((sceneId) => window.powderUi.loadScene(window.powderUi.scenes.find((s) => s.id === sceneId)), id);
    await harness.tab.waitForTimeout(Number(header('thumb') ?? 6) * 1000);
    // Without the game's information in the corners.
    await harness.frame().evaluate(() => window.powderUi.lua('ren.hud(false)'));
    const snap = header('snap');
    if (snap) await harness.frame().evaluate((code) => window.powderUi.lua(code), snap);
    await harness.tab.waitForTimeout(snap ? 34 : 50);
    const png = await harness.frame().locator('#viewport').screenshot();
    await harness.frame().evaluate(() => window.powderUi.lua('ren.hud(true)'));
    const webp = await harness.tab.evaluate(async (base64) => {
      const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
      const bitmap = await createImageBitmap(new Blob([bytes], { type: 'image/png' }));
      const canvas = new OffscreenCanvas(306, 192);
      const context = canvas.getContext('2d');
      context.imageSmoothingQuality = 'high';
      context.drawImage(bitmap, 0, 0, 306, 192);
      const blob = await canvas.convertToBlob({ type: 'image/webp', quality: 0.8 });
      const out = new Uint8Array(await blob.arrayBuffer());
      let text = '';
      for (const byte of out) text += String.fromCharCode(byte);
      return btoa(text);
    }, png.toString('base64'));
    const bytes = Buffer.from(webp, 'base64');
    writeFileSync(`${THUMBS}${id}.webp`, bytes);
    console.log(`${id}: ${bytes.length} bytes`);
  }
  if (harness.errors.length) console.log(`page errors: ${harness.errors.join(' | ')}`);
} finally {
  await harness.close();
}