Back to Pursuit PIT
SOURCE / PINNED RELEASE

Made of little things.

Pursuit PIT

Release
565f4981bdc3…
Author-recorded commit
197afdbed44b…
License
LICENSE
Author’s source reference
nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-951414e7fd7

Archive hash verified: 3b0485589942fd5c…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/game/birds.ts
// Bird strikes (pursuit and free drive): every 30 seconds to two minutes a flock of 20 to 35 birds
// comes straight down the highway from the direction the traffic is driving, head-on into it:
// high up the road at first, diving to car height around where they meet the player,
// skimming over every lane for a stretch, then climbing away behind. One to three birds in
// every flock make straight for the patrol car's windscreen, so each strike hits it at least
// once. The radio warns about it first (a 'birdsWarn' event). A bird that meets a car is
// gone: the sim reports a 'bird' event with the hit point, and the renderer turns it into a
// burst of blood and feathers and a splat that stays on the car.
import type { Car, SimEvent } from './sim.js';

export interface Bird {
  id: number;
  x: number;
  y: number;
  s: number;
  speed: number; // toward oncoming traffic (along -s), m/s
  sway: number; // sideways drift, m/s
  low: number; // lowest height over the road
  dipS: number; // where along the road it is lowest
  flap: number; // wingbeat phase, for the renderer
  aim?: number; // set on the birds headed for the patrol car: their offset across its windscreen
}

const WINDSCREEN_Y = 1.15; // height the aimed birds come in at

const FIRST = [30, 120]; // seconds to the next strike, counted afresh from each one
const WARNING = 9; // seconds of warning before the flock arrives
const SKIM = 25; // metres either side of the dip flown at the lowest height
const LANES_HALF = 10.5; // birds spread across the six lanes

let nextBird = 1;

/** Height of a bird at road position s: high up the road, skimming the traffic near its dip. */
export function birdHeight(b: Pick<Bird, 's' | 'low' | 'dipS'>): number {
  return Math.min(40, b.low + 0.004 * Math.max(0, Math.abs(b.s - b.dipS) - SKIM) ** 2);
}

/** Which way a bird faces (yaw, for a model facing -z) and its nose-up pitch from the swoop. */
export function birdPose(b: Bird): { yaw: number; pitch: number } {
  // In render space z = -(s - origin), so flying toward -s is flying toward +z.
  const yaw = Math.atan2(-b.sway, -b.speed);
  const off = b.s - b.dipS;
  const slope = 0.008 * Math.max(0, Math.abs(off) - SKIM) * Math.sign(off); // dy per metre of s
  return { yaw, pitch: Math.atan(-slope) }; // it flies toward -s, so it climbs where the slope is negative
}

const between = ([a, b]: number[]) => a + Math.random() * (b - a);

export class Birds {
  birds: Bird[] = [];
  /** Sim time of the next strike. */
  nextStrike: number;
  private warned = false;

  constructor(time: number) {
    this.nextStrike = time + between(FIRST);
  }

  update(dt: number, time: number, cars: readonly Car[], player: Car, events: SimEvent[]): void {
    if (!this.warned && time >= this.nextStrike - WARNING) {
      this.warned = true;
      events.push({ type: 'birdsWarn', strength: 0, x: player.x, s: player.s });
    }
    if (time >= this.nextStrike) {
      this.spawn(player, events);
      this.nextStrike = time + between(FIRST);
      this.warned = false;
    }
    if (!this.birds.length) return;

    for (const b of this.birds) {
      b.s -= b.speed * dt;
      if (b.aim !== undefined) {
        // Headed for the patrol car: dive onto it wherever it is, lining up across its windscreen.
        b.dipS = player.s;
        b.low = WINDSCREEN_Y;
        const ahead = b.s - player.s;
        if (ahead < 150) b.x += (player.x + b.aim - b.x) * Math.min(1, dt * (ahead < 40 ? 8 : 2));
      } else {
        b.x += b.sway * dt;
        if (Math.abs(b.x) > LANES_HALF) b.sway = -b.sway; // keep over the road
      }
      b.y = birdHeight(b);
      b.flap += dt * (b.y < 4 ? 9 : 6);
    }
    // Birds low enough to meet a car.
    for (const b of this.birds) {
      if (b.y > 2.6) continue;
      for (const car of cars) {
        // The aimed birds hit only the patrol car (nothing gets in their way), and the rest of
        // the flock only traffic, so every strike hits the patrol car exactly one to three times.
        if ((b.aim !== undefined) !== (car === player)) continue;

        const ds = b.s - car.s, dx = b.x - car.x;
        if (Math.abs(ds) > car.len / 2 + 1 || Math.abs(dx) > car.len / 2 + 1) continue;
        const lat = dx * Math.cos(car.yaw) - ds * Math.sin(car.yaw);
        const lon = dx * Math.sin(car.yaw) + ds * Math.cos(car.yaw);
        const height = car.len > 6 ? 2.3 : 1.45;
        if (Math.abs(lat) > car.wid / 2 + 0.25 || Math.abs(lon) > car.len / 2 + 0.25 || b.y > height + 0.15) continue;
        events.push({
          // Head-on: the car's speed plus the bird's.
          type: 'bird', strength: Math.hypot(car.vx - b.sway, car.vs + b.speed), x: b.x, s: b.s, y: b.y,
          car: car.id, vx: car.vx, vs: car.vs,
        });
        b.y = -1; // gone
        break;
      }
    }
    // Gone: hit something, or flown off behind the player.
    this.birds = this.birds.filter((b) => b.y >= 0 && b.s > player.s - 200 && b.s < player.s + 1200);
  }

  /** A flock coming down the road, timed to meet the player. */
  private spawn(player: Car, events: SimEvent[]): void {
    const v = Math.max(0, player.vs);
    const count = 20 + Math.floor(Math.random() * 16);
    // Seconds until the flock reaches the player: long enough that it starts about 100 m from
    // its dip, ~25 m up, so you see it coming down the road before it drops onto the traffic.
    const meetIn = 6 + Math.random() * 2;
    const meetS = player.s + v * meetIn;
    for (let i = 0; i < count; i++) {
      const speed = 12 + Math.random() * 6;
      const arrive = meetIn + (Math.random() - 0.5) * 1.6; // a loose flock, not a wall
      const b: Bird = {
        id: nextBird++,
        x: (Math.random() * 2 - 1) * LANES_HALF,
        // Placed so that, flying at `speed`, it reaches the player's future position on time.
        s: player.s + (v + speed) * arrive,
        y: 0, speed, sway: (Math.random() - 0.5) * 3,
        low: 0.5 + Math.random() * 1.3, dipS: meetS + (Math.random() - 0.3) * 40, flap: Math.random() * 6,
      };
      b.y = birdHeight(b);
      this.birds.push(b);
    }
    // One to three of them are headed straight for the patrol car's windscreen.
    const aimed = 1 + Math.floor(Math.random() * 3);
    for (const b of this.birds.slice(-count).sort(() => Math.random() - 0.5).slice(0, aimed)) {
      b.aim = (Math.random() - 0.5) * 1.1;
    }
    events.push({ type: 'birds', strength: count, x: 0, s: meetS });
  }
}