156 lines
6.5 KiB
JavaScript
156 lines
6.5 KiB
JavaScript
/**
|
|
* Dev-only probe: does IN-BROWSER PIXEL SAMPLING work in headless Brave?
|
|
*
|
|
* node dev/pixel-probe.mjs
|
|
*
|
|
* Boots the game, spawns ONE off-screen enemy through the production
|
|
* sandbox seam (__ORBIT_COMBAT__.spawn), computes the off-screen arrow
|
|
* position with the REAL js/combat/Combat.js function, drives
|
|
* combatHud.update(), then copies the game canvas into a 2D canvas and
|
|
* samples pixels:
|
|
* · center box → the ship must be visible (not a black canvas)
|
|
* · corner box → the space background must be dark
|
|
* · arrow box → reddish pixels where the arrow should be drawn
|
|
*
|
|
* Requires: the dev server on :8080 and headless 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 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 || !gs.combatHud) return 'no GameScene/combat/ship/hud';
|
|
const sys = gs.combat, hud = gs.combatHud;
|
|
try { if (gs.comms && gs.comms.paused) gs.comms._ack(); } catch {}
|
|
const cv = window.game.canvas || document.querySelector('canvas');
|
|
if (!cv) return 'no canvas element';
|
|
const info = {
|
|
renderer: window.game.renderer ? (window.game.renderer.type ?? window.game.renderer.name ?? window.game.renderer.constructor.name) : null,
|
|
canvasW: cv.width, canvasH: cv.height, cssW: gs.scale && gs.scale.width, cssH: gs.scale && gs.scale.height,
|
|
};
|
|
const vw = info.cssW || cv.width, vh = info.cssH || cv.height;
|
|
const DPR = cv.width / vw;
|
|
info.dpr = DPR;
|
|
// One hostile, off-screen (the sandbox seam's own spawn path).
|
|
window.__ORBIT_COMBAT__.spawn(1);
|
|
await sleep(150);
|
|
const e = sys.enemies.filter((x) => !x.dead)[0];
|
|
if (!e) return 'no enemy spawned';
|
|
const mod = await import('/js/combat/Combat.js');
|
|
const s = hud.worldToScreen(e.x, e.y);
|
|
const p = mod.offscreenArrow(vw, vh, s.x, s.y, 28);
|
|
info.enemyScreen = { x: Math.round(s.x), y: Math.round(s.y) };
|
|
info.arrow = p ? { x: Math.round(p.x), y: Math.round(p.y), angle: p.angle.toFixed(3) } : null;
|
|
hud.update(gs.time.now); // drive the HUD draw for this frame
|
|
const grab = (x0, y0, x1, y1) => {
|
|
const off = document.createElement('canvas');
|
|
off.width = cv.width; off.height = cv.height;
|
|
const ctx = off.getContext('2d');
|
|
ctx.drawImage(cv, 0, 0);
|
|
const rx = Math.max(0, Math.floor(x0 * DPR)), ry = Math.max(0, Math.floor(y0 * DPR));
|
|
const rw = Math.min(cv.width - rx, Math.max(1, Math.ceil((x1 - x0) * DPR)));
|
|
const rh = Math.min(cv.height - ry, Math.max(1, Math.ceil((y1 - y0) * DPR)));
|
|
const d = ctx.getImageData(rx, ry, rw, rh).data;
|
|
return Array.from(d);
|
|
};
|
|
const count = (arr, test) => { let n = 0; for (let i = 0; i < arr.length; i += 4) if (test(arr[i], arr[i + 1], arr[i + 2])) n++; return n; };
|
|
const bright = (r, g, b) => r > 130 && g > 150 && b > 160;
|
|
const dark = (r, g, b) => r + g + b < 150;
|
|
const red = (r, g, b) => r > 130 && r - g > 45 && r - b > 25;
|
|
const center = grab(vw / 2 - 10, vh / 2 - 10, vw / 2 + 10, vh / 2 + 10);
|
|
const corner = grab(0, 0, 10, 10);
|
|
info.centerBright = count(center, bright);
|
|
info.centerTotal = center.length / 4;
|
|
info.cornerDark = count(corner, dark);
|
|
if (p) {
|
|
const box = grab(p.x - 8, p.y - 8, p.x + 8, p.y + 8);
|
|
info.arrowRed = count(box, red);
|
|
info.arrowSample = box.slice(0, 12);
|
|
}
|
|
return info;
|
|
})()`;
|
|
|
|
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 out = await evalJs(c, PROBE);
|
|
console.log(JSON.stringify(out, null, 2));
|
|
} finally {
|
|
try { await fetch(`http://${CDP}/json/close/${target.id}`).catch(() => {}); } catch { /* ignore */ }
|
|
c.ws.close();
|
|
}
|
|
}
|
|
|
|
main().catch((e) => { console.error('pixel-probe error:', e.message); process.exit(2); });
|