// dev/escort-compass-click.mjs — REPRO: the escort's FINAL-STOP compass box, // clicked in a REAL BROWSER (Playwright + vendored Phaser, true per-frame // order). Run: // // node dev/server.mjs 8123 & // node dev/escort-compass-click.mjs // // Sets an escort contract whose destination is the CURRENT system's planet // (off-screen), waits for the compass to stand its magenta entry, then // CLICKS the chip with a real mouse event and asserts the ship retargets // to the planet's rim. const PORT = process.env.ORBIT_PORT || '8123'; const BASE = `http://127.0.0.1:${PORT}`; const { pathToFileURL } = await import('node:url'); const { chromium } = await import(pathToFileURL('/home/brianfertig/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.mjs').href); const consoleErrors = []; const browser = await chromium.launch({ headless: true, executablePath: '/home/brianfertig/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome', args: ['--no-sandbox', '--use-gl=swiftshader', '--enable-unsafe-swiftshader'], }); const page = await browser.newPage(); page.on('console', (m) => { if (m.type() === 'error') consoleErrors.push(m.text()); }); page.on('pageerror', (e) => consoleErrors.push(`pageerror: ${e.message}`)); await page.goto(`${BASE}/index.html`, { waitUntil: 'load' }); await page.waitForFunction(() => !!(window.game && window.game.scene && window.game.scene.getScene && window.game.scene.getScene('MenuScene')), null, { timeout: 20000 }); await page.waitForFunction(() => { const m = window.game?.scene?.getScene?.('MenuScene'); return !!(m && m.overlay && m.title && m.seedValue && !m.dead); }, null, { timeout: 20000 }); await page.evaluate(() => { const m = window.game.scene.getScene('MenuScene'); if (!m.seedValue) m.seedValue = 'ESCORT01'; m.startNewGame(); }); await page.waitForFunction(() => { const s = window.game.scene.getScene('GameScene'); return !!(s && s.systemRecord && s.ship); }, null, { timeout: 40000 }); console.log(' ✓ game booted, GameScene active'); // ---- stand the escort contract: destination = a planet of the current system const setup = await page.evaluate(() => { const s = window.game.scene.getScene('GameScene'); const sysId = s.systemRecord.id; const p = (s.systemPlanets ?? [])[0]; if (!p) return { error: 'no planets in the current system' }; // Park the ship far from the planet so the planet is off-screen. const away = Math.hypot(p.x, p.y) > 10 ? 1 : -1; s.ship.x = p.x + away * 3000; s.ship.y = p.y + 3000; s.ship.target = null; s.ship.thrustDir = null; const st = s.sideQuestState; const rec = (s.systemContent.planets ?? []).find((o) => o && o.name === p.discoveryName) ?? null; st.accept('repro-job'); const route = { kind: 'escort', targetSystemId: sysId, waypoints: [[10, 10]], credits: 50, xp: 5, escort: { phase: 'transit', originSystemId: 'sys-elsewhere', destSystemId: sysId, destObjectType: 'planet', destObjectId: rec ? rec.id : '', destObjectName: p.discoveryName, hops: 1, transportMaxHull: 1200, transportHull: 1200, transportMaxShields: 500, transportShields: 500, escortCount: 1, name: 'Repro Convoy', }, }; st.setRoute('repro-job', route); st.setActive('repro-job'); return { planet: { x: p.x, y: p.y, name: p.discoveryName, discoveryId: p.discoveryId }, ship: { x: s.ship.x, y: s.ship.y }, activeId: st.activeId }; }); console.log(' setup:', JSON.stringify(setup)); // Wait for the compass to stand the escort's entry. await page.waitForFunction(() => { const s = window.game.scene.getScene('GameScene'); const keys = [...s.compass.entries.keys()]; return keys.some((x) => x.startsWith('escort:obj:')); }, null, { timeout: 15000 }).catch(() => null); const entry = await page.evaluate(() => { const s = window.game.scene.getScene('GameScene'); const k = [...s.compass.entries.keys()].find((x) => x.startsWith('escort:obj:')); if (!k) return null; const e = s.compass.entries.get(k); return { id: k, chip: { x: e.chipRoot.x, y: e.chipRoot.y }, target: { x: e.x, y: e.y } }; }); if (!entry) { const diag = await page.evaluate(() => { const s = window.game.scene.getScene('GameScene'); return { keys: [...s.compass.entries.keys()], t: s.escortCompassTarget ? s.escortCompassTarget() : null }; }); console.log(' ✗ NO COMPASS ENTRY', JSON.stringify(diag), '\n CONSOLE', consoleErrors.slice(0, 10)); await browser.close(); process.exit(1); } console.log(` ✓ compass entry stands: ${entry.id} at chip (${Math.round(entry.chip.x)}, ${Math.round(entry.chip.y)})`); // ---- CLICK the chip with a real mouse event ------------------------------- // Isolate the chain from comm timing: in transit the escort lane can fire a // wave → an OAC comm starts and (by design) owns the input, so a click in // that window is a legitimate no-op. Stub the gate to false to test the // COMPASS → AUTOPILOT chain itself. await page.evaluate(() => { const s = window.game.scene.getScene('GameScene'); s._commInputBlocked = () => false; s.__setTargetCalls = []; const oSet = s.ship.setTarget.bind(s.ship); s.ship.setTarget = (x, y) => { s.__setTargetCalls.push({ x, y }); return oSet(x, y); }; }); await page.mouse.move(entry.chip.x, entry.chip.y); await page.mouse.down(); await page.mouse.up(); await page.waitForTimeout(300); // The REGRESSION assertion for the user's bug: the chip click must REACH // autopilotTo and issue a setTarget (before the fix the whole chain was a // silent no-op — the compass stored the content RECORD id and the // discovery lookup missed). Whether the target SURVIVES a frame later is // up to the rest of the game (a comm's pause contract legitimately stops // the ship — that is orthogonal to this chain). const result = await page.evaluate(() => { const s = window.game.scene.getScene('GameScene'); return { calls: s.__setTargetCalls, target: s.ship.target, state: s.ship.state }; }); console.log(' click result:', JSON.stringify(result)); const ok = result.calls.length > 0; console.log(ok ? ` ✓ chip click drove the autopilot → setTarget(${result.calls.at(-1).x?.toFixed(1)}, ${result.calls.at(-1).y?.toFixed(1)}) — the destination world's rim (tether-clamped)` : ' ✗ NO RETARGET — the click did nothing'); if (consoleErrors.length) console.log(' CONSOLE ERRORS\n ' + consoleErrors.slice(0, 10).join('\n ')); await browser.close(); process.exit(ok ? 0 : 1);