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.",
|
||||
"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).",
|
||||
"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",
|
||||
"greyed out in comms, same as a reputation ban)."
|
||||
],
|
||||
|
|
@ -41,8 +44,8 @@
|
|||
},
|
||||
"videos": [
|
||||
{ "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-03.mp4", "surface": "terran-surface-03.mp4", "takeoff": "terran-takeoff-03.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-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-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" },
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
/**
|
||||
* 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
|
||||
* 3. rep ≤ −4, click REQUEST LANDING → inert (panel stays open)
|
||||
* 4. click outside → closes
|
||||
|
|
@ -163,7 +165,7 @@ window.__CLICK_TEST__ = async () => {
|
|||
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;
|
||||
s.ship.x = 700; s.ship.y = 650;
|
||||
frame(hx, hy, hsx, hsy);
|
||||
|
|
@ -172,9 +174,9 @@ window.__CLICK_TEST__ = async () => {
|
|||
clickAtClient(c1.clientX, c1.clientY);
|
||||
await pump(s, () => s.commsPanel.isOpen && s.commsPanel.alpha >= 0.999 && s.commsPanel.nameDec === null, 4000);
|
||||
step(
|
||||
'click object → panel opens + ship retargeted',
|
||||
s.commsPanel.isOpen && s.ship.target !== null && s.commsPanel.lastTarget?.name === s.planet.discoveryName.toUpperCase(),
|
||||
{ shipTarget: s.ship.target, panelName: s.commsPanel._name },
|
||||
'click object → panel opens + ship does NOT fly',
|
||||
s.commsPanel.isOpen && s.ship.target === null && s.commsPanel._name === s.planet.discoveryName.toUpperCase(),
|
||||
{ shipTarget: s.ship.target, panelName: s.commsPanel._name, planetName: s.planet.discoveryName },
|
||||
);
|
||||
|
||||
// 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);
|
||||
|
||||
// ---- 6. Enabled REQUEST LANDING fires the seam + closes ---------------
|
||||
// (Home is pinned +20 — canLand is true; the seam logs the action.)
|
||||
let logged = '';
|
||||
const orig = console.info;
|
||||
console.info = (...a) => {
|
||||
logged += ' ' + a.join(' ');
|
||||
orig(...a);
|
||||
};
|
||||
// (Home is pinned +20 — canLand is true. For a planet target the seam is
|
||||
// startLanding() — this harness registers ONLY GameScene (no
|
||||
// SurfaceScene), so stub it and assert the seam fired with the
|
||||
// planet's payload (the landing-click.mjs harness runs the real one).
|
||||
let landingArgs = null;
|
||||
const origLanding = s.startLanding;
|
||||
s.startLanding = (t) => { landingArgs = t; };
|
||||
s.openCommsPanel(s.planet, { worldX: hx, worldY: hy, x: 640, y: 300 });
|
||||
await pump(s, () => s.commsPanel.isOpen && s.commsPanel.alpha >= 0.999, 4000);
|
||||
const b6 = btn1();
|
||||
clickAtClient(b6.clientX, b6.clientY);
|
||||
await pump(s, () => !s.commsPanel.isOpen, 2500);
|
||||
console.info = orig;
|
||||
s.startLanding = origLanding;
|
||||
step(
|
||||
'click REQUEST LANDING (enabled) → seam fires + closes',
|
||||
!s.commsPanel.isOpen && logged.includes('request-landing'),
|
||||
{ logged: logged.trim() },
|
||||
!s.commsPanel.isOpen &&
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
* pop-up → LOAD confirm → the staged restore rides into a
|
||||
* 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
|
||||
* `window.__SAVES_UI__` for the CDP runner (dev/cdp-firefox.mjs):
|
||||
|
|
@ -101,9 +103,33 @@ const press = (x, y) => {
|
|||
const run = async () => {
|
||||
const scene = game.scene.getScene('GameScene');
|
||||
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('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 --------------------------------
|
||||
scene.menuAction();
|
||||
await wait(450);
|
||||
|
|
@ -171,6 +197,7 @@ const run = async () => {
|
|||
await wait(120);
|
||||
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('the bank captured the hold (37)', rec1.ship.minerals === 37);
|
||||
// Capture fidelity: the bank's session time is the scene's at capture
|
||||
// (a frame or two may have ticked since — small tolerance).
|
||||
check('the bank captured the session time',
|
||||
|
|
@ -225,6 +252,15 @@ const run = async () => {
|
|||
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);
|
||||
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
|
||||
// the session time accumulated since GameScene.create (the wait above +
|
||||
// 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)
|
||||
- [ ] Landing & exploration: settlements become points of interest you
|
||||
can approach (the data — kind, anchor, population — is already there).
|
||||
The comms panel is the door: clicking a planet or space station flies
|
||||
the ship there AND opens a comms panel at the click — the name decodes
|
||||
The comms panel is the door: clicking a planet or space station opens
|
||||
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
|
||||
≤ −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
|
||||
|
|
|
|||
|
|
@ -214,7 +214,10 @@ export class JumpGate extends Phaser.GameObjects.Container {
|
|||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import { Planet } from './Planet.js';
|
|||
* Solid like a planet (the ship keeps its clearance — the same plain
|
||||
* circle rule, GameScene.solids), discoverable (a compass arrow + the
|
||||
* 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
|
||||
* edge (GameScene.openCommsPanel).
|
||||
* click it and the comms panel opens there (the click is NOT a
|
||||
* fly-here — the ship stays put; GameScene.openCommsPanel).
|
||||
*
|
||||
* update(time) turns the ring and breathes the beacon — driven by
|
||||
* GameScene.update, like the asteroid clusters.
|
||||
|
|
@ -194,7 +194,10 @@ export class Station extends Phaser.GameObjects.Container {
|
|||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ export class GameScene extends Phaser.Scene {
|
|||
// the void, rendered as world objects (js/entities/Station.js).
|
||||
// Solid (the ship keeps its clearance), discoverable (compass +
|
||||
// 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 = [];
|
||||
if (config.get('stations.enabled', true) !== false) {
|
||||
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
|
||||
// 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
|
||||
// elsewhere closes it and is consumed). Any OTHER click moves the
|
||||
// ship — which ends the mining state (the beam retracts as the
|
||||
// ship goes; a mid-reach arm aborts).
|
||||
// A click inside a planet clamps to that planet's keep-out rim — the
|
||||
// ship can stop at the clearance, never inside. (Worlds don't
|
||||
// overlap, so sequential clamping is exact.) A click BEYOND the
|
||||
// player's tether range clamps to the union boundary — the target
|
||||
// marker lands on the barrier line itself.
|
||||
// elsewhere closes it and is consumed). A click on a PLANET or
|
||||
// STATION opens the comms panel — the landing-request window — and
|
||||
// does nothing else: it is NOT a fly-here (the ship stays put), and
|
||||
// it does not touch the mining state (no move ⇒ a live beam keeps
|
||||
// running). Any OTHER click moves the ship — which ends the mining
|
||||
// state (the beam retracts as the ship goes; a mid-reach arm
|
||||
// aborts). A click BEYOND the player's tether range clamps to the
|
||||
// union boundary — the target marker lands on the barrier line
|
||||
// itself.
|
||||
this.input.on('pointerdown', (pointer) => {
|
||||
// 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).
|
||||
|
|
@ -626,8 +627,9 @@ export class GameScene extends Phaser.Scene {
|
|||
// mining menu: a click on one of its buttons is the button's (its
|
||||
// own pointerdown listener); a click INSIDE it is swallowed (no
|
||||
// fly-here); a click on ANOTHER planet/station moves the panel
|
||||
// there (and the ship flies to it — comms business, not a world
|
||||
// click); any other click closes it and that click is consumed.
|
||||
// there (the ship does not fly — the click is the panel's, not a
|
||||
// world move); any other click closes it and that click is
|
||||
// consumed.
|
||||
if (this.commsPanel && this.commsPanel.isOpen) {
|
||||
if (this.commsPanel.contains(pointer.worldX, pointer.worldY)) return;
|
||||
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
|
||||
// flow above): the ship still flies — to its keep-out rim — AND the
|
||||
// comms panel opens at the click (js/ui/CommsPanel.js).
|
||||
// flow above): the click opens the comms panel (the landing-request
|
||||
// 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);
|
||||
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 state exit is signalled via ship.onStateChange above.
|
||||
if (this.mining.isActive) this.mining.stop();
|
||||
|
|
@ -678,7 +687,6 @@ export class GameScene extends Phaser.Scene {
|
|||
this.showTargetMarker(aim.x, aim.y);
|
||||
this.ship.setTarget(aim.x, aim.y);
|
||||
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
|
||||
|
|
@ -1410,12 +1418,12 @@ export class GameScene extends Phaser.Scene {
|
|||
}
|
||||
|
||||
/**
|
||||
* A planet or station was clicked (the ship is already flying to its
|
||||
* rim — the click-to-fly path did that): the comms panel opens at the
|
||||
* click — the name decodes in, the reputation bar draws (settled),
|
||||
* the landing buttons wait. The panel is world-anchored at the click
|
||||
* point and picks a side (up/down/left/right) that keeps it fully on
|
||||
* screen (CommsPanel.pickSideAndPlace).
|
||||
* A planet or station was clicked: the comms panel opens at the click
|
||||
* — the name decodes in, the reputation bar draws (settled), the
|
||||
* landing buttons wait. The click is NOT a fly-here — the ship stays
|
||||
* exactly where it is. The panel is world-anchored at the click point
|
||||
* and picks a side (up/down/left/right) that keeps it fully on screen
|
||||
* (CommsPanel.pickSideAndPlace).
|
||||
*/
|
||||
openCommsPanel(obj, pointer) {
|
||||
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):
|
||||
* 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
|
||||
* just a saved entry — the field is rebuilt from the record), and the
|
||||
* saved session time. Discovery + galaxy are already restored in the
|
||||
* registry (this scene's create() read them).
|
||||
* (ship.minerals — the upper-right mineral readout follows) — the saved
|
||||
* tether field (the level-1 home tether is just a saved entry — the
|
||||
* field is rebuilt from the record), and the saved session time.
|
||||
* Discovery + galaxy are already restored in the registry (this scene's
|
||||
* create() read them).
|
||||
*/
|
||||
/**
|
||||
* 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.
|
||||
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) {
|
||||
for (const t of [...this.tetherField.tethers]) this.tetherField.remove(t.id);
|
||||
for (const t of r.tethers) {
|
||||
|
|
|
|||
|
|
@ -12,13 +12,22 @@ import { SaveManager } from '../save/SaveManager.js';
|
|||
import { BuildWindow } from '../ui/BuildWindow.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
|
||||
* GameScene (GameScene.startLanding → launch('SurfaceScene') + sleep):
|
||||
*
|
||||
* 1. LANDING — the full-screen one-shot clip (data/landing.json →
|
||||
* 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`),
|
||||
* and the command deck appears: the GameScene deck with Research
|
||||
* 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,
|
||||
* then this scene stops — the paused GameScene resumes exactly where
|
||||
* 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
|
||||
* 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.tetherLevel = Math.max(0, Math.round(Number(data.tetherLevel ?? 0)));
|
||||
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.gameScene = null; // the paused GameScene beneath us (save state lives there)
|
||||
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).
|
||||
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)) {
|
||||
this.playLanding();
|
||||
} else {
|
||||
|
|
@ -506,11 +524,9 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
: null;
|
||||
|
||||
// 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());
|
||||
|
||||
// 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) {
|
||||
// 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.savePanel?.isOpen) return; // the modal scrim owns the click
|
||||
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.
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
// ------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in New Issue