Two blind reviews of everything since the last pass (afa0e17), every
finding checked against the code, no behavior changed: all thirty
scene goldens match without a re-bless and the engine suite is
untouched.
Reel and renderer: the camera's look-down rule is stated once, beside
LOOK_DOWN, instead of twice in the effect; the empty aim branch that
stood where a cutaway used to be is gone (the guard it implied is now
explicit); the pit events and the punch are handled by their own
types, not through "in" casts; smoothstep is one export used by every
tween instead of eleven inline copies; the two floor rings share one
painter; project() takes a Billboard instead of a third hand-typed
copy of its fields; the strides-left figure and the web rim no longer
shadow the reel's steps and the pane's fx; the die card's verdict is
built from events, not by matching an emoji; the workshop asks for the
hover cue by name instead of passing an empty click handler.
Server and engine: one requestBase() for the origin, one slug pattern
in store.ts gating both the clip page and its files, one 404 for both;
LOOPBACK sits above its only caller; doCounteract names what a counter
is played against once; fearCells sits beside its own docblock rather
than between sightedCellsFor and its.
Deploy: chromiumExe, the private server, ffmpeg, and the reel rewind
live in deploy/lib/harness.mjs, shared by the gate, the recorder, and
the card cutter instead of pasted three times; the recorder drops its
duplicate frame counters and names its poster settle; the card uses the
gallery's exact gold; the one-time Sentry URL bootstrap leaves
deploy.sh; the backup comment states the rule rather than the incident.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
57 lines
2.6 KiB
JavaScript
57 lines
2.6 KiB
JavaScript
// Shared by the scene gate, the clip recorder, and the card cutter: the
|
|
// Playwright chromium they all drive, a private game server on a
|
|
// throwaway data dir, ffmpeg with the flags they all want, and the
|
|
// reel's rewind. Run from the repo root after `npx vite build` in
|
|
// packages/web.
|
|
import { execFileSync, spawn } from "node:child_process";
|
|
import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
/** The Playwright chromium executable: $WIZWAR_CHROME, else the newest
|
|
* build in the ms-playwright cache. */
|
|
export function chromiumExe() {
|
|
if (process.env.WIZWAR_CHROME) return process.env.WIZWAR_CHROME;
|
|
const cache = join(process.env.HOME ?? "", "Library", "Caches", "ms-playwright");
|
|
const builds = existsSync(cache)
|
|
? readdirSync(cache).filter((d) => /^chromium-\d+$/.test(d)).sort()
|
|
: [];
|
|
for (const build of builds.reverse()) {
|
|
const exe = join(cache, build, "chrome-mac-arm64",
|
|
"Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing");
|
|
if (existsSync(exe)) return exe;
|
|
}
|
|
throw new Error("no Playwright chromium found — run: npx playwright-core install chromium (or set WIZWAR_CHROME)");
|
|
}
|
|
|
|
/** A game server on `port` with its own empty data dir. `up()` resolves
|
|
* once it answers; `stop()` kills it and removes the dir. */
|
|
export function startServer(port, label = "wizwar-harness") {
|
|
const dataDir = mkdtempSync(join(tmpdir(), `${label}-`));
|
|
const server = spawn("npx", ["tsx", "src/index.ts"], {
|
|
cwd: join(import.meta.dirname, "..", "..", "packages", "server"),
|
|
env: { ...process.env, PORT: String(port), WIZWAR_DATA_DIR: join(dataDir, "rooms") },
|
|
stdio: "ignore",
|
|
});
|
|
const up = async () => {
|
|
for (let i = 0; i < 60; i++) {
|
|
try { if ((await fetch(`http://127.0.0.1:${port}/`)).ok) return; } catch { /* not yet */ }
|
|
await new Promise((r) => setTimeout(r, 500));
|
|
}
|
|
throw new Error("server never came up");
|
|
};
|
|
const stop = () => { server.kill(); rmSync(dataDir, { recursive: true, force: true }); };
|
|
return { up, stop };
|
|
}
|
|
|
|
/** ffmpeg, quiet and overwriting. */
|
|
export const ff = (...args) => execFileSync("ffmpeg", ["-loglevel", "error", "-y", ...args]);
|
|
|
|
/** Pause the reel and step it back to its first move. */
|
|
export async function rewindReel(page) {
|
|
await page.locator("button", { hasText: "❚❚" }).click();
|
|
const count = Number((await page.locator(".replay-count").textContent()).match(/of (\d+)/)[1]);
|
|
for (let i = 0; i < count; i++) await page.locator("button", { hasText: "◀" }).click();
|
|
return count;
|
|
}
|