orbit/dev/combat-flycheck.mjs

151 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Dev-only: the RANGE FLIGHT FEEL CHECK (headless, CDP).
//
// node dev/combat-flycheck.mjs [baseUrl]
//
// Loads the training range (/combat), then drives the SAME key states a
// WASD player would produce (scene.keys.*.isDown — the exact input the
// feel step reads) and measures the response:
//
// thrust — time from rest to 60% of maxSpeed (W) [want < 450 ms]
// turn — time from rest to a 30° heading change (D) [want < 250 ms]
// spin — sustained turn rate once the turn is up [want ≥ rotSpeed]
// brake — time from full speed to < 60 px/s (S) [want < 800 ms]
//
// (A headless tab ticks ~24 fps, so the budgets carry a frame-quantization
// margin — at 60 fps the same run is roughly 100–150 ms faster.)
//
// Prints a JSON summary + a PASS/FAIL line (exit 0 on PASS). Needs the
// dev server (node dev/server.mjs 8080) and a CDP browser on 9333 — the
// same setup as dev/cdp-shot.mjs.
const [url = 'http://127.0.0.1:8080/combat'] = process.argv.slice(2);
const CDP = process.env.CDP_PORT ? `127.0.0.1:${process.env.CDP_PORT}` : '127.0.0.1:9333';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// The page-side check (runs inside the tab — real scene, real ship, real
// feel step; the ship is made unkillable so a stray lance can't abort it).
const PAGE_CODE = `
(async () => {
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
if (!window.game) throw new Error('game not booted');
const scene = window.game.scene.getScene('CombatScene');
if (!scene || !scene.ship) throw new Error('CombatScene not ready');
const ship = scene.ship;
const maxSpeed = (window.__COMBAT_MAXSPEED__ ?? 560);
// Isolate the FEEL: no pack to ram into, no damage to respawn the ship
// (a respawn resets rotation/position and would poison the numbers).
scene.maxHull = 1e9; scene.hull = 1e9; scene.maxShields = 1e9; scene.shields = 1e9;
scene.enemies.length = 0;
scene.bullets.length = 0;
const press = (name, isDown) => { scene.keys[name].isDown = isDown; };
const speed = () => Math.hypot(ship.body.velocity.x, ship.body.velocity.y);
const alive = () => {
if (scene.playerDead) throw new Error('player died during the check (invincibility failed?)');
};
// --- thrust: rest → 60% of maxSpeed -----------------------------------
ship.stop();
await sleep(120);
press('up', true);
const t0 = performance.now();
while (performance.now() - t0 < 2000 && speed() < maxSpeed * 0.6) { await sleep(10); alive(); }
alive();
const t60 = Math.round(performance.now() - t0);
press('up', false);
await sleep(300);
ship.stop();
await sleep(150);
// --- turn: rest → 30°, then the sustained rate -------------------------
const r0 = ship.rotation;
press('right', true);
const t1 = performance.now();
while (performance.now() - t1 < 3000 && Math.abs(ship.rotation - r0) < Math.PI / 6) { await sleep(10); alive(); }
alive();
const t30 = Math.round(performance.now() - t1);
const r1 = ship.rotation;
await sleep(300);
alive();
const spinRate = (ship.rotation - r1) / 0.3; // rad/s once the turn is up
press('right', false);
await sleep(300);
ship.stop();
// --- brake: full speed → < 60 px/s (open space, nose ahead) -----------
ship.stop();
scene.feeel.rot = Math.PI / 2; // heading = east (+x), open space ahead
scene.feeel.turnVel = 0;
ship.x = 200; ship.y = 360; ship.rotation = Math.PI / 2;
ship.body.velocity.set(0, 0);
await sleep(120);
press('up', true);
await sleep(1000); // build up to full
alive();
const vFull = Math.round(speed());
press('up', false);
press('down', true);
const t2 = performance.now();
while (performance.now() - t2 < 4000 && speed() > 60) { await sleep(10); alive(); }
alive();
const tStop = Math.round(performance.now() - t2);
press('down', false);
ship.stop();
return { t60, t30, spinRate: Number(spinRate.toFixed(2)), vFull, tStop, maxSpeed };
})()
`;
async function main() {
// Open a fresh tab for the range (don't disturb other CDP users).
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 ws = new WebSocket(target.webSocketDebuggerUrl);
let id = 0;
const pending = new Map();
const send = (method, params = {}) => new Promise((res, rej) => {
const mid = ++id;
pending.set(mid, { res, rej });
ws.send(JSON.stringify({ id: mid, method, params }));
});
ws.onmessage = (m) => {
const msg = JSON.parse(m.data);
if (msg.id && pending.has(msg.id)) {
const { res, rej } = pending.get(msg.id);
pending.delete(msg.id);
msg.error ? rej(new Error(msg.error.message)) : res(msg.result);
}
};
await new Promise((res, rej) => {
ws.onopen = res;
ws.onerror = () => rej(new Error('ws error'));
});
await send('Runtime.enable');
await sleep(6000); // boot + config + textures + fonts
const r = await send('Runtime.evaluate', { expression: PAGE_CODE, returnByValue: true, awaitPromise: true });
if (r.exceptionDetails) throw new Error(JSON.stringify(r.exceptionDetails).slice(0, 500));
const s = r.result.value;
ws.close();
const ok = s.t60 < 450 && s.t30 < 250 && s.spinRate >= 5.5 && s.tStop < 800 && s.vFull > 400;
console.log(JSON.stringify(s, null, 2));
console.log(
`flycheck: thrust ${s.t60} ms to 60% · turn ${s.t30} ms to 30° · spin ${s.spinRate} rad/s · ` +
`brake ${s.tStop} ms to stop from ${s.vFull} px/s → ${ok ? 'PASS' : 'FAIL'}`,
);
// Close the tab again (best effort).
try { await fetch(`http://${CDP}/json/close/${target.id}`); } catch {}
process.exit(ok ? 0 : 1);
}
main().catch((e) => {
console.error('flycheck error:', e.message);
process.exit(2);
});