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/obstacle-meshes.ts
import {
  ConeGeometry,
  CylinderGeometry,
  DodecahedronGeometry,
  Group,
  IcosahedronGeometry,
  InstancedMesh,
  Matrix4,
  Mesh,
  MeshLambertMaterial,
  TorusGeometry,
  type BufferGeometry,
} from 'three';
import type { ObstacleField, ObstacleKind } from '../sim/obstacles.ts';
import { chunkOf, type HorizonCuller } from './chunks.ts';
import { merge, part, T } from './geometry.ts';
import { PALETTE as P } from './palette.ts';

const PI = Math.PI;

function templates(): Record<ObstacleKind, BufferGeometry> {
  return {
    arch: merge([
      part(new TorusGeometry(1, 0.26, 5, 14, PI), P.rock, undefined, 0.05),
      part(new CylinderGeometry(0.3, 0.4, 0.9, 6), P.rockDark, T(1, -0.4, 0)),
      part(new CylinderGeometry(0.3, 0.4, 0.9, 6), P.rockDark, T(-1, -0.4, 0)),
      part(new DodecahedronGeometry(0.22, 0), P.rockDark, T(0.55, 0.88, 0.1)),
      part(new IcosahedronGeometry(0.16, 0), P.grass, T(-0.2, 1.24, 0)),
    ]),
    spire: merge([
      part(new CylinderGeometry(0.02, 0.1, 0.72, 6, 3), P.rock, T(0, 0.64), 0.03),
      part(new CylinderGeometry(0.1, 0.15, 0.3, 6), P.rockDark, T(0, 0.15)),
      part(new ConeGeometry(0.035, 0.08, 5), P.snow, T(0, 1.02)),
    ]),
    hoodoo: merge([
      part(new CylinderGeometry(0.1, 0.16, 1, 7, 2), P.riftA, T(0, 0.5), 0.03),
      part(new CylinderGeometry(0.5, 0.4, 0.16, 8), P.rockDark, T(0, 1.05)),
      part(new CylinderGeometry(0.46, 0.5, 0.05, 8), P.grass, T(0, 1.15)),
      part(new IcosahedronGeometry(0.08, 0), P.leaf, T(0.2, 1.22, 0.1)),
    ]),
    island: merge([
      part(new ConeGeometry(1, 1.3, 7, 2), P.rockDark, T(0, -0.66, 0, PI), 0.08),
      part(new CylinderGeometry(1, 1, 0.12, 7), P.grass, T(0, 0)),
      part(new ConeGeometry(0.16, 0.4, 5), P.pine, T(0.35, 0.26, 0.2)),
      part(new ConeGeometry(0.13, 0.32, 5), P.pine, T(-0.3, 0.22, -0.35)),
      part(new IcosahedronGeometry(0.18, 0), P.leaf, T(-0.4, 0.22, 0.35)),
      part(new CylinderGeometry(0.06, 0.06, 0.9, 4), P.seaShallow, T(0.85, -0.45, 0)),
    ]),
    halo: merge([
      part(new TorusGeometry(1, 0.13, 6, 24), P.halo),
      part(new TorusGeometry(0.86, 0.035, 4, 24), P.haloGlow),
    ]),
    lighthouse: merge([
      part(new DodecahedronGeometry(0.2, 0), P.rockDark, T(0, 0.02)),
      part(new CylinderGeometry(0.1, 0.12, 0.25, 8), P.white, T(0, 0.125)),
      part(new CylinderGeometry(0.095, 0.1, 0.25, 8), P.red, T(0, 0.375)),
      part(new CylinderGeometry(0.09, 0.095, 0.25, 8), P.white, T(0, 0.625)),
      part(new CylinderGeometry(0.085, 0.09, 0.25, 8), P.red, T(0, 0.875)),
      part(new CylinderGeometry(0.13, 0.13, 0.04, 8), P.planeTrim, T(0, 1.02)),
      part(new CylinderGeometry(0.07, 0.07, 0.1, 8), P.lamp, T(0, 1.09)),
      part(new ConeGeometry(0.11, 0.12, 8), P.red, T(0, 1.2)),
    ]),
    windmill: merge([
      part(new CylinderGeometry(0.09, 0.16, 0.95, 8), P.white, T(0, 0.475)),
      part(new ConeGeometry(0.15, 0.2, 8), P.red, T(0, 1.05)),
      part(new CylinderGeometry(0.04, 0.04, 0.2, 6), P.planeTrim, T(0, 0.95, 0.1, PI / 2)),
    ]),
    tree: merge([
      part(new CylinderGeometry(0.04, 0.06, 0.4, 5), P.wood, T(0, 0.2)),
      part(new IcosahedronGeometry(0.32, 0), P.leaf, T(0, 0.62)),
      part(new IcosahedronGeometry(0.2, 0), P.leafDark, T(0.16, 0.45, 0.08)),
    ]),
    pine: merge([
      part(new CylinderGeometry(0.035, 0.05, 0.25, 5), P.wood, T(0, 0.12)),
      part(new ConeGeometry(0.26, 0.5, 6), P.pine, T(0, 0.45)),
      part(new ConeGeometry(0.18, 0.4, 6), P.leafDark, T(0, 0.78)),
    ]),
    cloud: merge([
      part(new IcosahedronGeometry(0.5, 0), P.cloud),
      part(new IcosahedronGeometry(0.38, 0), P.cloud, T(0.48, -0.06, 0.1)),
      part(new IcosahedronGeometry(0.35, 0), P.cloud, T(-0.46, -0.08, -0.05)),
      part(new IcosahedronGeometry(0.3, 0), P.cloud, T(0.15, 0.2, -0.25)),
    ]),
  };
}

