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