139 lines
7.2 KiB
JavaScript
139 lines
7.2 KiB
JavaScript
/**
|
|
* Dev-only: TETHER-L1 BUILD FLOW ON A NEWLY DISCOVERED WORLD — the bug
|
|
* report: land on a world with NO tether, build Tether - Level 1, and the
|
|
* world should actually GAIN a level-1 tether (range 5120 px anchored on
|
|
* its center), show "Tether Level 1", and then accept Tether - Level 2
|
|
* (which upgrades the SAME tether to 10240 px). Also checks the
|
|
* re-materialization: a scene rebuild (jump cut / restart) that empties
|
|
* the field to its native set must re-anchor the installed tether on its
|
|
* world (js/scenes/GameScene.js → _rematerializeBuiltTethers).
|
|
*
|
|
* node dev/server.mjs 8080
|
|
* node dev/cdp-firefox.mjs http://127.0.0.1:8080/dev/tether-newworld.html \
|
|
* 'return window.__TETHER_NEWWORLD__;' 60000
|
|
*/
|
|
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);
|
|
|
|
const errors = [];
|
|
const origWarn = console.warn.bind(console);
|
|
console.warn = (...a) => { errors.push(a.map(String).join(' ')); origWarn(...a); };
|
|
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 game = new Phaser.Game({ ...createGameConfig(), scene: [MenuScene, GameScene, SurfaceScene] });
|
|
window.game = game;
|
|
|
|
const sleepMs = (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 sleepMs(everyMs);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const tetherFor = (gs, name) =>
|
|
gs.tetherField?.tethers?.find((t) => t.label === name) ?? null;
|
|
const tetherInfo = (t) => (t ? { id: t.id, x: t.x, y: t.y, level: t.level, radius: t.radius } : null);
|
|
|
|
try {
|
|
// 1) Menu → NEW GAME (the real entry).
|
|
const menu = await waitFor(() => game.scene.getScene('MenuScene')?.newGameBtn ? game.scene.getScene('MenuScene') : 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?.buildState && g?.tetherField ? g : null; }, 300);
|
|
if (!gs) throw new Error('GameScene never booted');
|
|
await sleepMs(500);
|
|
|
|
// 2) Pick a NON-HOME world in the starting system (the bug's setting:
|
|
// a newly discovered world, which has no tether of its own).
|
|
const other = gs.systemPlanets?.[0];
|
|
if (!other) throw new Error('starting system has no non-home planet to test on');
|
|
const name = other.discoveryName;
|
|
const before = tetherFor(gs, name);
|
|
if (before) throw new Error(`world "${name}" unexpectedly has a tether at start`);
|
|
|
|
// 3) Land on it the game's own way, wait for the surface stage.
|
|
gs.startLanding(gs.commsTargetFor(other));
|
|
const ss = await waitFor(() => { const c = game.scene.getScene('SurfaceScene'); return c?.phase === 'surface' ? c : null; }, 150, 200);
|
|
if (!ss) throw new Error('surface stage never reached');
|
|
await sleepMs(300);
|
|
|
|
// 4) The build console's own rule pass agrees: L2 is gated on L1.
|
|
const l2GateBefore = gs.tetherLevelFor(name);
|
|
if (l2GateBefore !== 0) throw new Error(`tetherLevelFor("${name}") = ${l2GateBefore}, want 0 before the L1 build`);
|
|
|
|
// 5) Build Tether - Level 1 (free + instant per data/builds.json).
|
|
const res1 = gs.beginBuild(ss.planetName, 'tether-l1', game.loop.now);
|
|
if (!res1.ok) throw new Error(`L1 build refused: ${res1.reason}`);
|
|
// The surface's own tick completes it (duration 0 → 1 ms) and applies
|
|
// the effect on the game scene; wait for the built record.
|
|
await waitFor(() => gs.buildState.isBuilt(ss.planetName, 'tether-l1'), 60, 50);
|
|
if (!gs.buildState.isBuilt(ss.planetName, 'tether-l1')) throw new Error('L1 build never completed');
|
|
await sleepMs(1500); // HUD re-decode + window repaint settle
|
|
|
|
const l1 = tetherFor(gs, name);
|
|
if (!l1) throw new Error(`no tether anchored on "${name}" after the L1 build (the bug)`);
|
|
if (l1.level !== 1) throw new Error(`L1 tether level = ${l1.level}, want 1`);
|
|
if (l1.radius !== 5120) throw new Error(`L1 tether radius = ${l1.radius}, want 5120`);
|
|
if (l1.x !== other.x || l1.y !== other.y) throw new Error(`L1 tether anchored at (${l1.x},${l1.y}), want the world's center (${other.x},${other.y})`);
|
|
if (gs.tetherLevelFor(name) !== 1) throw new Error(`tetherLevelFor("${name}") = ${gs.tetherLevelFor(name)}, want 1 after the L1 build`);
|
|
if (ss.tetherLevel !== 1) throw new Error(`surface HUD level = ${ss.tetherLevel}, want 1 after the L1 build`);
|
|
|
|
// 6) Now L2 must be ACCEPTED on this world (research gate + minerals).
|
|
gs.researchState.unlock('exploration', 'tether_l2');
|
|
gs.ship.setMinerals(200);
|
|
const res2 = gs.beginBuild(ss.planetName, 'tether-l2', game.loop.now);
|
|
if (!res2.ok) throw new Error(`L2 build refused after L1 installed: ${res2.reason}`);
|
|
const active2 = gs.buildState.getActive();
|
|
if (active2) active2.startedAt -= active2.durationMs; // fast-forward the 20 s
|
|
await waitFor(() => gs.buildState.isBuilt(ss.planetName, 'tether-l2'), 60, 50);
|
|
if (!gs.buildState.isBuilt(ss.planetName, 'tether-l2')) throw new Error('L2 build never completed');
|
|
await sleepMs(1500);
|
|
|
|
const l2 = tetherFor(gs, name);
|
|
if (!l2) throw new Error('tether vanished after the L2 build');
|
|
if (l2.id !== l1.id) throw new Error(`L2 upgraded a DIFFERENT tether (${l2.id} vs ${l1.id}) — want the same anchor strengthened`);
|
|
if (l2.level !== 2) throw new Error(`L2 tether level = ${l2.level}, want 2`);
|
|
if (l2.radius !== 10240) throw new Error(`L2 tether radius = ${l2.radius}, want 10240`);
|
|
if (gs.tetherLevelFor(name) !== 2) throw new Error(`tetherLevelFor("${name}") = ${gs.tetherLevelFor(name)}, want 2 after the L2 build`);
|
|
|
|
// 7) Re-materialization: a scene rebuild empties the field to its
|
|
// native set (home + activated gates). The installed tether is world
|
|
// state — it must come back on its world at its installed level.
|
|
gs.tetherField.remove(l1.id);
|
|
if (tetherFor(gs, name)) throw new Error('removal failed (test premise)');
|
|
gs._rematerializeBuiltTethers();
|
|
const re = tetherFor(gs, name);
|
|
if (!re) throw new Error('re-materialization did NOT re-anchor the installed tether');
|
|
if (re.level !== 2) throw new Error(`re-materialized tether level = ${re.level}, want 2 (the installed max)`);
|
|
if (re.x !== other.x || re.y !== other.y) throw new Error('re-materialized tether anchored on the wrong world');
|
|
|
|
const out = {
|
|
world: name,
|
|
l1: tetherInfo(l1),
|
|
l2: tetherInfo(l2),
|
|
rematerialized: tetherInfo(re),
|
|
hudLine: ss.hudLine ? ss.hudLine.text : null,
|
|
built: [gs.buildState.buildsOn(ss.planetName)],
|
|
errors: errors.slice(0, 5),
|
|
};
|
|
window.__TETHER_NEWWORLD__ = out;
|
|
console.info('tether-newworld:', JSON.stringify(out));
|
|
} catch (err) {
|
|
window.__TETHER_NEWWORLD__ = { fatal: String(err.message), errors: errors.slice(0, 5) };
|
|
console.error('tether-newworld: fatal', err);
|
|
}
|