// GODSEED — headless screenshot harness (adapted from glassblower tools/shot.mjs). // Town Mode suite: the DEFAULT load is now a CLOSE TOWN VIEW (SimCity feel), the // full planet is the dramatic "behold the world" reveal. Serves web/ itself on a // free port (8200-8299), drives the mock world through deterministic seeks, and // saves shots to tools/shots/. // // node tools/shot.mjs # full suite // node tools/shot.mjs --base http://localhost:8234 # reuse a running server // // Playwright is resolved from this repo if installed, else from the glassblower // checkout next door (its node_modules + ~/Library/Caches/ms-playwright browsers). import { mkdirSync } from "node:fs"; import { createServer } from "node:net"; import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; import path from "node:path"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const WEB = path.join(HERE, "..", "web"); const OUT = path.join(HERE, "shots"); mkdirSync(OUT, { recursive: true }); async function loadPlaywright() { try { return await import("playwright"); } catch { const fallback = path.resolve(HERE, "../../glassblower/node_modules/playwright/index.mjs"); return import(`file://${fallback}`); } } function freePort(start = 8200, end = 8299) { return new Promise((resolve, reject) => { const tryPort = (p) => { if (p > end) return reject(new Error("no free port in 8200-8299")); const srv = createServer(); srv.once("error", () => tryPort(p + 1)); srv.once("listening", () => srv.close(() => resolve(p))); srv.listen(p, "127.0.0.1"); }; tryPort(start); }); } const argBase = process.argv.indexOf("--base"); let BASE = argBase > -1 ? process.argv[argBase + 1] : null; let server = null; if (!BASE) { const port = await freePort(); server = spawn("python3", ["-m", "http.server", String(port), "--directory", WEB], { stdio: "ignore", }); BASE = `http://127.0.0.1:${port}`; await new Promise((r) => setTimeout(r, 900)); // let it bind } const cleanup = () => { if (server) { server.kill(); server = null; } }; process.on("exit", cleanup); process.on("SIGINT", () => { cleanup(); process.exit(130); }); const { chromium } = await loadPlaywright(); const browser = await chromium.launch({ headless: true, args: [ "--enable-unsafe-swiftshader", "--use-gl=angle", "--use-angle=swiftshader", "--ignore-gpu-blocklist", "--enable-webgl", ], }); const page = await browser.newPage({ viewport: { width: 1512, height: 945 }, deviceScaleFactor: 2 }); const errors = []; page.on("console", (m) => { if (m.type() === "error") errors.push("console: " + m.text()); }); page.on("pageerror", (e) => errors.push("pageerror: " + e.message)); async function open(url) { console.log("→", url); await page.goto(`${BASE}/${url}`, { waitUntil: "networkidle", timeout: 45000 }); await page.waitForFunction(() => window.__godseed && window.__godseed.ready, { timeout: 30000 }); await page.evaluate(() => window.__godseed.pause(true)); await page.evaluate(async () => { if (document.fonts?.ready) await document.fonts.ready; }); } async function shot(name) { await page.screenshot({ path: path.join(OUT, name) }); const s = await page.evaluate(() => { const tc = window.__godseed.townCenter?.(); return { ...window.__godseed.stats(), mode: window.__godseed.rig?.mode, tc: tc ? `${tc.lat.toFixed(1)},${tc.lon.toFixed(1)}` : "?" }; }); console.log(` šŸ“ø ${name} mode=${s.mode} dist=${s.camDist} tc=${s.tc} sky=${s.sky}`); } // ------------------------------------------------------------------ suite // 1 Ā· DEFAULT load — the close TOWN view (SimCity feel: a town you stand over, // dense with houses, civic buildings, roads — NOT a tiny planet). await open("index.html?mock=1&static=1"); await page.evaluate(() => { window.__godseed.tick(6); }); // settle camera + grow-ins await shot("town-01-default.png"); // 2 Ā· a dense DISTRICT close-up — lean the gaze right into the south block so the // gabled houses + footpaths + civic crown read large at the close zoom. await page.evaluate(() => { window.__godseed.gazeLatLon(11, 36, 30); // the w_000003 quarter window.__godseed.tick(5); }); await shot("town-02-district-close.png"); // 3 Ā· roads + civic mix at the square — frame the bank/market/tower heart. await page.evaluate(() => { window.__godseed.gazeLatLon(14, 38, 30); window.__godseed.tick(5); }); await shot("town-03-square.png"); // 4 Ā· BEHOLD THE WORLD — pull back to the full planet; the town is one warm // patch ringed by the orbiting inscription on a whole shared world. await page.evaluate(() => { window.__godseed.beholdWorld(); window.__godseed.tick(9); }); await shot("town-04-behold-world.png"); // 5 Ā· return to the town (the toggle flips back to the close view). await page.evaluate(() => { window.__godseed.beholdWorld(); window.__godseed.tick(8); }); await shot("town-05-return.png"); // 6 Ā· a GRANTING moment, framed WITHIN the town — run the looping town-growth // wish (a new quarter east of the square) and freeze mid-grant with the // thought ticker live. await open("index.html?mock=1"); await page.evaluate(() => { window.__godseed.tick(2); // let the loop arm window.__godseed.runFeedOnce(); window.__godseed.tick(15); // reading → district + market + cafe deltas }); await shot("town-06-granting.png"); // 7 Ā· the same wish a beat later — road + carts land, the new quarter is joined. await page.evaluate(() => { window.__godseed.tick(9); }); await shot("town-07-granted.png"); // 8 Ā· the DESCENT within the town — swoop down over the just-granted quarter. await page.evaluate(() => { window.__godseed.descend(15, 43, 13); // dive over the new market quarter window.__godseed.tick(5.4); // mid-glide, just above the surface }); await shot("town-08-descent.png"); // 9 Ā· genesis-only world — fallback townCenter = the monolith, framed up close. await open("index.html?mock=1&upto=0&static=1"); await page.evaluate(() => { window.__godseed.tick(6); }); await shot("town-09-genesis.png"); // 10 Ā· Genesis Log — replay the town-build wish (w_000002): the district + bank + // market + tower + road rising, in the close town view. await open("book.html?mock=1"); await page.evaluate(() => { window.__godseed.replay("w_000002"); window.__godseed.tick(20); // through the district + civic deltas }); await shot("town-10-book-replay.png"); // ------------------------------------------------------------------ wrap if (errors.length) { console.log("\nāš ļø page errors:"); for (const e of errors.slice(0, 25)) console.log(" " + e); process.exitCode = 1; } else { console.log("\nāœ“ no page errors"); } await browser.close(); cleanup(); console.log("done →", OUT);