129 lines
6.6 KiB
JavaScript
129 lines
6.6 KiB
JavaScript
/** Focused probe: the kill feed. Kill a hostile and dump the feed state,
|
|
* the text object's alpha/position/visibility, the live game freeze state,
|
|
* and the actual pixel values at the feed band. */
|
|
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 CODE = `(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 GameScene/combat/ship';
|
|
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 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.__lf) { window.__lf = true; const o = ren.render; ren.render = function (...a) { const r = o.apply(this, a); try { octx.drawImage(cv, 0, 0); } catch {} return r; }; }
|
|
const BRIGHT = (r, g, b) => r > 130 && g > 150 && b > 160;
|
|
|
|
const out = {};
|
|
gs._spawnEncounter({ kind: 'mixed', pirates: 2, neutrals: 2 });
|
|
await sleep(200); unblock();
|
|
const P = sys.enemies.filter((e) => !e.dead);
|
|
const v = window.game.scale ? window.game.scale.width : cv.width;
|
|
const vw = cv.width, vh = cv.height;
|
|
|
|
// KILL one pirate (make it killable first)
|
|
const e = P[0];
|
|
e.maxHull = 10; e.hull = 1; e.maxShields = 0; e.shields = 0;
|
|
const killed = e.damage(9999);
|
|
if (killed) sys.killEnemy(e, sys._now());
|
|
out.killed = killed;
|
|
out.dead = e.dead;
|
|
out._killFx = e._killFx;
|
|
|
|
// let the feed ramp up (alpha fade-in is 250 ms)
|
|
await sleep(400);
|
|
unblock();
|
|
|
|
out.frozen = gs.comms ? { paused: gs.comms.paused, marquee: !!(gs.levelMarquee && gs.levelMarquee.active) } : null;
|
|
out.canText = typeof hud.scene.add?.text === 'function';
|
|
out.feedCfgEnabled = hud.feedCfg && hud.feedCfg.enabled;
|
|
out.sceneAddTextType = typeof hud.scene.add?.text;
|
|
// force a draw pass directly and see if the pool fills
|
|
try { hud._drawFeed(hud._now()); out.afterManualCount = hud.feedTexts.length; }
|
|
catch (err) { out.manualErr = String(err && err.message || err); }
|
|
// let several render frames land after the text object exists
|
|
await sleep(300);
|
|
unblock();
|
|
out.feedCount = hud.feed.length;
|
|
out.feedLabels = hud.feed.map((f) => f.label);
|
|
out.feed_e_t = hud.feed.map((f) => f.t);
|
|
out.now = (hud._now && hud._now()) || null;
|
|
out.feedTexts = (hud.feedTexts || []).map((t) => t ? { text: t.text, alpha: Number(t.alpha.toFixed(3)), x: Math.round(t.x), y: Math.round(t.y), visible: t.visible } : null);
|
|
|
|
// pixel sample at the feed band
|
|
const band = (x, y, w, h) => { const d = octx.getImageData(x, y, w, h).data; let n = 0; for (let i = 0; i < d.length; i += 4) if (BRIGHT(d[i], d[i+1], d[i+2])) n++; return n; };
|
|
out.feedBand = band(vw - 200, 55, 190, 50);
|
|
// dump a coarse grid of the feed band: any non-dark pixel (r+g+b > 90)
|
|
const grid = [];
|
|
for (let gy = 58; gy < 92; gy += 6) {
|
|
let row = '';
|
|
for (let gx = 1090; gx < 1270; gx += 8) {
|
|
const d = octx.getImageData(gx, gy, 1, 1).data;
|
|
const sum = d[0] + d[1] + d[2];
|
|
row += sum > 90 ? '#' : (sum > 40 ? '.' : ' ');
|
|
}
|
|
grid.push(row);
|
|
}
|
|
out.grid = grid;
|
|
out.nonDark = (function () { let n = 0; const d = octx.getImageData(1090, 58, 170, 34).data; for (let i = 0; i < d.length; i += 4) if (d[i] + d[i + 1] + d[i + 2] > 90) n++; return n; })();
|
|
// a few specific pixels near where the text should be (right-aligned at vw-18)
|
|
out.pixels = [1150, 1200, 1240, 1255].map((x) => { const d = octx.getImageData(x, 70, 1, 1).data; return [d[0], d[1], d[2], d[3]]; });
|
|
return out;
|
|
})()`;
|
|
|
|
(async () => {
|
|
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)', false); } catch { scoped = false; } if (scoped) break; }
|
|
if (!scoped) throw new Error('combat scope never appeared');
|
|
await sleep(800);
|
|
const out = await evalJs(c, CODE);
|
|
console.log(JSON.stringify(out, null, 2));
|
|
} finally {
|
|
try { await fetch(`http://${CDP}/json/close/${target.id}`).catch(() => {}); } catch {}
|
|
c.ws.close();
|
|
}
|
|
})().catch((e) => { console.error('probe err:', e.message); process.exit(2); });
|