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/beginner.test.mjs
import test from 'node:test';
import assert from 'node:assert/strict';
import { Vector3 } from 'three';
import { beginnerControls, createCrashHistory, recordBeginnerCrash, dismissBeginnerOffer } from '../src/sim/beginner.ts';
import { createFlightState, neutralInput, spawnAt, stepFlight } from '../src/sim/flight.ts';
const world = { surfaceHeight: () => 0, terrainHeight: () => -60, hitsObstacle: () => false };
function spawn(direction = new Vector3(0, 1, 0)) {
  const state=createFlightState();
  spawnAt(state,world,direction,new Vector3(0,0,-1));
  return state;
}
function fly(state, seconds, control) {
  for(let i=0; i<seconds*120; i++) stepFlight(state,beginnerControls(state,{...neutralInput(),...control}),world,1/120);
  return state;
}
test('beginners have full instantaneous pitch/roll, boost and fire authority', () => {
  const state=spawn();
  for(const pitch of [-1,-0.5,0,0.5,1]) for(const roll of [-1,-0.5,0.5,1]) {
    const input=beginnerControls(state,{...neutralInput(),pitch,roll,boost:true,fire:true});
    assert.ok(input.pitch === -pitch);
    assert.equal(input.roll, roll);
    assert.equal(input.boost,true);
    assert.equal(input.fire,true);
    assert.equal(input.throttleSet,1);
  }
});
/** Heading change (rad) over the surface while flying `seconds` with `control`. */
function turned(state, seconds, control) {
  const flat = () => state.velocity.clone().projectOnPlane(state.position.clone().normalize());
  let previous = flat(), total = 0;
  for(let i=0;i<seconds*120;i++) {
    stepFlight(state,control(state),world,1/120);
    const heading=flat();
    total+=previous.angleTo(heading)*Math.sign(previous.clone().cross(heading).dot(state.position));
    previous=heading;
  }
  return total;
}
test('full left/right steering turns much harder than rolling alone', () => {
  for (const roll of [-1, 1]) {
    const beginner=spawn(), rolling=spawn();
    const turn=turned(beginner,3,(s)=>beginnerControls(s,{...neutralInput(),roll}));
    const rollTurn=turned(rolling,3,()=>({...neutralInput(),roll,throttleSet:1}));
    assert.ok(beginner.alive);
    // Positive angle is a left turn about the local up.
    assert.ok(-turn * roll > Math.PI * 0.85, `turned only ${(turn*180/Math.PI).toFixed(0)} degrees`);
    assert.ok(Math.abs(turn) - Math.abs(rollTurn) > 1.2, 'turn should be much tighter than roll-only');
  }
});
test('a held full turn stays level instead of spiralling down', () => {
  const state=spawn();
  const start=state.altitude;
  const turn=turned(state,8,(s)=>beginnerControls(s,{...neutralInput(),roll:1}));
  assert.ok(state.alive);
  assert.ok(Math.abs(turn) > Math.PI * 2, `full circles in 8 s: ${(turn/Math.PI/2).toFixed(1)}`);
  assert.ok(Math.abs(state.altitude-start) < 30, `altitude changed ${(state.altitude-start).toFixed(0)} m`);
});
test('full horizontal stick holds a steep bank and commands normal full pull', () => {
  for(const roll of [-1,1]) {
    const state=fly(spawn(),1.5,{roll});
    const up=state.position.clone().normalize();
    const right=new Vector3(1,0,0).applyQuaternion(state.orientation);
    const bodyUp=new Vector3(0,1,0).applyQuaternion(state.orientation);
    const bank=Math.atan2(-right.dot(up),bodyUp.dot(up));
    assert.ok(bank*roll>1.4 && bank*roll<1.7, `bank ${bank}`);
    const assisted=beginnerControls(state,{...neutralInput(),roll});
    assert.ok(assisted.pitch>0.98);
    const manual=structuredClone(assisted);
    manual.pitch=1;
    const reference=spawn();
    reference.position.copy(state.position); reference.velocity.copy(state.velocity);
    reference.orientation.copy(state.orientation);reference.angularVelocity.copy(state.angularVelocity);
    reference.throttle=state.throttle;
    stepFlight(state,assisted,world,1/120);
    stepFlight(reference,manual,world,1/120);
    assert.ok(state.orientation.angleTo(reference.orientation)<0.001);
  }
});
test('held horizontal steering settles at its bank target without rolling through', () => {
  for (const roll of [-1, -0.5, 0.5, 1]) {
    const state=spawn();
    let maxBank=0;
    let bank=0;
    for(let i=0;i<3*120;i++) {
      stepFlight(state,beginnerControls(state,{...neutralInput(),roll}),world,1/120);
      const up=state.position.clone().normalize();
      const right=new Vector3(1,0,0).applyQuaternion(state.orientation);
      const bodyUp=new Vector3(0,1,0).applyQuaternion(state.orientation);
      bank=Math.atan2(-right.dot(up),bodyUp.dot(up));
      maxBank=Math.max(maxBank,Math.abs(bank));
    }
    assert.ok(maxBank <= Math.PI/2 + Math.PI/180, `overshot bank: ${maxBank}`);
    // A full turn gives back up to ~30 degrees of bank while sinking to stay level.
    const target=roll*Math.PI/2;
    assert.ok(Math.abs(roll)<1 ? Math.abs(bank-target)<0.1 : bank*roll>1.0 && bank*roll<target*roll+0.1, `target bank not held: ${bank}`);
  }
});
test('explicit vertical input overrides automatic turn pull', () => {
  const state=fly(spawn(),1,{roll:1});
  for(const pitch of [-1,1]) {
    assert.equal(beginnerControls(state,{...neutralInput(),roll:1,pitch}).pitch,-pitch);
  }
  assert.ok(beginnerControls(state,{...neutralInput(),roll:0}).pitch === 0);
});
test('beginner pilots can dive into the ground and crash', () => {
  assert.equal(fly(spawn(),10,{pitch:0.6}).alive,false);
});
test('neutral steering levels wings and supports cruise around the sphere', () => {
  for(const dir of [new Vector3(0,1,0),new Vector3(1,0,0),new Vector3(0,-1,0)]) {
    const state=fly(spawn(dir),60,{});
    assert.ok(state.alive);
    assert.ok(Math.abs(state.altitude-95)<30);
  }
  const state=fly(spawn(),0.2,{roll:1});
  fly(state,8,{});
  assert.ok(Math.abs(new Vector3(1,0,0).applyQuaternion(state.orientation).dot(state.position.clone().normalize()))<0.08);
});
test('destroyed parts remain fatal without assistance overriding damage', () => {
  const state=spawn();state.crippled=true;
  const input=beginnerControls(state,{...neutralInput(),pitch:-1,roll:1,fire:true});
  assert.equal(input.pitch,1); assert.equal(input.roll,1); assert.equal(input.fire,true);
  state.damage.left=1;
  fly(state,15,{pitch:-1,roll:1});
  assert.equal(state.alive,false);
});
test('crash offer excludes combat, expires old crashes and respects cooldown', () => {
  const h = createCrashHistory();
  assert.equal(recordBeginnerCrash(h, 'shotdown', 0), false);
  assert.equal(recordBeginnerCrash(h, 'midair', 1), false);
  for (const t of [0, 40, 150]) assert.equal(recordBeginnerCrash(h, 'ground', t), false);
  assert.equal(recordBeginnerCrash(h, 'obstacle', 151), true);
  assert.equal(recordBeginnerCrash(h, 'water', 152), false);
  dismissBeginnerOffer(h, 152);
  for (const t of [160, 170, 180]) assert.equal(recordBeginnerCrash(h, 'ground', t), false);
  for (const t of [453, 460]) assert.equal(recordBeginnerCrash(h, 'ground', t), false);
  assert.equal(recordBeginnerCrash(h, 'water', 470), true);
});

