163 lines
6.8 KiB
JavaScript
163 lines
6.8 KiB
JavaScript
/**
|
|
* 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()));
|