206 lines
9.0 KiB
JavaScript
206 lines
9.0 KiB
JavaScript
/**
|
|
* Dev-only focused PROBE — ground truth for the encounter-HUD pixel checks.
|
|
* Answers three questions before I finalize dev/encounter-hud-check.mjs:
|
|
*
|
|
* 1. Does the Phaser render loop keep presenting frames while the sim is
|
|
* FROZEN via the comms pause (game.comms._setPaused(true))? (A render
|
|
* counter on the hooked renderer tells us: if it doesn't, a manual
|
|
* hud.update() during the freeze never reaches a presented frame, which
|
|
* would explain zero threat-marker pixels.)
|
|
* 2. WHERE are the red pixels actually (off-screen arrows + threat
|
|
* markers)? A full-frame red scan with cluster reporting — no fixed
|
|
* boxes — so we see the true geometry instead of guessing.
|
|
* 3. Does a kill actually flip e.dead and land in the feed when the pirate
|
|
* is made killable (the cascade bug: killEnemy() alone does not set
|
|
* e.dead, so toughened pirates survived the sweep and poisoned b/c)?
|
|
*
|
|
* node dev/enc-probe2.mjs
|
|
*
|
|
* Needs the dev server on :8080 and headless Chromium CDP on 127.0.0.1:9333.
|
|
*/
|
|
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 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();
|
|
sys.maxHull = 1e9; sys.hull = 1e9; sys.maxShields = 1e9; sys.shields = 1e9;
|
|
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 = { q1: {}, q2: {}, q3: {} };
|
|
|
|
// ---- render hook + counter
|
|
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');
|
|
window.__renderCount = 0;
|
|
const orig = ren.render;
|
|
ren.render = function (...a) {
|
|
const ret = orig.apply(this, a);
|
|
try { octx.drawImage(cv, 0, 0); window.__renderCount++; } catch {}
|
|
return ret;
|
|
};
|
|
const vw = gs.scale.width, vh = gs.scale.height;
|
|
const RED = (r, g, b) => r > 130 && r - g > 45 && r - b > 25;
|
|
// Full-frame red scan -> 32px-bin clusters (center + count), biggest first.
|
|
const redClusters = (max = 12) => {
|
|
const bins = new Map();
|
|
const d = octx.getImageData(0, 0, cv.width, cv.height).data;
|
|
for (let y = 0; y < cv.height; y++) for (let x = 0; x < cv.width; x++) {
|
|
const i = (y * cv.width + x) * 4;
|
|
if (RED(d[i], d[i+1], d[i+2])) {
|
|
const k = ((x >> 5) & 1023) + ',' + ((y >> 5) & 1023);
|
|
const b = bins.get(k) || { n: 0, sx: 0, sy: 0 };
|
|
b.n++; b.sx += x; b.sy += y; bins.set(k, b);
|
|
}
|
|
}
|
|
return [...bins.values()].map((b) => ({ n: b.n, x: Math.round(b.sx/b.n), y: Math.round(b.sy/b.n) }))
|
|
.sort((a, b) => b.n - a.n).slice(0, max);
|
|
};
|
|
const mod = await import('/js/combat/Combat.js');
|
|
const arrowPt = (e) => { const s = hud.worldToScreen(e.x, e.y); return mod.offscreenArrow(vw, vh, s.x, s.y, 28); };
|
|
|
|
// ---- spawn the mix (tough, so nothing dies mid-probe)
|
|
gs._spawnEncounter({ kind: 'mixed', pirates: 2, neutrals: 2 });
|
|
for (const e of live()) tough(e);
|
|
for (const a of allies()) tough(a);
|
|
await sleep(200); unblock();
|
|
|
|
// ---- Q1: render loop alive? (normal, then comms-paused)
|
|
let c0 = window.__renderCount; await sleep(500);
|
|
R.q1.renderWhileRunning = window.__renderCount - c0;
|
|
const pause = async () => { if (gs.comms) gs.comms._setPaused(true); await sleep(400); };
|
|
const thaw = () => { if (gs.comms) gs.comms._setPaused(false); };
|
|
await pause();
|
|
c0 = window.__renderCount; await sleep(500);
|
|
R.q1.renderWhilePaused = window.__renderCount - c0;
|
|
R.q1.commsPaused = !!(gs.comms && gs.comms.paused);
|
|
|
|
// ---- Q2: where is the red? (arrows for off-screen pirates)
|
|
R.q2.live = live().length; R.q2.allies = allies().length;
|
|
R.q2.pirates = live().map((e) => ({
|
|
w: [Math.round(e.x), Math.round(e.y)],
|
|
s: (() => { const p = hud.worldToScreen(e.x, e.y); return [Math.round(p.x), Math.round(p.y)]; })(),
|
|
arrow: (() => { const p = arrowPt(e); return p ? { x: Math.round(p.x), y: Math.round(p.y), a: Number(p.angle.toFixed(2)) } : null; })(),
|
|
}));
|
|
R.q2.offscreenCfg = { ...hud.offscreen };
|
|
R.q2.redClustersArrows = redClusters();
|
|
|
|
// ---- Q2b: threat markers (flip on, manual update, rescan)
|
|
const threatOrig = hud.threat;
|
|
hud.threat = { ...threatOrig, enabled: true };
|
|
hud.update(gs.time.now);
|
|
await sleep(350);
|
|
R.q2b = {
|
|
threatCfg: { ...hud.threat },
|
|
redClustersThreat: redClusters(),
|
|
};
|
|
// also: is the marker's graphics actually holding a circle? (commandBuffer)
|
|
const tg = hud._threatG;
|
|
R.q2b.threatG_cmds = tg ? (tg.commandBuffer ? tg.commandBuffer.length : (tg.commands ? tg.commands.length : 'n/a')) : 'no _threatG';
|
|
hud.threat = threatOrig;
|
|
hud.update(gs.time.now);
|
|
await sleep(120);
|
|
thaw();
|
|
|
|
// ---- Q3: does a real kill flip dead + land in the feed?
|
|
const p0 = live()[0];
|
|
if (p0) {
|
|
R.q3.before = { dead: !!p0.dead, hull: p0.hull, feedLen: hud.feed.length, active: sys.active, kills: sys.kills };
|
|
p0.shields = 0; p0.maxShields = 0; p0.hull = 1; p0.maxHull = 1; // make it killable
|
|
const killed = p0.damage(9999);
|
|
if (killed) sys.killEnemy(p0, sys._now());
|
|
await sleep(300); unblock();
|
|
R.q3.after = {
|
|
dead: !!p0.dead, killed, feedLen: hud.feed.length,
|
|
feed: hud.feed.map((f) => f.label), active: sys.active, kills: sys.kills,
|
|
liveNow: live().length,
|
|
};
|
|
} else R.q3.fail = 'no pirate to kill';
|
|
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-probe2: 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-probe2 error:', e.message); process.exit(2); });
|