/** * Dev-only: boot GameScene, open the RESEARCH console, start a SHORT project * and let it COMPLETE LIVE — capturing the completion treatment: the lit * node (accent fill + glow ring + icon halo + stamped seal), the tab * progress pips (filled = researched / hollow = not yet), and the one-shot * ignition mid-flight (ring shells, the fast pulse down the newly-powered * edge, the child flare) with the status strip's PROJECT COMPLETE decode. * * node dev/server.mjs 8090 * node dev/cdp-shot.mjs http://127.0.0.1:8090/dev/research-complete-shot.html \ * /tmp/research-complete.png "window.__RESEARCH_SHOT && window.__RESEARCH_SHOT.ready" \ * 90000 "document.getElementById('report') && (document.getElementById('report').style.display='none')" * * The page sets window.__RESEARCH_SHOT (report + errors) when ready. */ 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'; const data = await ConfigLoader.load(); config.init(data); // Capture console errors + uncaught exceptions for the report. 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; // The report lives OUTSIDE the canvas — a DOM
the screenshot can
// always read (headless canvases don't have to cooperate).
const report = document.createElement('pre');
report.id = 'report';
report.style.cssText = 'position:fixed;left:10px;top:10px;z-index:9999;max-width:70%;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');
}
try {
const s = await waitScene();
await sleep(800); // let the boot settle (dossier decode, deck flicker)
// Open the console (the deck RESEARCH button does exactly this).
s.deckAction('research');
await sleep(900); // the boot reveal finishes
// Start a project with a SHORT duration and let it complete live — the
// completion tick fires _completeResearchFx → the console's ignition.
s.researchState.start('exploration', 'tether_l2', 500, s.time.now);
for (let i = 0; i < 100; i++) {
if (!s.researchState.getActive() && s.researchState.isUnlocked('exploration', 'tether_l2')) break;
await sleep(50);
}
const win = s.researchWindow;
// The natural ignition already played (and will be over by the time the
// capture harness lands ~0.5 s after ready). Re-fire it right before the
// handoff so the capture lands MID-FLIGHT: the soft white ring expanding,
// the ignition head riding down the edge, the child flare in bloom.
win.celebrate('exploration', 'tether_l2');
const burstsLive = win.bursts.length;
if (burstsLive === 0) errors.push('CELEBRATE MISS — no bursts queued by win.celebrate()');
const st = s.researchState;
const entry = win.trees.get('exploration');
const nodeStates = entry
? entry.tree.order.map((id) => `${id}=${win._nodeState(id)}`).join(' ')
: 'NO TREE';
const pips = win.tabs
.map((t) => `${t.id}:${t.pips.map((p) => p.state[0]).join('')}`)
.join(' ');
const lines = [
errors.length === 0 ? 'SMOKE OK — no console errors' : `ERRORS:\n${errors.slice(0, 3).join('\n')}`,
`window: ${win.openState} · cat=${win.activeCat} · sel=${win.selected ? win.selected.category + '/' + win.selected.id : '—'}`,
`state: ${st.unlocked.size} unlocked · active=${st.getActive() ? st.getActive().category + '/' + st.getActive().id : '—'}`,
`nodes: ${nodeStates}`,
`pips: ${pips}`,
`bursts: ${win.bursts.length} live (handoff) · ${burstsLive} at celebrate · celebration=${win.celebration ? (win.celebration.played ? 'played' : 'queued') : 'none'}`,
];
setReport(lines);
window.__RESEARCH_SHOT = { ready: true, lines, errors };
console.info('research-complete-shot: report painted');
} catch (err) {
errors.push(`FATAL: ${err.message}`);
setReport(['FATAL: ' + err.message, ...errors.slice(0, 3)]);
window.__RESEARCH_SHOT = { ready: true, fatal: String(err.message), errors };
console.error('research-complete-shot: fatal', err);
}