test('outside steering pitches toward planet up even with banked wings', async () => {
  const { planetBeginnerControls } = await import('../src/sim/beginner.ts');
  for (const bank of [-1.1, 1.1]) {
    const state=spawn();
    state.orientation.setFromAxisAngle(new Vector3(0,0,1),bank);
    const input=planetBeginnerControls(state,{...neutralInput(),pitch:-1});
    assert.ok(input.pitch>0);
    assert.ok(input.yaw*bank>0, 'rudder redirects the pull toward the horizon');
    assert.ok(input.roll*bank>0, 'ailerons roll the wings level for the climb');
  }
});
test('outside up steering continues through inverted attitude and release eases descent', async () => {
  const { planetBeginnerControls } = await import('../src/sim/beginner.ts');
  const state=spawn();
  state.orientation.setFromAxisAngle(new Vector3(1,0,0),Math.PI);
  assert.equal(planetBeginnerControls(state,{...neutralInput(),pitch:-1}).pitch,1);
  assert.equal(planetBeginnerControls(state,{...neutralInput(),pitch:-1}).roll,0);
  state.orientation.identity();
  state.velocity.y=-20;
  state.time+=4;
  const released=planetBeginnerControls(state,neutralInput());
  assert.ok(released.pitch>0 && released.pitch<=0.35);
});

