/** * Dev-only visual probe for the JOB BOARD console (js/ui/JobBoardWindow.js): * boots the real flow (menu → new game → land on the home world → surface), * then parks at the stage named in the URL hash, with a report in a DOM *
the screenshot can always read:
*
* #drawer (default) — the HOME world's EXPLORE: no drawer at all (the
* Job Board slot is absent — data/actionbar.json →
* `isHomeSystem: false`, safe space), the slot's press is a no-op
* with the console's ack ("NOTHING TO EXPLORE HERE").
* #board — the board console OPEN over the surface (nearly
* full-screen, depth 80 — over the deck + drawer + the upper-left
* HUD), driven through the scene's own seams (the slot's press →
* the toggle): the left feed (assets/videos/jobboard.mp4, muted
* loop), the right JOB BOARD title + the AVAILABLE JOBS / JOB
* DETAILS placeholder sections, the deck stood down (the slot's
* press stays live as the second door; the ✕ is the way-out).
* PROBE STAGING: the home world's isHomeSystem flag is flipped
* for this shot so the slot exists here (on a real non-home world
* it exists naturally — the drawer's item list is the
* where-clause's, covered by dev/explore.test.mjs).
* #left — …and back: the slot's press again (the second
* door) — the console down, the deck live, the slot still JOB
* BOARD (no relabel either way).
*
* node dev/server.mjs 8080
* brave --headless --disable-gpu --remote-debugging-port=9333 --window-size=1280,720 about:blank
* node dev/cdp-shot.mjs "http://127.0.0.1:8080/dev/jobboard-shot.html#drawer" jb-drawer.png "window.__JB && window.__JB.stage && !/FAILED/.test(window.__JB.stage)" 45000
* node dev/cdp-shot.mjs "http://127.0.0.1:8080/dev/jobboard-shot.html#board" jb-board.png "window.__JB && window.__JB.stage && !/FAILED/.test(window.__JB.stage)" 45000
* node dev/cdp-shot.mjs "http://127.0.0.1:8080/dev/jobboard-shot.html#left" jb-left.png "window.__JB && window.__JB.stage && !/FAILED/.test(window.__JB.stage)" 45000
*/
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 { MenuScene } from '../js/scenes/MenuScene.js';
import { GameScene } from '../js/scenes/GameScene.js';
import { SurfaceScene } from '../js/scenes/SurfaceScene.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 = [MenuScene, GameScene, SurfaceScene];
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:72%;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… (menu → new game → home world → surface)');
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function waitFor(fn, tries = 200, everyMs = 100) {
for (let i = 0; i < tries; i++) {
const v = fn();
if (v) return v;
await sleep(everyMs);
}
return null;
}
const STAGE = (location.hash || '#drawer').slice(1) || 'drawer';
const deckReport = (ss) =>
(ss.actionBar?.slots ?? []).filter((s) => s.live).map((s) => `${s.id}:${s.disabled ? 'off' : 'on'}`);
const slotLabel = (ss) => {
const b = (ss.exploreSubBar?.buttons ?? []).find((x) => x.id === 'jobBoard');
return b ? b.btn.labelText.text : '(absent — no drawer on the home world)';
};
try {
// 1) The real menu flow: NEW GAME.
const menu = await waitFor(() => { const m = game.scene.getScene('MenuScene'); return m?.newGameBtn ? m : null; });
if (!menu) throw new Error('menu never booted');
menu.startNewGame();
const gs = await waitFor(() => { const g = game.scene.getScene('GameScene'); return g?.ship ? g : null; }, 200);
if (!gs) throw new Error('GameScene never booted');
await sleep(600);
// 2) Land on the home world the game's own way (the target is built
// WHILE the home flag still reads true — the HUD's "Home World"
// label comes from it).
const target = gs.commsTargetFor(gs.planet);
if (STAGE !== 'drawer') gs.isHomeSystem = false; // PROBE STAGING — the slot's visual on this world
gs.startLanding(target);
const ss = await waitFor(() => { const c = game.scene.getScene('SurfaceScene'); return c?.jobBoardWindow ? c : null; }, 200);
if (!ss) throw new Error('surface never booted');
await sleep(700);
ss.startSurface(); // force the loop stage (don't wait the landing clip out)
const forcePlay = (v) => { if (v?.video) { v.video.muted = true; v.video.play().catch(() => {}); } };
forcePlay(ss.surfaceVideo);
await sleep(900);
const win = ss.jobBoardWindow;
if (STAGE === 'drawer') {
// The HOME world's EXPLORE — the slot is a no-op (no drawer: the
// Job Board's `isHomeSystem: false` clause leaves the item list
// empty, and a world with no items gets no drawer).
ss.toggleExplore();
await sleep(400); // the console note in + hold
window.__JB = {
stage: 'drawer',
home: ss.planetName,
isHomeSystem: gs.isHomeSystem === true,
drawerPresent: !!ss.exploreSubBar,
drawerItems: (ss.exploreSubBar?.buttons ?? []).map((b) => b.id),
noteG: ss.noteG ? ss.noteG.map((g) => g?.text) : null,
deck: deckReport(ss),
errors,
};
setReport(JSON.stringify(window.__JB, null, 1));
} else {
// The slot's player path: the drawer's JOB BOARD press → the toggle
// (the drawer's onAction seam — the same call the button's press
// makes on every non-home world).
ss.toggleExplore();
await sleep(1100); // the drawer's unfold
ss.exploreAction('jobBoard');
await sleep(1900); // the board’s reveal + the title’s decode
// Headless autoplay policy: force the feed visible (muted).
if (win.video?.video) { win.video.video.muted = true; win.video.video.play().catch(() => {}); }
await sleep(700); // let a frame land
window.__JB = {
stage: 'board',
world: ss.planetName,
staged: 'isHomeSystem flipped for the slot’s visual (a real non-home world has it naturally)',
jobBoardMode: ss.jobBoardMode,
openState: win?.openState,
title: win?.rightTitle?.text,
titlePos: win?.rightTitle ? [Math.round(win.rightTitle.x), Math.round(win.rightTitle.y)] : null,
jobs: win?.jobsSection ? { a: +win.jobsSection.bg.alpha.toFixed(2), y: Math.round(win.jobsSection.bg.y), head: win.jobsSection.headTxt.text, ha: +win.jobsSection.headTxt.alpha.toFixed(2) } : null,
detail: win?.detailSection ? { a: +win.detailSection.bg.alpha.toFixed(2), y: Math.round(win.detailSection.bg.y), head: win.detailSection.headTxt.text, ha: +win.detailSection.headTxt.alpha.toFixed(2) } : null,
geo: win?.geo ? { bodyY: win.geo.bodyY, bodyBottom: win.geo.bodyBottom, rightX: win.geo.rightX, rightW: win.geo.rightW, availTop: win.geo.availTop, availH: win.geo.availH, detailPanelY: win.geo.detailPanelY, detailH: win.geo.detailH } : null,
video: win?.video ? { muted: win.video.video.muted, loop: win.video.loop, volume: win.video.volume } : null,
slotLabel: slotLabel(ss),
drawerItems: (ss.exploreSubBar?.buttons ?? []).map((b) => b.id),
deck: deckReport(ss),
errors,
};
setReport(JSON.stringify(window.__JB, null, 1));
if (STAGE === 'left') {
// LEAVE — the slot's press again (the player's exact way out).
ss.exploreAction('jobBoard');
await sleep(900); // the close + the relabel back
window.__JB = {
stage: 'left',
world: ss.planetName,
jobBoardMode: ss.jobBoardMode,
openState: win?.openState,
slotLabel: slotLabel(ss),
deck: deckReport(ss),
errors,
};
setReport(JSON.stringify(window.__JB, null, 1));
}
}
} catch (err) {
window.__JB = { stage: 'FAILED', error: String(err?.message ?? err), errors };
setReport(JSON.stringify(window.__JB, null, 1));
}