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.
// 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 });
}
}
