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.

src/world/rockets.ts
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;
    },
  };
}