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.
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);
}
});
