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 {
ConeGeometry,
CylinderGeometry,
BoxGeometry,
Group,
Mesh,
MeshBasicMaterial,
MeshLambertMaterial,
Quaternion,
Vector3,
type BufferGeometry,
} from 'three';
import type { Rocket } from '../sim/rockets.ts';
import { merge, part, T } from './geometry.ts';
import { PALETTE as P } from './palette.ts';
const PI = Math.PI;
let shared: BufferGeometry | null = null;
/** A stubby low-poly rocket about 1.5 m long, nose toward -Z, centred on its body (shared). */
export function rocketGeometry(): BufferGeometry {
return (shared ??= merge([
part(new CylinderGeometry(0.15, 0.15, 1.1, 6), P.white, T(0, 0, 0, PI / 2)),
part(new ConeGeometry(0.15, 0.4, 6), P.red, T(0, 0, -0.75, -PI / 2)),
part(new CylinderGeometry(0.16, 0.16, 0.12, 6), P.red, T(0, 0, 0.2, PI / 2)),
part(new BoxGeometry(0.6, 0.04, 0.3), P.planeTrim, T(0, 0, 0.45)),
part(new BoxGeometry(0.04, 0.6, 0.3), P.planeTrim, T(0, 0, 0.45)),
]));
}
const flameGeometry = new ConeGeometry(0.2, 1.3, 6);
const SMOKE_INTERVAL = 1 / 30;
/** Rockets in flight: a small pool of meshes with flickering exhaust. */
export function createRocketMeshes() {
const group = new Group();
const geometry = rocketGeometry();
const material = new MeshLambertMaterial({ vertexColors: true, flatShading: true });
const flameMaterial = new MeshBasicMaterial({ color: P.flame, transparent: true, opacity: 0.9 });
const pool: { body: Mesh; flame: Mesh }[] = [];
const nose = new Vector3(0, 0, -1);
const dir = new Vector3();
const back = new Vector3();
const drift = new Vector3();
const turn = new Quaternion();
let time = 0;
let smokeClock = 0;
const meshFor = (index: number) => {
let entry = pool[index];
if (!entry) {
const body = new Mesh(geometry, material);
const flame = new Mesh(flameGeometry, flameMaterial);
flame.rotation.x = -PI / 2;
flame.position.z = 1.05;
body.add(flame);
group.add(body);
entry = { body, flame };
pool[index] = entry;
}
return entry;
};
return {
group,
/**
* Place every rocket; while the game runs, also lay a smoke trail through
* `puff` (about 30 puffs a second per rocket) so rockets read from afar.
*/
update(rockets: readonly Rocket[], dt: number, puff: (position: Vector3, drift: Vector3) => void) {
time += dt;
smokeClock += dt;
const smoke = smokeClock >= SMOKE_INTERVAL;
if (smoke) smokeClock %= SMOKE_INTERVAL;
rockets.forEach((rocket, i) => {
const { body, flame } = meshFor(i);
body.visible = true;
dir.copy(rocket.velocity).normalize();
body.position.copy(rocket.position);
body.quaternion.copy(turn.setFromUnitVectors(nose, dir));
const s = 0.8 + 0.4 * Math.sin(time * 45 + i * 2.1);
flame.scale.set(1, s, 1);
flame.position.z = 0.8 + 0.65 * s;
if (smoke && dt > 0) {
back.copy(rocket.position).addScaledVector(dir, 1.4);
drift.copy(rocket.position).normalize().multiplyScalar(0.6);
puff(back, drift);
}
});
for (let i = rockets.length; i < pool.length; i++) pool[i]!.body.visible = false;
},
};
}