test('pitch release gives a grace period, then gradually restores recovery for each pilot', async () => {
  const { planetBeginnerControls } = await import('../src/sim/beginner.ts');
  const state=spawn(), other=spawn();
  planetBeginnerControls(state,{...neutralInput(),pitch:-1});
  state.velocity.y=20;
  other.velocity.y=20;
  for(const elapsed of [0,1,1.5]) {
    state.time=elapsed;
    const input=planetBeginnerControls(state,neutralInput());
    assert.equal(input.pitchRecovery,0);
    assert.ok(input.pitch === 0);
  }
  state.time=2.5;
  const halfway=planetBeginnerControls(state,neutralInput());
  state.time=3.5;
  const recovered=planetBeginnerControls(state,neutralInput());
  assert.ok(halfway.pitchRecovery>0 && halfway.pitchRecovery<recovered.pitchRecovery);
  assert.ok(Math.abs(halfway.pitch)<Math.abs(recovered.pitch));
  assert.equal(planetBeginnerControls(other,neutralInput()).pitchRecovery,0.35);
  planetBeginnerControls(state,{...neutralInput(),pitch:1});
  assert.equal(planetBeginnerControls(state,neutralInput()).pitchRecovery,0);
  spawnAt(state,world,new Vector3(0,1,0),new Vector3(0,0,-1));
  assert.equal(planetBeginnerControls(state,neutralInput()).pitchRecovery,0.35);
});

test('released climbs and dives persist for two seconds before easing toward level', async () => {
  const { planetBeginnerControls } = await import('../src/sim/beginner.ts');
  for(const pitch of [-1,1]) {
    const state=spawn();
    state.position.y+=300;
    const tick=(pitch)=>stepFlight(state,planetBeginnerControls(state,{...neutralInput(),pitch}),world,1/120);
    const angle=()=>Math.asin(new Vector3(0,0,-1).applyQuaternion(state.orientation).dot(state.position.clone().normalize()));
    for(let i=0;i<48;i++) tick(pitch);
    for(let i=0;i<240;i++) tick(0);
    const heldAngle=angle();
    assert.ok(heldAngle*-pitch>0.3, 'chosen climb/dive should persist after release');
    for(let i=0;i<480;i++) tick(0);
    assert.ok(Math.abs(angle())<Math.abs(heldAngle)*0.85, 'recovery should gradually reduce pitch');
    assert.ok(state.alive);
  }
});
test('holding outside up completes a loop with the real flight simulation', async () => {
  const { planetBeginnerControls } = await import('../src/sim/beginner.ts');
  const state=spawn();
  let inverted=false, diving=false, climbedAgain=false;
  for(let i=0;i<8*120;i++) {
    stepFlight(state,planetBeginnerControls(state,{...neutralInput(),pitch:-1}),world,1/120);
    const up=state.position.clone().normalize();
    const bodyUp=new Vector3(0,1,0).applyQuaternion(state.orientation);
    const forward=new Vector3(0,0,-1).applyQuaternion(state.orientation);
    if(bodyUp.dot(up)<-0.8) inverted=true;
    if(inverted && forward.dot(up)<-0.8) diving=true;
    if(diving && bodyUp.dot(up)>0 && forward.dot(up)>0.5) climbedAgain=true;
  }
  assert.ok(state.alive);
  assert.ok(inverted && diving && climbedAgain, 'loop returns upright into the next climb');
});
test('up-only assistance recovers a full bank on either side using all three axes', async () => {
  const { planetBeginnerControls } = await import('../src/sim/beginner.ts');
  for(const bank of [-Math.PI/2,Math.PI/2]) {
    const state=spawn();
    state.orientation.setFromAxisAngle(new Vector3(0,0,1),bank);
    state.angularVelocity.z=Math.sign(bank)*0.6;
    const initial=planetBeginnerControls(state,{...neutralInput(),pitch:-1});
    assert.ok(initial.roll*bank>0, 'roll must oppose the existing bank');
    assert.ok(initial.yaw*bank>0, 'rudder helps redirect the banked nose');
    let elevator=false;
    for(let i=0;i<120;i++) {
      const input=planetBeginnerControls(state,{...neutralInput(),pitch:-1});
      elevator ||= input.pitch>0.3;
      stepFlight(state,input,world,1/120);
    }
    const up=state.position.clone().normalize();
    assert.ok(Math.abs(new Vector3(1,0,0).applyQuaternion(state.orientation).dot(up))<0.5, 'wings recover toward horizontal');
    assert.ok(new Vector3(0,0,-1).applyQuaternion(state.orientation).dot(up)>0.2, 'nose climbs toward planet up');
    assert.ok(elevator && state.alive);
  }
});

