// 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); });