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.
// Roadside signs: a schedule that is a pure function of s (and the road seed), so every sign
// is where it was when you drive past it again or watch the replay.
import { MERGE_LEN, MERGE_TAPER, RAMP_CURVE, RAMP_EVERY, rampAt } from './sim.js';
import { SEG, curvature, grade, roadSeed } from './road.js';
import { town } from './towns.js';
import { TOWN_EVERY, closureOf, isCity, townCentre, townSpan, zoneAt, type Zone } from './zones.js';
export type SignKind = 'welcome' | 'tagline' | 'leave' | 'curve' | 'chevron' | 'hill' | 'speed' | 'mile' | 'guide' | 'warn' | 'work';
export interface Sign {
key: string; // unique per sign; also identifies its artwork
kind: SignKind;
s: number;
side: 1 | -1; // +1 right of the road, -1 left (the median side)
lines: string[];
dir?: 1 | -1; // curve arrows and chevrons: which way the road bends
}
const MILE = 1609;
const SPEED_EVERY = 3000;
export const BRIDGE_OFFSET = 920; // overpasses sit between ramp zones, once per ramp period
const SHARP = 1 / 1300;
const RAMP_LEN = RAMP_CURVE + 260;
export const mileAt = (s: number): number => 74 + Math.floor(s / MILE);
export { townCentre };
/** The town at s, or the next one ahead. */
export function townNear(s: number): { j: number; inside: boolean } {
let j = Math.max(0, Math.round((s - 1500) / TOWN_EVERY));
while (j > 0 && townSpan(j - 1).end > s) j--;
while (townSpan(j).end < s) j++;
return { j, inside: s > townSpan(j).start };
}
/** Name as signposted: cities get "City" on the end. */
export function placeName(j: number, seed: number): string {
const name = town(j, seed).name.toUpperCase();
return isCity(j) ? `${name} CITY` : name;
}
/** Right-side signs move across to the median where the slip road would be in the way. */
function sideAt(s: number): 1 | -1 {
const d = s - rampAt(s);
return d > -RAMP_LEN - 30 && d < MERGE_LEN + MERGE_TAPER + 10 ? -1 : 1;
}
export function signsIn(from: number, to: number): Sign[] {
const out: Sign[] = [];
const seed = roadSeed();
const add = (sign: Omit<Sign, 'side'> & { side?: 1 | -1 }) => {
if (sign.s >= from && sign.s < to) out.push({ side: sideAt(sign.s), ...sign } as Sign);
};
// Towns and cities: welcome, a boast, a lower limit, and a goodbye.
for (let j = Math.max(0, Math.floor((from - 3200) / TOWN_EVERY)); townSpan(j).start - 100 < to; j++) {
const t = town(j, seed);
const { start, end } = townSpan(j);
const name = placeName(j, seed);
const pop = isCity(j) ? `${t.pop},000` : t.pop;
add({ key: `w${j}`, kind: 'welcome', s: start, lines: ['WELCOME TO', name, `POP. ${pop}`] });
add({ key: `t${j}`, kind: 'tagline', s: start + 300, lines: [name, t.tagline] });
add({ key: `p${j}t`, kind: 'speed', s: start + 150, lines: ['SPEED', 'LIMIT', isCity(j) ? '50' : '55'] });
add({ key: `l${j}`, kind: 'leave', s: end, lines: ['NOW LEAVING', name, 'COME BACK SOON!'] });
}
// The country between towns: a warning for each kind, and road works signed in full.
for (let j = Math.max(0, Math.floor((from - 3200) / TOWN_EVERY)); townSpan(j).end < to + 600; j++) {
for (const z of [zoneAt(townSpan(j).end + 1), zoneAt(townSpan(j + 1).start - 1)]) areaSigns(z, add);
}
// Bends: a warning before each one, then chevrons round the outside.
for (let k = Math.floor(from / SEG) - 1; k * SEG < to + 300; k++) {
const kappa = curvature(k * SEG + SEG - 1), before = curvature(k * SEG - 1);
if (Math.abs(kappa) < SHARP || Math.sign(kappa) === Math.sign(before) && Math.abs(before) >= SHARP) continue;
const dir = kappa > 0 ? 1 : -1;
add({ key: `c${k}`, kind: 'curve', s: k * SEG - 200, lines: [], dir });
for (let s = k * SEG + 80; s < (k + 1) * SEG + 80; s += 55) {
add({ key: `v${Math.round(s)}`, kind: 'chevron', s, side: dir > 0 ? -1 : 1, lines: [], dir });
}
}
// Hills.
for (let k = Math.floor(from / SEG); k * SEG < to + 300; k++) {
const g = grade(k * SEG + SEG - 1), before = grade(k * SEG - 1);
if (Math.abs(g) < 0.035 || Math.abs(before) >= 0.035 && Math.sign(before) === Math.sign(g)) continue;
const pct = `${Math.round(Math.abs(g) * 100)}%`;
add({
key: `h${k}`, kind: 'hill', s: k * SEG - 150,
lines: g < 0 ? ['STEEP GRADE', pct, 'TRUCKS USE LOW GEAR'] : ['HILL', pct, 'NO PASSING ZONE (IGNORED)'],
});
}
for (let n = Math.ceil(from / SPEED_EVERY); n * SPEED_EVERY + 150 < to; n++) {
if (n > 0) add({ key: `p${n}`, kind: 'speed', s: n * SPEED_EVERY + 150, lines: ['SPEED', 'LIMIT', '65'] });
}
for (let n = Math.ceil(from / MILE); n * MILE < to; n++) {
add({ key: `m${n}`, kind: 'mile', s: n * MILE, lines: ['MILE', String(74 + n)] });
}
// Overhead signs on each overpass: distances to the next two towns.
for (let b = Math.floor((from - BRIDGE_OFFSET) / RAMP_EVERY); b * RAMP_EVERY + BRIDGE_OFFSET < to; b++) {
const s = b * RAMP_EVERY + BRIDGE_OFFSET - 6.2;
const { j } = townNear(s);
const next = townCentre(j) - 600 > s ? j : j + 1;
const lines: string[] = [];
for (const n of [next, next + 1]) {
const miles = Math.max(1, Math.round((townCentre(n) - s) / MILE));
lines.push(`${placeName(n, seed)}|${miles}`);
}
add({ key: `g${b}`, kind: 'guide', s, side: 1, lines });
}
return out;
}
type Add = (sign: Omit<Sign, 'side'> & { side?: 1 | -1 }) => void;
/** Signs for one area between towns. */
function areaSigns(z: Zone, add: Add): void {
const k = Math.round(z.start);
if (z.area === 'forest') add({ key: `a${k}`, kind: 'warn', s: z.start + 80, lines: ['DEER', 'CROSSING', 'NEXT 2 MILES'] });
if (z.area === 'mountains') {
add({ key: `a${k}`, kind: 'warn', s: z.start + 260, lines: ['WATCH FOR', 'FALLING ROCKS', ''] });
if (closureOf(z)) {
add({ key: `n${k}`, kind: 'warn', s: z.start - 400, lines: ['ROAD', 'NARROWS', '3 LANES'] });
add({ key: `o${k}`, kind: 'warn', s: z.start - 150, side: 1, lines: ['LEFT LANES', 'END', 'MERGE RIGHT'] });
add({ key: `q${k}`, kind: 'speed', s: z.start + 120, lines: ['SPEED', 'LIMIT', '50'] });
}
}
if (z.area !== 'construction') return;
const c = closureOf(z);
add({ key: `a${k}`, kind: 'work', s: z.start - 350, lines: ['ROAD', 'WORK', 'AHEAD'] });
if (c) {
add({ key: `b${k}`, kind: 'work', s: z.start - 100, lines: ['LEFT', c.lanes > 1 ? 'TWO LANES' : 'LANE', 'CLOSED AHEAD'] });
add({ key: `d${k}`, kind: 'work', s: c.taperIn - 60, side: 1, lines: ['MERGE', 'RIGHT', '→'] });
}
add({ key: `e${k}`, kind: 'speed', s: z.start + 60, lines: ['SPEED', 'LIMIT', '45'] });
add({ key: `f${k}`, kind: 'work', s: z.start + 90, lines: ['FINES', 'DOUBLED', 'IN WORK ZONES'] });
add({ key: `g${k}`, kind: 'work', s: z.end + 40, lines: ['END', 'ROAD WORK', ''] });
}
