orbit/dev/load-repro.mjs

191 lines
10 KiB
JavaScript

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