test('released pitch recovery retains elevator authority all the way to vertical', async () => {
  const { planetBeginnerControls } = await import('../src/sim/beginner.ts');
  for(const degrees of [60,85,90]) {
    const state=spawn();
    state.orientation.setFromAxisAngle(new Vector3(1,0,0),degrees*Math.PI/180);
    // Nose elevation can lead the velocity; recovery must still push down.
    const control=planetBeginnerControls(state,neutralInput());
    assert.ok(control.pitch < -0.3, `insufficient recovery at ${degrees} degrees: ${control.pitch}`);
    assert.ok(Math.abs(control.yaw)<1e-6);
  }
});

test('a released sustained climb settles near the horizon instead of drifting vertical', async () => {
  const { planetBeginnerControls } = await import('../src/sim/beginner.ts');
  for(const hold of [1,2.5]) {
    const state=spawn();
    state.position.y+=300;
    const tick=(pitch)=>stepFlight(state,planetBeginnerControls(state,{...neutralInput(),pitch}),world,1/120);
    for(let i=0;i<hold*120;i++) tick(-1);
    for(let i=0;i<20*120;i++) tick(0);
    const elevation=Math.asin(new Vector3(0,0,-1).applyQuaternion(state.orientation).dot(state.position.clone().normalize()));
    assert.ok(state.alive);
    assert.ok(Math.abs(elevation)<5*Math.PI/180, `nose failed to settle: ${elevation*180/Math.PI} degrees`);
  }
});

test('combined outside steering turns immediately while climbing or diving', async () => {
  const { planetBeginnerControls } = await import('../src/sim/beginner.ts');
  for(const pitch of [-1,1]) for(const roll of [-1,1]) {
    const state=spawn();
    state.position.y+=300;
    // Start the climb/dive first, then add the horizontal key while it is held.
    for(let i=0;i<60;i++) stepFlight(state,planetBeginnerControls(state,{...neutralInput(),pitch}),world,1/120);
    const initial=planetBeginnerControls(state,{...neutralInput(),pitch,roll});
    assert.ok(initial.yaw*roll>0.5, 'turn command starts before wings reach their bank target');
    const startForward=new Vector3(0,0,-1).applyQuaternion(state.orientation);
    for(let i=0;i<90;i++) stepFlight(state,planetBeginnerControls(state,{...neutralInput(),pitch,roll}),world,1/120);
    const up=state.position.clone().normalize();
    const elevation=new Vector3(0,0,-1).applyQuaternion(state.orientation).dot(up);
    const forward=new Vector3(0,0,-1).applyQuaternion(state.orientation);
    const headingChange=startForward.clone().projectOnPlane(up).normalize().cross(forward.clone().projectOnPlane(up).normalize()).dot(up);
    assert.ok(-headingChange*roll>0.04, `horizontal turn missing: ${headingChange}`);
    assert.ok(elevation*-pitch>0.3, `vertical steering lost: ${elevation}`);
    assert.ok(state.alive);
  }
});