/** * The mineral HOLD across game entry points — the upper-right readout * (js/ui/MineralHud.js) must show the player's current inventory in all * three cases the player can enter the game: * * 1. NEW GAME — the hold is empty; the HUD reads 0 / capacity. * 2. MINE — minerals come aboard; the live seam (mining's onOre → * GameScene.refreshMineralHud) pushes them to the readout. * 3. CONTINUE / LOAD — the save carries the hold (captureState), * prepareLoad stages it (the exact seam both menu buttons use), * and the fresh GameScene's applyRestore must land it BOTH on the * ship AND the upper-right HUD. (The regression this guards: the * HUD was seeded with the fresh ship's 0 in create() and never * followed the restored value — the corner read 0 until the next * mining run.) * * Two-scene harness: the real entry flow SWITCHES scenes (game → menu → * game) — and this Phaser build's scene.start is a no-op on the scene * that is already active — so a bare DummyMenu scene stands in for the * real MenuScene. The data seams are driven directly: * captureState → prepareLoad → scene.start('DummyMenu') → * scene.start('GameScene') → consumeRestore → applyRestore. * (The full UI version of this flow lives in dev/saves-ui-test.mjs.) * * Served by dev/hold-restore.html; results land in * `window.__HOLD_RESTORE__` for the CDP runner: * * python3 -m http.server 8080 * node dev/cdp-firefox.mjs http://localhost:8080/dev/hold-restore.html \ * 'return window.__HOLD_RESTORE__;' */ import Phaser from '../js/vendor/phaser.js'; import { ConfigLoader } from '../js/config/ConfigLoader.js'; import { config } from '../js/config/Config.js'; import { createGameConfig } from '../js/config/GameConfig.js'; import { GameScene } from '../js/scenes/GameScene.js'; import { captureState, prepareLoad } from '../js/save/SaveData.js'; // The real MenuScene's job in this flow is to be the OTHER side of the // scene switch — a bare scene suffices (no art, no logic). class DummyMenu extends Phaser.Scene { constructor() { super('DummyMenu'); } } const data = await ConfigLoader.load(); config.init(data); globalThis.__ORBIT_DEV_SEED = 'HOLDTEST'; // deterministic galaxy const gameConfig = createGameConfig(); gameConfig.scene = [GameScene, DummyMenu]; // GameScene boots first if (typeof Phaser !== 'undefined') Phaser.NoAudioContext = true; // quiet run 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}`); }; const run = async () => { const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const waitUntil = async (fn, label, budgetMs = 180000) => { const t = Date.now(); while (Date.now() - t < budgetMs) { if (fn()) return true; await sleep(250); } throw new Error( `${label} (budget ${budgetMs}ms) — page errors: ${JSON.stringify((window.__BOOT_ERRORS__ || []).slice(0, 4))}`, ); }; // Wait for boot — poll for the scene's UI to exist (throttling-proof; // this box starves fixed timeouts). let s = null; await waitUntil(() => { s = game.scene.getScene('GameScene'); return !!s && !!s.commsPanel && !!s.ship && !!s.mineralHud; }, 'boot', 180000); check('boots into GameScene with the HUD', !!s && !!s.ship && !!s.mineralHud); if (!s || !s.ship) throw new Error('scene never came up'); // ---- 1. NEW GAME ----------------------------------------------------- const cap = s.ship.stats.mineralStorage; check(`new game: the hold is empty (ship 0, HUD 0 / ${cap})`, s.ship.minerals === 0 && s.mineralHud.value === 0 && s.mineralHud.max === cap); // ---- 2. MINE ---------------------------------------------------------- // The live path: mining's onOre seam fires GameScene.refreshMineralHud // — drive the same seam directly (the beam itself is covered by the // mining tests). s.ship.addMinerals(37); s.refreshMineralHud(); await sleep(700); // the 420 ms count-up settles check(`mined: the HUD reads the live hold (37 / ${cap})`, s.ship.minerals === 37 && s.mineralHud.value === 37 && s.mineralHud.label.text === s.mineralHud.format(37)); // ---- 3. CONTINUE / LOAD ---------------------------------------------- // captureState = what the Save panel writes to the bank; prepareLoad = // the exact seam Continue (newest save) and Load Game (slot) both run; // then the flow SWITCHES game → menu → game. Reproduce that switch // through DummyMenu (the real MenuScene's role in the flow): scene // instances are singletons, so the recreate re-runs GameScene.create() // on the SAME object and consumeRestore() picks up the staged state. // (The GameScene shutdown here also exercises the entity destroy path // — JumpGate/Station — the v4 removeAll() migration.) const rec = captureState(s); check('the save record carries the hold (37)', rec.ship.minerals === 37); prepareLoad(s.registry, rec); game.scene.start('DummyMenu'); await waitUntil( () => game.scene.isActive('DummyMenu') && !game.scene.isActive('GameScene'), 'switch to the menu (GameScene shutdown)', ); game.scene.start('GameScene'); const gs = game.scene.getScene('GameScene'); await waitUntil( () => game.scene.isActive('GameScene') && gs.ship && gs.mineralHud, 'recreate GameScene from the staged restore', ); check('the game is back from the restore (GameScene recreated)', game.scene.isActive('GameScene') && !!gs.ship); check('the restored hold is on the ship (37)', gs.ship.minerals === 37); check('the restored value landed on the HUD immediately (value 37)', gs.mineralHud.value === 37); // set() is immediate; only the label animates await sleep(700); // the count-up settles check('the upper-right HUD reads the RESTORED hold (37 / capacity)', gs.mineralHud.value === 37 && gs.mineralHud.label.text === gs.mineralHud.format(37)); }; let done = false; game.events.once('ready', async () => { try { await run(); } catch (err) { console.error(err); results.push({ label: `DRIVER CRASHED: ${err.message}`, pass: false }); } const pass = results.length > 0 && results.every((r) => r.pass); window.__HOLD_RESTORE__ = { pass, results }; done = true; console.log(`HOLD-RESTORE ${pass ? 'PASS' : 'FAIL'} (${results.filter((r) => r.pass).length}/${results.length})`); }); // Hard stop so a hung flow can't hang the runner. const hardStop = (t0) => { if (performance.now() - t0 >= 600000) { if (!done) { window.__HOLD_RESTORE__ = { pass: false, results: [...results, { label: 'TIMED OUT (600s)', pass: false }] }; console.log('HOLD-RESTORE FAIL (timed out)'); } return; } requestAnimationFrame(() => hardStop(t0)); }; requestAnimationFrame(() => hardStop(performance.now()));