orbit/dev/userflow.mjs

223 lines
12 KiB
JavaScript

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