Back to Poly Dogfight
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.

tests/flight.test.mjs
// 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));
});