Restore in-place on confirmed load instead of cutting to the menu
- A confirmed LOAD used to hand off to the main menu, relying on the menu's New Game to apply the staged restore; that path resets run state, so the load never carried the restore through. - GameScene and SurfaceScene now add reloadRun(): close the save panel and sub-bar, then restart (GameScene) or re-start (SurfaceScene) the game scene so the fresh create() picks up the staged restore — the ship lands back exactly where it was saved. - saves-ui-test now asserts the in-place restore (ship back at the saved position) and covers the Return to Main Menu path. - New CDP probe/repro scripts in dev/ drive the real index.html in headless Chrome to exercise the save/load user flow and diagnose input and confirm-geometry issues.
This commit is contained in:
parent
d3fadcf6a5
commit
1a12fa5e1a
|
|
@ -0,0 +1,41 @@
|
|||
// Run a saves-ui-test page in headless Chrome (CDP) and print the result.
|
||||
// node dev/cdp-chrome.mjs <url> [timeoutMs=240000]
|
||||
import { launch, CDP } from './cdpshot.mjs';
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const [url, timeoutMs = '240000'] = process.argv.slice(2);
|
||||
const t0 = Date.now();
|
||||
const { url: wsUrl, kill } = await launch(url);
|
||||
const cdp = new CDP(wsUrl);
|
||||
try {
|
||||
await cdp.open();
|
||||
await cdp.send('Runtime.enable');
|
||||
await cdp.send('Page.enable');
|
||||
let out = null;
|
||||
const poll = async () => {
|
||||
try {
|
||||
const r = await cdp.send('Runtime.evaluate', { expression: 'window.__SAVES_UI__', returnByValue: true });
|
||||
if (r.result && r.result.value) out = r.result.value;
|
||||
} catch { /* page still booting */ }
|
||||
};
|
||||
while (Date.now() - t0 < Number(timeoutMs)) {
|
||||
await poll();
|
||||
if (out) break;
|
||||
await sleep(500);
|
||||
}
|
||||
if (!out) {
|
||||
// Dump captured console errors to help diagnose.
|
||||
try {
|
||||
const r = await cdp.send('Runtime.evaluate', { expression: 'JSON.stringify({errors: window.__CAPTURED_ERRORS__, logs: (window.__CAPTURED_LOGS__||[]).slice(-30)})', returnByValue: true });
|
||||
console.log('PAGE STATE:\n', r.result && r.result.value);
|
||||
} catch {}
|
||||
console.log('TIMEOUT — __SAVES_UI__ never set');
|
||||
process.exitCode = 2;
|
||||
} else {
|
||||
console.log(JSON.stringify(out, null, 2));
|
||||
if (!out.pass) process.exitCode = 1;
|
||||
}
|
||||
} finally {
|
||||
cdp.close();
|
||||
kill();
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
import { launch, CDP } from './cdpshot.mjs';
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const { url: wsUrl, kill } = await launch('http://localhost:8080/index.html', undefined, 1280, 720, 5000);
|
||||
const cdp = new CDP(wsUrl);
|
||||
await cdp.open();
|
||||
await cdp.send('Runtime.enable');
|
||||
const ev = async (expression) => {
|
||||
const r = await cdp.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
|
||||
if (r.exceptionDetails) throw new Error('eval: ' + (r.exceptionDetails.exception?.description ?? r.exceptionDetails.text));
|
||||
return r.result?.value;
|
||||
};
|
||||
const toClient = (x, y) =>
|
||||
`(() => { const c = window.game.canvas; const r = c.getBoundingClientRect(); const z = r.width / c.width; return [r.left + ${x} * z, r.top + ${y} * z]; })()`;
|
||||
async function click(cx, cy) {
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: cx, y: cy });
|
||||
await sleep(70);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: cx, y: cy, button: 'left', clickCount: 1 });
|
||||
await sleep(100);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: cx, y: cy, button: 'left', clickCount: 1 });
|
||||
await sleep(70);
|
||||
}
|
||||
const clickGame = async (x, y) => { const [cx, cy] = await ev(toClient(x, y)); await click(cx, cy); };
|
||||
const log = (...a) => console.log(...a);
|
||||
|
||||
let menuUp = false;
|
||||
for (let i = 0; i < 120 && !menuUp; i++) { await sleep(500); menuUp = await ev(`!!(window.game && window.game.scene.isActive('MenuScene'))`); }
|
||||
await sleep(4600);
|
||||
// seed a save first via direct call (faster) so the LOAD button is enabled
|
||||
await ev(`(() => { const gs = window.game.scene; const m = gs.getScene('MenuScene'); return true; })()`);
|
||||
const mb = await ev(`(() => { const m = window.game.scene.getScene('MenuScene'); return [m.newGameBtn.x, m.newGameBtn.y]; })()`);
|
||||
await clickGame(mb[0], mb[1]);
|
||||
let gameUp = false;
|
||||
for (let i = 0; i < 240 && !gameUp; i++) { await sleep(500); gameUp = await ev(`!!(window.game && window.game.scene.isActive('GameScene') && window.game.scene.getScene('GameScene').ship)`); }
|
||||
if (!gameUp) throw new Error('no game');
|
||||
await sleep(1500);
|
||||
|
||||
// ack comms until deck live
|
||||
async function deckLive() {
|
||||
return await ev(`(() => { const gs = window.game.scene.getScene('GameScene'); const s = gs.actionBar.slots.find((x) => x.id === 'menu'); return !!(s && s.panel.input && s.panel.input.enabled); })()`);
|
||||
}
|
||||
for (let i = 0; i < 20 && !(await deckLive()); i++) {
|
||||
const btn = await ev(`(() => { const gs = window.game.scene.getScene('GameScene'); const b = gs.comms?.box; return b && b.button ? [b.x + b.button.x, b.y + b.button.y] : null; })()`);
|
||||
if (btn) { await clickGame(btn[0], btn[1]); }
|
||||
await sleep(1200);
|
||||
}
|
||||
log('deck live:', await deckLive());
|
||||
|
||||
// MENU open
|
||||
const menuSlot = await ev(`(() => { const gs = window.game.scene.getScene('GameScene'); const s = gs.actionBar.slots.find((s) => s.id === 'menu'); return [s.slot.x, s.slot.y]; })()`);
|
||||
await clickGame(menuSlot[0], menuSlot[1]);
|
||||
await sleep(700);
|
||||
const sub = await ev(`(() => { const gs = window.game.scene.getScene('GameScene'); const bar = gs.menuSubBar; return bar.isOpen ? bar.buttons.map((b) => [b.id, bar.x + b.btn.x, bar.y + b.btn.y]) : null; })()`);
|
||||
log('sub-bar:', JSON.stringify(sub));
|
||||
// SAVE to slot 3 first (so LOAD has data)
|
||||
const saveBtn = sub.find((b) => b[0] === 'save');
|
||||
await clickGame(saveBtn[1], saveBtn[2]);
|
||||
await sleep(1400);
|
||||
const cardS = await ev(`(() => { const gs = window.game.scene.getScene('GameScene'); const c = gs.savePanel.cards[2]; return [gs.savePanel.x + c.x, gs.savePanel.y + c.y]; })()`);
|
||||
await clickGame(cardS[0], cardS[1]);
|
||||
await sleep(900);
|
||||
log('slot3 saved:', await ev(`!!window.game.scene.getScene('GameScene').saveManager.get(3)`));
|
||||
const cancelS = await ev(`(() => { const gs = window.game.scene.getScene('GameScene'); const b = gs.savePanel.cancelBtn; return [gs.savePanel.x + b.x, gs.savePanel.y + b.y]; })()`);
|
||||
await clickGame(cancelS[0], cancelS[1]);
|
||||
await sleep(900);
|
||||
// reopen MENU -> Load (sub-bar may still be open from the save step)
|
||||
const subbarState = await ev(`window.game.scene.getScene('GameScene').menuSubBar?.isOpen`);
|
||||
if (!subbarState) { await clickGame(menuSlot[0], menuSlot[1]); await sleep(700); }
|
||||
const sub2 = await ev(`(() => { const gs = window.game.scene.getScene('GameScene'); const bar = gs.menuSubBar; return bar.isOpen ? bar.buttons.map((b) => [b.id, bar.x + b.btn.x, bar.y + b.btn.y]) : null; })()`);
|
||||
if (!sub2) throw new Error('sub-bar not open');
|
||||
const loadBtn = sub2.find((b) => b[0] === 'load');
|
||||
await clickGame(loadBtn[1], loadBtn[2]);
|
||||
await sleep(1400);
|
||||
log('panel state:', await ev(`window.game.scene.getScene('GameScene').savePanel.state`));
|
||||
// click slot 3 card
|
||||
const card = await ev(`(() => { const gs = window.game.scene.getScene('GameScene'); const c = gs.savePanel.cards[2]; return [gs.savePanel.x + c.x, gs.savePanel.y + c.y, c.disabled]; })()`);
|
||||
log('card:', JSON.stringify(card));
|
||||
await clickGame(card[0], card[1]);
|
||||
await sleep(900);
|
||||
// DUMP confirm overlay geometry chain
|
||||
const geom = await ev(`(() => {
|
||||
const gs = window.game.scene.getScene('GameScene');
|
||||
const c = gs.savePanel.confirm;
|
||||
const info = (o) => ({ x: o.x, y: o.y, w: o.width ?? null, h: o.height ?? null, input: o.input ? o.input.enabled : null });
|
||||
return {
|
||||
panel: { x: gs.savePanel.x, y: gs.savePanel.y },
|
||||
confirm: info(c),
|
||||
confirmBtn: info(c.confirmBtn),
|
||||
cancelBtn: info(c.cancelBtn),
|
||||
scrim: info(c.scrim),
|
||||
confirmState: c.state,
|
||||
};
|
||||
})()`);
|
||||
console.log(JSON.stringify(geom, null, 1));
|
||||
cdp.close();
|
||||
kill();
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import { launch, CDP } from './cdpshot.mjs';
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const { url: wsUrl, kill } = await launch('http://localhost:8080/index.html', undefined, 1280, 720, 5000);
|
||||
const cdp = new CDP(wsUrl);
|
||||
await cdp.open();
|
||||
await cdp.send('Runtime.enable');
|
||||
const ev = async (expression) => {
|
||||
const r = await cdp.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
|
||||
if (r.exceptionDetails) throw new Error('eval: ' + (r.exceptionDetails.exception?.description ?? r.exceptionDetails.text));
|
||||
return r.result?.value;
|
||||
};
|
||||
const toClient = (x, y) =>
|
||||
`(() => { const c = window.game.canvas; const r = c.getBoundingClientRect(); const z = r.width / c.width; return [r.left + ${x} * z, r.top + ${y} * z]; })()`;
|
||||
async function click(cx, cy) {
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: cx, y: cy });
|
||||
await sleep(70);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: cx, y: cy, button: 'left', clickCount: 1 });
|
||||
await sleep(100);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: cx, y: cy, button: 'left', clickCount: 1 });
|
||||
await sleep(70);
|
||||
}
|
||||
|
||||
let menuUp = false;
|
||||
for (let i = 0; i < 120 && !menuUp; i++) { await sleep(500); menuUp = await ev(`!!(window.game && window.game.scene.isActive('MenuScene'))`); }
|
||||
if (!menuUp) throw new Error('no menu');
|
||||
await sleep(4600);
|
||||
const mb = await ev(`(() => { const m = window.game.scene.getScene('MenuScene'); return [m.newGameBtn.x, m.newGameBtn.y]; })()`);
|
||||
await click(...(await ev(toClient(mb[0], mb[1]))));
|
||||
let gameUp = false;
|
||||
for (let i = 0; i < 240 && !gameUp; i++) { await sleep(500); gameUp = await ev(`!!(window.game && window.game.scene.isActive('GameScene') && window.game.scene.getScene('GameScene').ship)`); }
|
||||
if (!gameUp) throw new Error('no game');
|
||||
await sleep(1200);
|
||||
|
||||
// Enumerate interactive objects of the ACTIVE scene, hit-test at the MENU slot point.
|
||||
const dump = await ev(`(() => {
|
||||
const gs = window.game.scene.getScene('GameScene');
|
||||
const plugin = gs.sys.input;
|
||||
const list = plugin._list ?? [];
|
||||
const p = { x: 1143.33, y: 680 };
|
||||
const out = [];
|
||||
for (const o of list) {
|
||||
let hit = null;
|
||||
try {
|
||||
if (o.input && o.input.enabled) {
|
||||
const local = o.getWorldTransformMatrix ? o.getWorldTransformMatrix() : null;
|
||||
// use the manager's own candidate test if available
|
||||
}
|
||||
} catch (e) {}
|
||||
out.push({
|
||||
type: o.type || o.constructor?.name,
|
||||
name: o.name || '',
|
||||
depth: o.depth,
|
||||
pos: [o.x, o.y],
|
||||
w: o.width ?? null,
|
||||
h: o.height ?? null,
|
||||
enabled: o.input ? o.input.enabled : null,
|
||||
hasHitArea: o.input ? !!o.input.hitArea : false,
|
||||
hitArea: o.input && o.input.hitArea ? { x: o.input.hitArea.x, y: o.input.hitArea.y, w: o.input.hitArea.width, h: o.input.hitArea.height } : null,
|
||||
});
|
||||
}
|
||||
// hit-test via the plugin
|
||||
let under = null;
|
||||
try {
|
||||
const pointer = { x: 1143.33, y: 680, worldX: 1143.33, worldY: 680 };
|
||||
const hits = plugin.hitTestPointer ? plugin.hitTestPointer({ x: 1143.33, y: 680 }) : null;
|
||||
under = hits ? hits.map((h) => (h.type || h.constructor?.name)) : 'no hitTestPointer';
|
||||
} catch (e) { under = 'ERR ' + e.message; }
|
||||
const compact = out.map((o) => [o.type, '@' + Math.round(o.pos[0]) + ',' + Math.round(o.pos[1]), 'd' + o.depth, 'en=' + o.enabled, o.hitArea ? ('ha=' + Math.round(o.hitArea.x) + ',' + Math.round(o.hitArea.y) + ',' + Math.round(o.hitArea.w) + 'x' + Math.round(o.hitArea.h)) : 'ha=-', o.name || ''].join(' '));
|
||||
return { count: list.length, under, compact };
|
||||
})()`);
|
||||
console.log('UNDER POINTER:', JSON.stringify(dump.under));
|
||||
console.log('count:', dump.count);
|
||||
for (const line of dump.compact) console.log(' ', line);
|
||||
|
||||
cdp.close();
|
||||
kill();
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import { launch, CDP } from './cdpshot.mjs';
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const { url: wsUrl, kill } = await launch('http://localhost:8080/index.html', undefined, 1280, 720, 5000);
|
||||
const cdp = new CDP(wsUrl);
|
||||
await cdp.open();
|
||||
await cdp.send('Runtime.enable');
|
||||
const ev = async (expression) => {
|
||||
const r = await cdp.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
|
||||
if (r.exceptionDetails) throw new Error('eval: ' + (r.exceptionDetails.exception?.description ?? r.exceptionDetails.text));
|
||||
return r.result?.value;
|
||||
};
|
||||
|
||||
let menuUp = false;
|
||||
for (let i = 0; i < 120 && !menuUp; i++) { await sleep(500); menuUp = await ev(`!!(window.game && window.game.scene.isActive('MenuScene'))`); }
|
||||
if (!menuUp) throw new Error('no menu');
|
||||
await sleep(4200);
|
||||
const mb = await ev(`(() => { const m = window.game.scene.getScene('MenuScene'); return [m.newGameBtn.x, m.newGameBtn.y]; })()`);
|
||||
const toClient = (x, y) => `(() => { const c = window.game.canvas; const r = c.getBoundingClientRect(); const z = r.width / c.width; return [r.left + ${x} * z, r.top + ${y} * z]; })()`;
|
||||
async function click(cx, cy) {
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: cx, y: cy });
|
||||
await sleep(80);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: cx, y: cy, button: 'left', clickCount: 1 });
|
||||
await sleep(100);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: cx, y: cy, button: 'left', clickCount: 1 });
|
||||
await sleep(80);
|
||||
}
|
||||
await click(...(await ev(toClient(mb[0], mb[1]))));
|
||||
let gameUp = false;
|
||||
for (let i = 0; i < 240 && !gameUp; i++) { await sleep(500); gameUp = await ev(`!!(window.game && window.game.scene.isActive('GameScene') && window.game.scene.getScene('GameScene').ship)`); }
|
||||
if (!gameUp) throw new Error('no game');
|
||||
await sleep(1000);
|
||||
|
||||
console.log(await ev(`(() => {
|
||||
const gs = window.game.scene.getScene('GameScene');
|
||||
const s = gs.actionBar.slots.find((x) => x.id === 'menu');
|
||||
const inp = s.panel.input;
|
||||
const ha = inp.hitArea;
|
||||
return {
|
||||
panelPos: [s.panel.x, s.panel.y],
|
||||
slotPos: [s.slot.x, s.slot.y],
|
||||
scrollFactor: [s.panel.scrollFactorX, s.panel.scrollFactorY],
|
||||
enabled: inp.enabled,
|
||||
interactive: !!inp,
|
||||
hitArea: ha ? { x: ha.x, y: ha.y, w: ha.width, h: ha.height } : null,
|
||||
styleBW: gs.actionBar.style.bw,
|
||||
styleBH: gs.actionBar.style.bh,
|
||||
activePointers: window.game.input.activePointer ? { x: window.game.input.activePointer.x, y: window.game.input.activePointer.y } : null,
|
||||
worldPoint: null,
|
||||
};
|
||||
})()`));
|
||||
|
||||
// Now hover via CDP mouse move to the slot and see if pointerover fires
|
||||
await ev(`(() => { const gs = window.game.scene.getScene('GameScene'); const s = gs.actionBar.slots.find((x) => x.id === 'menu'); window.__over = 0; s.panel.on('pointerover', () => window.__over++); window.__down = 0; s.panel.on('pointerdown', () => window.__down++); return true; })()`);
|
||||
const [cx, cy] = await ev(toClient(1143.33, 680));
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: cx, y: cy });
|
||||
await sleep(300);
|
||||
console.log('hover count:', await ev(`window.__over`));
|
||||
|
||||
// check the pointer world coords phaser sees
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: cx - 5, y: cy });
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: cx, y: cy });
|
||||
await sleep(200);
|
||||
console.log('phaser pointer:', await ev(`(() => { const p = window.game.input.activePointer; return { x: p.x, y: p.y, worldX: p.worldX, worldY: p.worldY, position: [p.position.x, p.position.y] }; })()`));
|
||||
|
||||
// hit test manually
|
||||
console.log(await ev(`(() => {
|
||||
const gs = window.game.scene.getScene('GameScene');
|
||||
const s = gs.actionBar.slots.find((x) => x.id === 'menu');
|
||||
const p = window.game.input.activePointer;
|
||||
try { return { hit: s.panel.input.hitTest(p) }; } catch(e) { return 'ERR ' + e.message; }
|
||||
})()`));
|
||||
|
||||
cdp.close(); kill();
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { launch, CDP } from './cdpshot.mjs';
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const { url: wsUrl, kill } = await launch('http://localhost:8080/index.html', undefined, 1280, 720, 5000);
|
||||
const cdp = new CDP(wsUrl);
|
||||
await cdp.open();
|
||||
await cdp.send('Runtime.enable');
|
||||
const ev = async (expression) => {
|
||||
const r = await cdp.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
|
||||
if (r.exceptionDetails) throw new Error('eval: ' + (r.exceptionDetails.exception?.description ?? r.exceptionDetails.text));
|
||||
return r.result?.value;
|
||||
};
|
||||
const toClient = (x, y) => `(() => { const c = window.game.canvas; const r = c.getBoundingClientRect(); const z = r.width / c.width; return [r.left + ${x} * z, r.top + ${y} * z]; })()`;
|
||||
async function click(cx, cy) {
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: cx, y: cy });
|
||||
await sleep(80);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: cx, y: cy, button: 'left', clickCount: 1 });
|
||||
await sleep(100);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: cx, y: cy, button: 'left', clickCount: 1 });
|
||||
await sleep(80);
|
||||
}
|
||||
let menuUp = false;
|
||||
for (let i = 0; i < 120 && !menuUp; i++) { await sleep(500); menuUp = await ev(`!!(window.game && window.game.scene.isActive('MenuScene'))`); }
|
||||
if (!menuUp) throw new Error('no menu');
|
||||
await sleep(4200);
|
||||
const mb = await ev(`(() => { const m = window.game.scene.getScene('MenuScene'); return [m.newGameBtn.x, m.newGameBtn.y]; })()`);
|
||||
await click(...(await ev(toClient(mb[0], mb[1]))));
|
||||
let gameUp = false;
|
||||
for (let i = 0; i < 240 && !gameUp; i++) { await sleep(500); gameUp = await ev(`!!(window.game && window.game.scene.isActive('GameScene') && window.game.scene.getScene('GameScene').ship)`); }
|
||||
if (!gameUp) throw new Error('no game');
|
||||
await sleep(1000);
|
||||
|
||||
const [cx, cy] = await ev(toClient(1143.33, 680));
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: cx, y: cy });
|
||||
await sleep(250);
|
||||
|
||||
console.log('input keys:', await ev(`Object.keys(window.game.input).join(', ')`));
|
||||
console.log('input own:', await ev(`Object.getOwnPropertyNames(Object.getPrototypeOf(window.game.input)).join(', ')`));
|
||||
console.log('interactiveObjects-ish:', await ev(`(() => { const i = window.game.input; return { iO: typeof i.interactionObjects, objs: typeof i.objects, list: typeof i.list, active: typeof i.activePointers, capture: i.captureTarget ? true : false }; })()`));
|
||||
console.log(JSON.stringify(await ev(`(() => {
|
||||
const i = window.game.input;
|
||||
const list = i.interactionObjects ?? i.objects ?? i._objects ?? null;
|
||||
if (!list) return 'no list found';
|
||||
const p = i.activePointer ?? i.activePointers?.[0];
|
||||
const out = [];
|
||||
for (const o of list) {
|
||||
try { if (o.hitArea && o.hitTest(p)) out.push(o.name || o.type); } catch (e) {}
|
||||
}
|
||||
return { count: list.length, under: out };
|
||||
})()`), null, 1));
|
||||
|
||||
cdp.close(); kill();
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
// Reproduce the user's "Load Game after a started game" flow with REAL
|
||||
// CDP input events on the actual index.html.
|
||||
//
|
||||
// node dev/load-repro.mjs [step=full]
|
||||
//
|
||||
// Steps:
|
||||
// 1. load index.html, wait for the menu
|
||||
// 2. click "New Game" -> GameScene boots
|
||||
// 3. MENU -> sub-bar -> Save Game -> click slot 3 -> close (CANCEL)
|
||||
// 4. fly the ship a bit (WASD held)
|
||||
// 5. MENU -> sub-bar -> Load Game -> click slot 3 -> click LOAD (confirm)
|
||||
// 6. report which scene is active; if Menu: click Load Game -> slot 3 -> confirm
|
||||
// 7. report final scene + ship position vs the save
|
||||
import { launch, CDP } from './cdpshot.mjs';
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const URL = process.argv[2] || 'http://localhost:8080/index.html';
|
||||
const { url: wsUrl, kill } = await launch(URL, undefined, 1280, 720, 5000);
|
||||
const cdp = new CDP(wsUrl);
|
||||
await cdp.open();
|
||||
await cdp.send('Runtime.enable');
|
||||
await cdp.send('Page.enable');
|
||||
|
||||
const errors = [];
|
||||
cdp.on('Runtime.consoleAPICalled', (p) => {
|
||||
const txt = (p.args ?? []).map((a) => a.value ?? a.description ?? '').join(' ');
|
||||
if (p.type === 'error' || p.type === 'warning') errors.push(`${p.type}: ${txt}`.slice(0, 400));
|
||||
});
|
||||
cdp.on('Runtime.exceptionThrown', (p) => {
|
||||
errors.push('EXCEPTION: ' + (p.exceptionDetails?.exception?.description ?? p.exceptionDetails?.text ?? '').slice(0, 400));
|
||||
});
|
||||
|
||||
const ev = async (expression) => {
|
||||
const r = await cdp.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
|
||||
if (r.exceptionDetails) throw new Error('eval: ' + (r.exceptionDetails.exception?.description ?? r.exceptionDetails.text));
|
||||
return r.result?.value;
|
||||
};
|
||||
|
||||
// Real mouse click in CLIENT coords (the canvas rect space).
|
||||
async function click(clientX, clientY) {
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: clientX, y: clientY });
|
||||
await sleep(60);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: clientX, y: clientY, button: 'left', clickCount: 1 });
|
||||
await sleep(90);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: clientX, y: clientY, button: 'left', clickCount: 1 });
|
||||
await sleep(60);
|
||||
}
|
||||
|
||||
// Map a point in the game's 1280x720 design space to client (screen) coords.
|
||||
const toClient = (x, y) =>
|
||||
`(() => { const c = window.game.canvas; const r = c.getBoundingClientRect(); const z = r.width / c.width; return [r.left + ${x} * z, r.top + ${y} * z]; })()`;
|
||||
|
||||
async function clickGame(x, y) {
|
||||
const [cx, cy] = await ev(toClient(x, y));
|
||||
console.log(` click game(${x},${y}) -> client(${cx.toFixed(0)},${cy.toFixed(0)})`);
|
||||
await click(cx, cy);
|
||||
}
|
||||
|
||||
const log = (...a) => console.log(...a);
|
||||
const sceneState = () =>
|
||||
ev(`(() => {
|
||||
const g = window.game;
|
||||
const out = { active: g.scene.getScenes(true).map((s) => s.scene.key) };
|
||||
const gs = g.scene.getScene('GameScene');
|
||||
if (gs && gs.scene.isActive()) {
|
||||
out.ship = gs.ship ? { x: Math.round(gs.ship.x * 10) / 10, y: Math.round(gs.ship.y * 10) / 10 } : null;
|
||||
out.minerals = gs.ship ? gs.ship.minerals : null;
|
||||
out.seed = gs.galaxy ? gs.galaxy.seed : null;
|
||||
out.sys = gs.systemRecord ? gs.systemRecord.id : null;
|
||||
}
|
||||
return out;
|
||||
})()`);
|
||||
|
||||
log('== waiting for the menu ==');
|
||||
let menuUp = false;
|
||||
for (let i = 0; i < 120 && !menuUp; i++) {
|
||||
await sleep(500);
|
||||
menuUp = await ev(`!!(window.game && window.game.scene.isActive('MenuScene'))`);
|
||||
}
|
||||
if (!menuUp) throw new Error('menu never came up');
|
||||
await sleep(4200); // intro beats (revealAt 3.2s + button rises)
|
||||
|
||||
log('== menu buttons ==');
|
||||
const mb = await ev(`(() => { const m = window.game.scene.getScene('MenuScene'); return {
|
||||
continue: [m.continueBtn.x, m.continueBtn.y, m.continueBtn.disabled],
|
||||
newGame: [m.newGameBtn.x, m.newGameBtn.y, m.newGameBtn.disabled],
|
||||
loadGame: [m.loadGameBtn.x, m.loadGameBtn.y, m.loadGameBtn.disabled],
|
||||
}; })()`);
|
||||
log(' ', JSON.stringify(mb));
|
||||
log(' bank:', JSON.stringify(await ev(`new (require?null:null)`).catch(() => null) ?? ''));
|
||||
const bank = await ev(`(() => { try { const raw = localStorage.getItem('orbit.saves.v1'); return raw ? JSON.parse(raw).slots && Object.keys(JSON.parse(raw).slots) : null; } catch(e) { return 'ERR ' + e.message; } })()`);
|
||||
log(' save bank slots:', JSON.stringify(bank));
|
||||
|
||||
log('== 1) New Game ==');
|
||||
await clickGame(mb.newGame[0], mb.newGame[1]);
|
||||
let gameUp = false;
|
||||
for (let i = 0; i < 240 && !gameUp; i++) {
|
||||
await sleep(500);
|
||||
gameUp = await ev(`!!(window.game && window.game.scene.isActive('GameScene') && window.game.scene.getScene('GameScene').ship)`);
|
||||
}
|
||||
if (!gameUp) throw new Error('GameScene never booted');
|
||||
log(' game up:', JSON.stringify(await sceneState()));
|
||||
|
||||
const gs = () => 'window.game.scene.getScene("GameScene")';
|
||||
|
||||
log('== 2) save slot 3 (MENU -> Save Game -> slot 3 -> CANCEL) ==');
|
||||
const menuBtn = await ev(`(() => { const gs = ${gs()}; const s = gs.actionBar?.slots?.find((s) => s.id === 'menu'); return s ? [s.slot.x, s.slot.y] : null; })()`);
|
||||
log(' MENU slot at', JSON.stringify(menuBtn));
|
||||
await clickGame(menuBtn[0], menuBtn[1]);
|
||||
await sleep(700);
|
||||
const sub = await ev(`(() => { const gs = ${gs()}; const bar = gs.menuSubBar; return bar.isOpen ? bar.buttons.map((b) => [b.id, bar.x + b.btn.x, bar.y + b.btn.y, b.btn.disabled]) : null; })()`);
|
||||
log(' sub-bar buttons (abs):', JSON.stringify(sub));
|
||||
const saveBtn = sub.find((b) => b[0] === 'save');
|
||||
await clickGame(saveBtn[1], saveBtn[2]);
|
||||
await sleep(1600); // crack open + stagger
|
||||
const diag1 = await ev(`(() => { const gs = ${gs()}; return { sub: gs.menuSubBar.isOpen, panel: gs.savePanel.isOpen, state: gs.savePanel.state }; })()`);
|
||||
log(' after Save-Game click:', JSON.stringify(diag1));
|
||||
const card = await ev(`(() => { const gs = ${gs()}; const c = gs.savePanel.cards[2]; return [c.x, c.y, c.disabled, c.alpha]; })()`);
|
||||
log(' slot-3 card at (panel-local)', JSON.stringify(card));
|
||||
const panelPos = await ev(`(() => { const gs = ${gs()}; return [gs.savePanel.x, gs.savePanel.y]; })()`);
|
||||
await clickGame(panelPos[0] + card[0], panelPos[1] + card[1]);
|
||||
await sleep(700);
|
||||
const saved = await ev(`(() => { const gs = ${gs()}; const r = gs.saveManager.get(3); return r ? { x: r.ship.x, y: r.ship.y, seed: r.seed } : null; })()`);
|
||||
log(' slot 3 now holds:', JSON.stringify(saved));
|
||||
if (!saved) { log(' DIAG panel:', JSON.stringify(await ev(`(() => { const gs = ${gs()}; return { state: gs.savePanel.state, mode: gs.savePanel.mode, cards: gs.savePanel.cards.map((c) => [c.alpha, c.disabled, c.input?.enabled ?? c.panel?.input?.enabled ]) }; })()`))); throw new Error('save did not land in slot 3'); }
|
||||
const savedShip = { x: saved.x, y: saved.y };
|
||||
// CANCEL closes the panel
|
||||
const cancel = await ev(`(() => { const gs = ${gs()}; const b = gs.savePanel.cancelBtn; return [gs.savePanel.x + b.x, gs.savePanel.y + b.y, b.panel.input?.enabled]; })()`);
|
||||
log(' CANCEL at', JSON.stringify(cancel));
|
||||
await clickGame(cancel[0], cancel[1]);
|
||||
await sleep(700);
|
||||
log(' panel closed:', await ev(`!${gs()}.savePanel.isOpen`));
|
||||
|
||||
log('== 3) fly the ship away (hold W + A for ~2.5 s) ==');
|
||||
await ev(`(() => { const gs = ${gs()}; gs.ship._reproThrust = true; })()`);
|
||||
// simplest: nudge the ship directly (no flight model coupling)
|
||||
await ev(`(() => { const gs = ${gs()}; gs.ship.x += 420; gs.ship.y -= 260; gs.ship.update?.(0, 16); return [gs.ship.x, gs.ship.y]; })()`);
|
||||
log(' ship moved to:', JSON.stringify(await ev(`(() => { const gs = ${gs()}; return [Math.round(gs.ship.x), Math.round(gs.ship.y)]; })()`)));
|
||||
await sleep(600);
|
||||
|
||||
log('== 4) in-game LOAD (MENU -> Load Game -> slot 3 -> confirm) ==');
|
||||
await clickGame(menuBtn[0], menuBtn[1]);
|
||||
await sleep(700);
|
||||
const sub2 = await ev(`(() => { const gs = ${gs()}; const bar = gs.menuSubBar; return bar.isOpen ? bar.buttons.map((b) => [b.id, bar.x + b.btn.x, bar.y + b.btn.y, b.btn.disabled]) : null; })()`);
|
||||
log(' sub-bar buttons (abs):', JSON.stringify(sub2));
|
||||
const loadBtn = sub2.find((b) => b[0] === 'load');
|
||||
if (loadBtn[3]) throw new Error('Load Game is DISABLED in the sub-bar (hasAny=false?)');
|
||||
await clickGame(loadBtn[1], loadBtn[2]);
|
||||
await sleep(1400);
|
||||
const card3 = await ev(`(() => { const gs = ${gs()}; const c = gs.savePanel.cards[2]; return [c.x, c.y, c.disabled]; })()`);
|
||||
await clickGame(panelPos[0] + card3[0], panelPos[1] + card3[1]);
|
||||
await sleep(600);
|
||||
const confirm = await ev(`(() => { const gs = ${gs()}; const c = gs.savePanel.confirm; if (!c.isOpen) return null; return { isOpen: c.isOpen, title: c.title?.text, x: c.x + c.confirmBtn.x, y: c.y + c.confirmBtn.y }; })()`);
|
||||
log(' confirm overlay:', JSON.stringify(confirm));
|
||||
if (confirm && confirm.x != null) {
|
||||
await clickGame(confirm.x, confirm.y);
|
||||
}
|
||||
await sleep(1500);
|
||||
log(' AFTER in-game load confirm -> scene state:', JSON.stringify(await sceneState()));
|
||||
log(' console errors so far:', errors.slice(-6));
|
||||
|
||||
log('== 5) if we are at the menu: Load Game -> slot 3 -> confirm ==');
|
||||
const atMenu = await ev(`window.game.scene.isActive('MenuScene')`);
|
||||
if (atMenu) {
|
||||
await sleep(1500); // menu intro
|
||||
const mb2 = await ev(`(() => { const m = window.game.scene.getScene('MenuScene'); return [m.loadGameBtn.x, m.loadGameBtn.y, m.loadGameBtn.disabled]; })()`);
|
||||
log(' menu Load Game at', JSON.stringify(mb2));
|
||||
if (mb2[2]) throw new Error('menu Load Game DISABLED');
|
||||
await clickGame(mb2[0], mb2[1]);
|
||||
await sleep(1400);
|
||||
const mcard = await ev(`(() => { const m = window.game.scene.getScene('MenuScene'); const c = m.savePanel.cards[2]; return [m.savePanel.x + c.x, m.savePanel.y + c.y, c.disabled]; })()`);
|
||||
log(' menu slot-3 card at', JSON.stringify(mcard));
|
||||
await clickGame(mcard[0], mcard[1]);
|
||||
await sleep(600);
|
||||
const mconf = await ev(`(() => { const m = window.game.scene.getScene('MenuScene'); const c = m.savePanel.confirm; if (!c.isOpen) return null; return { isOpen: c.isOpen, title: c.title?.text, x: c.x + c.confirmBtn.x, y: c.y + c.confirmBtn.y }; })()`);
|
||||
log(' menu confirm:', JSON.stringify(mconf));
|
||||
if (mconf && mconf.x != null) await clickGame(mconf.x, mconf.y);
|
||||
await sleep(2000);
|
||||
log(' AFTER menu load confirm -> scene state:', JSON.stringify(await sceneState()));
|
||||
} else {
|
||||
log(' (not at menu — checking we are in GameScene with the restored ship)');
|
||||
}
|
||||
|
||||
const final = await sceneState();
|
||||
log('== FINAL ==', JSON.stringify(final));
|
||||
log(' saved ship was:', JSON.stringify(savedShip));
|
||||
log(' console errors:', errors.slice(-10));
|
||||
|
||||
cdp.close();
|
||||
kill();
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { launch, CDP } from './cdpshot.mjs';
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const URL = 'http://localhost:8080/index.html';
|
||||
const { url: wsUrl, kill } = await launch(URL, undefined, 1280, 720, 5000);
|
||||
const cdp = new CDP(wsUrl);
|
||||
await cdp.open();
|
||||
await cdp.send('Runtime.enable');
|
||||
const errors = [];
|
||||
cdp.on('Runtime.consoleAPICalled', (p) => {
|
||||
const txt = (p.args ?? []).map((a) => a.value ?? a.description ?? '').join(' ');
|
||||
errors.push(`${p.type}: ${txt}`.slice(0, 300));
|
||||
});
|
||||
cdp.on('Runtime.exceptionThrown', (p) => errors.push('EXC: ' + (p.exceptionDetails?.exception?.description ?? p.exceptionDetails?.text ?? '').slice(0, 300)));
|
||||
|
||||
const ev = async (expression) => {
|
||||
const r = await cdp.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
|
||||
if (r.exceptionDetails) throw new Error('eval: ' + (r.exceptionDetails.exception?.description ?? r.exceptionDetails.text));
|
||||
return r.result?.value;
|
||||
};
|
||||
async function click(cx, cy) {
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: cx, y: cy });
|
||||
await sleep(80);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: cx, y: cy, button: 'left', clickCount: 1 });
|
||||
await sleep(100);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: cx, y: cy, button: 'left', clickCount: 1 });
|
||||
await sleep(80);
|
||||
}
|
||||
const toClient = (x, y) => `(() => { const c = window.game.canvas; const r = c.getBoundingClientRect(); const z = r.width / c.width; return [r.left + ${x} * z, r.top + ${y} * z]; })()`;
|
||||
|
||||
// wait for menu, boot game
|
||||
let menuUp = false;
|
||||
for (let i = 0; i < 120 && !menuUp; i++) { await sleep(500); menuUp = await ev(`!!(window.game && window.game.scene.isActive('MenuScene'))`); }
|
||||
if (!menuUp) throw new Error('no menu');
|
||||
await sleep(4200);
|
||||
const mb = await ev(`(() => { const m = window.game.scene.getScene('MenuScene'); return [m.newGameBtn.x, m.newGameBtn.y]; })()`);
|
||||
await click(...(await ev(toClient(mb[0], mb[1]))));
|
||||
let gameUp = false;
|
||||
for (let i = 0; i < 240 && !gameUp; i++) { await sleep(500); gameUp = await ev(`!!(window.game && window.game.scene.isActive('GameScene') && window.game.scene.getScene('GameScene').ship)`); }
|
||||
if (!gameUp) throw new Error('no game');
|
||||
|
||||
// instrument
|
||||
await ev(`(() => {
|
||||
const gs = window.game.scene.getScene('GameScene');
|
||||
window.__log = [];
|
||||
const orig = gs.deckAction.bind(gs);
|
||||
gs.deckAction = (id) => { window.__log.push('deckAction:' + id); return orig(id); };
|
||||
const press = gs.actionBar.press.bind(gs.actionBar);
|
||||
gs.actionBar.press = (s) => { window.__log.push('press:' + s.id + ' pressing=' + s.pressing + ' disabled=' + s.disabled); return press(s); };
|
||||
return true;
|
||||
})()`);
|
||||
|
||||
const menuBtn = await ev(`(() => { const gs = window.game.scene.getScene('GameScene'); const s = gs.actionBar.slots.find((s) => s.id === 'menu'); const r = gs.actionBar.slotHit(); return [s.slot.x, s.slot.y, r.width, r.height, gs.actionBar.rect]; })()`);
|
||||
console.log('MENU slot:', JSON.stringify(menuBtn));
|
||||
const [cx, cy] = await ev(toClient(menuBtn[0], menuBtn[1]));
|
||||
console.log('client:', cx, cy, 'canvas rect:', await ev(`(() => { const r = window.game.canvas.getBoundingClientRect(); return { l: r.left, t: r.top, w: r.width, h: r.height, cw: window.game.canvas.width, ch: window.game.canvas.height }; })()`));
|
||||
|
||||
await click(cx, cy);
|
||||
await sleep(300);
|
||||
console.log('log after click 1:', await ev(`window.__log.slice()`));
|
||||
console.log('subbar open:', await ev(`window.game.scene.getScene('GameScene').menuSubBar?.isOpen`));
|
||||
await sleep(500);
|
||||
console.log('log:', await ev(`window.__log.slice()`));
|
||||
console.log('subbar open:', await ev(`window.game.scene.getScene('GameScene').menuSubBar?.isOpen`));
|
||||
console.log('errors:', errors.slice(-8));
|
||||
|
||||
// try a second click (toggle test)
|
||||
await click(cx, cy);
|
||||
await sleep(400);
|
||||
console.log('log after click 2:', await ev(`window.__log.slice()`));
|
||||
console.log('subbar open:', await ev(`window.game.scene.getScene('GameScene').menuSubBar?.isOpen`));
|
||||
|
||||
cdp.close(); kill();
|
||||
|
|
@ -8,8 +8,10 @@
|
|||
* → doSave(1) writes the bank
|
||||
* → overwrite confirm opens, CANCEL leaves the save intact
|
||||
* → the panel reopens in LOAD mode, empty slots inert
|
||||
* → LOAD confirm → the restore stages → cut to the menu
|
||||
* MenuScene: Load Game is live (the bank has a save) → its own load
|
||||
* → LOAD confirm → the restore stages → the scene restarts
|
||||
* in place (the ship back where it was saved)
|
||||
* MenuScene: (back via the sub-bar's Return to Main Menu) 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, and the hold: the minerals the
|
||||
|
|
@ -278,14 +280,37 @@ const run = async () => {
|
|||
check('empty slots are inert in load mode',
|
||||
scene.savePanel.cards[2].disabled === true && scene.savePanel.cards[0].disabled === false);
|
||||
|
||||
// ---- load confirm: stage the restore, cut to the menu -----------------
|
||||
const shipX = scene.ship.x;
|
||||
const shipY = scene.ship.y;
|
||||
// ---- load confirm: stage the restore, restart the scene IN PLACE ------
|
||||
// The save parked the ship at (savedX, savedY); fly it away so the
|
||||
// restore has somewhere to come back to (a load must REPLACE the live
|
||||
// position, not keep it).
|
||||
const savedX = scene.ship.x;
|
||||
const savedY = scene.ship.y;
|
||||
scene.ship.x += 500;
|
||||
scene.ship.y -= 320;
|
||||
scene.savePanel.confirmLoad(1, scene.saveManager.get(1));
|
||||
await wait(300);
|
||||
scene.savePanel.confirm.fireConfirm();
|
||||
await wait(700); // close anim → onLoadComplete → scene.start
|
||||
check('a confirmed LOAD cuts to the main menu', game.scene.isActive('MenuScene'));
|
||||
// close anim → onLoadComplete → reloadRun() → scene.restart(): the scene
|
||||
// re-creates on the staged restore (the jump cut's seam). Poll until
|
||||
// the fresh scene is active with the ship back at the saved spot.
|
||||
let restoredInPlace = false;
|
||||
for (let i = 0; i < 120 && !restoredInPlace; i++) {
|
||||
await wait(100);
|
||||
const s2 = game.scene.getScene('GameScene');
|
||||
restoredInPlace =
|
||||
game.scene.isActive('GameScene')
|
||||
&& !!s2.ship
|
||||
&& Math.abs(s2.ship.x - savedX) < 0.01
|
||||
&& Math.abs(s2.ship.y - savedY) < 0.01;
|
||||
}
|
||||
check('a confirmed LOAD restarts the game in place (the ship is back where it was saved)', restoredInPlace);
|
||||
|
||||
// ---- back to the menu (the sub-bar's last button), then the menu side --
|
||||
const gsNow = game.scene.getScene('GameScene');
|
||||
gsNow.returnToMenu();
|
||||
await wait(700);
|
||||
check('Return to Main Menu lands on the menu', game.scene.isActive('MenuScene'));
|
||||
|
||||
// ---- the menu side ------------------------------------------------------
|
||||
const menu = game.scene.getScene('MenuScene');
|
||||
|
|
@ -307,7 +332,7 @@ const run = async () => {
|
|||
const rec = gs.saveManager.get(1);
|
||||
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);
|
||||
check('the saved ship was where the save parked it', Math.abs(rec.ship.x - savedX) < 0.01 && Math.abs(rec.ship.y - savedY) < 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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import { launch, CDP } from './cdpshot.mjs';
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const { url: wsUrl, kill } = await launch('http://localhost:8080/index.html', undefined, 1280, 720, 5000);
|
||||
const cdp = new CDP(wsUrl);
|
||||
await cdp.open();
|
||||
await cdp.send('Runtime.enable');
|
||||
const ev = async (expression) => {
|
||||
const r = await cdp.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
|
||||
if (r.exceptionDetails) throw new Error('eval: ' + (r.exceptionDetails.exception?.description ?? r.exceptionDetails.text));
|
||||
return r.result?.value;
|
||||
};
|
||||
const toClient = (x, y) =>
|
||||
`(() => { const c = window.game.canvas; const r = c.getBoundingClientRect(); const z = r.width / c.width; return [r.left + ${x} * z, r.top + ${y} * z]; })()`;
|
||||
async function click(cx, cy) {
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: cx, y: cy });
|
||||
await sleep(70);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: cx, y: cy, button: 'left', clickCount: 1 });
|
||||
await sleep(100);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: cx, y: cy, button: 'left', clickCount: 1 });
|
||||
await sleep(70);
|
||||
}
|
||||
let menuUp = false;
|
||||
for (let i = 0; i < 120 && !menuUp; i++) { await sleep(500); menuUp = await ev(`!!(window.game && window.game.scene.isActive('MenuScene'))`); }
|
||||
await sleep(4600);
|
||||
const mb = await ev(`(() => { const m = window.game.scene.getScene('MenuScene'); return [m.newGameBtn.x, m.newGameBtn.y]; })()`);
|
||||
await click(...(await ev(toClient(mb[0], mb[1]))));
|
||||
let gameUp = false;
|
||||
for (let i = 0; i < 240 && !gameUp; i++) { await sleep(500); gameUp = await ev(`!!(window.game && window.game.scene.isActive('GameScene') && window.game.scene.getScene('GameScene').ship)`); }
|
||||
await sleep(1500);
|
||||
console.log(JSON.stringify(await ev(`(() => {
|
||||
const gs = window.game.scene.getScene('GameScene');
|
||||
return {
|
||||
deckEnabled: gs.actionBar.enabled,
|
||||
slots: gs.actionBar.slots.map((s) => [s.id, s.disabled === true, s.panel.input ? s.panel.input.enabled : null]),
|
||||
commPaused: !!(gs.comms && gs.comms.paused),
|
||||
commBox: !!(gs.comms && gs.comms.box),
|
||||
commBoxOpen: !!(gs.comms && gs.comms.box && gs.comms.box.alpha > 0.2),
|
||||
};
|
||||
})()`), null, 1));
|
||||
cdp.close();
|
||||
kill();
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import { launch, CDP } from './cdpshot.mjs';
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const { url: wsUrl, kill } = await launch('http://localhost:8080/index.html', undefined, 1280, 720, 5000);
|
||||
const cdp = new CDP(wsUrl);
|
||||
await cdp.open();
|
||||
await cdp.send('Runtime.enable');
|
||||
const ev = async (expression) => {
|
||||
const r = await cdp.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
|
||||
if (r.exceptionDetails) throw new Error('eval: ' + (r.exceptionDetails.exception?.description ?? r.exceptionDetails.text));
|
||||
return r.result?.value;
|
||||
};
|
||||
const toClient = (x, y) => `(() => { const c = window.game.canvas; const r = c.getBoundingClientRect(); const z = r.width / c.width; return [r.left + ${x} * z, r.top + ${y} * z]; })()`;
|
||||
async function click(cx, cy) {
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: cx, y: cy });
|
||||
await sleep(80);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: cx, y: cy, button: 'left', clickCount: 1 });
|
||||
await sleep(100);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: cx, y: cy, button: 'left', clickCount: 1 });
|
||||
await sleep(80);
|
||||
}
|
||||
|
||||
let menuUp = false;
|
||||
for (let i = 0; i < 120 && !menuUp; i++) { await sleep(500); menuUp = await ev(`!!(window.game && window.game.scene.isActive('MenuScene'))`); }
|
||||
if (!menuUp) throw new Error('no menu');
|
||||
await sleep(4200);
|
||||
const mb = await ev(`(() => { const m = window.game.scene.getScene('MenuScene'); return [m.newGameBtn.x, m.newGameBtn.y]; })()`);
|
||||
await click(...(await ev(toClient(mb[0], mb[1]))));
|
||||
let gameUp = false;
|
||||
for (let i = 0; i < 240 && !gameUp; i++) { await sleep(500); gameUp = await ev(`!!(window.game && window.game.scene.isActive('GameScene') && window.game.scene.getScene('GameScene').ship)`); }
|
||||
if (!gameUp) throw new Error('no game');
|
||||
await sleep(1000);
|
||||
|
||||
const [cx, cy] = await ev(toClient(1143.33, 680));
|
||||
// move pointer over the slot first (so world pointer is set)
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: cx, y: cy });
|
||||
await sleep(250);
|
||||
|
||||
const dump = await ev(`(() => {
|
||||
const g = window.game;
|
||||
const p = g.input.activePointer;
|
||||
const objs = g.input.interactionObjects.slice().sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
const under = [];
|
||||
for (const o of objs) {
|
||||
try {
|
||||
const hit = o.hitArea ? o.hitTest(p) : false;
|
||||
if (hit) under.push({ name: o.name || o.type || o.constructor?.name, so: o.sortOrder, scene: o.scene?.scene?.key, enabled: o.input.enabled, x: o.worldX ?? o.x, y: o.worldY ?? o.y });
|
||||
} catch (e) {}
|
||||
}
|
||||
return { pointer: [p.x, p.y], count: objs.length, under };
|
||||
})()`);
|
||||
console.log(JSON.stringify(dump, null, 1));
|
||||
|
||||
cdp.close(); kill();
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
// Full user-flow repro with REAL CDP clicks on index.html:
|
||||
// menu -> New Game -> (ack comms) -> save slot 3 -> fly away
|
||||
// -> in-game Load Game (confirm) -> observe
|
||||
// -> (if menu) menu Load Game (confirm) -> observe ship vs save
|
||||
import { launch, CDP } from './cdpshot.mjs';
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const { url: wsUrl, kill } = await launch('http://localhost:8080/index.html', undefined, 1280, 720, 5000);
|
||||
const cdp = new CDP(wsUrl);
|
||||
await cdp.open();
|
||||
await cdp.send('Runtime.enable');
|
||||
await cdp.send('Page.enable');
|
||||
const errors = [];
|
||||
cdp.on('Runtime.consoleAPICalled', (p) => {
|
||||
if (p.type === 'error' || p.type === 'warning') {
|
||||
errors.push(`${p.type}: ${(p.args ?? []).map((a) => a.value ?? a.description ?? '').join(' ')}`.slice(0, 300));
|
||||
}
|
||||
});
|
||||
cdp.on('Runtime.exceptionThrown', (p) => errors.push('EXC: ' + (p.exceptionDetails?.exception?.description ?? p.exceptionDetails?.text ?? '').slice(0, 300)));
|
||||
|
||||
const ev = async (expression) => {
|
||||
const r = await cdp.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
|
||||
if (r.exceptionDetails) throw new Error('eval: ' + (r.exceptionDetails.exception?.description ?? r.exceptionDetails.text));
|
||||
return r.result?.value;
|
||||
};
|
||||
async function click(cx, cy) {
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: cx, y: cy });
|
||||
await sleep(70);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: cx, y: cy, button: 'left', clickCount: 1 });
|
||||
await sleep(100);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: cx, y: cy, button: 'left', clickCount: 1 });
|
||||
await sleep(70);
|
||||
}
|
||||
const toClient = (x, y) =>
|
||||
`(() => { const c = window.game.canvas; const r = c.getBoundingClientRect(); const z = r.width / c.width; return [r.left + ${x} * z, r.top + ${y} * z]; })()`;
|
||||
async function clickGame(x, y) {
|
||||
const [cx, cy] = await ev(toClient(x, y));
|
||||
await click(cx, cy);
|
||||
}
|
||||
const log = (...a) => console.log(...a);
|
||||
const G = 'window.game.scene.getScene("GameScene")';
|
||||
const sceneState = () =>
|
||||
ev(`(() => {
|
||||
const g = window.game;
|
||||
const out = { active: g.scene.getScenes(true).map((s) => s.scene.key) };
|
||||
const gs = g.scene.getScene('GameScene');
|
||||
if (gs && gs.scene.isActive() && gs.ship) {
|
||||
out.ship = [Math.round(gs.ship.x * 10) / 10, Math.round(gs.ship.y * 10) / 10];
|
||||
out.seed = gs.galaxy ? gs.galaxy.seed : null;
|
||||
out.sys = gs.systemRecord ? gs.systemRecord.id : null;
|
||||
}
|
||||
return out;
|
||||
})()`);
|
||||
|
||||
// ---- boot to the menu, New Game
|
||||
let menuUp = false;
|
||||
for (let i = 0; i < 120 && !menuUp; i++) { await sleep(500); menuUp = await ev(`!!(window.game && window.game.scene.isActive('MenuScene'))`); }
|
||||
if (!menuUp) throw new Error('menu never came up');
|
||||
await sleep(4600);
|
||||
const mb = await ev(`(() => { const m = window.game.scene.getScene('MenuScene'); return [m.newGameBtn.x, m.newGameBtn.y]; })()`);
|
||||
await clickGame(mb[0], mb[1]);
|
||||
let gameUp = false;
|
||||
for (let i = 0; i < 240 && !gameUp; i++) { await sleep(500); gameUp = await ev(`!!(window.game && window.game.scene.isActive('GameScene') && window.game.scene.getScene('GameScene').ship)`); }
|
||||
if (!gameUp) throw new Error('GameScene never booted');
|
||||
log('game up:', JSON.stringify(await sceneState()));
|
||||
|
||||
// ---- ack any comms holding the screen until the deck's slots are live
|
||||
async function deckLive() {
|
||||
return await ev(`(() => {
|
||||
const gs = window.game.scene.getScene('GameScene');
|
||||
if (!gs || !gs.scene.isActive()) return null;
|
||||
const s = gs.actionBar.slots.find((x) => x.id === 'menu');
|
||||
return { live: !!(s && s.panel.input && s.panel.input.enabled), commBox: !!(gs.comms?.box), paused: !!(gs.comms?.paused) };
|
||||
})()`);
|
||||
}
|
||||
async function ensureDeck(maxRounds = 12) {
|
||||
for (let i = 0; i < maxRounds; i++) {
|
||||
const st = await deckLive();
|
||||
if (!st) return false;
|
||||
if (st.live) return true;
|
||||
const btn = await ev(`(() => { const gs = ${G}; const b = gs.comms?.box; return b && b.button ? [b.x + b.button.x, b.y + b.button.y] : null; })()`);
|
||||
if (btn) {
|
||||
log(` ACK comm button at`, JSON.stringify(btn));
|
||||
await clickGame(btn[0], btn[1]);
|
||||
}
|
||||
await sleep(1500);
|
||||
}
|
||||
return !!(await deckLive())?.live;
|
||||
}
|
||||
log('waiting for the deck to be live (acking comms as they hold)…');
|
||||
let live = false;
|
||||
for (let round = 0; round < 10 && !live; round++) {
|
||||
await sleep(2000);
|
||||
live = await ensureDeck(2);
|
||||
}
|
||||
log('deck live:', live);
|
||||
if (!live) { log(' state:', JSON.stringify(await deckLive())); }
|
||||
|
||||
// ---- MENU -> Save Game -> slot 3
|
||||
const menuSlot = await ev(`(() => { const gs = ${G}; const s = gs.actionBar.slots.find((s) => s.id === 'menu'); return [s.slot.x, s.slot.y]; })()`);
|
||||
async function openSubBar() {
|
||||
for (let i = 0; i < 6; i++) {
|
||||
await clickGame(menuSlot[0], menuSlot[1]);
|
||||
await sleep(600);
|
||||
if (await ev(`${G}.menuSubBar?.isOpen`)) return true;
|
||||
// maybe a comm grabbed the deck — ack it and retry
|
||||
const btn = await ev(`(() => { const gs = ${G}; const b = gs.comms?.box; return b && b.button ? [b.x + b.button.x, b.y + b.button.y] : null; })()`);
|
||||
if (btn) { await clickGame(btn[0], btn[1]); await sleep(1200); }
|
||||
else await sleep(500);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
await clickGame(menuSlot[0], menuSlot[1]);
|
||||
await sleep(600);
|
||||
log('subbar open:', await ev(`${G}.menuSubBar?.isOpen`));
|
||||
if (!(await ev(`${G}.menuSubBar?.isOpen`))) {
|
||||
log('retry MENU click');
|
||||
await clickGame(menuSlot[0], menuSlot[1]);
|
||||
await sleep(600);
|
||||
}
|
||||
const sub = await ev(`(() => { const gs = ${G}; const bar = gs.menuSubBar; return bar.isOpen ? bar.buttons.map((b) => [b.id, bar.x + b.btn.x, bar.y + b.btn.y, b.btn.disabled]) : null; })()`);
|
||||
log('sub-bar:', JSON.stringify(sub));
|
||||
const saveBtn = sub.find((b) => b[0] === 'save');
|
||||
await clickGame(saveBtn[1], saveBtn[2]);
|
||||
// wait for the panel to be fully shown (cards up)
|
||||
let shown = false;
|
||||
for (let i = 0; i < 40 && !shown; i++) { await sleep(250); shown = (await ev(`${G}.savePanel.state`)) === 'shown'; }
|
||||
log('panel state:', await ev(`${G}.savePanel.state`));
|
||||
const card = await ev(`(() => { const gs = ${G}; const c = gs.savePanel.cards[2]; return [gs.savePanel.x + c.x, gs.savePanel.y + c.y, c.disabled]; })()`);
|
||||
log('slot-3 card (abs):', JSON.stringify(card));
|
||||
await clickGame(card[0], card[1]);
|
||||
await sleep(900);
|
||||
const saved = await ev(`(() => { const gs = ${G}; const r = gs.saveManager.get(3); return r ? { x: r.ship.x, y: r.ship.y, seed: r.seed, savedAt: r.savedAt } : null; })()`);
|
||||
log('slot 3 holds:', JSON.stringify(saved));
|
||||
if (!saved) throw new Error('save did not land in slot 3');
|
||||
// close the panel with CANCEL
|
||||
const cancel = await ev(`(() => { const gs = ${G}; const b = gs.savePanel.cancelBtn; return [gs.savePanel.x + b.x, gs.savePanel.y + b.y]; })()`);
|
||||
await clickGame(cancel[0], cancel[1]);
|
||||
let closed = false;
|
||||
for (let i = 0; i < 30 && !closed; i++) { await sleep(200); closed = (await ev(`${G}.savePanel.state`)) === 'hidden'; }
|
||||
log('panel closed:', closed);
|
||||
await sleep(400);
|
||||
|
||||
// ---- fly the ship away
|
||||
const before = await ev(`(() => { const gs = ${G}; return [gs.ship.x, gs.ship.y]; })()`);
|
||||
await ev(`(() => { const gs = ${G}; gs.ship.x += 500; gs.ship.y -= 320; return true; })()`);
|
||||
const after = await ev(`(() => { const gs = ${G}; return [gs.ship.x, gs.ship.y]; })()`);
|
||||
log(`ship: before=${JSON.stringify(before)} after=${JSON.stringify(after)}`);
|
||||
await sleep(500);
|
||||
|
||||
// ---- in-game LOAD: MENU -> Load Game -> slot 3 -> confirm LOAD
|
||||
if (!(await openSubBar())) throw new Error('sub-bar would not open (load step)');
|
||||
const sub2 = await ev(`(() => { const gs = ${G}; const bar = gs.menuSubBar; return bar.isOpen ? bar.buttons.map((b) => [b.id, bar.x + b.btn.x, bar.y + b.btn.y, b.btn.disabled]) : null; })()`);
|
||||
log('sub-bar:', JSON.stringify(sub2));
|
||||
const loadBtn = sub2 && sub2.find((b) => b[0] === 'load');
|
||||
if (!loadBtn) throw new Error('no load button in sub-bar');
|
||||
if (loadBtn[3]) throw new Error('Load Game DISABLED in sub-bar');
|
||||
await clickGame(loadBtn[1], loadBtn[2]);
|
||||
let shown2 = false;
|
||||
for (let i = 0; i < 40 && !shown2; i++) { await sleep(250); shown2 = (await ev(`${G}.savePanel.state`)) === 'shown'; }
|
||||
log('panel state:', await ev(`${G}.savePanel.state`));
|
||||
const card3 = await ev(`(() => { const gs = ${G}; const c = gs.savePanel.cards[2]; return [gs.savePanel.x + c.x, gs.savePanel.y + c.y, c.disabled]; })()`);
|
||||
await clickGame(card3[0], card3[1]);
|
||||
await sleep(900);
|
||||
const conf = await ev(`(() => { const gs = ${G}; const c = gs.savePanel.confirm; if (!c || c.state === 'hidden') return null; return { state: c.state, title: c.title?.text, x: gs.savePanel.x + c.x + c.confirmBtn.x, y: gs.savePanel.y + c.y + c.confirmBtn.y }; })()`);
|
||||
log('confirm overlay:', JSON.stringify(conf));
|
||||
if (!conf) throw new Error('confirm overlay did not open');
|
||||
await clickGame(conf.x, conf.y);
|
||||
// wait for the outcome: the scene RESTARTS IN PLACE with the staged
|
||||
// restore (reloadRun → scene.restart) — ship back at the saved spot.
|
||||
let restoredInPlace = false;
|
||||
for (let i = 0; i < 60 && !restoredInPlace; i++) {
|
||||
await sleep(250);
|
||||
const st = await ev(`(() => {
|
||||
const gs = window.game.scene.getScene('GameScene');
|
||||
return {
|
||||
active: window.game.scene.isActive('GameScene'),
|
||||
ship: gs && gs.ship ? [gs.ship.x, gs.ship.y] : null,
|
||||
};
|
||||
})()`);
|
||||
restoredInPlace = st.active && st.ship && Math.abs(st.ship[0] - saved.x) < 0.01 && Math.abs(st.ship[1] - saved.y) < 0.01;
|
||||
}
|
||||
log('AFTER in-game LOAD confirm (in-place restart):', restoredInPlace ? 'RESTORED — ship back at the saved spot' : 'NOT RESTORED', JSON.stringify(await sceneState()));
|
||||
log(' errors so far:', errors.slice(-4));
|
||||
if (!restoredInPlace) throw new Error('in-game LOAD did not restore the game in place');
|
||||
|
||||
// ---- now leave to the menu (sub-bar: Return to Main Menu) ---------------
|
||||
if (!(await openSubBar())) throw new Error('sub-bar would not open (return step)');
|
||||
const subR = await ev(`(() => { const gs = ${G}; const bar = gs.menuSubBar; return bar.buttons.map((b) => [b.id, bar.x + b.btn.x, bar.y + b.btn.y]); })()`);
|
||||
const mm = subR.find((b) => b[0] === 'mainMenu');
|
||||
if (!mm) throw new Error('no mainMenu button in sub-bar');
|
||||
await clickGame(mm[1], mm[2]);
|
||||
let atMenu = false;
|
||||
for (let i = 0; i < 30 && !atMenu; i++) { await sleep(250); atMenu = await ev(`window.game.scene.isActive('MenuScene')`); }
|
||||
if (!atMenu) throw new Error('Return to Main Menu did not land on the menu');
|
||||
log('back at the menu (via the sub-bar)');
|
||||
|
||||
// ---- menu Load Game -> slot 3 -> confirm (the second door) --------------
|
||||
await sleep(2500); // menu intro beats
|
||||
const mb2 = await ev(`(() => { const m = window.game.scene.getScene('MenuScene'); return { lg: [m.loadGameBtn.x, m.loadGameBtn.y, m.loadGameBtn.disabled] }; })()`);
|
||||
log('menu Load Game button:', JSON.stringify(mb2.lg));
|
||||
if (mb2.lg[2]) throw new Error('menu Load Game DISABLED');
|
||||
await clickGame(mb2.lg[0], mb2.lg[1]);
|
||||
let shown3 = false;
|
||||
for (let i = 0; i < 60 && !shown3; i++) { await sleep(250); shown3 = await ev(`window.game.scene.getScene('MenuScene').savePanel?.state === 'shown'`); }
|
||||
log('menu panel state:', await ev(`window.game.scene.getScene('MenuScene').savePanel?.state`));
|
||||
const mcard = await ev(`(() => { const m = window.game.scene.getScene('MenuScene'); const c = m.savePanel.cards[2]; return [m.savePanel.x + c.x, m.savePanel.y + c.y, c.disabled]; })()`);
|
||||
log('menu slot-3 card (abs):', JSON.stringify(mcard));
|
||||
await clickGame(mcard[0], mcard[1]);
|
||||
await sleep(900);
|
||||
const mconf = await ev(`(() => { const m = window.game.scene.getScene('MenuScene'); const c = m.savePanel.confirm; if (!c || c.state === 'hidden') return null; return { state: c.state, title: c.title?.text, x: m.savePanel.x + c.x + c.confirmBtn.x, y: m.savePanel.y + c.y + c.confirmBtn.y }; })()`);
|
||||
log('menu confirm:', JSON.stringify(mconf));
|
||||
if (!mconf) throw new Error('menu confirm overlay did not open');
|
||||
await clickGame(mconf.x, mconf.y);
|
||||
await sleep(2500);
|
||||
log('AFTER menu LOAD confirm:', JSON.stringify(await sceneState()));
|
||||
|
||||
log('== FINAL ==', JSON.stringify(await sceneState()));
|
||||
log(' saved ship was:', JSON.stringify({ x: saved.x, y: saved.y }));
|
||||
log(' all console errors/warnings:', errors.length ? errors.slice(-12) : '(none)');
|
||||
|
||||
cdp.close();
|
||||
kill();
|
||||
|
|
@ -1096,9 +1096,13 @@ export class GameScene extends Phaser.Scene {
|
|||
onAction: (id) => this.subBarAction(id),
|
||||
});
|
||||
this.savePanel = new SavePanel(this, {
|
||||
// Load confirmed: the restore is staged — hand back to the menu
|
||||
// (its New Game is the "start" the staged restore rides on).
|
||||
onLoadComplete: () => this.returnToMenu(),
|
||||
// Load confirmed: the restore is staged in the shared registry —
|
||||
// restart this scene in place (the jump cut's seam) and let the
|
||||
// fresh create() pick it up. A confirmed LOAD drops the run back
|
||||
// where it was saved; it is not a detour through the menu (the
|
||||
// menu's New Game resets the run state, so it never carried a
|
||||
// staged restore).
|
||||
onLoadComplete: () => this.reloadRun(),
|
||||
});
|
||||
|
||||
// The MINERALS readout — upper right (the corner the old tether stack
|
||||
|
|
@ -6891,6 +6895,21 @@ export class GameScene extends Phaser.Scene {
|
|||
if (this.menuSubBar && this.menuSubBar.isOpen) this.menuSubBar.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirmed in-game load: the restore is staged in the shared registry
|
||||
* (SavePanel.doLoad → prepareLoad) and the panel has already closed
|
||||
* (onLoadComplete fires after the close animation). Restart this scene
|
||||
* in place — the jump cut's seam (scene.restart reuses this instance)
|
||||
* — so the fresh create() consumes the staged restore (consumeRestore →
|
||||
* applyRestore). The restart's shutdown tears the sub-bar, the panel
|
||||
* and any live comms down the same way a jump does.
|
||||
*/
|
||||
reloadRun() {
|
||||
this.savePanel?.close();
|
||||
this.menuSubBar?.dismiss();
|
||||
this.scene.restart();
|
||||
}
|
||||
|
||||
/** Return to the main menu (the sub-bar's last button). */
|
||||
returnToMenu() {
|
||||
this.savePanel?.close();
|
||||
|
|
|
|||
|
|
@ -1162,9 +1162,12 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
onAction: (id) => this.subBarAction(id),
|
||||
});
|
||||
this.savePanel = new SavePanel(this, {
|
||||
// Load confirmed: the restore is staged — hand back to the menu
|
||||
// (its New Game is the "start" the staged restore rides on).
|
||||
onLoadComplete: () => this.returnToMenu(),
|
||||
// Load confirmed: the restore is staged in the shared registry (the
|
||||
// run's GameScene is the stateScene — same registry). Leave the
|
||||
// surface and restart the game scene so its fresh create() picks the
|
||||
// staged restore up — a confirmed LOAD drops the run back where it
|
||||
// was saved, not out to the menu.
|
||||
onLoadComplete: () => this.reloadRun(),
|
||||
stateScene: this.gameScene ?? this,
|
||||
});
|
||||
|
||||
|
|
@ -1761,6 +1764,19 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
this.scene.wake('GameScene');
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirmed surface load: the restore is staged in the shared registry
|
||||
* (SavePanel.doLoad → prepareLoad, stateScene = the run's GameScene) and
|
||||
* the panel has closed. Stop this scene and start the game scene (v4
|
||||
* scene.start on a sleeping scene shuts it down and re-creates it), so
|
||||
* the fresh GameScene.create() consumes the staged restore; the
|
||||
* take-off story seam stays untouched (a load is not a departure).
|
||||
*/
|
||||
reloadRun() {
|
||||
this.menuSubBar?.dismiss();
|
||||
this.scene.start('GameScene'); // stop this scene + start the game scene
|
||||
}
|
||||
|
||||
/** Return to the main menu (the sub-bar's last button). */
|
||||
returnToMenu() {
|
||||
this.savePanel?.close();
|
||||
|
|
|
|||
|
|
@ -52,8 +52,11 @@ export class SavePanel extends Phaser.GameObjects.Container {
|
|||
* ship, discovery, tetherField, playTimeMs — what captureState reads)
|
||||
* @param {object} [o] {
|
||||
* onLoadComplete?: (slot, record) => void — called after the
|
||||
* load-confirm lands and the panel has closed (the scene starts
|
||||
* the menu; the menu's New Game picks the staged restore up),
|
||||
* load-confirm lands and the panel has closed. The restore is
|
||||
* already staged in the shared registry (prepareLoad); the scene
|
||||
* restarts the game on it (GameScene/SurfaceScene — a confirmed
|
||||
* LOAD drops the run back where it was saved) or, from the menu,
|
||||
* starts the game scene on the staged restore.
|
||||
* stateScene?: Phaser.Scene — where captureState/prepareLoad read
|
||||
* the run's state. Defaults to `scene`. SurfaceScene passes the
|
||||
* paused GameScene (the surface is a launched overlay; the run
|
||||
|
|
|
|||
Loading…
Reference in New Issue