/** * Dev-only MenuScene intro driver (headless browser — NOT a Node test). * * Verifies the two-beat menu intro synced to the menu track's crescendo * (assets/music/mainmenu.mp3 swells at ~2.2 s, data/menu.json revealDelay): * * beat 1 (t≈0): the console is up — corner frame, chrome text, and * the "THE GALAXY AWAITS" subtitle + rule are visible, * while the title, bloom, buttons, and seed panel * are STILL DARK. * beat 2 (t≈2200): the title flickers in (boot flicker armed), the * bloom blooms, the buttons rise in priority order, * the seed decodes, one signature glitch fires. * * Served by dev/menu-intro-test.html; results land in * `window.__MENU_INTRO__` for the CDP runner (dev/cdp-firefox.mjs): * * python3 -m http.server 8080 * node dev/cdp-firefox.mjs http://localhost:8080/dev/menu-intro-test.html */ 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'; const data = await ConfigLoader.load(); config.init(data); // A quiet run (no audio files to fetch in headless). if (typeof Phaser !== 'undefined') Phaser.NoAudioContext = true; const gameConfig = createGameConfig(); gameConfig.scene = [MenuScene]; // the menu boots first const game = new Phaser.Game(gameConfig); window.game = game; const results = []; const check = (label, cond) => { const pass = !!cond; results.push({ label, pass }); console.log(`${pass ? '✔' : '✘ FAIL'} ${label}`); }; // WAIT — throttling-proof. This headless box starves setTimeout and // stretches its compositor clock, so the only honest barrier is the // GAME'S OWN CLOCK: poll scene time.now on rAF until it has advanced // past the base (same pattern as dev/saves-ui-test.mjs). const gameClock = () => { try { const s = window.game.scene.getScenes(true)[0]; if (s && typeof s.time.now === 'number') return s.time.now; } catch { /* not booted yet */ } return null; }; const wait = (ms) => new Promise((resolve) => { const base = gameClock(); if (base === null) { // pre-boot: fall back to wall clock const start = performance.now(); setTimeout(() => resolve(), ms); return; } const poll = () => { const now = gameClock(); if (now !== null && now - base >= ms) return resolve(); requestAnimationFrame(poll); }; requestAnimationFrame(poll); }); const near = (a, v) => Math.abs(a - v) < 0.02; const run = async () => { // ---- boot: the menu must come up -------------------------------------- const bootT0 = Date.now(); while (!game.scene.isActive('MenuScene') || !game.scene.getScene('MenuScene').title) { if (Date.now() - bootT0 > 90000) throw new Error('MenuScene never came up'); await wait(200); } const scene = game.scene.getScene('MenuScene'); check('the menu boots', game.scene.isActive('MenuScene')); // Beat flags for dev/shot-firefox.mjs (screenshot at the right moment). window.__MENU_BEAT1__ = null; window.__MENU_BEAT2__ = null; const revealAt = config.get('menu.revealDelay', 3200); // ---- beat 1: console up, title + buttons still dark -------------------- // Sample at +1000 ms: past beat 1's fade (60+450) and well before the // reveal (revealDelay, default 3200). await wait(1000); window.__MENU_BEAT1__ = true; check('beat 1: the "THE GALAXY AWAITS" subtitle is up', scene.subtitle.alpha > 0.95); check('beat 1: the subtitle rule is up', scene.rule.alpha > 0.95); check('beat 1: the main title is STILL DARK', near(scene.title.alpha, 0) && scene.title.bootT0 === null); check('beat 1: the title bloom is STILL DARK', near(scene.titleBloom.main.alpha, 0) && near(scene.titleBloom.fringe.alpha, 0)); check('beat 1: the buttons are STILL DARK', near(scene.continueBtn.alpha, 0) && near(scene.newGameBtn.alpha, 0) && near(scene.loadGameBtn.alpha, 0)); check('beat 1: the seed panel is STILL DARK', scene.seedIntroTargets.every((t) => near(t.alpha, 0)) && near(scene.rerollBtn.alpha, 0)); // ---- beat 2: the downbeat — title, bloom, buttons, seed, decode ------- // The last beat 2 effect settles at revealAt + 540 + 620 (decode end); // sample at revealAt + 1300 to be comfortably past everything. await wait(revealAt + 1300 - 1000); window.__MENU_BEAT2__ = true; check('beat 2: the main title is up (boot flicker finished)', near(scene.title.alpha, 1) && scene.title.bootT0 === null); check('beat 2: the title bloom is up', near(scene.titleBloom.main.alpha, 1) && near(scene.titleBloom.fringe.alpha, 1)); check('beat 2: the buttons are up (Continue / New Game / Load Game)', near(scene.continueBtn.alpha, 1) && near(scene.newGameBtn.alpha, 1) && near(scene.loadGameBtn.alpha, 1)); check('beat 2: the seed panel is up', scene.seedIntroTargets.every((t) => near(t.alpha, 1)) && near(scene.rerollBtn.alpha, 1)); check('beat 2: the seed decoded to its final value', scene.decode === null && scene.seedText.text.startsWith(scene.seedValue)); const failed = results.filter((r) => !r.pass).length; console.log(failed === 0 ? `MENU INTRO OK (${results.length} checks)` : `MENU INTRO FAILED (${failed}/${results.length})`); window.__MENU_INTRO__ = { results, failed }; }; run().catch((err) => { console.error('menu-intro-test crashed:', err); window.__MENU_INTRO__ = { results, failed: results.length, error: String(err && err.stack || err) }; });