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.
// The areas the highway runs through, as a pure function of s and the road seed (like the
// bends), so the sim, the signs and the renderer agree and a replay sees the same land.
//
// Towns sit about every TOWN_EVERY metres; about one in four is a city, which runs longer.
// Each stretch between two towns is split into two different areas: open countryside,
// forest, mountains or road works. Road works close one or two of the left lanes behind a
// taper of barrels, and through the mountains the highway narrows to its three right lanes
// (the on-ramps join on the right, so a closure never meets one).
import { hash01, roadSeed } from './road.js';
export type Area = 'countryside' | 'forest' | 'mountains' | 'construction' | 'town' | 'city';
export const AREAS: Area[] = ['countryside', 'forest', 'mountains', 'construction', 'town', 'city'];
export interface Zone {
area: Area;
start: number;
end: number;
town?: number; // town index, for town and city zones
}
export const TOWN_EVERY = 3800;
const FIRST_TOWN = 1500;
const GAP_AREAS: [Area, number][] = [['countryside', 3], ['forest', 3], ['mountains', 2.5], ['construction', 1.5]];
/** Metres over which one area's look fades into the next. */
export const BLEND = 150;
const LANE_W = 3.7, LANES = 6;
const EDGE = (LANES * LANE_W) / 2;
const ROAD_HALF = EDGE + 1.3;
/** Centre of town j. */
export function townCentre(j: number): number {
return FIRST_TOWN + j * TOWN_EVERY + (hash01(j, 40) - 0.5) * 600;
}
/** About one town in four is a city (never the first). */
export function isCity(j: number): boolean {
return j > 0 && hash01(j, 41) < 0.25;
}
/** From the welcome sign to the "now leaving" sign. */
export function townSpan(j: number): { start: number; end: number } {
const c = townCentre(j);
return isCity(j) ? { start: c - 1000, end: c + 1400 } : { start: c - 600, end: c + 900 };
}
function pickGap(j: number, salt: number, not?: Area): Area {
const options = GAP_AREAS.filter(([a]) => a !== not);
let r = hash01(j, salt) * options.reduce((n, [, w]) => n + w, 0);
for (const [a, w] of options) if ((r -= w) < 0) return a;
return options[0][0];
}
/** The two areas between town j and town j + 1, and where one gives way to the other. */
function gapZones(j: number): [Zone, Zone] {
const from = townSpan(j).end;
const to = townSpan(j + 1).start;
const a = pickGap(j, 43);
const b = pickGap(j, 44, a);
// Mountains and road works take the bigger share of the gap, so they last a bit longer.
const share = (area: Area) => (area === 'mountains' || area === 'construction' ? 1.6 : 1);
const split = from + (to - from) * (share(a) / (share(a) + share(b)) + 0.1 * (hash01(j, 42) - 0.5));
return [{ area: a, start: from, end: split }, { area: b, start: split, end: to }];
}
// Zones are hundreds of metres long and asked about constantly (every car, every step), so
// the last few found are kept and checked first.
const recent: Zone[] = [];
let recentSeed = NaN;
/** The zone containing s. */
export function zoneAt(s: number): Zone {
if (recentSeed !== roadSeed()) {
recent.length = 0;
recentSeed = roadSeed();
}
for (const z of recent) if (s >= z.start && s < z.end) return z;
const z = findZone(s);
recent.push(z);
if (recent.length > 24) recent.shift();
return z;
}
function findZone(s: number): Zone {
let j = Math.max(-1, Math.floor((s - FIRST_TOWN) / TOWN_EVERY));
// Step to the town whose span or following gap holds s.
while (j >= 0 && s < townSpan(j).start) j--;
while (s >= townSpan(j + 1).start) j++;
if (j >= 0 && s < townSpan(j).end) return { area: isCity(j) ? 'city' : 'town', ...townSpan(j), town: j };
// Before the first town: open country to start the drive.
if (j < 0) return { area: 'countryside', start: -1e9, end: townSpan(0).start };
const [a, b] = gapZones(j);
return s < a.end ? a : b;
}
/** How much each area shapes the look at s: 1 for the zone's own, blended near its edges. */
export function areaWeights(s: number): Record<Area, number> {
const w = { countryside: 0, forest: 0, mountains: 0, construction: 0, town: 0, city: 0 };
const z = zoneAt(s);
const toStart = s - z.start, toEnd = z.end - s;
if (toStart < BLEND / 2) {
const k = 0.5 + toStart / BLEND;
w[z.area] += k;
w[zoneAt(z.start - 1).area] += 1 - k;
} else if (toEnd < BLEND / 2) {
const k = 0.5 + toEnd / BLEND;
w[z.area] += k;
w[zoneAt(z.end + 1).area] += 1 - k;
} else {
w[z.area] = 1;
}
return w;
}
/** Traffic in each area, relative to the chosen density. */
const TRAFFIC: Record<Area, number> = { city: 1.15, town: 1, countryside: 1, construction: 0.9, forest: 0.7, mountains: 0.35 };
export function trafficAt(s: number): number {
return TRAFFIC[zoneAt(s).area];
}
// ---- Road works
export interface Closure {
kind: 'works' | 'narrows'; // road works behind barrels, or the mountain road narrowing to three lanes
lanes: number; // left lanes closed (1 or 2 at road works, 3 in the mountains)
taperIn: number; // barrels start angling across here...
closed: number; // ...and the lanes are fully shut from here
open: number; // the barrier angles back out from here...
end: number; // ...and the road is clear again here
}
/**
* The lane closure in a zone: road works (short ones only line the shoulder with barrels),
* or the mountains, where the highway narrows to its three right lanes.
*/
export function closureOf(z: Zone): Closure | null {
if (z.area === 'mountains' && z.end - z.start >= 500) {
return { kind: 'narrows', lanes: 3, taperIn: z.start, closed: z.start + 220, open: z.end - 220, end: z.end };
}
if (z.area !== 'construction' || z.end - z.start < 700) return null;
return {
kind: 'works',
lanes: hash01(Math.floor(z.start), 45) < 0.5 ? 1 : 2,
taperIn: z.start + 200, closed: z.start + 380, open: z.end - 250, end: z.end - 120,
};
}
/** Where the barrier line sits when `lanes` left lanes are closed. */
export const barrierX = (lanes: number): number => -EDGE + lanes * LANE_W - 0.3;
/** The left edge of the drivable road: the guardrail, or the road-works barrier line. */
export function leftWall(s: number): number {
const c = closureOf(zoneAt(s));
if (!c || s < c.taperIn || s > c.end) return -ROAD_HALF;
const x = barrierX(c.lanes);
if (s < c.closed) return -ROAD_HALF + (x + ROAD_HALF) * ((s - c.taperIn) / (c.closed - c.taperIn));
if (s > c.open) return x + (-ROAD_HALF - x) * ((s - c.open) / (c.end - c.open));
return x;
}
/** Left lanes closed at s (from the start of the taper to the end of the closure). */
export function closedLanes(s: number): number {
const c = closureOf(zoneAt(s));
return c && s >= c.taperIn && s <= c.end ? c.lanes : 0;
}
/** Left lanes closed anywhere from s to `ahead` metres on. */
export function closedAhead(s: number, ahead: number): number {
let n = 0;
for (let d = 0; d <= ahead; d += 50) n = Math.max(n, closedLanes(s + d));
return n;
}
