153 lines
6.6 KiB
JavaScript
153 lines
6.6 KiB
JavaScript
/**
|
|
* Dev-only CDP harness for the RAMMING CHARGE's FX set (js/combat/Fx.js →
|
|
* cometStart / trailEmbers / ramBlast, driven through the production
|
|
* CombatSystem in the MAIN game): boots a fresh Vanguard run with the
|
|
* dev door (?skill=max — every skill at its max), spawns a hostile ahead
|
|
* of the ship, casts seat 1, and captures the three money frames —
|
|
* the comet head + ember wake MID-CHARGE, the fireball + damage-radius
|
|
* ring AT IMPACT, and the trail LINGERING ~1 s after.
|
|
*
|
|
* node dev/ram-shot.mjs [outDir]
|
|
*
|
|
* Needs the dev server (http://127.0.0.1:8080) + a headless CDP browser
|
|
* on 127.0.0.1:9333 (same idiom as dev/combat-shot.mjs).
|
|
*/
|
|
import fs from 'node:fs';
|
|
|
|
const CDP = process.env.CDP_PORT ? `127.0.0.1:${process.env.CDP_PORT}` : '127.0.0.1:9333';
|
|
const BASE = process.env.BASE ?? 'http://127.0.0.1:8080';
|
|
const OUT = process.argv[2] ?? 'dev/shots';
|
|
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
async function main() {
|
|
fs.mkdirSync(OUT, { recursive: true });
|
|
|
|
async function openPage(url) {
|
|
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 }));
|
|
});
|
|
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = () => rej(new Error('ws error')); });
|
|
ws.onmessage = (m) => {
|
|
const x = JSON.parse(m.data);
|
|
if (x.id && pending.has(x.id)) {
|
|
const { res, rej } = pending.get(x.id);
|
|
pending.delete(x.id);
|
|
x.error ? rej(new Error(x.error.message)) : res(x.result);
|
|
}
|
|
};
|
|
await send('Runtime.enable');
|
|
await send('Page.enable');
|
|
await send('Network.enable');
|
|
await send('Network.setCacheDisabled', { cacheDisabled: true });
|
|
await send('Page.navigate', { url });
|
|
const ev = async (expr) => {
|
|
const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true });
|
|
if (r.exceptionDetails) throw new Error('page: ' + JSON.stringify(r.exceptionDetails.exception?.description ?? r.exceptionDetails.text));
|
|
return r.result?.value;
|
|
};
|
|
const waitTrue = async (expr, timeoutMs = 45000) => {
|
|
const end = Date.now() + timeoutMs;
|
|
while (Date.now() < end) {
|
|
const v = await ev(expr).catch(() => null);
|
|
if (v) return v;
|
|
await sleep(250);
|
|
}
|
|
throw new Error('timed out waiting for: ' + expr.slice(0, 60));
|
|
};
|
|
const shot = async (name, delayMs = 110) => {
|
|
await sleep(delayMs); // let the last paint land
|
|
const r = await send('Page.captureScreenshot', { format: 'png' });
|
|
fs.writeFileSync(`${OUT}/${name}.png`, Buffer.from(r.data, 'base64'));
|
|
console.log(` shot: ${OUT}/${name}.png`);
|
|
};
|
|
return { ev, waitTrue, shot, target };
|
|
}
|
|
|
|
const p = await openPage(`${BASE}/index.html?combat&skill=max`);
|
|
try {
|
|
await p.waitTrue('Boolean(window.game && window.game.scene.getScene("MenuScene") && window.game.scene.getScene("MenuScene").seedValue)');
|
|
await p.ev('window.game.scene.getScene("MenuScene").startNewGame()');
|
|
await p.waitTrue('Boolean(window.__ORBIT_COMBAT__)');
|
|
|
|
// The new-run tutorial comms own the screen and pause the sim —
|
|
// end the active one, drop the queue, un-pause (same path combat-shot uses).
|
|
await p.ev(`(() => {
|
|
const g = window.game.scene.getScene('GameScene');
|
|
const h = g.comms;
|
|
if (!h) return true;
|
|
if (h.current) { try { h._finish(); } catch (e) {} }
|
|
if (Array.isArray(h.queue)) h.queue.length = 0;
|
|
h._setPaused(false);
|
|
try { h.box?.close(); } catch (e) {}
|
|
h.box = null;
|
|
return true;
|
|
})()`);
|
|
|
|
// The camera follows the ship smoothly — wait until it is centered.
|
|
await p.waitTrue(`(() => {
|
|
const g = window.game.scene.getScene('GameScene');
|
|
const cam = g.cameras.main;
|
|
const w = g.scale.width, h = g.scale.height;
|
|
const cx = (cam.worldView?.x ?? cam.scrollX ?? cam.x) + w / 2;
|
|
const cy = (cam.worldView?.y ?? cam.scrollY ?? cam.y) + h / 2;
|
|
return Math.abs(cx - g.ship.x) < 24 && Math.abs(cy - g.ship.y) < 24;
|
|
})()`, 8000).catch(() => null);
|
|
await sleep(600); // the pack settles, the sim breathes
|
|
|
|
// One hostile ~420 px ahead of the ship + a FULL energy orb, then the
|
|
// cast through the production seam (seat 1 = Ramming Charge 5/5).
|
|
const setup = await p.ev(`(() => {
|
|
const oc = window.__ORBIT_COMBAT__;
|
|
const sys = oc.system;
|
|
const s = sys.ship;
|
|
sys.spawnEnemy(s.x + 420, s.y - 140, 991);
|
|
sys.energy = sys.maxEnergy;
|
|
const res = oc.cast(0);
|
|
return { ok: !!res?.ok, reason: res?.reason ?? null, id: res?.id ?? null, rank: res?.rank ?? 0 };
|
|
})()`);
|
|
console.log('cast:', JSON.stringify(setup));
|
|
if (!setup?.ok) throw new Error(`cast refused: ${JSON.stringify(setup)}`);
|
|
|
|
// EVENT-DRIVEN CAPTURE — poll the production dash state (oc.system.dash)
|
|
// instead of guessing ms: the impact lands wherever arrival or the
|
|
// clock puts it, and the frames must sit INSIDE each phase.
|
|
const dashing = () => p.ev('Boolean(window.__ORBIT_COMBAT__?.system?.dash)');
|
|
|
|
// Phase 1 — MID-CHARGE: wait for the dash to be live, capture early in
|
|
// it (the comet head over the hull + the ember wake streaming behind).
|
|
const t0 = Date.now();
|
|
while (Date.now() - t0 < 4000 && !(await dashing().catch(() => false))) await sleep(30);
|
|
await sleep(90); // a few frames in — the wake has length, the head is up
|
|
await p.shot('ram-dash', 60);
|
|
|
|
// Phase 2 — AT IMPACT: the dash ends the instant the blast fires —
|
|
// catch the fireball + the damage-radius ring near their peak.
|
|
const t1 = Date.now();
|
|
while (Date.now() - t1 < 6000 && (await dashing().catch(() => false))) await sleep(25);
|
|
await p.shot('ram-impact', 120);
|
|
|
|
// Phase 3 — the wake LINGERING: ~0.8 s after impact the late embers
|
|
// (the trail must outlive the blast — that's the point of it) are still
|
|
// cooling where the charge flew.
|
|
await p.shot('ram-trail', 800);
|
|
} finally {
|
|
await fetch(`http://${CDP}/json/close/${p.target.id}`).catch(() => {});
|
|
}
|
|
|
|
console.log('ram shot: done');
|
|
}
|
|
|
|
main().catch((err) => { console.error('ram-shot failed:', err.message); process.exit(1); });
|