From 1a12fa5e1a61519ac1d0fc9d0ca7ef77bafa9d8b Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Fri, 18 Sep 2026 12:13:34 -0600 Subject: [PATCH] Restore in-place on confirmed load instead of cutting to the menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- dev/cdp-chrome.mjs | 41 +++++++ dev/confirm-geom.mjs | 95 ++++++++++++++++ dev/enum-probe.mjs | 76 +++++++++++++ dev/hit-probe.mjs | 73 +++++++++++++ dev/input-probe.mjs | 51 +++++++++ dev/load-repro.mjs | 190 ++++++++++++++++++++++++++++++++ dev/menu-click-probe.mjs | 72 +++++++++++++ dev/saves-ui-test.mjs | 41 +++++-- dev/slots-probe.mjs | 41 +++++++ dev/under-probe.mjs | 53 +++++++++ dev/userflow.mjs | 222 ++++++++++++++++++++++++++++++++++++++ js/scenes/GameScene.js | 25 ++++- js/scenes/SurfaceScene.js | 22 +++- js/ui/SavePanel.js | 7 +- 14 files changed, 993 insertions(+), 16 deletions(-) create mode 100644 dev/cdp-chrome.mjs create mode 100644 dev/confirm-geom.mjs create mode 100644 dev/enum-probe.mjs create mode 100644 dev/hit-probe.mjs create mode 100644 dev/input-probe.mjs create mode 100644 dev/load-repro.mjs create mode 100644 dev/menu-click-probe.mjs create mode 100644 dev/slots-probe.mjs create mode 100644 dev/under-probe.mjs create mode 100644 dev/userflow.mjs diff --git a/dev/cdp-chrome.mjs b/dev/cdp-chrome.mjs new file mode 100644 index 0000000..462682c --- /dev/null +++ b/dev/cdp-chrome.mjs @@ -0,0 +1,41 @@ +// Run a saves-ui-test page in headless Chrome (CDP) and print the result. +// node dev/cdp-chrome.mjs [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(); +} diff --git a/dev/confirm-geom.mjs b/dev/confirm-geom.mjs new file mode 100644 index 0000000..5e761ba --- /dev/null +++ b/dev/confirm-geom.mjs @@ -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(); diff --git a/dev/enum-probe.mjs b/dev/enum-probe.mjs new file mode 100644 index 0000000..dded609 --- /dev/null +++ b/dev/enum-probe.mjs @@ -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(); diff --git a/dev/hit-probe.mjs b/dev/hit-probe.mjs new file mode 100644 index 0000000..2b84e1f --- /dev/null +++ b/dev/hit-probe.mjs @@ -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(); diff --git a/dev/input-probe.mjs b/dev/input-probe.mjs new file mode 100644 index 0000000..bd66ade --- /dev/null +++ b/dev/input-probe.mjs @@ -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(); diff --git a/dev/load-repro.mjs b/dev/load-repro.mjs new file mode 100644 index 0000000..07f5cf8 --- /dev/null +++ b/dev/load-repro.mjs @@ -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(); diff --git a/dev/menu-click-probe.mjs b/dev/menu-click-probe.mjs new file mode 100644 index 0000000..c0d1d73 --- /dev/null +++ b/dev/menu-click-probe.mjs @@ -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(); diff --git a/dev/saves-ui-test.mjs b/dev/saves-ui-test.mjs index 8f5d1c5..e5adbe8 100644 --- a/dev/saves-ui-test.mjs +++ b/dev/saves-ui-test.mjs @@ -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 diff --git a/dev/slots-probe.mjs b/dev/slots-probe.mjs new file mode 100644 index 0000000..86f4db7 --- /dev/null +++ b/dev/slots-probe.mjs @@ -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(); diff --git a/dev/under-probe.mjs b/dev/under-probe.mjs new file mode 100644 index 0000000..5c01cf2 --- /dev/null +++ b/dev/under-probe.mjs @@ -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(); diff --git a/dev/userflow.mjs b/dev/userflow.mjs new file mode 100644 index 0000000..f025d6d --- /dev/null +++ b/dev/userflow.mjs @@ -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(); diff --git a/js/scenes/GameScene.js b/js/scenes/GameScene.js index 11e6fab..1fba9bc 100644 --- a/js/scenes/GameScene.js +++ b/js/scenes/GameScene.js @@ -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(); diff --git a/js/scenes/SurfaceScene.js b/js/scenes/SurfaceScene.js index 56d56bb..5d88f05 100644 --- a/js/scenes/SurfaceScene.js +++ b/js/scenes/SurfaceScene.js @@ -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(); diff --git a/js/ui/SavePanel.js b/js/ui/SavePanel.js index b0716da..2b7d58d 100644 --- a/js/ui/SavePanel.js +++ b/js/ui/SavePanel.js @@ -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