/** * Dev-only PROBE 5: decisive test of THREAT-MARKER rendering. * Moves pirates ON-SCREEN, enables the (data-disabled) threat layer, lets the * NATURAL scene render draw it (no manual update), then freezes and samples * pixels at worldToScreen(pirate.x, pirate.y-36). Also sanity-checks that the * pirate body itself is visible, and that a neutral's slot stays clean. * node dev/enc-probe5.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(t) { const ws = new WebSocket(t.webSocketDebuggerUrl); let id = 0; const p = new Map(); ws.onmessage = (x) => { const m = JSON.parse(x.data); if (m.id && p.has(m.id)) { const { res, rej } = p.get(m.id); p.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; p.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 t; try { t = await (await fetch(`http://${CDP}/json/new?${encodeURIComponent(url)}`, { method: 'PUT' })).json(); } catch { t = await (await fetch(`http://${CDP}/json/new?${encodeURIComponent(url)}`, { method: 'POST' })).json(); } const c = connect(t); await c.open; await c.send('Runtime.enable'); return { c, target: t }; } const PAGE = `(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 || !gs.combatHud) return 'no GameScene/combat/ship/combatHud'; const sys = gs.combat, hud = gs.combatHud, ship = gs.ship; const unblock = () => { try { if (gs.comms && gs.comms.paused) gs.comms._ack(); } catch {} }; unblock(); const live = () => sys.enemies.filter((e) => !e.dead); const allies = () => sys.allies.filter((a) => !a.dead); const tough = (o) => { o.maxHull = 1e9; o.hull = 1e9; o.maxShields = 1e9; o.shields = 1e9; }; const vw = gs.scale.width, vh = gs.scale.height; const R = {}; const ren = window.game.renderer, cv = ren.canvas; const off = document.createElement('canvas'); off.width = cv.width; off.height = cv.height; const octx = off.getContext('2d'); if (!window.__hooked5) { window.__hooked5 = true; const orig = ren.render; ren.render = function (...a) { const r = orig.apply(this, a); try { octx.drawImage(cv, 0, 0); } catch {} return r; }; } const RED = (r, g, b) => r > 130 && r - g > 45 && r - b > 25; const count = (x, y, w, h, test) => { const X = Math.max(0, Math.round(x)), Y = Math.max(0, Math.round(y)); const W = Math.min(w, cv.width - X), H = Math.min(h, cv.height - Y); if (W <= 0 || H <= 0) return -1; // off-canvas const d = octx.getImageData(X, Y, W, H).data; let n = 0; for (let i = 0; i < d.length; i += 4) if (test(d[i], d[i+1], d[i+2])) n++; return n; }; const redBox = (cx, cy, half) => count(cx - half, cy - half, half * 2, half * 2, RED); // spawn 2 pirates + 1 neutral; toughen; hold pirates ON-SCREEN near center. gs._spawnEncounter({ kind: 'mixed', pirates: 2, neutrals: 1 }); for (const e of live()) tough(e); for (const a of allies()) tough(a); const [p0, p1] = live(), [n0] = allies(); if (p0) { p0.x = ship.x - 40; p0.y = ship.y - 120; } // on-screen, above player if (p1) { p1.x = ship.x + 120; p1.y = ship.y + 60; } // on-screen, right of player if (n0) { n0.x = ship.x + 20; n0.y = ship.y + 160; } // on-screen ally (should NOT get a marker) await sleep(150); unblock(); R.before = { live: live().length, allies: allies().length, p0: p0 ? (() => { const s = hud.worldToScreen(p0.x, p0.y); return { w: [Math.round(p0.x), Math.round(p0.y)], s: [Math.round(s.x), Math.round(s.y)] }; })() : null, n0: n0 ? (() => { const s = hud.worldToScreen(n0.x, n0.y); return { w: [Math.round(n0.x), Math.round(n0.y)], s: [Math.round(s.x), Math.round(s.y)] }; })() : null, threatCfg: { ...hud.threat } }; // Baseline (threat still disabled by data): pirate slot should be clean. const slot = (o) => { const s = hud.worldToScreen(o.x, o.y - 36); return { x: s.x, y: s.y }; }; R.base = { p0slot: p0 ? redBox(slot(p0).x, slot(p0).y, 16) : null, n0slot: n0 ? redBox(slot(n0).x, slot(n0).y, 16) : null, p0body: p0 ? redBox(hud.worldToScreen(p0.x, p0.y).x, hud.worldToScreen(p0.x, p0.y).y, 16) : null }; // ENABLE the threat layer and let the NATURAL scene render draw it. const tOrig = hud.threat; hud.threat = { ...tOrig, enabled: true }; await sleep(500); unblock(); // re-pin (AI may have nudged them) then one more natural draw if (p0) { p0.x = ship.x - 40; p0.y = ship.y - 120; } if (n0) { n0.x = ship.x + 20; n0.y = ship.y + 160; } await sleep(300); unblock(); // freeze so the presented frame is stable, then sample if (gs.comms) gs.comms._setPaused(true); await sleep(400); R.after = { p0slot: p0 ? redBox(slot(p0).x, slot(p0).y, 16) : null, p1slot: p1 ? redBox(slot(p1).x, slot(p1).y, 16) : null, n0slot: n0 ? redBox(slot(n0).x, slot(n0).y, 16) : null, p0body: p0 ? redBox(hud.worldToScreen(p0.x, p0.y).x, hud.worldToScreen(p0.x, p0.y).y, 16) : null, threatG_cmds: hud._threatG ? (hud._threatG.commandBuffer ? hud._threatG.commandBuffer.length : 'n/a') : 'no _threatG', }; R.after.p0slotPos = p0 ? (() => { const s = slot(p0); return [Math.round(s.x), Math.round(s.y)]; })() : null; R.after.n0slotPos = n0 ? (() => { const s = slot(n0); return [Math.round(s.x), Math.round(s.y)]; })() : null; hud.threat = tOrig; if (gs.comms) gs.comms._setPaused(false); return R; })()`; async function main() { const { c, target } = await openPage('/index.html'); try { let ready = false; for (let i = 0; i < 80; i++) { await sleep(500); try { ready = await evalJs(c, 'Boolean(window.game)', false); } catch { ready = false; } if (ready) break; } if (!ready) throw new Error('game never booted'); 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); let scoped = false; for (let i = 0; i < 60; i++) { await sleep(500); try { scoped = await evalJs(c, 'Boolean(window.__ORBIT_COMBAT__ && window.game.scene.getScene("GameScene").ship && window.game.scene.getScene("GameScene").combat)', false); } catch { scoped = false; } if (scoped) break; } if (!scoped) throw new Error('combat scope never appeared'); await sleep(800); const R = await evalJs(c, PAGE); if (typeof R === 'string') throw new Error('probe: ' + R); console.log(JSON.stringify(R, null, 2)); return true; } catch (e) { console.error('enc-probe5: FAIL —', e.message); return false; } finally { try { await fetch(`http://${CDP}/json/close/${target.id}`).catch(() => {}); } catch {} c.ws.close(); } } main().then((ok) => process.exit(ok ? 0 : 1)).catch((e) => { console.error('enc-probe5 error:', e.message); process.exit(2); });