98 lines
5.9 KiB
JavaScript
98 lines
5.9 KiB
JavaScript
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');
|
||
}
|
||
|