/** * Dev-only: live CDP PROBE of the CombatHud + Phaser 4.2.1 surface (one-shot). * Answers: which scene is the hud wired to, does add.text/add.graphics exist, * what storage does Graphics use (no `.commands` in this build), and does * `_drawFeed` create its text objects when driven directly. * * node dev/hud-probe.mjs */ const CDP = process.env.CDP_PORT ? `127.0.0.1:${process.env.CDP_PORT}` : '127.0.0.1:9333'; const BASE = process.env.BASE_URL ?? 'http://127.0.0.1:8080'; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); function connect(target) { const ws = new WebSocket(target.webSocketDebuggerUrl); let id = 0; const pending = new Map(); ws.onmessage = (x) => { const m = JSON.parse(x.data); if (m.id && pending.has(m.id)) { const { res, rej } = pending.get(m.id); pending.delete(m.id); m.error ? rej(new Error(m.error.message)) : res(m.result); } }; const open = new Promise((r) => { ws.onopen = r; ws.onerror = () => r(); }); const send = (method, params = {}) => new Promise((res, rej) => { const mid = ++id; pending.set(mid, { res, rej }); ws.send(JSON.stringify({ id: mid, method, params })); }); return { ws, send, open }; } async function evalJs(c, expression, awaitPromise = true) { const r = await c.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise }); if (r.exceptionDetails) throw new Error(JSON.stringify(r.exceptionDetails, null, 2)); return r.result?.value; } async function openPage(path) { const url = `${BASE}${path}`; let target; try { target = await (await fetch(`http://${CDP}/json/new?${encodeURIComponent(url)}`, { method: 'PUT' })).json(); } catch { target = await (await fetch(`http://${CDP}/json/new?${encodeURIComponent(url)}`, { method: 'POST' })).json(); } const c = connect(target); await c.open; await c.send('Runtime.enable'); return { c, target }; } const PROBE = `(async () => { const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const gs = window.game && window.game.scene ? window.game.scene.getScene('GameScene') : null; if (!gs || !gs.combat || !gs.ship) return 'no scene/combat/ship yet'; const hud = gs.combatHud; if (!hud) return 'no combatHud'; const out = { sameScene: hud.scene === gs, sceneName: gs.sys && gs.sys.name, add: { type: gs.add ? gs.add.constructor && gs.add.constructor.name : String(gs.add), text: gs.add ? typeof gs.add.text : 'no add', graphics: gs.add ? typeof gs.add.graphics : 'no add', }, }; const describe = (g) => { if (g === null) return 'null'; if (!g) return String(g); let protoNames = []; try { protoNames = Object.getOwnPropertyNames(Object.getPrototypeOf(g)).slice(0, 60); } catch (e) { protoNames = ['ERR:' + e.message]; } let own = []; try { own = Object.keys(g); } catch ( e) { own = ['ERR:' + e.message]; } return { proto: protoNames, own }; }; out.threatG = describe(hud._threatG); out.arrowG = describe(hud._arrowG); out.reticleG = describe(hud._reticleG); // Drive the feed draw directly with a synthetic entry; catch anything. try { hud.feed.length = 0; hud.feedTexts.length = 0; hud.feed.push({ label: 'PROBE ENTRY', t: hud._now() }); let drew = 'ok'; try { hud._drawFeed(hud._now()); } catch (e) { drew = 'THREW: ' + e.message; } out.feedDrive = { drew, feedLen: hud.feed.length, feedTextsLen: hud.feedTexts.length, t0: hud.feedTexts[0] ? { text: hud.feedTexts[0].text, x: hud.feedTexts[0].x, visible: hud.feedTexts[0].visible, scene: hud.feedTexts[0].scene ? hud.feedTexts[0].scene.sys && hud.feedTexts[0].scene.sys.name : null } : null }; } catch (e) { out.feedDrive = 'outer THREW: ' + e.message; } // A plain scene text, the way the rest of the game makes text. try { const t = gs.add.text(1, 1, 'x', { fontSize: '10px' }); out.sceneText = t ? (t.constructor && t.constructor.name) + ' w=' + t.width : 'falsy'; if (t && typeof t.destroy === 'function') t.destroy(); } catch (e) { out.sceneText = 'THREW: ' + e.message; } // Full update() through the real path, with the feed primed. try { hud.feed.push({ label: 'SECOND', t: hud._now() }); let upd = 'ok'; try { hud.update(gs.time.now); } catch (e) { upd = 'THREW: ' + e.message; } out.fullUpdate = { upd, feedTextsLen: hud.feedTexts.length }; } catch (e) { out.fullUpdate = 'outer THREW: ' + e.message; } return out; })()`; async function main() { const { c, target } = await openPage('/index.html'); try { for (let i = 0; i < 80; i++) { await sleep(500); let ok = false; try { ok = await evalJs(c, 'Boolean(window.game)', false); } catch { ok = false; } if (ok) break; } const started = await evalJs(c, `(async () => { const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); for (let i = 0; i < 120; i++) { const m = window.game && window.game.scene && window.game.scene.getScene('MenuScene'); if (m && typeof m.seedValue === 'string') { m.startNewGame(); return 'started'; } await sleep(500); } return 'menu never ready'; })()`); if (started !== 'started') throw new Error(`start: ${started}`); for (let i = 0; i < 60; i++) { await sleep(500); let ok = false; try { ok = await evalJs(c, 'Boolean(window.__ORBIT_COMBAT__ && window.game.scene.getScene("GameScene").ship)', false); } catch { ok = false; } if (ok) break; } await sleep(800); const out = await evalJs(c, PROBE); console.log(typeof out === 'string' ? out : JSON.stringify(out, null, 1)); return true; } catch (err) { console.error(`hud-probe: FAIL — ${err.message}`); return false; } finally { try { await fetch(`http://${CDP}/json/close/${target.id}`).catch(() => {}); } catch { /* ignore */ } c.ws.close(); } } main().then((ok) => process.exit(ok ? 0 : 1)).catch((e) => { console.error('hud-probe error:', e.message); process.exit(2); });