Decouple comms panel from fly-to and add double-click clip skip
- Clicking a planet or station now opens the comms panel without moving the ship; world clicks are no longer fly-here targets, keeping mining beams live during panel interaction - Add double-click (350 ms) skip for landing and takeoff clips in SurfaceScene so players can cut to the deck or flight world immediately - Push restored mineral hold through the upper-right HUD on load so the readout matches the saved value instead of staying at zero - Replace removed Phaser v4 Container.removeChildren() with removeAll(true) in JumpGate and Station destroy paths - Update landing.json shop entries for terran frames 2–3 and document the new skip behavior - Add dev harnesses for surface-skip, hold-restore, scene-restart, container-probe, and import probes; extend comms-click and saves-ui tests to cover the new interactions
This commit is contained in:
parent
35a20416d1
commit
cb82da4935
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -18,6 +18,9 @@
|
||||||
"exist yet, so they loop a terran surface clip as a placeholder.",
|
"exist yet, so they loop a terran surface clip as a placeholder.",
|
||||||
"An entry may set `land`/`surface` to null to skip that stage entirely",
|
"An entry may set `land`/`surface` to null to skip that stage entirely",
|
||||||
"(land goes straight to the surface; a null surface shows the flat deck).",
|
"(land goes straight to the surface; a null surface shows the flat deck).",
|
||||||
|
"A DOUBLE-CLICK (two quick presses) during the `land` or `takeoff`",
|
||||||
|
"clips skips the rest of the clip — the deck / the flight world comes",
|
||||||
|
"up immediately (no setting; the 350 ms window lives in SurfaceScene).",
|
||||||
"Set `enabled` to false to disable landing entirely (REQUEST LANDING is",
|
"Set `enabled` to false to disable landing entirely (REQUEST LANDING is",
|
||||||
"greyed out in comms, same as a reputation ban)."
|
"greyed out in comms, same as a reputation ban)."
|
||||||
],
|
],
|
||||||
|
|
@ -41,8 +44,8 @@
|
||||||
},
|
},
|
||||||
"videos": [
|
"videos": [
|
||||||
{ "land": "terran-land-01.mp4", "surface": "terran-surface-01.mp4", "takeoff": "terran-takeoff-01.mp4", "shop": "terran-shop-01.mp4" },
|
{ "land": "terran-land-01.mp4", "surface": "terran-surface-01.mp4", "takeoff": "terran-takeoff-01.mp4", "shop": "terran-shop-01.mp4" },
|
||||||
{ "land": "terran-land-02.mp4", "surface": "terran-surface-02.mp4", "takeoff": "terran-takeoff-02.mp4", "shop": "terran-shop-01.mp4" },
|
{ "land": "terran-land-02.mp4", "surface": "terran-surface-02.mp4", "takeoff": "terran-takeoff-02.mp4", "shop": "terran-shop-02.mp4" },
|
||||||
{ "land": "terran-land-03.mp4", "surface": "terran-surface-03.mp4", "takeoff": "terran-takeoff-03.mp4", "shop": "terran-shop-01.mp4" },
|
{ "land": "terran-land-03.mp4", "surface": "terran-surface-03.mp4", "takeoff": "terran-takeoff-03.mp4", "shop": "terran-shop-03.mp4" },
|
||||||
{ "land": "gasgiant-land-01.mp4", "surface": "gasgiant-surface-01.mp4", "takeoff": "gasgiant-takeoff-01.mp4", "shop": "terran-shop-01.mp4" },
|
{ "land": "gasgiant-land-01.mp4", "surface": "gasgiant-surface-01.mp4", "takeoff": "gasgiant-takeoff-01.mp4", "shop": "terran-shop-01.mp4" },
|
||||||
{ "land": "gasgiant-land-02.mp4", "surface": "gasgiant-surface-02.mp4", "takeoff": "gasgiant-takeoff-02.mp4", "shop": "terran-shop-01.mp4" },
|
{ "land": "gasgiant-land-02.mp4", "surface": "gasgiant-surface-02.mp4", "takeoff": "gasgiant-takeoff-02.mp4", "shop": "terran-shop-01.mp4" },
|
||||||
{ "land": "gasgiant-land-03.mp4", "surface": "terran-surface-01.mp4", "takeoff": "gasgiant-takeoff-03.mp4", "shop": "terran-shop-01.mp4" },
|
{ "land": "gasgiant-land-03.mp4", "surface": "terran-surface-01.mp4", "takeoff": "gasgiant-takeoff-03.mp4", "shop": "terran-shop-01.mp4" },
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* Dev: real-input test for the CommsPanel interaction paths:
|
* Dev: real-input test for the CommsPanel interaction paths:
|
||||||
* 1. click an object → panel opens + ship retargeted
|
* 1. click an object → panel opens, ship does NOT fly (a planet
|
||||||
|
* click is not a fly-here — it is the
|
||||||
|
* landing-request window, not a move)
|
||||||
* 2. click CANCEL → closes
|
* 2. click CANCEL → closes
|
||||||
* 3. rep ≤ −4, click REQUEST LANDING → inert (panel stays open)
|
* 3. rep ≤ −4, click REQUEST LANDING → inert (panel stays open)
|
||||||
* 4. click outside → closes
|
* 4. click outside → closes
|
||||||
|
|
@ -163,7 +165,7 @@ window.__CLICK_TEST__ = async () => {
|
||||||
s.cameraFollowShip = false;
|
s.cameraFollowShip = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- 1. Real click ON the home world → fly + panel opens -------------
|
// ---- 1. Real click ON the home world → panel opens, ship stays put --
|
||||||
const hx = 0, hy = 0, hsx = 640, hsy = 300;
|
const hx = 0, hy = 0, hsx = 640, hsy = 300;
|
||||||
s.ship.x = 700; s.ship.y = 650;
|
s.ship.x = 700; s.ship.y = 650;
|
||||||
frame(hx, hy, hsx, hsy);
|
frame(hx, hy, hsx, hsy);
|
||||||
|
|
@ -172,9 +174,9 @@ window.__CLICK_TEST__ = async () => {
|
||||||
clickAtClient(c1.clientX, c1.clientY);
|
clickAtClient(c1.clientX, c1.clientY);
|
||||||
await pump(s, () => s.commsPanel.isOpen && s.commsPanel.alpha >= 0.999 && s.commsPanel.nameDec === null, 4000);
|
await pump(s, () => s.commsPanel.isOpen && s.commsPanel.alpha >= 0.999 && s.commsPanel.nameDec === null, 4000);
|
||||||
step(
|
step(
|
||||||
'click object → panel opens + ship retargeted',
|
'click object → panel opens + ship does NOT fly',
|
||||||
s.commsPanel.isOpen && s.ship.target !== null && s.commsPanel.lastTarget?.name === s.planet.discoveryName.toUpperCase(),
|
s.commsPanel.isOpen && s.ship.target === null && s.commsPanel._name === s.planet.discoveryName.toUpperCase(),
|
||||||
{ shipTarget: s.ship.target, panelName: s.commsPanel._name },
|
{ shipTarget: s.ship.target, panelName: s.commsPanel._name, planetName: s.planet.discoveryName },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Button centers in panel-local y: btn1 = +23.5, btn2 = +76.5.
|
// Button centers in panel-local y: btn1 = +23.5, btn2 = +76.5.
|
||||||
|
|
@ -230,23 +232,25 @@ window.__CLICK_TEST__ = async () => {
|
||||||
step('ESC → closes', !s.commsPanel.isOpen);
|
step('ESC → closes', !s.commsPanel.isOpen);
|
||||||
|
|
||||||
// ---- 6. Enabled REQUEST LANDING fires the seam + closes ---------------
|
// ---- 6. Enabled REQUEST LANDING fires the seam + closes ---------------
|
||||||
// (Home is pinned +20 — canLand is true; the seam logs the action.)
|
// (Home is pinned +20 — canLand is true. For a planet target the seam is
|
||||||
let logged = '';
|
// startLanding() — this harness registers ONLY GameScene (no
|
||||||
const orig = console.info;
|
// SurfaceScene), so stub it and assert the seam fired with the
|
||||||
console.info = (...a) => {
|
// planet's payload (the landing-click.mjs harness runs the real one).
|
||||||
logged += ' ' + a.join(' ');
|
let landingArgs = null;
|
||||||
orig(...a);
|
const origLanding = s.startLanding;
|
||||||
};
|
s.startLanding = (t) => { landingArgs = t; };
|
||||||
s.openCommsPanel(s.planet, { worldX: hx, worldY: hy, x: 640, y: 300 });
|
s.openCommsPanel(s.planet, { worldX: hx, worldY: hy, x: 640, y: 300 });
|
||||||
await pump(s, () => s.commsPanel.isOpen && s.commsPanel.alpha >= 0.999, 4000);
|
await pump(s, () => s.commsPanel.isOpen && s.commsPanel.alpha >= 0.999, 4000);
|
||||||
const b6 = btn1();
|
const b6 = btn1();
|
||||||
clickAtClient(b6.clientX, b6.clientY);
|
clickAtClient(b6.clientX, b6.clientY);
|
||||||
await pump(s, () => !s.commsPanel.isOpen, 2500);
|
await pump(s, () => !s.commsPanel.isOpen, 2500);
|
||||||
console.info = orig;
|
s.startLanding = origLanding;
|
||||||
step(
|
step(
|
||||||
'click REQUEST LANDING (enabled) → seam fires + closes',
|
'click REQUEST LANDING (enabled) → seam fires + closes',
|
||||||
!s.commsPanel.isOpen && logged.includes('request-landing'),
|
!s.commsPanel.isOpen &&
|
||||||
{ logged: logged.trim() },
|
landingArgs?.name === s.planet.discoveryName &&
|
||||||
|
landingArgs?.isPlanet === true,
|
||||||
|
{ landingArgs: landingArgs && { name: landingArgs.name, isPlanet: landingArgs.isPlanet, frame: landingArgs.frame } },
|
||||||
);
|
);
|
||||||
|
|
||||||
out.allPass = out.steps.every((x) => x.pass);
|
out.allPass = out.steps.every((x) => x.pass);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<!doctype html><html><head><meta charset="utf-8"><base href="../"><script src="lib/phaser.min.js"></script></head>
|
||||||
|
<body><script type="module" src="dev/container-probe.mjs"></script></body></html>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import Phaser from '../js/vendor/phaser.js';
|
||||||
|
// Minimal headless-ish game just to get a scene/context.
|
||||||
|
const game = new Phaser.Game({ type: Phaser.HEADLESS, width: 100, height: 100,
|
||||||
|
scene: { create() { this.__done = true; } } });
|
||||||
|
window.__CP__ = null;
|
||||||
|
game.events.once('ready', () => {
|
||||||
|
const s = game.scene.getScenes(true)[0];
|
||||||
|
const out = { steps: [] };
|
||||||
|
const mk = () => {
|
||||||
|
const c = new Phaser.GameObjects.Container(s, 0, 0);
|
||||||
|
const ch = new Phaser.GameObjects.Sprite(s, 0, 0);
|
||||||
|
c.add(ch);
|
||||||
|
return { c, ch };
|
||||||
|
};
|
||||||
|
// 1. does destroy() destroy children?
|
||||||
|
{ const { c, ch } = mk(); c.destroy(); out.steps.push('destroy() -> child.isDestroyed=' + ch.isDestroyed + ' child.scene=' + (ch.scene === null)); }
|
||||||
|
// 2. removeAll(true)
|
||||||
|
{ const { c, ch } = mk(); c.removeAll(true); out.steps.push('removeAll(true) -> child.isDestroyed=' + ch.isDestroyed + ' stillInList=' + c.list.includes(ch)); }
|
||||||
|
// 3. removeAll(false) then destroy
|
||||||
|
{ const { c, ch } = mk(); c.removeAll(false); out.steps.push('removeAll(false) -> child.isDestroyed=' + ch.isDestroyed + ' inList=' + c.list.includes(ch)); c.destroy(); out.steps.push(' then destroy() -> child.isDestroyed=' + ch.isDestroyed); }
|
||||||
|
window.__CP__ = out;
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>orbit — hold across game entry (dev test)</title>
|
||||||
|
<!-- This page lives in /dev, but the game's relative asset paths are
|
||||||
|
rooted at the project root — resolve them against it. -->
|
||||||
|
<base href="../" />
|
||||||
|
<style>
|
||||||
|
html, body { margin: 0; padding: 0; height: 100%; background: #0a0e14; }
|
||||||
|
body { display: flex; align-items: center; justify-content: center; }
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
window.__BOOT_ERRORS__ = [];
|
||||||
|
window.addEventListener('error', (e) =>
|
||||||
|
window.__BOOT_ERRORS__.push(String(e.message) + ' @ ' + (e.filename || '') + ':' + e.lineno));
|
||||||
|
window.addEventListener('unhandledrejection', (e) =>
|
||||||
|
window.__BOOT_ERRORS__.push('reject: ' + ((e.reason && e.reason.stack) || e.reason)));
|
||||||
|
</script>
|
||||||
|
<script src="lib/phaser.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="module" src="dev/hold-restore.mjs"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
/**
|
||||||
|
* 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()));
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!doctype html><html><head><meta charset="utf-8"></head><body>
|
||||||
|
<script type="module">
|
||||||
|
window.__PROBE__ = 'start';
|
||||||
|
try {
|
||||||
|
await import('../js/vendor/phaser.js');
|
||||||
|
await import('../js/config/ConfigLoader.js');
|
||||||
|
await import('../js/config/GameConfig.js');
|
||||||
|
await import('../js/scenes/GameScene.js');
|
||||||
|
await import('../js/save/SaveData.js');
|
||||||
|
window.__PROBE__ = 'all imports OK';
|
||||||
|
} catch (e) { window.__PROBE__ = 'FAIL: ' + (e && e.message); }
|
||||||
|
</script></body></html>
|
||||||
|
|
@ -12,7 +12,9 @@
|
||||||
* MenuScene: Load Game is live (the bank has a save) → its own load
|
* MenuScene: Load Game is live (the bank has a save) → its own load
|
||||||
* pop-up → LOAD confirm → the staged restore rides into a
|
* pop-up → LOAD confirm → the staged restore rides into a
|
||||||
* fresh GameScene — ship back where it was, the session
|
* fresh GameScene — ship back where it was, the session
|
||||||
* time, the tether field.
|
* time, the tether field, and the hold: the minerals the
|
||||||
|
* save carried show on the ship AND the upper-right
|
||||||
|
* mineral readout (the load seam the HUD refresh guards).
|
||||||
*
|
*
|
||||||
* Served by dev/saves-ui-test.html; the results land in
|
* Served by dev/saves-ui-test.html; the results land in
|
||||||
* `window.__SAVES_UI__` for the CDP runner (dev/cdp-firefox.mjs):
|
* `window.__SAVES_UI__` for the CDP runner (dev/cdp-firefox.mjs):
|
||||||
|
|
@ -101,9 +103,33 @@ const press = (x, y) => {
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
const scene = game.scene.getScene('GameScene');
|
const scene = game.scene.getScene('GameScene');
|
||||||
await wait(900); // boot: galaxy, system, deck, sub-bar, panel
|
await wait(900); // boot: galaxy, system, deck, sub-bar, panel
|
||||||
|
// This box can starve the boot — poll until the scene is REALLY up
|
||||||
|
// (active + ship + HUD built) instead of trusting the fixed wait.
|
||||||
|
const bootT0 = Date.now();
|
||||||
|
while (!game.scene.isActive('GameScene') || !scene.ship || !scene.mineralHud) {
|
||||||
|
if (Date.now() - bootT0 > 90000) throw new Error('GameScene never came up');
|
||||||
|
await wait(200);
|
||||||
|
}
|
||||||
check('boots into GameScene', game.scene.isActive('GameScene'));
|
check('boots into GameScene', game.scene.isActive('GameScene'));
|
||||||
check('the sub-bar + panel exist', !!scene.menuSubBar && !!scene.savePanel);
|
check('the sub-bar + panel exist', !!scene.menuSubBar && !!scene.savePanel);
|
||||||
|
|
||||||
|
// ---- the mineral hold (upper-right readout) ---------------------------
|
||||||
|
// FRESH GAME: the hold is empty and the HUD says exactly that (create()
|
||||||
|
// seeded the readout from the fresh ship's 0).
|
||||||
|
check('a fresh game: empty hold, HUD reads 0 / capacity',
|
||||||
|
scene.ship.minerals === 0 && scene.mineralHud.value === 0
|
||||||
|
&& scene.mineralHud.max === scene.ship.stats.mineralStorage
|
||||||
|
&& scene.mineralHud.label.text === scene.mineralHud.format(0));
|
||||||
|
// MINED: minerals aboard — the live seam (refreshMineralHud, what
|
||||||
|
// mining's onOre fires) pushes them into the readout, and the count-up
|
||||||
|
// (420 ms tween) settles on the exact value.
|
||||||
|
scene.ship.addMinerals(37);
|
||||||
|
scene.refreshMineralHud();
|
||||||
|
await wait(700);
|
||||||
|
check('the HUD reads the live hold (37 / capacity)',
|
||||||
|
scene.ship.minerals === 37 && scene.mineralHud.value === 37
|
||||||
|
&& scene.mineralHud.label.text === scene.mineralHud.format(37));
|
||||||
|
|
||||||
// ---- the sub-bar folds up out of MENU --------------------------------
|
// ---- the sub-bar folds up out of MENU --------------------------------
|
||||||
scene.menuAction();
|
scene.menuAction();
|
||||||
await wait(450);
|
await wait(450);
|
||||||
|
|
@ -171,6 +197,7 @@ const run = async () => {
|
||||||
await wait(120);
|
await wait(120);
|
||||||
const rec1 = scene.saveManager.get(1);
|
const rec1 = scene.saveManager.get(1);
|
||||||
check('doSave(1) writes the bank', rec1 !== null && rec1.seed === scene.galaxy.seed && rec1.ship.x === scene.ship.x);
|
check('doSave(1) writes the bank', rec1 !== null && rec1.seed === scene.galaxy.seed && rec1.ship.x === scene.ship.x);
|
||||||
|
check('the bank captured the hold (37)', rec1.ship.minerals === 37);
|
||||||
// Capture fidelity: the bank's session time is the scene's at capture
|
// Capture fidelity: the bank's session time is the scene's at capture
|
||||||
// (a frame or two may have ticked since — small tolerance).
|
// (a frame or two may have ticked since — small tolerance).
|
||||||
check('the bank captured the session time',
|
check('the bank captured the session time',
|
||||||
|
|
@ -225,6 +252,15 @@ const run = async () => {
|
||||||
check('the ship is back where the save parked it',
|
check('the ship is back where the save parked it',
|
||||||
rec && Math.abs(gs.ship.x - rec.ship.x) < 0.01 && Math.abs(gs.ship.y - rec.ship.y) < 0.01);
|
rec && Math.abs(gs.ship.x - rec.ship.x) < 0.01 && Math.abs(gs.ship.y - rec.ship.y) < 0.01);
|
||||||
check('the saved ship was where it had flown', Math.abs(rec.ship.x - shipX) < 0.01 && Math.abs(rec.ship.y - shipY) < 0.01);
|
check('the saved ship was where it had flown', Math.abs(rec.ship.x - shipX) < 0.01 && Math.abs(rec.ship.y - shipY) < 0.01);
|
||||||
|
// The HOLD rides the restore too — on the ship, and on the upper-right
|
||||||
|
// readout: applyRestore pushes the saved value through the HUD (the
|
||||||
|
// seam this test guards; without it the corner reads 0 until the next
|
||||||
|
// mining run). The count-up settles (420 ms) on the bank's value.
|
||||||
|
check('the restored hold is on the ship (37)', gs.ship.minerals === 37);
|
||||||
|
await wait(700);
|
||||||
|
check('the HUD reads the RESTORED hold (upper right)',
|
||||||
|
gs.mineralHud.value === 37
|
||||||
|
&& gs.mineralHud.label.text === gs.mineralHud.format(37));
|
||||||
// Restore fidelity: the restored value is the BANK's value (rec1), plus
|
// Restore fidelity: the restored value is the BANK's value (rec1), plus
|
||||||
// the session time accumulated since GameScene.create (the wait above +
|
// the session time accumulated since GameScene.create (the wait above +
|
||||||
// a few frames). update() caps per-frame accumulation at 100ms
|
// a few frames). update() caps per-frame accumulation at 100ms
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<!doctype html><html><head><meta charset="utf-8"><base href="../">
|
||||||
|
<script>
|
||||||
|
window.__PAGE_ERRORS__ = [];
|
||||||
|
window.addEventListener('error', (e) => window.__PAGE_ERRORS__.push('error: ' + e.message + ' @ ' + (e.filename||'') + ':' + e.lineno));
|
||||||
|
window.addEventListener('unhandledrejection', (e) => window.__PAGE_ERRORS__.push('reject: ' + (e.reason && (e.reason.stack || e.reason) || e.reason)));
|
||||||
|
const oe = console.error.bind(console);
|
||||||
|
console.error = (...a) => { window.__PAGE_ERRORS__.push(a.map(String).join(' ').slice(0,800)); oe(...a); };
|
||||||
|
</script>
|
||||||
|
<script src="lib/phaser.min.js"></script></head>
|
||||||
|
<body><script type="module" src="dev/scene-restart-probe.mjs"></script></body></html>
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
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';
|
||||||
|
const data = await ConfigLoader.load();
|
||||||
|
config.init(data);
|
||||||
|
globalThis.__ORBIT_DEV_SEED = 'PROBE';
|
||||||
|
const gameConfig = createGameConfig();
|
||||||
|
gameConfig.scene = [GameScene];
|
||||||
|
if (typeof Phaser !== 'undefined') Phaser.NoAudioContext = true;
|
||||||
|
const game = new Phaser.Game(gameConfig);
|
||||||
|
const out = { steps: [] };
|
||||||
|
window.__PROBE__ = out;
|
||||||
|
const tick = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
const until = async (fn, label, budgetMs) => {
|
||||||
|
const t0 = Date.now();
|
||||||
|
while (Date.now() - t0 < budgetMs) {
|
||||||
|
if (fn()) return true;
|
||||||
|
await tick(250);
|
||||||
|
}
|
||||||
|
out.steps.push(`TIMEOUT: ${label}`);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
game.events.once('ready', async () => {
|
||||||
|
const s = game.scene.getScene('GameScene');
|
||||||
|
if (!await until(() => game.scene.isActive('GameScene') && s.ship && s.commsPanel, 'boot', 180000)) { window.__PROBE__ = out; return; }
|
||||||
|
out.steps.push('booted: ship=' + s.ship.x + ' gates=' + (s.systemContent?.gates?.length ?? 'n/a'));
|
||||||
|
s.scene.start('GameScene');
|
||||||
|
await until(() => !game.scene.isActive('GameScene'), 'shutdown', 120000);
|
||||||
|
out.steps.push('shutdown observed');
|
||||||
|
const up = await until(() => game.scene.isActive('GameScene') && s.ship && s.commsPanel && s.mineralHud, 'reboot', 240000);
|
||||||
|
out.steps.push('reboot ok=' + up + ' active=' + game.scene.isActive('GameScene') + ' ship=' + !!s.ship + ' hud=' + !!s.mineralHud + ' minerals=' + (s.ship && s.ship.minerals));
|
||||||
|
out.steps.push('pageErrors=' + JSON.stringify((window.__PAGE_ERRORS__ || []).slice(0, 6)));
|
||||||
|
window.__PROBE__ = out;
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Orbit — dev double-click skip test</title>
|
||||||
|
<!-- This page lives in /dev, but the game's relative asset paths are rooted
|
||||||
|
at the project root — resolve them against it. ("../" keeps this
|
||||||
|
working even if the project is served from a subdirectory.) -->
|
||||||
|
<base href="../" />
|
||||||
|
<style>
|
||||||
|
html, body { margin: 0; height: 100%; background: #04060d; overflow: hidden; }
|
||||||
|
#game { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
window.__CAPTURED_ERRORS__ = [];
|
||||||
|
window.addEventListener('error', (e) => window.__CAPTURED_ERRORS__.push('window: ' + e.message + ' @ ' + (e.filename||'') + ':' + (e.lineno||'')));
|
||||||
|
window.addEventListener('unhandledrejection', (e) => window.__CAPTURED_ERRORS__.push('rejection: ' + String(e.reason && e.reason.stack || e.reason)));
|
||||||
|
</script>
|
||||||
|
<script src="lib/phaser.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="game"></div>
|
||||||
|
<script type="module" src="dev/surface-skip.mjs"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
/**
|
||||||
|
* DOUBLE-CLICK SKIP (headless browser — NOT a Node test).
|
||||||
|
*
|
||||||
|
* Drives the REAL input pipeline (dispatched mouse events on the canvas
|
||||||
|
* → v4 pointer → the scene's pointerdown handler) and checks:
|
||||||
|
*
|
||||||
|
* 1. a SINGLE click during the landing clip does NOT skip it
|
||||||
|
* (negative control — one press is not a double-click)
|
||||||
|
* 2. a DOUBLE-click (two quick presses) during the landing clip skips
|
||||||
|
* the rest of the clip — the deck comes up now, landing video gone
|
||||||
|
* 3. a DOUBLE-click during the takeoff clip leaves immediately —
|
||||||
|
* SurfaceScene stopped, GameScene woken, the ship where it was
|
||||||
|
*
|
||||||
|
* PUMP-DRIVEN (like dev/landing-click.mjs): this box's headless Firefox
|
||||||
|
* freezes rAF/timers after first paint, so the page is advanced by the
|
||||||
|
* runner's wake — each poll calls __SS_TICK__(), which steps the engine
|
||||||
|
* once and advances the stage machine one step. Real time between polls
|
||||||
|
* (the poll period) is the clock the double-click window is measured
|
||||||
|
* against, so the barrier stages gate on performance.now() (wall clock).
|
||||||
|
*
|
||||||
|
* python3 -m http.server 8123
|
||||||
|
* node dev/cdp-firefox.mjs http://127.0.0.1:8123/dev/surface-skip.html \
|
||||||
|
* 'window.__SS_TICK__(); return window.__SURFACE_SKIP__;' 240000 500
|
||||||
|
*/
|
||||||
|
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';
|
||||||
|
import { SurfaceScene } from '../js/scenes/SurfaceScene.js';
|
||||||
|
|
||||||
|
const data = await ConfigLoader.load();
|
||||||
|
config.init(data);
|
||||||
|
|
||||||
|
const gameConfig = createGameConfig();
|
||||||
|
gameConfig.scene = [GameScene, SurfaceScene];
|
||||||
|
|
||||||
|
const game = new Phaser.Game(gameConfig);
|
||||||
|
window.game = game;
|
||||||
|
|
||||||
|
const out = { pass: false, steps: [], error: null };
|
||||||
|
const step = (label, cond, diag) => {
|
||||||
|
out.steps.push(label);
|
||||||
|
if (!cond) {
|
||||||
|
out.error = `${label}${diag ? ': ' + JSON.stringify(diag).slice(0, 300) : ''}`;
|
||||||
|
window.__SURFACE_SKIP__ = out;
|
||||||
|
throw new Error(out.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const diag = () => {
|
||||||
|
let gs = null, ss = null;
|
||||||
|
try { gs = game.scene?.getScene('GameScene') ?? null; } catch { /* not up yet */ }
|
||||||
|
try { ss = game.scene?.getScene('SurfaceScene') ?? null; } catch { /* not up yet */ }
|
||||||
|
window.__SS_STATE__ = {
|
||||||
|
stage,
|
||||||
|
steps: out.steps.slice(),
|
||||||
|
error: out.error,
|
||||||
|
gs: gs ? { status: gs.sys.getStatus(), ship: !!gs.ship } : 'none',
|
||||||
|
ss: ss ? {
|
||||||
|
status: ss.sys.getStatus(),
|
||||||
|
phase: ss.phase,
|
||||||
|
landVideo: !!ss.landVideo,
|
||||||
|
surfaceVideo: !!ss.surfaceVideo,
|
||||||
|
takeoffVideo: !!ss.takeoffVideo,
|
||||||
|
deck: !!ss.actionBar,
|
||||||
|
lastClickT: ss.lastClickT ?? null,
|
||||||
|
} : 'none',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let stage = 0;
|
||||||
|
let negT = 0; // wall time of the negative-control click (barrier gate)
|
||||||
|
let takeoffT = 0; // wall time the takeoff clip went up (barrier gate)
|
||||||
|
const shipBefore = { x: null, y: null };
|
||||||
|
|
||||||
|
const canvas = () => game.canvas;
|
||||||
|
// A real mouse press at CANVAS screen coords (clickout-test pattern:
|
||||||
|
// v4 binds mousedown/mouseup on the canvas; FIT mode scales, so map
|
||||||
|
// canvas → client space with the rendered rect). `times` = the presses.
|
||||||
|
const press = (x, y, times = 1) => {
|
||||||
|
const canvasEl = canvas();
|
||||||
|
const r = canvasEl.getBoundingClientRect();
|
||||||
|
const zoom = r.width / canvasEl.width;
|
||||||
|
const mk = (type, buttons) => new MouseEvent(type, {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
view: window,
|
||||||
|
button: 0,
|
||||||
|
buttons,
|
||||||
|
clientX: r.left + x * zoom,
|
||||||
|
clientY: r.top + y * zoom,
|
||||||
|
});
|
||||||
|
canvasEl.dispatchEvent(mk('mouseover', 0));
|
||||||
|
for (let i = 0; i < times; i++) {
|
||||||
|
canvasEl.dispatchEvent(mk('mousedown', 1));
|
||||||
|
canvasEl.dispatchEvent(mk('mouseup', 0));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const scene = (key) => { try { return game.scene?.getScene(key) ?? null; } catch { return null; } };
|
||||||
|
|
||||||
|
function tick() {
|
||||||
|
diag();
|
||||||
|
// v4: do NOT step the loop before boot completes (landing-click rule).
|
||||||
|
if (!game.loop || !game.scene) return;
|
||||||
|
try {
|
||||||
|
const gs = scene('GameScene');
|
||||||
|
const ss = scene('SurfaceScene');
|
||||||
|
|
||||||
|
switch (stage) {
|
||||||
|
case 0: { // boot: the flight scene is live
|
||||||
|
if (!(gs && gs.ship)) return;
|
||||||
|
step('boot', true, { frame: gs.planet.sheetFrame });
|
||||||
|
shipBefore.x = gs.ship.x;
|
||||||
|
shipBefore.y = gs.ship.y;
|
||||||
|
gs.startLanding(gs.commsTargetFor(gs.planet));
|
||||||
|
stage = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 1: { // the landing stage is up with its clip in flight
|
||||||
|
if (!(ss && ss.sys.isActive() && ss.phase === 'landing')) return;
|
||||||
|
step('landing-stage', true, { status: ss.sys.getStatus(), phase: ss.phase });
|
||||||
|
step('landing-clip', ss.landVideo !== null, { frame: ss.planetFrame, landKey: ss.landKey });
|
||||||
|
stage = 2;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 2: { // NEGATIVE: one press is not a double-click — no skip
|
||||||
|
press(640, 360, 1);
|
||||||
|
game.loop.step(performance.now()); // flush the queued pointerdown
|
||||||
|
step('single-click-no-skip', ss.phase === 'landing' && ss.landVideo !== null,
|
||||||
|
{ phase: ss.phase, landVideo: !!ss.landVideo });
|
||||||
|
negT = performance.now();
|
||||||
|
stage = 3;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 3: { // barrier: out of the double-click window before the real one
|
||||||
|
if (performance.now() - negT < 600) return;
|
||||||
|
stage = 4;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 4: { // DOUBLE-CLICK the landing clip — the rest of it is skipped
|
||||||
|
press(640, 360, 2);
|
||||||
|
game.loop.step(performance.now());
|
||||||
|
step('double-click-skips-landing',
|
||||||
|
ss.phase === 'surface' && ss.landVideo === null,
|
||||||
|
{ phase: ss.phase, landVideo: !!ss.landVideo });
|
||||||
|
step('deck-came-up', ss.actionBar !== null && ss.sys.isActive() === true,
|
||||||
|
{ deck: !!ss.actionBar, status: ss.sys.getStatus() });
|
||||||
|
// the skipped landing must not have stranded the surface stage:
|
||||||
|
const loopKey = `__surf_loop_${ss.planetFrame}`;
|
||||||
|
step('surface-stage', ss.phase === 'surface' && (ss.cache.video.has(loopKey) ? ss.surfaceVideo !== null : true),
|
||||||
|
{ loopKey, surfaceVideo: !!ss.surfaceVideo });
|
||||||
|
stage = 5;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 5: { // take off — the takeoff clip goes up (or leaves instantly)
|
||||||
|
ss.takeOff();
|
||||||
|
game.loop.step(performance.now());
|
||||||
|
if (ss.sys.isActive()) {
|
||||||
|
step('takeoff-clip', ss.takeoffVideo !== null, { frame: ss.planetFrame });
|
||||||
|
takeoffT = performance.now();
|
||||||
|
stage = 6;
|
||||||
|
} else {
|
||||||
|
step('no-takeoff-clip-left-instantly', gs.sys.isActive() === true, { gsStatus: gs.sys.getStatus() });
|
||||||
|
stage = 8;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 6: { // barrier: out of the double-click window before the real one
|
||||||
|
if (performance.now() - takeoffT < 600) return;
|
||||||
|
stage = 7;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 7: { // DOUBLE-CLICK the takeoff clip — the flight world wakes now
|
||||||
|
press(640, 360, 2);
|
||||||
|
game.loop.step(performance.now());
|
||||||
|
step('double-click-skips-takeoff',
|
||||||
|
ss.sys.isActive() === false && gs.sys.isActive() === true,
|
||||||
|
{ ssStatus: ss.sys.getStatus(), gsStatus: gs.sys.getStatus() });
|
||||||
|
// the run resumed exactly where the ship left it — shared check
|
||||||
|
/* falls through to case 8 */
|
||||||
|
}
|
||||||
|
|
||||||
|
case 8: { // the run resumed exactly where the ship left it
|
||||||
|
step('ship-preserved',
|
||||||
|
Math.abs(gs.ship.x - shipBefore.x) < 1e-6 && Math.abs(gs.ship.y - shipBefore.y) < 1e-6,
|
||||||
|
{ before: shipBefore, after: { x: gs.ship.x, y: gs.ship.y } });
|
||||||
|
out.pass = true;
|
||||||
|
window.__SURFACE_SKIP__ = out;
|
||||||
|
stage = 99;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
out.error = String(err?.message ?? err);
|
||||||
|
window.__SURFACE_SKIP__ = out;
|
||||||
|
stage = 99; // stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.__SS_TICK__ = tick;
|
||||||
|
|
||||||
|
// Hard timeout: a hung flow must not hang the driver forever.
|
||||||
|
const T0 = Date.now();
|
||||||
|
const arm = setTimeout(() => {
|
||||||
|
if (!out.pass) {
|
||||||
|
out.error = out.error ?? `TIMED OUT after ${Math.round((Date.now() - T0) / 1000)}s`;
|
||||||
|
window.__SURFACE_SKIP__ = out;
|
||||||
|
}
|
||||||
|
}, 300000);
|
||||||
|
arm.unref?.();
|
||||||
|
|
@ -813,8 +813,10 @@ The player holds a REPUTATION (standing) on each planet and space station:
|
||||||
(js/reputation/Reputation.js, data/reputation.json)
|
(js/reputation/Reputation.js, data/reputation.json)
|
||||||
- [ ] Landing & exploration: settlements become points of interest you
|
- [ ] Landing & exploration: settlements become points of interest you
|
||||||
can approach (the data — kind, anchor, population — is already there).
|
can approach (the data — kind, anchor, population — is already there).
|
||||||
The comms panel is the door: clicking a planet or space station flies
|
The comms panel is the door: clicking a planet or space station opens
|
||||||
the ship there AND opens a comms panel at the click — the name decodes
|
a comms panel at the click (NOT a fly-here — the ship stays put; a
|
||||||
|
world click is the landing request, the compass autopilot is how the
|
||||||
|
ship flies to a world) — the name decodes
|
||||||
in, the standing bar draws (settled), REQUEST LANDING (gated at standing
|
in, the standing bar draws (settled), REQUEST LANDING (gated at standing
|
||||||
≤ −4) or LAND on an unsettled world + CANCEL (`js/ui/CommsPanel.js`, a
|
≤ −4) or LAND on an unsettled world + CANCEL (`js/ui/CommsPanel.js`, a
|
||||||
rusty-metal frame around a scanlined green CRT). Free-space stations are
|
rusty-metal frame around a scanlined green CRT). Free-space stations are
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,10 @@ export class JumpGate extends Phaser.GameObjects.Container {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.removeChildren();
|
// v4: Container no longer has removeChildren() — removeAll(true) is
|
||||||
|
// the equivalent (detach the art and destroy it); v4's super.destroy()
|
||||||
|
// would do the same for whatever was still attached.
|
||||||
|
this.removeAll(true);
|
||||||
super.destroy();
|
super.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ import { Planet } from './Planet.js';
|
||||||
* Solid like a planet (the ship keeps its clearance — the same plain
|
* Solid like a planet (the ship keeps its clearance — the same plain
|
||||||
* circle rule, GameScene.solids), discoverable (a compass arrow + the
|
* circle rule, GameScene.solids), discoverable (a compass arrow + the
|
||||||
* discovery toast, at the scale of its keepout), and a comms target —
|
* discovery toast, at the scale of its keepout), and a comms target —
|
||||||
* click it and the comms panel opens there while the ship flies to its
|
* click it and the comms panel opens there (the click is NOT a
|
||||||
* edge (GameScene.openCommsPanel).
|
* fly-here — the ship stays put; GameScene.openCommsPanel).
|
||||||
*
|
*
|
||||||
* update(time) turns the ring and breathes the beacon — driven by
|
* update(time) turns the ring and breathes the beacon — driven by
|
||||||
* GameScene.update, like the asteroid clusters.
|
* GameScene.update, like the asteroid clusters.
|
||||||
|
|
@ -194,7 +194,10 @@ export class Station extends Phaser.GameObjects.Container {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.removeChildren();
|
// v4: Container no longer has removeChildren() — removeAll(true) is
|
||||||
|
// the equivalent (detach the art and destroy it); v4's super.destroy()
|
||||||
|
// would do the same for whatever was still attached.
|
||||||
|
this.removeAll(true);
|
||||||
super.destroy();
|
super.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -276,7 +276,7 @@ export class GameScene extends Phaser.Scene {
|
||||||
// the void, rendered as world objects (js/entities/Station.js).
|
// the void, rendered as world objects (js/entities/Station.js).
|
||||||
// Solid (the ship keeps its clearance), discoverable (compass +
|
// Solid (the ship keeps its clearance), discoverable (compass +
|
||||||
// toast), and comms targets — a click opens the comms panel there
|
// toast), and comms targets — a click opens the comms panel there
|
||||||
// (openCommsPanel below) and the ship flies to the rim.
|
// (openCommsPanel below) — it is NOT a fly-here (the ship stays put).
|
||||||
this.systemStations = [];
|
this.systemStations = [];
|
||||||
if (config.get('stations.enabled', true) !== false) {
|
if (config.get('stations.enabled', true) !== false) {
|
||||||
for (const s of this.systemContent.settlements ?? []) {
|
for (const s of this.systemContent.settlements ?? []) {
|
||||||
|
|
@ -562,14 +562,15 @@ export class GameScene extends Phaser.Scene {
|
||||||
// the caret beside it) toggles the dossier open/closed — none of
|
// the caret beside it) toggles the dossier open/closed — none of
|
||||||
// those is a fly-here. A click on an ASTEROID opens the mining
|
// those is a fly-here. A click on an ASTEROID opens the mining
|
||||||
// pop-up (a context menu — its buttons own their clicks; any click
|
// pop-up (a context menu — its buttons own their clicks; any click
|
||||||
// elsewhere closes it and is consumed). Any OTHER click moves the
|
// elsewhere closes it and is consumed). A click on a PLANET or
|
||||||
// ship — which ends the mining state (the beam retracts as the
|
// STATION opens the comms panel — the landing-request window — and
|
||||||
// ship goes; a mid-reach arm aborts).
|
// does nothing else: it is NOT a fly-here (the ship stays put), and
|
||||||
// A click inside a planet clamps to that planet's keep-out rim — the
|
// it does not touch the mining state (no move ⇒ a live beam keeps
|
||||||
// ship can stop at the clearance, never inside. (Worlds don't
|
// running). Any OTHER click moves the ship — which ends the mining
|
||||||
// overlap, so sequential clamping is exact.) A click BEYOND the
|
// state (the beam retracts as the ship goes; a mid-reach arm
|
||||||
// player's tether range clamps to the union boundary — the target
|
// aborts). A click BEYOND the player's tether range clamps to the
|
||||||
// marker lands on the barrier line itself.
|
// union boundary — the target marker lands on the barrier line
|
||||||
|
// itself.
|
||||||
this.input.on('pointerdown', (pointer) => {
|
this.input.on('pointerdown', (pointer) => {
|
||||||
// The save pop-up is MODAL — while it's up it owns all input
|
// The save pop-up is MODAL — while it's up it owns all input
|
||||||
// (its scrim / cards / dialog eat the click; the world stays put).
|
// (its scrim / cards / dialog eat the click; the world stays put).
|
||||||
|
|
@ -626,8 +627,9 @@ export class GameScene extends Phaser.Scene {
|
||||||
// mining menu: a click on one of its buttons is the button's (its
|
// mining menu: a click on one of its buttons is the button's (its
|
||||||
// own pointerdown listener); a click INSIDE it is swallowed (no
|
// own pointerdown listener); a click INSIDE it is swallowed (no
|
||||||
// fly-here); a click on ANOTHER planet/station moves the panel
|
// fly-here); a click on ANOTHER planet/station moves the panel
|
||||||
// there (and the ship flies to it — comms business, not a world
|
// there (the ship does not fly — the click is the panel's, not a
|
||||||
// click); any other click closes it and that click is consumed.
|
// world move); any other click closes it and that click is
|
||||||
|
// consumed.
|
||||||
if (this.commsPanel && this.commsPanel.isOpen) {
|
if (this.commsPanel && this.commsPanel.isOpen) {
|
||||||
if (this.commsPanel.contains(pointer.worldX, pointer.worldY)) return;
|
if (this.commsPanel.contains(pointer.worldX, pointer.worldY)) return;
|
||||||
const obj = this.worldObjectAt(pointer.worldX, pointer.worldY);
|
const obj = this.worldObjectAt(pointer.worldX, pointer.worldY);
|
||||||
|
|
@ -661,11 +663,18 @@ export class GameScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
// A PLANET or STATION under the cursor (a rock click is the mining
|
// A PLANET or STATION under the cursor (a rock click is the mining
|
||||||
// flow above): the ship still flies — to its keep-out rim — AND the
|
// flow above): the click opens the comms panel (the landing-request
|
||||||
// comms panel opens at the click (js/ui/CommsPanel.js).
|
// window, js/ui/CommsPanel.js) and NOTHING ELSE — it is not a
|
||||||
|
// fly-here (the ship stays put), and it does not end the mining
|
||||||
|
// state (no move, so a live beam keeps running).
|
||||||
const obj = this.worldObjectAt(pointer.worldX, pointer.worldY);
|
const obj = this.worldObjectAt(pointer.worldX, pointer.worldY);
|
||||||
|
if (obj) {
|
||||||
|
this.hideHint();
|
||||||
|
this.openCommsPanel(obj, pointer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Any other click MOVES the ship — which ends the mining state
|
// Any OTHER click MOVES the ship — which ends the mining state
|
||||||
// (the beam retracts as the ship goes; a mid-reach arm aborts).
|
// (the beam retracts as the ship goes; a mid-reach arm aborts).
|
||||||
// The state exit is signalled via ship.onStateChange above.
|
// The state exit is signalled via ship.onStateChange above.
|
||||||
if (this.mining.isActive) this.mining.stop();
|
if (this.mining.isActive) this.mining.stop();
|
||||||
|
|
@ -678,7 +687,6 @@ export class GameScene extends Phaser.Scene {
|
||||||
this.showTargetMarker(aim.x, aim.y);
|
this.showTargetMarker(aim.x, aim.y);
|
||||||
this.ship.setTarget(aim.x, aim.y);
|
this.ship.setTarget(aim.x, aim.y);
|
||||||
this.hideHint();
|
this.hideHint();
|
||||||
if (obj) this.openCommsPanel(obj, pointer); // comms: the panel opens at the click
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ESC: the topmost open thing closes — the confirm dialog, then the
|
// ESC: the topmost open thing closes — the confirm dialog, then the
|
||||||
|
|
@ -1410,12 +1418,12 @@ export class GameScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A planet or station was clicked (the ship is already flying to its
|
* A planet or station was clicked: the comms panel opens at the click
|
||||||
* rim — the click-to-fly path did that): the comms panel opens at the
|
* — the name decodes in, the reputation bar draws (settled), the
|
||||||
* click — the name decodes in, the reputation bar draws (settled),
|
* landing buttons wait. The click is NOT a fly-here — the ship stays
|
||||||
* the landing buttons wait. The panel is world-anchored at the click
|
* exactly where it is. The panel is world-anchored at the click point
|
||||||
* point and picks a side (up/down/left/right) that keeps it fully on
|
* and picks a side (up/down/left/right) that keeps it fully on screen
|
||||||
* screen (CommsPanel.pickSideAndPlace).
|
* (CommsPanel.pickSideAndPlace).
|
||||||
*/
|
*/
|
||||||
openCommsPanel(obj, pointer) {
|
openCommsPanel(obj, pointer) {
|
||||||
const t = this.commsTargetFor(obj);
|
const t = this.commsTargetFor(obj);
|
||||||
|
|
@ -2154,10 +2162,11 @@ export class GameScene extends Phaser.Scene {
|
||||||
/**
|
/**
|
||||||
* Apply a staged restore (js/save/SaveData.js → prepareLoad parked it):
|
* Apply a staged restore (js/save/SaveData.js → prepareLoad parked it):
|
||||||
* the ship back where it was — and with its hold full or empty as saved
|
* the ship back where it was — and with its hold full or empty as saved
|
||||||
* (ship.minerals) — the saved tether field (the level-1 home tether is
|
* (ship.minerals — the upper-right mineral readout follows) — the saved
|
||||||
* just a saved entry — the field is rebuilt from the record), and the
|
* tether field (the level-1 home tether is just a saved entry — the
|
||||||
* saved session time. Discovery + galaxy are already restored in the
|
* field is rebuilt from the record), and the saved session time.
|
||||||
* registry (this scene's create() read them).
|
* Discovery + galaxy are already restored in the registry (this scene's
|
||||||
|
* create() read them).
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* The `starting` installs (data/builds.json → `starting`): rule-level
|
* The `starting` installs (data/builds.json → `starting`): rule-level
|
||||||
|
|
@ -2186,6 +2195,11 @@ export class GameScene extends Phaser.Scene {
|
||||||
// setMinerals clamps to the ship's capacity.
|
// setMinerals clamps to the ship's capacity.
|
||||||
if (typeof r.ship.minerals === 'number') this.ship.setMinerals(r.ship.minerals);
|
if (typeof r.ship.minerals === 'number') this.ship.setMinerals(r.ship.minerals);
|
||||||
}
|
}
|
||||||
|
// The corner readout (upper right) was built with the FRESH hold (0)
|
||||||
|
// earlier in create() — push the restored value through it. No-op
|
||||||
|
// when the save held nothing (set() skips unchanged values — no
|
||||||
|
// count-up, no flash), so mineral-less saves stay quiet.
|
||||||
|
this.refreshMineralHud();
|
||||||
if (Array.isArray(r.tethers) && r.tethers.length > 0) {
|
if (Array.isArray(r.tethers) && r.tethers.length > 0) {
|
||||||
for (const t of [...this.tetherField.tethers]) this.tetherField.remove(t.id);
|
for (const t of [...this.tetherField.tethers]) this.tetherField.remove(t.id);
|
||||||
for (const t of r.tethers) {
|
for (const t of r.tethers) {
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,22 @@ import { SaveManager } from '../save/SaveManager.js';
|
||||||
import { BuildWindow } from '../ui/BuildWindow.js';
|
import { BuildWindow } from '../ui/BuildWindow.js';
|
||||||
import { buildDefs } from '../research/ResearchModel.js';
|
import { buildDefs } from '../research/ResearchModel.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The double-click window (ms): two presses this close together during a
|
||||||
|
* one-shot flight clip (the landing or the takeoff) skip the rest of the
|
||||||
|
* clip (onPointerDown → skipFlightClip). On the deck itself there is no
|
||||||
|
* clip in flight, so the skip is a no-op there.
|
||||||
|
*/
|
||||||
|
const DOUBLE_CLICK_MS = 350;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SURFACE — the planet's surface, started ON TOP of the sleeping
|
* SURFACE — the planet's surface, started ON TOP of the sleeping
|
||||||
* GameScene (GameScene.startLanding → launch('SurfaceScene') + sleep):
|
* GameScene (GameScene.startLanding → launch('SurfaceScene') + sleep):
|
||||||
*
|
*
|
||||||
* 1. LANDING — the full-screen one-shot clip (data/landing.json →
|
* 1. LANDING — the full-screen one-shot clip (data/landing.json →
|
||||||
* videos[planets.png frame] → `land`), cover-scaled over an opaque
|
* videos[planets.png frame] → `land`), cover-scaled over an opaque
|
||||||
* plate. No UI during the flight down.
|
* plate. No UI during the flight down. A double-click (two quick
|
||||||
|
* presses) skips the rest of the clip — the deck comes up now.
|
||||||
* 2. SURFACE — the clip is swapped for the looping world clip (`surface`),
|
* 2. SURFACE — the clip is swapped for the looping world clip (`surface`),
|
||||||
* and the command deck appears: the GameScene deck with Research
|
* and the command deck appears: the GameScene deck with Research
|
||||||
* replaced by SHOP (swaps the loop for the world's `shop` clip —
|
* replaced by SHOP (swaps the loop for the world's `shop` clip —
|
||||||
|
|
@ -33,7 +42,8 @@ import { buildDefs } from '../research/ResearchModel.js';
|
||||||
* 3. TAKE OFF — the world's one-shot take-off clip (`takeoff`) plays,
|
* 3. TAKE OFF — the world's one-shot take-off clip (`takeoff`) plays,
|
||||||
* then this scene stops — the paused GameScene resumes exactly where
|
* then this scene stops — the paused GameScene resumes exactly where
|
||||||
* it was (ship, tethers, discovery, standing, everything). A world
|
* it was (ship, tethers, discovery, standing, everything). A world
|
||||||
* with no `takeoff` clip leaves immediately.
|
* with no `takeoff` clip leaves immediately. A double-click skips
|
||||||
|
* the rest of the clip — the flight world wakes now.
|
||||||
*
|
*
|
||||||
* Video selection is by the planet's planets.png SHEET FRAME (the same
|
* Video selection is by the planet's planets.png SHEET FRAME (the same
|
||||||
* index its sprite uses — frames 0..2 the terran worlds, 3..5 the gas
|
* index its sprite uses — frames 0..2 the terran worlds, 3..5 the gas
|
||||||
|
|
@ -55,6 +65,7 @@ export class SurfaceScene extends Phaser.Scene {
|
||||||
this.planetType = String(data.type ?? ''); // e.g. "Rocky World" (planets.typeLabels)
|
this.planetType = String(data.type ?? ''); // e.g. "Rocky World" (planets.typeLabels)
|
||||||
this.tetherLevel = Math.max(0, Math.round(Number(data.tetherLevel ?? 0)));
|
this.tetherLevel = Math.max(0, Math.round(Number(data.tetherLevel ?? 0)));
|
||||||
this.phase = 'landing'; // 'landing' → 'surface'
|
this.phase = 'landing'; // 'landing' → 'surface'
|
||||||
|
this.lastClickT = 0; // last pointerdown (performance.now(), ms) — the double-click skip
|
||||||
this.musicSpec = 'music.frames.' + this.planetFrame; // the surface loop (data/music.json)
|
this.musicSpec = 'music.frames.' + this.planetFrame; // the surface loop (data/music.json)
|
||||||
this.gameScene = null; // the paused GameScene beneath us (save state lives there)
|
this.gameScene = null; // the paused GameScene beneath us (save state lives there)
|
||||||
this.backdrop = null;
|
this.backdrop = null;
|
||||||
|
|
@ -127,6 +138,13 @@ export class SurfaceScene extends Phaser.Scene {
|
||||||
// hum's stop is also hooked on the event (Return to Menu path).
|
// hum's stop is also hooked on the event (Return to Menu path).
|
||||||
this.events.once('shutdown', () => this.setSurfaceMusic(false));
|
this.events.once('shutdown', () => this.setSurfaceMusic(false));
|
||||||
|
|
||||||
|
// Pointer input — bound once per visit from the LANDING stage on
|
||||||
|
// (the scene's input plugin drops its listeners on shutdown, v4, so
|
||||||
|
// this is fresh every launch; buildDeck no longer re-binds it):
|
||||||
|
// a double-click skips the in-flight clip (skipFlightClip), and on
|
||||||
|
// the deck the handler owns the sub-bar / outside-click behavior.
|
||||||
|
this.input.on('pointerdown', (pointer) => this.onPointerDown(pointer));
|
||||||
|
|
||||||
if (this.hasVideo(this.landKey)) {
|
if (this.hasVideo(this.landKey)) {
|
||||||
this.playLanding();
|
this.playLanding();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -506,11 +524,9 @@ export class SurfaceScene extends Phaser.Scene {
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// ESC — topmost open thing first (mirror of GameScene.escAction).
|
// ESC — topmost open thing first (mirror of GameScene.escAction).
|
||||||
|
// (The pointerdown binding lives in create() — it must also be
|
||||||
|
// live on the landing stage, before the deck exists.)
|
||||||
this.input.keyboard?.on('keydown-ESC', () => this.escAction());
|
this.input.keyboard?.on('keydown-ESC', () => this.escAction());
|
||||||
|
|
||||||
// Clicks: outside the sub-bar, close it. (No click-to-fly on the
|
|
||||||
// surface — the world is a backdrop.)
|
|
||||||
this.input.on('pointerdown', (pointer) => this.onPointerDown(pointer));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
@ -723,6 +739,15 @@ export class SurfaceScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
onPointerDown(pointer) {
|
onPointerDown(pointer) {
|
||||||
|
// Double-click (two presses inside the window) — the rest of the
|
||||||
|
// in-flight one-shot clip is skipped: a landing in flight lands now,
|
||||||
|
// a takeoff in flight leaves now (a no-op on the deck, where no clip
|
||||||
|
// is in flight). Checked before the UI ownership rules — a flight
|
||||||
|
// clip owns the whole screen anyway (no deck up during one).
|
||||||
|
const now = performance.now();
|
||||||
|
if (now - this.lastClickT <= DOUBLE_CLICK_MS) this.skipFlightClip();
|
||||||
|
this.lastClickT = now;
|
||||||
|
|
||||||
if (this.buildWindow?.isOpen) return; // the console owns the click
|
if (this.buildWindow?.isOpen) return; // the console owns the click
|
||||||
if (this.savePanel?.isOpen) return; // the modal scrim owns the click
|
if (this.savePanel?.isOpen) return; // the modal scrim owns the click
|
||||||
if (this.menuSubBar?.isOpen) {
|
if (this.menuSubBar?.isOpen) {
|
||||||
|
|
@ -734,6 +759,27 @@ export class SurfaceScene extends Phaser.Scene {
|
||||||
// A click on the surface itself — the seam for surface interactions.
|
// A click on the surface itself — the seam for surface interactions.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The double-click skip — the remainder of the in-flight one-shot
|
||||||
|
* clip, dropped: a landing in flight lands now (startSurface — the
|
||||||
|
* clip's complete/error guards see the phase change and stand down),
|
||||||
|
* a takeoff in flight leaves now (finishTakeoff — idempotent). The
|
||||||
|
* tick plays before the cut, so a skip always answers with a sound.
|
||||||
|
* A no-op on the surface (no clip in flight) — the window keeps
|
||||||
|
* ticking there, but the skip never fires.
|
||||||
|
*/
|
||||||
|
skipFlightClip() {
|
||||||
|
if (this.phase === 'landing' && this.landVideo) {
|
||||||
|
this.playSfx('ui_click');
|
||||||
|
this.startSurface();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.takeoffVideo) {
|
||||||
|
this.playSfx('ui_click');
|
||||||
|
this.finishTakeoff();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Plumbing
|
// Plumbing
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue