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/radio-script.ts
// Every recorded radio clip: who says it, what they say (as spoken), and the subtitle.
// tools/voices/build.ts renders these with Piper into src/assets/radio-voices.bin; edit this
// file and re-run `bun tools/voices/build.ts` to change what the radio says.
//
// Written the way people actually talk on the radio: short phrases with commas for the
// breaks, the odd "uh", contractions. Numbers always sit inside a whole sentence (never
// stitched in as separate words), so the intonation carries through them.
// `urgent` lines are rendered faster and higher, like someone who's excited.

import { SUSPECT_COLORS } from './colors.js';

export type Speaker = 'DISPATCH' | 'UNIT 14' | 'AIR 1' | 'UNIT 9' | 'SGT 20';

/**
 * Piper voice for each speaker; pitch < 1 lowers and slows it. All five are speakers of the
 * libritts-high model (LibriTTS, CC BY 4.0), chosen by tools/voices/audition.ts as the closest
 * to human speech on intonation, jitter, shimmer and harmonics-to-noise, with no glitches.
 */
export const VOICES: Record<Speaker, { model: string; speaker: number; lengthScale: number; pitch: number }> = {
  DISPATCH: { model: 'en_US-libritts-high', speaker: 558, lengthScale: 1, pitch: 1 },
  'UNIT 14': { model: 'en_US-libritts-high', speaker: 520, lengthScale: 0.95, pitch: 1 },
  'AIR 1': { model: 'en_US-libritts-high', speaker: 407, lengthScale: 0.97, pitch: 1 },
  'UNIT 9': { model: 'en_US-libritts-high', speaker: 803, lengthScale: 1, pitch: 1 },
  'SGT 20': { model: 'en_US-libritts-high', speaker: 388, lengthScale: 1.04, pitch: 0.94 },
};

export interface Clip {
  who: Speaker;
  say: string;
  show?: string; // subtitle, when it differs from what's said
  urgent?: boolean;
}

export const SPEEDS = [60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160];
const SPEED_WORDS = ['sixty', 'seventy', 'eighty', 'ninety', 'a hundred', 'one-ten', 'one-twenty', 'one-thirty', 'one-forty', 'one-fifty', 'one-sixty'];
/** Air 1 only calls out speeds over this. */
export const FAST = 120;

export const CLIPS: Record<string, Clip> = {
  // Dispatch
  'd.start': { who: 'DISPATCH', say: "All units, we've got a pursuit, northbound on I eighty-seven, at mile marker seventy-four.", show: "All units, we've got a pursuit, northbound on I-87, at mile marker 74." },
  'd.copy1': { who: 'DISPATCH', say: 'Copy, fourteen.' },
  'd.copy2': { who: 'DISPATCH', say: 'Ten four.', show: '10-4.' },
  'd.copy3': { who: 'DISPATCH', say: 'Fourteen, copy that.' },
  'd.clear': { who: 'DISPATCH', say: "Copy, fourteen. All units, let's keep the channel clear, emergency traffic only." },
  'd.stolen': { who: 'DISPATCH', say: 'Fourteen, that plate comes back stolen, out of Albany, yesterday. Use caution.', show: 'Fourteen, that plate comes back stolen out of Albany, yesterday. Use caution.' },
  'd.spikes': { who: 'DISPATCH', say: 'Fourteen, highway patrol is setting up spikes, about six miles ahead of you.' },
  'd.pit': { who: 'DISPATCH', say: 'Copy that. Suspect vehicle is disabled. All units, move in, and use caution.' },
  'd.crash': { who: 'DISPATCH', say: 'Copy, the suspect has crashed. I have fire and E M S on the way.', show: 'Copy, the suspect has crashed. I have fire and EMS on the way.' },
  // Bird strikes (free drive)
  'd.birds1': { who: 'DISPATCH', say: "Fourteen, be advised, we're getting reports of a big flock of birds, flying low down the interstate, headed right at you." },
  'd.birds2': { who: 'DISPATCH', say: "All units, heads up. There's a flock of birds coming down the highway toward you, real low. Watch your windshields." },
  // Unit 14 (the player)
  'u.start1': { who: 'UNIT 14', say: "Fourteen, I'm right behind him. Show me primary." },
  'u.start2': { who: 'UNIT 14', say: "Fourteen, yep, I've got him. I'm primary." },
  'u.status1': { who: 'UNIT 14', say: 'Fourteen, still on him, northbound.' },
  'u.status2': { who: 'UNIT 14', say: "Fourteen, uh, still northbound. He's not slowing down." },
  // espeak voices the 's of "traffic's" as z; "traffics" gets the s right.
  'u.status3': { who: 'UNIT 14', say: 'Fourteen, still with him. Traffics moderate.', show: "Fourteen, still with him. Traffic's moderate." },
  'u.bumper1': { who: 'UNIT 14', say: "I'm right on his bumper, he's not stopping." },
  'u.bumper2': { who: 'UNIT 14', say: "I'm on him. He's still not giving it up." },
  'u.losing': { who: 'UNIT 14', say: "He's pulling away, I'm losing him!", urgent: true },
  'u.pit': { who: 'UNIT 14', say: "Pit, pit, pit! He's spinning!", show: "PIT, PIT, PIT! He's spinning!", urgent: true },
  'u.done': { who: 'UNIT 14', say: "Fourteen, he's done, vehicle's disabled! I'm moving in.", urgent: true },
  'u.lost': { who: 'UNIT 14', say: "Fourteen, I lost him. I lost him.", urgent: true },
  'u.birdhit1': { who: 'UNIT 14', say: "Fourteen, uh, I just took a bird, right in the windshield. I'm okay.", urgent: true },
  'u.birdhit2': { who: 'UNIT 14', say: "Ugh. Fourteen, I hit one of those birds. That's, uh, that's gonna need a car wash." },
  // Air 1
  'a.eye.left': { who: 'AIR 1', say: "Air one, we've got the eye. He's weaving through traffic, in the left lanes." },
  'a.eye.middle': { who: 'AIR 1', say: "Air one, we've got the eye. He's weaving through traffic, in the middle lanes." },
  'a.eye.right': { who: 'AIR 1', say: "Air one, we've got the eye. He's weaving through traffic, in the right lanes." },
  'a.moving': { who: 'AIR 1', say: "Fourteen, traffics moving over for you. You've got a hole, right up the middle.", show: "Fourteen, traffic's moving over for you. You've got a hole, right up the middle." },
  'a.gap': { who: 'AIR 1', say: "Air one, we've still got him. He's pulling away from you, fourteen." },
  // "onramp" as one word gets the stress on ON, like people say it.
  'a.ramp': { who: 'AIR 1', say: "Heads up, you've got cars merging from the onramp, just ahead of him.", show: "Heads up, you've got cars merging from the on-ramp, just ahead of him." },
  'a.fire': { who: 'AIR 1', say: "Whoa, he just hit the rail! The car's on fire! Get fire and E M S rolling!", show: "Whoa, he just hit the rail! The car's on fire! Get fire and EMS rolling!", urgent: true },
  'a.crash': { who: 'AIR 1', say: "He lost it! He's crashed out! Units, move in.", urgent: true },
  // espeak stresses "overpass" on the last syllable whatever the spelling, so: bridge.
  'a.lost': { who: 'AIR 1', say: 'Yep, we lost him too, under the bridge. Last seen northbound.' },
  'a.birds': { who: 'AIR 1', say: "Fourteen, heads up! Big flock of birds, diving onto the road, right in front of you!", urgent: true },
  // Unit 9 (backup)
  'n.backup2': { who: 'UNIT 9', say: "Nine, I'm about two miles back, coming up code three." },
  'n.backup3': { who: 'UNIT 9', say: "Unit nine, I'm about three miles back. Coming up code three." },
  'n.backup4': { who: 'UNIT 9', say: "Nine, I'm about four miles out, coming up code three." },
  // Sergeant
  's.pitok': { who: 'SGT 20', say: 'Fourteen, sergeant. You are cleared to pit, when traffic allows.', show: 'Fourteen, sergeant. You are cleared to PIT when traffic allows.' },
};

// Speeds, each said as part of a whole sentence.
SPEEDS.forEach((mph, i) => {
  CLIPS[`u.speed${mph}`] = { who: 'UNIT 14', say: `We're doing about ${SPEED_WORDS[i]}.`, show: `We're doing about ${mph}.` };
  if (mph >= FAST) {
    CLIPS[`a.fast${mph}`] = { who: 'AIR 1', say: `He's doing over ${SPEED_WORDS[i]} now, driving really reckless.`, show: `He's doing over ${mph} now, driving really reckless.` };
  }
});

// The suspect's description, one whole sentence per paint colour (colors.ts), so the colour
// carries the sentence's intonation instead of being stitched in.
for (const { name } of SUSPECT_COLORS) {
  const a = /^[aeiou]/.test(name) ? 'an' : 'a';
  CLIPS[`d.start2.${name}`] = { who: 'DISPATCH', say: `The vehicle is ${a} ${name} sedan, and the driver's refusing to stop.` };
  CLIPS[`d.escape.${name}`] = { who: 'DISPATCH', say: `Copy. All units, go ahead and terminate. Be on the lookout for ${a} ${name} sedan, last seen northbound.` };
}

// Air 1 calling in a civilian crash at the patrol car's mile marker:
//   "Air one, a civilian has crashed at" + "mile marker seventy-four." + "Requesting EMS and fire."
// and, if the car is burning, "...The car is burning, we need EMS and fire, immediately!"
// Each mile marker is its own phrase, said the radio way (101 is "one-oh-one", 123 is
// "one-twenty-three"). Past the last one Air 1 says "near your location" instead.
export const MILES = { first: 74, last: 149 };
const ONES = ['oh', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve',
  'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'];
const TENS = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'];
const twoDigits = (n: number) => (n < 20 ? ONES[n] : TENS[Math.floor(n / 10)] + (n % 10 ? '-' + ONES[n % 10] : ''));
const mileWords = (n: number) => (n < 100 ? twoDigits(n) : n === 100 ? 'one hundred' : n < 110 ? `one oh ${ONES[n % 10]}` : `one ${twoDigits(n % 100)}`);
CLIPS['a.civ'] = { who: 'AIR 1', say: 'Air one, a civilian has crashed at' };
CLIPS['a.civ.near'] = { who: 'AIR 1', say: 'Air one, a civilian has crashed near your location.' };
CLIPS['a.civ.ems'] = { who: 'AIR 1', say: 'Requesting E M S and fire.', show: 'Requesting EMS and fire.' };
CLIPS['a.civ.burning'] = { who: 'AIR 1', say: 'The car is burning, we need E M S and fire, immediately!', show: 'The car is burning, we need EMS and fire, immediately!', urgent: true };
for (let n = MILES.first; n <= MILES.last; n++) {
  CLIPS[`a.mile${n}`] = { who: 'AIR 1', say: `mile marker ${mileWords(n)}.`, show: `mile marker ${n}.` };
}