/** * Deep scan flow (headless browser — NOT a Node test). * * Boots the REAL GameScene (seeded galaxy), presses the command deck's * SCAN button through the real dispatch path (deckAction('scan')), and * walks the whole pulse: * * deckAction('scan') → the sweep arms (busy), the TARGET SET is the * discoverable objects inside the tether union, a SECOND press while * busy does not re-arm it * the charge ends → the front is out: the radius GROWS MONOTONICALLY, * the camera rolls/breathes with the wave (zoom/rotation off rest), * the barrier is EXCITED when the front touches a tether rim * the sweep completes → every in-region object was HIT (the front * reaches the union boundary, so dist ≤ maxR for all of them), the * camera is RESTORED (zoom 1, rotation 0), the console says * "SCAN COMPLETE", and the object list is logged (the result seam) * a SECOND scan runs clean (state resets between sweeps) * no console errors were captured * * The pulse's frame-stepping is driven by the runner's pump * (window.__SCAN_PUMP__ steps one game frame + advances the stage * machine) — same headless pump as dev/mining-test.mjs. Results land in * `window.__SCAN__`. * * python3 -m http.server 8080 * node dev/cdp-firefox.mjs http://localhost:8080/dev/scan-test.html \ * 'window.__SCAN_PUMP__(); return window.__SCAN__;' */ 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); // A deterministic galaxy (same clusters every run) + quiet audio in headless. globalThis.__ORBIT_DEV_SEED = 'SCAN'; const gameConfig = createGameConfig(); gameConfig.scene = [GameScene]; // GameScene boots first if (typeof Phaser !== 'undefined') Phaser.NoAudioContext = true; const game = new Phaser.Game(gameConfig); window.game = game; // ---------------------------------------------------------------------- // Results + checks // ---------------------------------------------------------------------- const results = []; const check = (label, cond) => { const pass = !!cond; results.push({ label, pass }); console.log(`${pass ? '\u2714' : '\u2718 FAIL'} ${label}`); }; // ---------------------------------------------------------------------- // Manual frame pump (the only clock this box has) // ---------------------------------------------------------------------- const pumpErrors = []; const pump = (maxSteps = 20, cpuBudgetMs = 700) => { const g = window.game; if (!g || !g.loop) return 0; const t0 = performance.now(); let n = 0; while (n < maxSteps) { if (performance.now() - t0 > cpuBudgetMs) break; try { if (g.input && typeof g.input.update === 'function') g.input.update(); g.loop.step(performance.now()); } catch (e) { pumpErrors.push(`pump: ${String((e && e.message) || e)}`); break; } n++; } return n; }; let finished = false; const finish = (pass) => { if (finished) return; finished = true; const all = [...results]; if (pumpErrors.length) { for (const e of pumpErrors) all.push({ label: e, pass: false }); } const errors = (window.__CAPTURED_ERRORS__ || []).concat(pumpErrors); if (errors.length === 0) all.push({ label: 'no console errors were captured', pass: true }); const ok = all.every((r) => r.pass); window.__SCAN__ = { pass: ok, results: all, errors, dbg: window.__DBG__STATE__ || null }; console.log(ok ? 'SCAN PASS' : 'SCAN FAIL'); }; const fail = (label) => { check(label, false); finish(false); }; /** Resumable wait across runner wakes (see mining-test.mjs). */ let stage = 0; let stageSince = -1; let stageSinceMs = 0; const wait = (pred, inProgress, note, cpuBudgetMs = 1200, maxTotalMs = 60000) => { if (stageSince !== stage) { stageSince = stage; stageSinceMs = performance.now(); } const t0 = performance.now(); for (;;) { if (pred()) { if (note) note(); return true; } if (!inProgress()) return false; if (performance.now() - stageSinceMs > maxTotalMs) return false; if (performance.now() - t0 > cpuBudgetMs) return null; pump(8, 120); } }; // ---------------------------------------------------------------------- // The stage machine // ---------------------------------------------------------------------- let sweep1 = null; // { total, hits, excites, sawCameraWobble, radiusSamples } let exciteSpy = 0; let sawCompleteToast = false; let sawEmitToast = false; const scene = () => game.scene.getScene('GameScene'); const stepMachine = () => { const s = scene(); switch (stage) { // ---- 0) scene booted ------------------------------------------------ case 0: { const r = wait(() => s && s.ship && s.scanPulse && s.tetherField, () => true, null, 1200, 30000); if (r === null) return; if (!r) fail('the scene booted (ship + scan pulse + tether field)'); check('the scene booted (ship + scan pulse + tether field)', true); // Spy on the barrier excitation (the scan → TetherField seam). const tf = s.tetherField; const origExcite = tf.excite.bind(tf); tf.excite = (i) => { exciteSpy++; return origExcite(i); }; const objs = s.discoverableObjects(); const inRegion = objs.filter((o) => s.tetherField.contains(o.x, o.y)); check('the tether region holds at least one discoverable object', inRegion.length >= 1); check('the home world is inside its tether region', inRegion.some((o) => o.id === 'home')); stage = 1; return; } // ---- 1) press SCAN — the sweep arms ---------------------------------- case 1: { s.deckAction('scan'); // the real dispatch path (ActionBar → deckAction) const armed = s.scanPulse.started && s.scanPulse.busy; if (!armed) fail('the SCAN button arms the sweep'); if (s.scanObjects === null) fail('the sweep built its target set'); check('the SCAN button arms the sweep', true); check('the sweep built its target set (objects in the tether region)', true); const total = s.scanObjects.length; check('the target set matches the in-region discoverables', total >= 1 && total === s.discoverableObjects().filter((o) => s.tetherField.contains(o.x, o.y)).length); check('the home world is in the target set', s.scanObjects.some((o) => o.id === 'home')); const t0front = s.scanPulse.frontT0; s.deckAction('scan'); // a second press while busy — must be ignored check('a second SCAN press while busy does not re-arm the sweep', s.scanPulse.frontT0 === t0front); sweep1 = { total, hits: 0, excites: 0, sawCameraWobble: false, radiusSamples: [], }; stage = 2; return; } // ---- 2) the sweep is out — watch it run ------------------------------ case 2: { const done = wait( () => !s.scanPulse.busy, () => { // While the front is out, sample it (growth + camera + excite). if (sweep1 && !sweep1.sealed) { const r = s.scanPulse.radius; if (r > 0) { if (sweep1.radiusSamples.length === 0 || r > sweep1.radiusSamples[sweep1.radiusSamples.length - 1]) { sweep1.radiusSamples.push(r); } const cam = s.cameras.main; if (Math.abs(cam.rotation) > 1e-5 || Math.abs(cam.zoom - 1) > 1e-4) { sweep1.sawCameraWobble = true; } sweep1.hits = s.scanObjects.filter((o) => o.hit).length; } } return s.scanPulse.started; // the sweep was armed (never un-armed early) }, null, 1500, 40000, ); if (done === null) return; if (!done) fail('the sweep ran to completion'); check('the sweep ran to completion', true); // The radius must have grown (front emitted and expanded). check('the front radius grew during the sweep', sweep1.radiusSamples.length >= 2 && sweep1.radiusSamples[sweep1.radiusSamples.length - 1] > sweep1.radiusSamples[0]); // The camera wobbled with the wave. check('the camera rolled/breathed while the wave passed', sweep1.sawCameraWobble); // Every in-region object was hit (dist ≤ maxR for all of them). check(`every in-region object was hit (${sweep1.hits}/${sweep1.total})`, sweep1.hits === sweep1.total && sweep1.total >= 1); // The barrier was excited where the wave was absorbed. sweep1.excites = exciteSpy; check('the barrier was excited as the wave was absorbed', exciteSpy >= 1); // The result seam fired (finishScan logged the object list). const logged = (window.__SCAN_LOGS__ || []).some((l) => l.includes('scan complete')); check('the scan-complete seam logged the in-region objects', logged); // The camera was restored. const cam = s.cameras.main; check('the camera zoom was restored', Math.abs(cam.zoom - 1) < 0.002); check('the camera rotation was restored', Math.abs(cam.rotation) < 1e-3); // The console said SCAN COMPLETE. const toast = s.consoleToastG && s.consoleToastG[1] ? s.consoleToastG[1].text : ''; check('the console reported the sweep (SCAN COMPLETE)', /SCAN COMPLETE/.test(toast)); sawCompleteToast = true; // The sweep state reset — a second scan must be armable. check('the sweep state reset (busy=false after completion)', !s.scanPulse.busy); check('the target set was released', s.scanObjects === null); stage = 3; return; } // ---- 3) a second scan runs clean -------------------------------------- case 3: { s.deckAction('scan'); if (!s.scanPulse.busy) fail('a second SCAN press arms a fresh sweep'); check('a second SCAN press arms a fresh sweep', true); const r = wait(() => !s.scanPulse.busy, () => true, null, 1500, 40000); if (r === null) return; if (!r) fail('the second sweep ran to completion'); check('the second sweep ran to completion', true); check('the second sweep hit every in-region object again', (window.__SCAN_LOGS__ || []).filter((l) => l.includes('scan complete')).length >= 2); const cam = s.cameras.main; check('the camera was restored after the second sweep', Math.abs(cam.zoom - 1) < 0.002 && Math.abs(cam.rotation) < 1e-3); stage = 4; return; } // ---- 4) done ----------------------------------------------------------- case 4: { finish(results.every((x) => x.pass)); return; } } }; window.__SCAN_PUMP__ = () => { try { stepMachine(); } catch (e) { (window.__CAPTURED_ERRORS__ || (window.__CAPTURED_ERRORS__ = [])).push(`stage: ${String((e && e.stack) || e)}`); check(`THREW: ${String((e && e.message) || e)}`, false); finish(false); } };