226 lines
10 KiB
JavaScript
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);
|
|
}
|