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.
// Flight-model and world regression tests. Node strips the TypeScript types.
import assert from 'node:assert/strict';
import test from 'node:test';
import { Vector3 } from 'three';
import { FLIGHT, PLANET_RADIUS } from '../src/sim/constants.ts';
import {
createFlightState,
estimateLevelSpeed,
liftCoefficient,
neutralInput,
spawnAt,
stepFlight,
} from '../src/sim/flight.ts';
import { generateObstacles } from '../src/sim/obstacles.ts';
import { terrainHeight } from '../src/sim/terrain.ts';
const DT = 1 / 120;
/** Open sky over deep water: isolates the flight model from terrain. */
const sky = { surfaceHeight: () => 0, terrainHeight: () => -60, hitsObstacle: () => false };
function spawn(altitude = 150) {
const state = createFlightState();
spawnAt(state, sky, new Vector3(0, 1, 0), new Vector3(0, 0, -1));
state.position.setLength(PLANET_RADIUS + altitude);
return state;
}
function fly(state, seconds, input) {
for (let i = 0; i < seconds * 120; i++) stepFlight(state, { ...neutralInput(), ...input }, sky, DT);
return state;
}
test('full-throttle level speed is the intended cruise speed', () => {
assert.ok(Math.abs(estimateLevelSpeed() - FLIGHT.cruiseSpeed) < 2);
});
test('hands-off cruise roughly holds altitude', () => {
const state = fly(spawn(100), 20, { throttleSet: 1 });
assert.ok(state.alive);
assert.ok(Math.abs(state.altitude - 100) < 25, `altitude drifted to ${state.altitude}`);
assert.ok(Math.abs(state.airspeed - FLIGHT.cruiseSpeed) < 6, `speed ${state.airspeed}`);
});
test('circling the planet at cruise takes about a minute', () => {
const state = spawn(40);
let angle = 0;
let time = 0;
const previous = state.position.clone();
while (angle < Math.PI * 2 && time < 120) {
stepFlight(state, { ...neutralInput(), throttleSet: 1 }, sky, DT);
angle += previous.angleTo(state.position);
previous.copy(state.position);
time += DT;
}
assert.ok(time > 50 && time < 75, `circumnavigation took ${time.toFixed(1)} s`);
});
test('lift collapses past the critical angle of attack', () => {
const peak = liftCoefficient(FLIGHT.stallAoA);
assert.ok(liftCoefficient(FLIGHT.stallAoA * 1.6) < peak * 0.8);
assert.equal(Math.sign(liftCoefficient(-0.1)), -1);
});
test('climbing at idle bleeds speed until the wings stall', () => {
const state = spawn(300);
let stalled = false;
for (let i = 0; i < 15 * 120 && !stalled; i++) {
stepFlight(state, { ...neutralInput(), throttleSet: 0, pitch: 0.6 }, sky, DT);
stalled ||= state.stalled && state.stallAmount > 0.2;
}
assert.ok(stalled, `never stalled; speed ${state.airspeed.toFixed(1)} aoa ${state.aoa.toFixed(3)}`);
assert.ok(state.airspeed < FLIGHT.cruiseSpeed * 0.75);
});
test('pushing the nose down recovers from a stall', () => {
const state = spawn(300);
for (let i = 0; i < 15 * 120 && !(state.stalled && state.stallAmount > 0.2); i++)
stepFlight(state, { ...neutralInput(), throttleSet: 0, pitch: 0.6 }, sky, DT);
assert.ok(state.stalled);
// The sharp elevator stalls it nose-high, so the push takes a moment longer.
fly(state, 2, { throttleSet: 1, pitch: -1 });
fly(state, 4, { throttleSet: 1 });
assert.ok(!state.stalled, `still stalled at aoa ${state.aoa.toFixed(3)}`);
assert.ok(state.airspeed > FLIGHT.stallSpeed, `speed ${state.airspeed.toFixed(1)}`);
assert.ok(state.alive);
});
test('boost drains, locks out when empty, then recharges', () => {
const state = spawn(200);
const start = state.airspeed;
fly(state, 2, { throttleSet: 1, boost: true });
assert.ok(state.airspeed > start + 8, 'boost should accelerate');
let emptied = false;
let lockedOut = false;
for (let i = 0; i < 3 * 120; i++) {
stepFlight(state, { ...neutralInput(), throttleSet: 1, boost: true }, sky, DT);
emptied ||= state.boost === 0;
lockedOut ||= state.boostLockout > 0 && !state.boosting;
}
assert.ok(emptied && lockedOut, 'meter should empty and lock out briefly');
fly(state, 3, { throttleSet: 1 });
assert.ok(state.boost > 0.15);
});
test('hitting the ground crashes and the respawn timer runs ~5 s', () => {
const state = spawn(30);
fly(state, 6, { throttleSet: 1, pitch: -1 });
assert.equal(state.alive, false);
assert.ok(state.crashCause === 'water' || state.crashCause === 'ground');
fly(state, FLIGHT.respawnDelay, {});
assert.ok(state.crashTimer <= 0);
spawnAt(state, sky, new Vector3(1, 0, 0), new Vector3(0, 1, 0));
assert.ok(state.alive && state.altitude >= FLIGHT.spawnAltitude - 1);
});
test('terrain and obstacle placement are deterministic', () => {
assert.equal(terrainHeight(0.3, 0.8, 0.52), terrainHeight(0.3, 0.8, 0.52));
const a = generateObstacles();
const b = generateObstacles();
assert.equal(a.obstacles.length, b.obstacles.length);
assert.ok(a.obstacles.length > 500);
assert.ok(a.obstacles[10].matrix.equals(b.obstacles[10].matrix));
// A collider sits at every non-cloud obstacle's position.
const spire = a.obstacles.find((o) => o.kind === 'spire');
const base = new Vector3().setFromMatrixPosition(spire.matrix);
const up = base.clone().normalize();
assert.ok(a.hits(base.addScaledVector(up, spire.scale * 0.3), 1));
});
