orbit/dev/enc-probe4.mjs

123 lines
7.2 KiB
JavaScript

/**
* Dev-only PROBE 4: instrument the CombatHud draw calls to learn EXACTLY where
* the HUD draws (arrow triangles / threat circles / feed text) and confirm
* pixels land there. This tells me whether "renders correctly" is a geometry
* mismatch in my test or a real rendering gap.
* node dev/enc-probe4.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 R = { arrows: [], threats: [], feedTexts: [] };
// ---- instrument the HUD's draw calls (capture every geometry it issues)
const ag = hud._arrowG, tg = hud._threatG;
const origTri = ag.fillTriangle.bind(ag);
ag.fillTriangle = function (x1, y1, x2, y2, x3, y3) { R.arrows.push([Math.round(x1), Math.round(y1), Math.round(x2), Math.round(y2), Math.round(x3), Math.round(y3)]); return origTri(x1, y1, x2, y2, x3, y3); };
const origCirc = tg.fillCircle.bind(tg);
tg.fillCircle = function (x, y, r) { R.threats.push({ x: Math.round(x), y: Math.round(y), r: Math.round(r) }); return origCirc(x, y, r); };
// ---- clean scenario: ONE off-screen pirate (top) + ONE on-screen neutral (ally)
gs._spawnEncounter({ kind: 'mixed', pirates: 1, neutrals: 1 });
const p = live()[0], n = allies()[0];
for (const e of live()) tough(e); for (const a of allies()) tough(a);
if (p) { p.x = ship.x; p.y = ship.y - 1500; } // off-screen above -> should get an ARROW
if (n) { n.x = ship.x - 120; n.y = ship.y + 10; } // on-screen ally -> should get NEITHER
// enable threat so the layer draws
const tOrig = hud.threat; hud.threat = { ...tOrig, enabled: true };
R.arrows.length = 0; R.threats.length = 0;
await sleep(700); unblock(); // let the scene draw several frames
R.arrows = R.arrows.slice(0, 6);
R.threats = R.threats.slice(-6);
R.pirate = p ? { x: Math.round(p.x), y: Math.round(p.y), s: (() => { const q = hud.worldToScreen(p.x, p.y); return [Math.round(q.x), Math.round(q.y)]; })() } : null;
R.neutral = n ? { x: Math.round(n.x), y: Math.round(n.y), s: (() => { const q = hud.worldToScreen(n.x, n.y); return [Math.round(q.x), Math.round(q.y)]; })() } : null;
// ---- pixel at the drawn positions
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.__hooked4) { window.__hooked4 = true; const orig = ren.render; ren.render = function (...a) { const r = orig.apply(this, a); try { octx.drawImage(cv, 0, 0); } catch {} return r; }; }
await sleep(250);
const px = (x, y, half = 10) => {
const X = Math.max(0, Math.round(x - half)), Y = Math.max(0, Math.round(y - half));
const W = Math.min(half * 2, cv.width - X), H = Math.min(half * 2, cv.height - Y);
if (W <= 0 || H <= 0) return null;
const d = octx.getImageData(X, Y, W, H).data;
let red = 0, maxR = 0, sample = [0, 0, 0];
for (let i = 0; i < d.length; i += 4) {
const r = d[i], g = d[i + 1], b = d[i + 2];
if (r > 130 && r - g > 45 && r - b > 25) red++;
if (r > maxR) { maxR = r; sample = [r, g, b]; }
}
return { red, maxR, sample };
};
if (R.arrows.length) {
const c = R.arrows[R.arrows.length - 1];
const cx = (c[0] + c[2] + c[4]) / 3, cy = (c[1] + c[3] + c[5]) / 3;
R.pixelAtArrow = px(cx, cy, 14);
}
if (R.threats.length) {
const c = R.threats[R.threats.length - 1];
R.pixelAtThreat = px(c.x, c.y, 12);
}
// neutral slot (should be clean)
if (R.neutral) R.pixelAtNeutralSlot = px(R.neutral.s[0], R.neutral.s[1] - 36, 12);
hud.threat = tOrig;
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-probe4: 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-probe4 error:', e.message); process.exit(2); });