import assert from 'node:assert/strict'; import fs from 'node:fs'; import { dirname, join } from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const file = (p) => pathToFileURL(join(__dirname, '../js', p)).href; // The real data/*.json into the config singleton (the classes.json numbers // under test are the shipped ones — no private copy). const { config } = await import(file('config/Config.js')); const { barrels, angleDiff, intercept, rawAngle } = await import(file('combat/Aiming.js')); const { perkDef } = await import(file('player/ClassModel.js')); const dataDir = join(__dirname, '../data'); const configData = {}; for (const f of fs.readdirSync(dataDir)) { if (!f.endsWith('.json') || f === 'manifest.json') continue; configData[f.replace(/\.json$/i, '')] = JSON.parse(fs.readFileSync(join(dataDir, f), 'utf8')); } config.init(configData); const ok = (m) => console.log(' \u2714 ' + m); const TAU = Math.PI * 2; // ────────────────────────────────────────────────────────── the intercept { // 1 · a stationary target needs no lead at all. const hit = intercept(0, 0, 300, 0, 0, 0, 600, 0.9); assert.ok(Math.abs(hit.lead) < 1e-12, 'a parked target gets no lead'); assert.equal(hit.t, Math.hypot(300, 0) / 600, 'the flight time is still the straight distance'); assert.ok(Math.abs(hit.angle - rawAngle(0, 0, 300, 0)) < 1e-12, 'raw aim is the answer'); ok('stationary target: no lead, raw aim'); // 2 · a target crossing right in front of the muzzle, dead ahead at 90° away. // Muzzle (0,0), bolt 600 px/s; target at (0,300) moving +x at 200 px/s. // t solves (200t)² + 300² = (600t)² → t = 300/√(600²−200²) = 0.5303 s; // it will be at x = 106.07 → angle atan2(300, 106.07) = 1.2310 rad (70.5°). const t = intercept(0, 0, 0, 300, 200, 0, 600, 0); assert.equal(t.solved, true, 'a crossing target IS interceptable'); assert.ok(Math.abs(t.t - 300 / Math.sqrt(600 ** 2 - 200 ** 2)) < 1e-9, `flight time ${t.t}`); assert.ok(Math.abs(t.x - 200 * t.t) < 1e-9 && Math.abs(t.y - 300) < 1e-9, 'the meeting point rides the target’s velocity'); assert.ok(Math.abs(t.angle - Math.atan2(300, 200 * t.t)) < 1e-12, `lead angle ${t.angle}`); // the bolt, fired on that heading, really does arrive where the target gets to const bx = Math.cos(t.angle) * 600 * t.t; const by = Math.sin(t.angle) * 600 * t.t; assert.ok(Math.hypot(bx - t.x, by - t.y) < 1e-9, 'bolt and target meet at the solved point'); // …and the lead is a HEAD-WARD correction, not a random swing const raw = rawAngle(0, 0, 0, 300); assert.ok(Math.abs(t.lead) > 0.3 && Math.sign(t.lead) === Math.sign(angleDiff(Math.atan2(300, 106), raw)), 'the nose moves AHEAD of the target'); ok('crossing target: leads ahead by the bullet-speed-correct angle (bolt meets target)'); // 3 · a target running straight at the muzzle needs no lead either. const head = intercept(0, 0, 400, 0, -150, 0, 600, 0.9); assert.ok(Math.abs(head.angle) < 1e-9 || Math.abs(Math.abs(head.angle) - Math.PI) < 1e-9, 'still on the line'); assert.ok(Math.abs(head.lead) < 1e-9, 'no lead down the barrel line'); ok('head-on target: no lead (the solve is degenerate, and honest about it)'); // 4 · a target faster than the bolt can never be caught → raw aim, solved:false. const run = intercept(0, 0, 100, 0, 1400, 200, 600, 0); assert.equal(run.solved, false, 'unreachable'); assert.equal(run.angle, rawAngle(0, 0, 100, 0), 'falls back to raw aim'); ok('target outruns the bolt: falls back to raw aim'); // 5 · a perfect broadside pass at extreme range: an intercept exists but the // computer's cap (data/classes.json → maxLead) refuses the swing. const far = intercept(0, 0, 6000, 400, 0, 900, 600, Number(perkDef('targetingComputer').maxLead)); assert.ok(far.t === 0 && far.solved === false, `the ${perkDef('targetingComputer').maxLead}s lead cap holds (raw solve would need ~1.5s)`); assert.equal(far.angle, rawAngle(0, 0, 6000, 400), 'and the nose stays on the ship'); ok(`out of the computer's reach (cap ${perkDef('targetingComputer').maxLead}s): no wild nose, raw aim`); // 6 · numbers come from the data, not from code. assert.ok(Number(perkDef('targetingComputer').maxLead) > 0, 'maxLead is configured'); assert.equal(Number(perkDef('dualCannon').barrels), 2, 'the Vanguard fires two'); ok('the perk numbers ride data/classes.json'); } // ─────────────────────────────────────────────────────────────── barrels { const one = barrels(0, 1, 9, 20); assert.equal(one.length, 1); assert.deepEqual({ x: one[0].x, y: one[0].y }, { x: 20, y: 0 }, 'a single barrel sits on the centreline'); const two = barrels(0, 2, 9, 20); assert.equal(two.length, 2); assert.ok(Math.abs(two[0].x - 20) < 1e-9 && Math.abs(two[1].x - 20) < 1e-9, 'both barrels are at the same station'); assert.ok(Math.abs(two[0].y + 9) < 1e-9 && Math.abs(two[1].y - 9) < 1e-9, `offset ±9 off the centreline (${two[0].y}, ${two[1].y})`); // rotate the firing line: the pair squares away with it (perpendicular offsets) const up = barrels(-Math.PI / 2, 2, 9, 20); // firing "up" the screen assert.ok(Math.abs(up[0].x) === 9 || Math.abs(Math.abs(up[0].x) - 9) < 1e-9, 'the pair swings out along x'); assert.ok(Math.abs(up[0].y + 20) < 1e-9 && Math.abs(up[1].y + 20) < 1e-9, 'both still at the nose station'); assert.ok(Math.sign(up[0].x) !== Math.sign(up[1].x), 'one barrel each side of the centreline'); const sep = Math.hypot(two[0].x - two[1].x, two[0].y - two[1].y); assert.ok(Math.abs(sep - 18) < 1e-9, `the muzzles are ${sep}px apart — never inside each other`); ok('dual barrels: 2 muzzles, symmetric ±barrelOffset, turning with the nose'); }