orbit/dev/marquee-shot.mjs

185 lines
8.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Dev-only CDP screenshot harness for the LEVEL-UP MARQUEE — drives the
* real game (index.html?combat) and plays the marquee's beats through the
* production component (GameScene.levelMarquee, js/ui/LevelUpMarquee.js),
* capturing one labeled frame per beat phase so the full sequence is on
* record.
*
* node dev/marquee-shot.mjs [outDir]
*
* Requires the dev server (http://127.0.0.1:8080) and a headless
* Chromium-CDP browser on 127.0.0.1:9333 (same as dev/cdp-shot.mjs):
* /opt/brave.com/brave/brave --headless --disable-gpu \
* --remote-debugging-port=9333 about:blank
*
* Outputs (committed as evidence under the given outDir, default
* dev/shots/) — the beat plays 7 → 9 (+2 LEVELS), then 78 → 80 (the
* FIRMWARE-MAXED branch):
* marquee-old.png — the old level booted in (decode line live)
* marquee-shatter.png — corruption: slices + the shatter into shards
* marquee-scramble.png — the new level's digit slots cycling/locking
* marquee-lock.png — the lock-in: flash, shockwave, sparks
* marquee-sync.png — the hold: SYNC COMPLETE ▸ +2 LEVELS + armor
* marquee-peel.png — the neon shutter peeling the frame away
* marquee-max.png — the cap: FIRMWARE MAXED ▸ LEVEL 80
*/
const fs = await import('node:fs');
const CDP = process.env.CDP_PORT ? `127.0.0.1:${process.env.CDP_PORT}` : '127.0.0.1:9333';
const BASE = process.env.BASE ?? 'http://127.0.0.1:8080';
const OUT = process.argv[2] ?? 'dev/shots';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function main() {
fs.mkdirSync(OUT, { recursive: true });
async function openPage(url) {
let target;
try {
target = await (await fetch(`http://${CDP}/json/new?${encodeURIComponent(url)}`, { method: 'PUT' })).json();
} catch {
target = await (await fetch(`http://${CDP}/json/new?${encodeURIComponent(url)}`, { method: 'POST' })).json();
}
const ws = new WebSocket(target.webSocketDebuggerUrl);
let id = 0;
const pending = new Map();
const send = (method, params = {}) => new Promise((res, rej) => {
const mid = ++id;
pending.set(mid, { res, rej });
ws.send(JSON.stringify({ id: mid, method, params }));
});
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = () => rej(new Error('ws error')); });
ws.onmessage = (m) => {
const x = JSON.parse(m.data);
if (x.id && pending.has(x.id)) {
const { res, rej } = pending.get(x.id);
pending.delete(x.id);
x.error ? rej(new Error(x.error.message)) : res(x.result);
}
};
await send('Runtime.enable');
await send('Page.enable');
await send('Network.enable');
await send('Network.setCacheDisabled', { cacheDisabled: true });
await send('Page.navigate', { url });
const ev = async (expr) => {
const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true });
if (r.exceptionDetails) throw new Error('page: ' + JSON.stringify(r.exceptionDetails.exception?.description ?? r.exceptionDetails.text));
return r.result?.value;
};
const waitTrue = async (expr, timeoutMs = 45000) => {
const end = Date.now() + timeoutMs;
while (Date.now() < end) {
const v = await ev(expr).catch(() => null);
if (v) return v;
await sleep(250);
}
throw new Error('timed out waiting for: ' + expr.slice(0, 60));
};
const shot = async (path, delayMs = 90) => {
await sleep(delayMs); // let the last paint land
const r = await send('Page.captureScreenshot', { format: 'png' });
fs.writeFileSync(path, Buffer.from(r.data, 'base64'));
console.log(` shot: ${path}`);
};
const close = async () => {
await fetch(`http://${CDP}/json/close/${target.id}`).catch(() => {});
ws.close();
};
return { ev, waitTrue, shot, close, target };
}
const p = await openPage(`${BASE}/index.html?combat`);
try {
await p.waitTrue('Boolean(window.game && window.game.scene.getScene("MenuScene") && window.game.scene.getScene("MenuScene").seedValue)');
await p.ev('window.game.scene.getScene("MenuScene").startNewGame()');
await p.waitTrue('Boolean(window.__ORBIT_COMBAT__)');
// The new-run tutorial comms own the screen (ack pause) — clear the
// chain through the hub's own finish path so the world behind the
// marquee is the live game, not a comm panel.
await p.ev(`(() => {
const g = window.game.scene.getScene('GameScene');
const h = g.comms;
if (!h) return true;
if (h.current) { try { h._finish(); } catch (e) {} }
if (Array.isArray(h.queue)) h.queue.length = 0;
h._setPaused(false);
try { h.box?.close(); } catch (e) {}
h.box = null;
return true;
})()`);
// The camera follows the ship; wait until it is centered on it.
await p.waitTrue(`(() => {
const g = window.game.scene.getScene('GameScene');
const cam = g.cameras.main;
const w = g.scale.width, h = g.scale.height;
const cx = (cam.worldView?.x ?? cam.scrollX ?? cam.x) + w / 2;
const cy = (cam.worldView?.y ?? cam.scrollY ?? cam.y) + h / 2;
return Math.abs(cx - g.ship.x) < 24 && Math.abs(cy - g.ship.y) < 24;
})()`, 8000).catch(() => null);
await p.ev('document.fonts.ready.then(() => true)');
await sleep(400); // the pack settles; the sim ticks
// Play the beat through the production component (the exact path a
// real kill's XP grant drives — the ledger's event → this.play()),
// then capture each phase with debugSeek() so the shot lands on the
// exact beat moment (no wall-clock racing; the scene keeps stepping).
// Timeline (two-digit level): old 180–950, corrupt 950, scramble 1250,
// locks 1580/1720, lock-in 1810, peel 2630, end 2960.
const seek = async (ms) => {
await p.ev(`(() => {
const g = window.game.scene.getScene('GameScene');
if (g.levelMarquee.active) g.levelMarquee.debugSeek(${ms});
return g.levelMarquee.active;
})()`);
await sleep(150); // the latch is synchronous — let the paint land
};
const release = async () => {
await p.ev(`(() => {
const g = window.game.scene.getScene('GameScene');
if (g.levelMarquee.active) g.levelMarquee.debugRelease();
return true;
})()`);
};
await p.ev(`(() => {
const g = window.game.scene.getScene('GameScene');
g.levelMarquee.play({ from: 7, to: 9, levelsGained: 2 });
return g.levelMarquee.active;
})()`);
await seek(700); // old level booted in, the decode line live
await p.shot(`${OUT}/marquee-old.png`);
await seek(1020); // the corruption burst + shatter (shards mid-fall)
await p.shot(`${OUT}/marquee-shatter.png`);
await seek(1560); // the digit slots cycling, the first lock landing
await p.shot(`${OUT}/marquee-scramble.png`);
await seek(1820); // the lock-in: flash + shockwave + sparks
await p.shot(`${OUT}/marquee-lock.png`);
await seek(2250); // the hold: SYNC COMPLETE ▸ +2 LEVELS + the armor line
await p.shot(`${OUT}/marquee-sync.png`);
await seek(2720); // the neon shutter peeling the frame away
await p.shot(`${OUT}/marquee-peel.png`);
await release();
await p.waitTrue('!window.game.scene.getScene("GameScene").levelMarquee.active', 8000);
// The cap branch: FIRMWARE MAXED ▸ LEVEL 80.
await p.ev(`(() => {
const g = window.game.scene.getScene('GameScene');
g.levelMarquee.play({ from: 78, to: 80, levelsGained: 2 });
return g.levelMarquee.active;
})()`);
await seek(2200); // the lock-in + the MAXED line up
await p.shot(`${OUT}/marquee-max.png`);
await release();
// Let the beat finish (no beat left running when we close).
await p.waitTrue('!window.game.scene.getScene("GameScene").levelMarquee.active', 8000);
} finally {
await p.close();
}
console.log('marquee screenshots: done');
}
main().catch((err) => { console.error('marquee-shot failed:', err.message); process.exit(1); });