SOURCE / PINNED RELEASE
Made of little things.
Poly Dogfight
- Release
- b8511ee374e3…
- Author-recorded commit
- 5579434b9d8b…
- License
- LICENSE
- Author’s source reference
- nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.napplet.soy%2F/n-44f63e5422b
Archive hash verified: bcadd2430d8ea2bc…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Heat-seeking rockets: ammunition, locking, dodging and coasting round the planet.
import assert from 'node:assert/strict';
import test from 'node:test';
import { Vector3 } from 'three';
import { PLANET_RADIUS, ROCKETS } from '../src/sim/constants.ts';
import { beginnerControls } from '../src/sim/beginner.ts';
import { createFlightState, neutralInput, spawnAt, stepFlight } from '../src/sim/flight.ts';
import { createRockets, seekerTarget, stepRockets } from '../src/sim/rockets.ts';
const DT = 1 / 120;
const sky = { surfaceHeight: () => 0, terrainHeight: () => -60, hitsObstacle: () => false };
function plane(altitude = 200) {
const state = createFlightState();
spawnAt(state, sky, new Vector3(0, 1, 0), new Vector3(0, 0, -1));
state.position.setLength(PLANET_RADIUS + altitude);
state.rocketHeld = false;
return state;
}
/** A shooter `gap` metres behind the target, same heading and speed. */
function chase(gap, offset = new Vector3()) {
const target = plane();
const shooter = plane();
shooter.position.copy(target.position).add(new Vector3(0, 0, gap)).add(offset);
return { target, shooter };
}
/** Fly both planes and the rockets together; the target steers with `steer`. */
function duel(shooter, target, seconds, steer = () => neutralInput(), launchAt = 0) {
const rockets = createRockets();
const hits = [];
const locks = [];
for (let i = 0; i < seconds * 120; i++) {
stepFlight(shooter, neutralInput(), sky, DT);
stepFlight(target, steer(target), sky, DT);
const events = stepRockets(rockets, [shooter, target], [i === Math.round(launchAt * 120), false], sky, DT);
hits.push(...events.hits);
for (const rocket of rockets.rockets) if (rocket.target) locks.push(rocket.target);
}
return { hits, locks, rockets };
}
test('one rocket per press, two on the wings, a 2 s gap and an 8 s reload each', () => {
const pilot = plane();
const rockets = createRockets();
let launched = 0;
const step = (trigger) => (launched += stepRockets(rockets, [pilot], [trigger], sky, DT).launches.length);
step(true);
assert.equal(launched, 1);
for (let i = 0; i < 360; i++) step(true);
assert.equal(launched, 1, 'holding the trigger fires once');
step(false);
step(true);
assert.equal(launched, 2, 'a fresh press after the cooldown fires the other wing');
assert.equal(pilot.rockets, 0);
// Pressed again inside the 2 s cooldown: refused.
const fresh = plane();
const rack = createRockets();
stepRockets(rack, [fresh], [true], sky, DT);
stepRockets(rack, [fresh], [false], sky, DT);
assert.equal(stepRockets(rack, [fresh], [true], sky, DT).launches.length, 0);
assert.equal(fresh.rockets, 1);
// Empty: presses do nothing. Reloading runs one rocket at a time from the
// first launch: one is back 8 s after it, the second 8 s after that.
let t = (362 + 2) / 120;
const until = (seconds, trigger = false) => {
while (t < seconds) { step(trigger); step(false); t += 2 / 120; }
};
until(7.8, true);
assert.equal(launched, 2, 'empty rack refuses');
assert.equal(pilot.rockets, 0);
until(8.1);
assert.equal(pilot.rockets, 1);
until(15.8);
assert.equal(pilot.rockets, 1);
until(16.1);
assert.equal(pilot.rockets, ROCKETS.capacity);
});
test('a rocket locks onto a plane flying straight ahead and halves the part it hits', () => {
const { target, shooter } = chase(150);
const { hits } = duel(shooter, target, 6);
assert.equal(hits.length, 1);
assert.equal(hits[0].owner, shooter);
const total = target.damage.left + target.damage.right + target.damage.tail;
assert.ok(Math.abs(total - ROCKETS.damage) < 1e-9, `one hit deals ${ROCKETS.damage}, got ${total}`);
assert.equal(target.damage[hits[0].part], 0.5);
assert.equal(hits[0].destroyed, false);
assert.equal(target.crippled, false);
});
test('a second rocket into the same part destroys it', () => {
const { target, shooter } = chase(150);
const rockets = createRockets();
const hits = [];
for (let i = 0; i < 120 * 8; i++) {
stepFlight(shooter, neutralInput(), sky, DT);
stepFlight(target, neutralInput(), sky, DT);
const trigger = i === 0 || i === 120 * 2.5;
hits.push(...stepRockets(rockets, [shooter, target], [trigger, false], sky, DT).hits);
}
assert.equal(hits.length, 2);
assert.equal(hits[0].part, hits[1].part, 'a rocket from dead astern hits the same part twice');
assert.equal(hits[1].destroyed, true);
assert.equal(target.crippled, true);
});
test('the seeker ignores planes well off the nose or out of range', () => {
const origin = new Vector3(0, PLANET_RADIUS + 200, 0);
const heading = new Vector3(0, 0, -1);
const off = plane();
off.position.copy(origin).add(new Vector3(-Math.sin(Math.PI / 3), 0, -Math.cos(Math.PI / 3)).multiplyScalar(150));
const far = plane();
far.position.copy(origin).add(new Vector3(0, 0, -500));
assert.equal(seekerTarget(origin, heading, [off, far], null, sky), null);
const near = plane();
near.position.copy(origin).add(new Vector3(20, 0, -150));
assert.equal(seekerTarget(origin, heading, [off, far, near], null, sky), near);
// Fired with both out of its cone: the rocket never locks either.
const shooter = plane();
shooter.position.copy(origin);
const { locks } = duel(shooter, off, 1.5);
assert.equal(locks.length, 0);
});
test('a hard banking turn shakes a rocket fired from behind', () => {
const { target, shooter } = chase(150);
// Full right in beginner steering: roll into a 90° bank and pull.
const { hits, locks, rockets } = duel(shooter, target, 10, (state) => beginnerControls(state, { ...neutralInput(), roll: 1 }), 0);
assert.ok(locks.includes(target), 'the rocket locked on before the dodge');
assert.equal(hits.length, 0, 'the rocket overshoots the turn and misses');
assert.ok(rockets.rockets.every((rocket) => rocket.target !== target), 'and loses its lock');
});
test('with nothing to chase a rocket coasts round the planet at its altitude, then finds its launcher', () => {
const launcher = plane(120);
const launch = launcher.position.clone();
const rockets = createRockets();
stepRockets(rockets, [launcher], [true], sky, DT);
const rocket = rockets.rockets[0];
const radius = rocket.position.length();
// Keep the launcher parked where it fired (alive, unmoving) to be found again.
launcher.velocity.set(0, 0, 0);
let maxDrift = 0;
let hit = null;
let minAge = Infinity;
for (let i = 0; i < 120 * ROCKETS.life && !hit; i++) {
const events = stepRockets(rockets, [launcher], [false], sky, DT);
if (rockets.rockets[0] && !rockets.rockets[0].target) maxDrift = Math.max(maxDrift, Math.abs(rockets.rockets[0].position.length() - radius));
if (rockets.rockets[0]?.target === launcher) minAge = Math.min(minAge, rockets.rockets[0].age);
hit = events.hits[0] ?? null;
}
assert.ok(maxDrift < 2, `holds altitude within 2 m while coasting (drifted ${maxDrift.toFixed(2)})`);
assert.ok(hit, 'the rocket came all the way round and struck its launcher');
assert.equal(hit.owner, launcher);
const lap = (2 * Math.PI * radius) / ROCKETS.speed;
assert.ok(minAge > lap * 0.8, `locked on only after a lap (${minAge.toFixed(1)} s, lap ${lap.toFixed(1)} s)`);
assert.ok(launch.distanceTo(launcher.position) < 1e-9);
});
test('the launcher is safe from its own rocket at launch', () => {
const pilot = plane();
const rockets = createRockets();
const hits = [];
for (let i = 0; i < 120 * 2; i++) {
stepFlight(pilot, neutralInput(), sky, DT);
hits.push(...stepRockets(rockets, [pilot], [i === 0], sky, DT).hits);
}
assert.equal(hits.length, 0);
});
test('the armoured nose does not stop a rocket: the blast takes a wing', () => {
const target = plane();
const shooter = plane();
// Head-on: shooter 200 m ahead of the target, facing it.
shooter.position.copy(target.position).add(new Vector3(0, 0, -200));
spawnAt(shooter, sky, shooter.position.clone().normalize(), new Vector3(0, 0, 1));
shooter.position.setLength(target.position.length());
shooter.rocketHeld = false;
const { hits } = duel(shooter, target, 3);
assert.equal(hits.length, 1);
assert.ok(hits[0].part === 'left' || hits[0].part === 'right', `nose hit lands on a wing, got ${hits[0].part}`);
});