function bladeTemplate(): BufferGeometry {
  const blades: BufferGeometry[] = [];
  for (let i = 0; i < 4; i++) {
    const a = (i / 4) * PI * 2;
    const box = new CylinderGeometry(0.02, 0.06, 0.55, 4);
    blades.push(part(box, P.planeWing, new Matrix4().makeRotationZ(a).multiply(T(0, 0.3, 0))));
  }
  blades.push(part(new IcosahedronGeometry(0.05, 0), P.red));
  return merge(blades);
}

export type ObstacleMeshes = {
  group: Group;
  /** Solid obstacles per planet patch, for baking into the land (same material). */
  solidChunks: Map<number, BufferGeometry>;
  update(dt: number): void;
};

/**
 * Obstacles never move, so each planet patch gets them baked into one
 * geometry. Clouds (translucent) become one culled mesh per patch here; solid
 * obstacles are returned for the land to absorb.
 */
export function createObstacleMeshes(field: ObstacleField, culler: HorizonCuller): ObstacleMeshes {
  const material = new MeshLambertMaterial({ vertexColors: true, flatShading: true });
  const cloudMaterial = new MeshLambertMaterial({
    vertexColors: true,
    flatShading: true,
    transparent: true,
    opacity: 0.92,
    emissive: 0x334455,
  });
  const group = new Group();
  const geometries = templates();
  const buckets = new Map<string, BufferGeometry[]>();
  const mills: Matrix4[] = [];
  for (const o of field.obstacles) {
    if (o.kind === 'windmill') mills.push(o.matrix);
    const e = o.matrix.elements;
    const key = `${o.kind === 'cloud' ? 'cloud' : 'solid'}:${chunkOf(e[12]!, e[13]!, e[14]!)}`;
    const list = buckets.get(key) ?? [];
    list.push(geometries[o.kind].clone().applyMatrix4(o.matrix));
    buckets.set(key, list);
  }
  const solidChunks = new Map<number, BufferGeometry>();
  for (const [key, parts] of buckets) {
    const geometry = merge(parts);
    for (const p of parts) p.dispose();
    const [kind, chunk] = key.split(':');
    if (kind === 'solid') {
      solidChunks.set(Number(chunk), geometry);
      continue;
    }
    const mesh = new Mesh(geometry, cloudMaterial);
    group.add(mesh);
    culler.add(mesh, geometry);
  }

  const blades = new InstancedMesh(bladeTemplate(), material, Math.max(1, mills.length));
  blades.count = mills.length;
  group.add(blades);
  const hub = T(0, 0.95, 0.21);
  const temp = new Matrix4();
  const spin = new Matrix4();
  let angle = 0;

  return {
    group,
    solidChunks,
    update(dt) {
      angle += dt * 1.4;
      mills.forEach((m, i) => {
        spin.makeRotationZ(angle + i * 0.7);
        temp.copy(m).multiply(hub).multiply(spin);
        blades.setMatrixAt(i, temp);
      });
      blades.instanceMatrix.needsUpdate = true;
      if (mills.length) blades.computeBoundingSphere();
    },
  };
}