151 lines
5.8 KiB
JavaScript
151 lines
5.8 KiB
JavaScript
// 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);
|
||
});
|