orbit/dev/research-cue-shot.mjs

226 lines
10 KiB
JavaScript

/**
* Dev-only: boot GameScene and VERIFY the READY-RESEARCH attention cue
* end-to-end in a real browser (the node tests pin the model + the deck;
* this drives the live scene + the console):
*
* 1. deck (window closed) — the RESEARCH slot's cue is armed (a tech
* can start), and the cycle paints: face band → edge comet → rest;
* 2. console (window open) — the deck's slot rests, the ready category
* tabs wear the edge shimmer, and the system's "Unlock Jumpgates"
* tech wears the ACK pulse (forced chart-complete): on its plate
* while the SYSTEM tree is on screen, on the SYSTEM tab otherwise;
* 3. in-flight — starting a project rests BOTH halves (maxConcurrent 1).
*
* The report (window.__CUE_SHOT + the #report <pre>) lists every check;
* the two screenshots pin the two faces of the cue (each capture freezes
* its cycle mid-window so the live clock paints the same frame):
*
* node dev/server.mjs 8091
* /…/chrome --headless --disable-gpu --remote-debugging-port=9333 about:blank
* node dev/cdp-shot.mjs http://127.0.0.1:8091/dev/research-cue-shot.html \
* /tmp/cue-deck.png "window.__CUE_SHOT && window.__CUE_SHOT.ready" 90000 \
* "window.__CUE_SHOT.captureDeck()"
* node dev/cdp-shot.mjs http://127.0.0.1:8091/dev/research-cue-shot.html \
* /tmp/cue-console.png "window.__CUE_SHOT && window.__CUE_SHOT.ready" 90000 \
* "window.__CUE_SHOT.captureConsole()"
*/
import Phaser from '../js/vendor/phaser.js';
import { config } from '../js/config/Config.js';
import { ConfigLoader } from '../js/config/ConfigLoader.js';
import { createGameConfig } from '../js/config/GameConfig.js';
import { GameScene } from '../js/scenes/GameScene.js';
import { gatesReady } from '../js/research/SystemCategory.js';
const data = await ConfigLoader.load();
config.init(data);
const errors = [];
const origErr = console.error.bind(console);
console.error = (...a) => { errors.push(a.map(String).join(' ')); origErr(...a); };
window.addEventListener('error', (e) => errors.push(String(e.message)));
window.addEventListener('unhandledrejection', (e) => errors.push(`rejection: ${e.reason}`));
const gameConfig = createGameConfig();
gameConfig.scene = [GameScene];
const game = new Phaser.Game(gameConfig);
window.game = game;
const report = document.createElement('pre');
report.id = 'report';
report.style.cssText = 'position:fixed;left:10px;top:10px;z-index:9999;max-width:75%;margin:0;padding:8px 12px;font:13px/1.5 monospace;color:#eaf6ff;background:rgba(6,20,16,0.92);border:1px solid #1b3a5a;white-space:pre-wrap;';
document.body.appendChild(report);
const setReport = (lines) => { report.textContent = (Array.isArray(lines) ? lines : [lines]).join('\n'); };
setReport('booting…');
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function waitScene() {
for (let i = 0; i < 100; i++) {
const s = game.scene.getScene('GameScene');
if (s && s.ship && s.researchWindow) return s;
await sleep(100);
}
throw new Error('GameScene never booted');
}
// Recorder over a Phaser Graphics (counts the cue's own call surface).
const recorder = (g) => {
const r = { fillRect: 0, strokePoints: 0, fillPoints: 0, clear: 0 };
const orig = {
fillRect: g.fillRect.bind(g),
strokePoints: g.strokePoints.bind(g),
fillPoints: g.fillPoints.bind(g),
clear: g.clear.bind(g),
};
g.fillRect = (...a) => { r.fillRect++; return orig.fillRect(...a); };
g.strokePoints = (...a) => { r.strokePoints++; return orig.strokePoints(...a); };
g.fillPoints = (...a) => { r.fillPoints++; return orig.fillPoints(...a); };
g.clear = (...a) => { r.clear++; return orig.clear(...a); };
r.reset = () => { r.fillRect = 0; r.strokePoints = 0; r.fillPoints = 0; r.clear = 0; };
return r;
};
try {
const s = await waitScene();
await sleep(900); // let the boot settle
const lines = [];
const check = (label, cond) => lines.push(`${cond ? '✔' : '✘ FAIL'} ${label}`);
const bar = s.actionBar;
const win = s.researchWindow;
const st = s.researchState;
const slot = bar.slots.find((x) => x.id === 'research');
// ---- 1. the deck's RESEARCH slot (the window is closed) --------------
s._applyResearchCue(); // the scene's per-frame beat
check('deck: cue armed on the RESEARCH slot (fresh run — a tech can start)', bar.shimmerId === 'research');
const r = recorder(bar.shimmerG);
const cfg = bar.cfgShimmer;
const off = slot.phase * 350; // the bar's per-slot stagger
const P = cfg.periodMs;
const at = (u) => (((u % P) + P) % P);
const step = (t) => { r.reset(); bar.update(t, 16); return { ...r }; };
let a = step(at(cfg.faceMs / 2 - off));
check(`deck: face window — the band paints (fillRects ${a.fillRect} ≥ 3)`, a.fillRect >= 3 && a.strokePoints === 0);
a = step(at(cfg.faceMs + cfg.gapMs + cfg.edgeMs / 2 - off));
check(`deck: edge window — the comet runs (stroke passes ${a.strokePoints} ≥ 4)`, a.strokePoints >= 4 && a.fillRect === 0);
a = step(at(P - 1 - off));
check(`deck: rest — the layer idles (cleared=${a.clear})`, a.fillRect === 0 && a.strokePoints === 0 && a.clear >= 1);
// ---- 2. the console takes the cue over --------------------------------
s.deckAction('research');
for (let i = 0; i < 50 && win.openState !== 'open'; i++) await sleep(60);
await sleep(800); // the boot reveal finishes
s._applyResearchCue();
check('deck: the slot rests while the console is open', bar.shimmerId === null);
// Force the SYSTEM chart complete (the scene's isNavComplete hook) so
// the "Unlock Jumpgates" tech is ready to be researched.
s.systemTree.available = () => true;
s.systemTree.lockNote = () => null; // the readout agrees (AVAILABLE, not LOCKED)
win.refresh();
const gid = gatesReady(s.systemTree, st);
check('console: the gates tech is READY (chart forced complete)', !!gid);
const rw = recorder(win.cueG);
const wcfg = win.cfgShimmer;
const W = (t) => { rw.reset(); win.update(t); return { ...rw }; };
// A time inside the comet window (the tabs stagger i*420 — the first
// tab's window is [0, edgeMs); every tab's is within a few hundred ms).
const tw = Math.min(200, wcfg.edgeMs);
let w = W(s.time.now + tw);
check(`console: the ready tabs wear the edge shimmer (strokes ${w.strokePoints} ≥ 6)`, w.strokePoints >= 6);
w = W(s.time.now + tw);
check(`console: the gates plate wears the ACK pulse (halo fills ${w.fillPoints} ≥ 1)`, w.fillPoints >= 1);
// Off the SYSTEM tab → the pulse moves to the SYSTEM tab itself.
win.switchCategory('exploration');
await sleep(30);
w = W(s.time.now + tw);
check(`console: on another tab the pulse moves to the SYSTEM tab (halo fills ${w.fillPoints} ≥ 1)`, w.fillPoints >= 1);
win.switchCategory('system');
await sleep(30);
// ---- 3. in-flight — nothing can start, the cue rests everywhere ------
s.beginResearch('exploration', 'tether_l2');
s._applyResearchCue();
await sleep(30);
check('deck: in flight — the slot rests (maxConcurrent 1)', bar.shimmerId === null);
w = W(s.time.now + tw);
check(`console: in flight — the cues rest (strokes ${w.strokePoints}, fills ${w.fillPoints})`,
w.strokePoints === 0 && w.fillPoints === 0);
st.active = null; // dev poke — put the run back to "a tech can start"
win.refresh();
// ---- the capture poses (beforeCaptureExpr) ---------------------------
// The scene's live clock keeps redrawing, so each capture widens the
// cycle's window around the live time — every frame paints the same
// (frozen-mid-window) pose until the restore lands well after the shot.
window.__CUE_SHOT = {
ready: true,
lines,
errors,
captureDeck: async () => {
// The deck's face: the comms quiet (the intro's ACK queue is
// dev-drained — in play the hold owns the screen until the player
// presses through, and the cue rests under it by design), the
// console closed, the face window frozen mid-sweep.
const h = s.comms;
if (h) {
for (const q of h.queue) if (q?.def?.id) h.played.add(q.def.id);
h.queue.length = 0;
if (h.current) h._finish(); // the one in play: end it (the pause lifts)
await sleep(60);
}
if (win.isOpen) win.close(); // straight to the window's API (deckAction's input guard may still hold the ACK click)
for (let i = 0; i < 30 && win.isOpen; i++) await sleep(60);
const now = s.time.now;
const saved = { ...cfg };
Object.assign(cfg, { periodMs: now * 6, faceMs: now * 1.5, gapMs: 40, edgeMs: 500 });
await sleep(120); // a couple of scene frames at the pose
window.__CUE_SHOT.dbg = {
shimmerId: bar.shimmerId,
enabled: bar.enabled,
slotDisabled: slot?.disabled,
highlightId: bar.highlightId,
anyAvail: s.researchAnyAvailable(),
commsPaused: s.comms?.paused === true,
winOpen: win.openState,
now,
cfg: { ...cfg },
layer: typeof bar.shimmerG.geometry?.commands?.length,
};
document.getElementById('report').style.display = 'none';
setTimeout(() => Object.assign(cfg, saved), 2500);
return 'deck pose set';
},
captureConsole: async () => {
// The console's face: open on SYSTEM, gates ready, comet mid-lap.
if (!win.isOpen) win.open();
for (let i = 0; i < 50 && win.openState !== 'open'; i++) await sleep(60);
await sleep(900); // the reveal
s.systemTree.available = () => true;
s.systemTree.lockNote = () => null;
win.refresh();
win.switchCategory('system');
const now = s.time.now;
const savedW = { ...wcfg };
// edgeMs = 2·now → u = now/(2·now) = 0.5 (the comet mid-lap, full
// envelope) for every staggered tab; periodMs > now keeps ct = now.
Object.assign(wcfg, { periodMs: now * 4, faceMs: 0, gapMs: 0, edgeMs: Math.max(400, now * 2) });
await sleep(120);
document.getElementById('report').style.display = 'none';
setTimeout(() => Object.assign(wcfg, savedW), 2500);
return 'console pose set';
},
};
setReport(lines);
console.info('research-cue-shot: report painted');
} catch (err) {
errors.push(`FATAL: ${err.message}`);
setReport(['FATAL: ' + err.message, ...errors.slice(0, 3)]);
window.__CUE_SHOT = { ready: true, fatal: String(err.message), errors };
console.error('research-cue-shot: fatal', err);
}