// 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();