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.
// Guns, hit zones and battle damage.
import assert from 'node:assert/strict';
import test from 'node:test';
import { Vector3 } from 'three';
import { PLANET_RADIUS } from '../src/sim/constants.ts';
import { createFlightState, neutralInput, spawnAt, stepFlight } from '../src/sim/flight.ts';
import { createGuns, segmentBoxEntry, stepGuns } from '../src/sim/guns.ts';
const DT = 1 / 120;
const sky = { surfaceHeight: () => 0, terrainHeight: () => -60, hitsObstacle: () => false };
function plane(altitude = 200, angle = 0) {
const state = createFlightState();
spawnAt(state, sky, new Vector3(0, Math.cos(angle), -Math.sin(angle)), 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 && state.alive; i++) stepFlight(state, { ...neutralInput(), ...input }, sky, DT);
return state;
}
/** Bank angle: positive when the right wing is up (rolled left). */
const bank = (state) => {
const right = new Vector3(1, 0, 0).applyQuaternion(state.orientation);
return Math.asin(right.dot(state.position.clone().normalize()));
};
test('segment-box sweep finds the entry point and misses cleanly', () => {
const min = [-1, -1, -1];
const max = [1, 1, 1];
assert.equal(segmentBoxEntry(new Vector3(-3, 0, 0), new Vector3(1, 0, 0), min, max), 0.5);
assert.equal(segmentBoxEntry(new Vector3(-3, 2, 0), new Vector3(3, 2, 0), min, max), null);
assert.equal(segmentBoxEntry(new Vector3(-3, 0, 0), new Vector3(-2, 0, 0), min, max), null);
});
/** A shooter 60 m behind the target, offset sideways so the guns line up on one part. */
function chase(offsetX) {
const target = plane(200);
const shooter = createFlightState();
shooter.position.copy(target.position).add(new Vector3(offsetX, 0, 60));
shooter.orientation.copy(target.orientation);
shooter.velocity.copy(target.velocity);
shooter.alive = true;
return { target, shooter };
}
function shoot(shooter, target, seconds) {
const guns = createGuns();
const hits = [];
for (let i = 0; i < seconds * 120; i++) {
const events = stepGuns(guns, [shooter, target], [true, false], sky, DT);
hits.push(...events.hits);
}
return hits;
}
test('rounds into the left wing damage only the left wing', () => {
const { target, shooter } = chase(-2.2);
const hits = shoot(shooter, target, 0.5);
assert.ok(hits.length >= 3, `only ${hits.length} hits`);
assert.ok(hits.every((hit) => hit.part === 'left' && hit.target === 1));
assert.ok(target.damage.left > 0.2);
assert.equal(target.damage.right, 0);
assert.equal(target.damage.tail, 0);
assert.equal(shooter.damage.left + shooter.damage.right + shooter.damage.tail, 0);
});
test('rounds from dead astern hit the tail; the engine block is armoured', () => {
const { target, shooter } = chase(0);
shooter.position.y += 0.2; // guns sit high on the cowling; aim along the tail
const hits = shoot(shooter, target, 0.5);
assert.ok(hits.length > 0 && hits.every((hit) => hit.part === 'tail'));
assert.ok(target.damage.tail > 0);
});
test('a holed wing rolls the plane toward it; an intact plane stays level', () => {
const healthy = fly(plane(), 2.5, { throttleSet: 1 });
const left = plane();
left.damage.left = 0.6;
fly(left, 2.5, { throttleSet: 1 });
const right = plane();
right.damage.right = 0.6;
fly(right, 2.5, { throttleSet: 1 });
assert.ok(Math.abs(bank(healthy)) < 0.1);
assert.ok(bank(left) > 0.5, `left-damaged bank ${bank(left)}`);
assert.ok(bank(right) < -0.5, `right-damaged bank ${bank(right)}`);
assert.ok(left.alive && right.alive);
});
test('a damaged wing can still be held level, but only with steady opposite stick', () => {
const state = plane();
state.damage.left = 0.6;
let stick = 0;
const steps = 6 * 120;
for (let i = 0; i < steps; i++) {
// A simple pilot: roll right in proportion to the left bank.
const roll = Math.max(-1, Math.min(1, bank(state) * 3));
stick += roll;
stepFlight(state, { ...neutralInput(), throttleSet: 1, roll }, sky, DT);
}
assert.ok(state.alive);
assert.ok(Math.abs(bank(state)) < 0.3, `bank ${bank(state)}`);
assert.ok(stick / steps > 0.1, `average stick ${(stick / steps).toFixed(2)}`);
});
test('a shot-up tail slowly pitches the nose down', () => {
const healthy = fly(plane(), 8, { throttleSet: 1 });
const hurt = plane();
hurt.damage.tail = 0.8;
fly(hurt, 8, { throttleSet: 1 });
const lost = healthy.altitude - hurt.altitude;
assert.ok(hurt.alive);
assert.ok(lost > 15, `only ${lost.toFixed(1)} m lower`);
assert.ok(lost < 150, `dropped too fast: ${lost.toFixed(1)} m`);
});
for (const part of ['left', 'right', 'tail']) {
test(`losing the ${part} ${part === 'tail' ? '' : 'wing '}sends the plane down without exploding`, () => {
const state = plane(200);
state.damage[part] = 1;
state.crippled = true;
// Still flying (not exploded) a couple of seconds later, even with full back stick.
fly(state, 2, { throttleSet: 1, pitch: 1 });
assert.ok(state.alive);
fly(state, 40, { throttleSet: 1, pitch: 1, roll: part === 'left' ? 1 : part === 'right' ? -1 : 0 });
assert.equal(state.alive, false, `still flying at ${state.altitude.toFixed(0)} m`);
assert.equal(state.crashCause, 'shotdown');
});
}
test('respawning repairs the plane', () => {
const state = plane();
state.damage.left = 1;
state.damage.tail = 0.5;
state.crippled = true;
spawnAt(state, sky, new Vector3(1, 0, 0), new Vector3(0, 1, 0));
assert.deepEqual(state.damage, { left: 0, right: 0, tail: 0 });
assert.equal(state.crippled, false);
});
