import type { EnvironmentChapter } from '@/journey/types'; /** * Night traffic as a small deterministic car-following simulation (no * three.js here, so it is unit-testable and allocation free). * * A three-lane expressway: the reader drives the CENTRE lane, LEFT is the fast * lane, RIGHT is freight and slow cars. Every vehicle runs IDM-lite against * the nearest vehicle ahead whose lateral span (including the rest of a lane * change) overlaps its own. The hero car is a virtual vehicle in CENTRE that * nothing can touch: the only lane changes are right-lane cars borrowing the * centre lane far ahead to pass a slower vehicle, and they leave it long * before the reader reaches them (time to reach, a hard deadline, a flash). * * Coordinates are the hero frame: z grows toward the camera, a vehicle's * origin is its rear face at road level, its body runs forward to z - length. */ export const TRAFFIC_LANE_LEFT = 0; export const TRAFFIC_LANE_CENTRE = 1; export const TRAFFIC_LANE_RIGHT = 2; /** Lane centres (world x). The hero drives CENTRE. */ export const TRAFFIC_LANE_X: readonly number[] = Object.freeze([-5.4, 0, 5.4]); /** Max lateral offset of a vehicle inside its lane (seeded per entry). */ export const TRAFFIC_LANE_JITTER = 0.1; /** The asphalt's half width (road.ts). */ export const TRAFFIC_ASPHALT_HALF_WIDTH = 7.78; export const TRAFFIC_CAR = 0; export const TRAFFIC_TRUCK = 1; export interface TrafficKindSize { /** Render scale over the 1x body (traffic.ts). */ readonly scale: number; readonly halfWidth: number; readonly length: number; readonly roof: number; } /** Cars 1.7x, trucks 1.4x the 1x bodies, so they read beside the 2.78x hero. */ export const TRAFFIC_KINDS: readonly TrafficKindSize[] = Object.freeze([ Object.freeze({ scale: 1.7, halfWidth: 1.87, length: 8.16, roof: 2.72 }), Object.freeze({ scale: 1.4, halfWidth: 1.75, length: 21, roof: 5.67 }), ]); /** The hero car's envelope in the model (x +-2.6, nose -6, tail 10). */ export const HERO_HALF_WIDTH = 2.6; export const HERO_NOSE_Z = -6; export const HERO_TAIL_Z = 10; /** A vehicle's rear beyond this (pulling away) leaves into the haze. */ export const TRAFFIC_FAR_Z = -660; /** Slower-than-hero vehicles enter here, inside the far fog. */ export const TRAFFIC_ENTRY_FAR_Z = -640; /** * When a lane's far stretch is empty (after a scroll has streamed everything * past), a slower vehicle may instead screen-door in at the edge of the haze, * so the road ahead fills again in seconds rather than the minute a vehicle * at 8 u/s closing takes from -640. Entry z in [NEAR - SPAN, NEAR]; only when * nothing of that lane is in the band around it. */ export const TRAFFIC_ENTRY_NEAR_Z = -420; export const TRAFFIC_ENTRY_NEAR_SPAN = 90; export const TRAFFIC_ENTRY_NEAR_BAND_FAR = -560; export const TRAFFIC_ENTRY_NEAR_BAND_NEAR = -270; export const FADE_IN_SECONDS = 0.6; /** * A right car entering from the haze may join a truck's queue instead: the * pair then drifts into the overtake band together, so a pass is on screen * even when the pool is small (lite) and cars would not catch a truck in * time on their own. The truck's rear z must lie in this window (the car * fades in behind it, still in the haze) with no follower within the gap. */ export const TRAFFIC_CONVOY_FAR_Z = -600; export const TRAFFIC_CONVOY_NEAR_Z = -450; const CONVOY_FOLLOWER_GAP = 150; const CONVOY_GAP_MIN = 30; const CONVOY_GAP_SPAN = 20; /** The farthest a queued car's rear sits behind its truck's rear, past its own length (m). */ export const TRAFFIC_CONVOY_GAP_MAX = CONVOY_GAP_MIN + CONVOY_GAP_SPAN; /** * Lite (6-vehicle pool, phones): the convoy window is nearer, so a queued * pair reaches the overtake band in ~10-17 s at cruise instead of ~25 s. */ export const TRAFFIC_LITE_CONVOY_FAR_Z = -420; export const TRAFFIC_LITE_CONVOY_NEAR_Z = -330; /** * Lite: a pass is staged at each chapter entry (TrafficInput.cue) for up to * TRAFFIC_LITE_STAGE_SECONDS, and whenever the road has gone this long with * no lane change: a truck screen-doors in at the haze (if none is coming) * and one right car more than the chapter wants may queue behind it. The * first lane change ends the staging. */ export const TRAFFIC_LITE_DROUGHT_SECONDS = 10; export const TRAFFIC_LITE_STAGE_SECONDS = 20; /** Where a staged truck's rear enters: [Z, Z + SPAN], inside the lite convoy window. */ export const TRAFFIC_LITE_STAGE_Z = -410; export const TRAFFIC_LITE_STAGE_SPAN = 40; /** Lite staging: a right-lane truck further out than the window by this much does not count as on its way. */ export const TRAFFIC_LITE_STAGE_REACH = 140; /** Lite staging: a truck on its way counts only with a passer that will be this close behind it... */ const TRAFFIC_LITE_STAGE_FOLLOW = 60; /** ...by the time it is this far into the entry band. */ const TRAFFIC_LITE_STAGE_BAND = 30; /** Lite staging ahead of a car already out there: no nearer than this (rear z; it screen-doors in). */ export const TRAFFIC_LITE_STAGE_NEAR_Z = -260; /** * A vehicle whose nose has passed this z has gone beneath the chase camera * (z 23-26, y >= 5.6) and out of every chapter's frame; it is still simulated * (state BEHIND) so a car just overtaken can come back past the reader. */ export const TRAFFIC_EXIT_NOSE_Z = 20; /** Behind the camera this far (rear z), a vehicle is forgotten. */ export const TRAFFIC_BEHIND_Z = 150; /** Near-fade limits: never (cars, most trucks) or a camera-side truck's band. */ export const TRAFFIC_NEAR_OPEN = 64; export const TRAFFIC_NEAR_CAMERA_SIDE = -30; /** A truck's near limit is decided once, as its rear crosses this z. */ export const TRAFFIC_LATCH_Z = -60; /** IDM-lite (SI units: m, m/s). */ export const TRAFFIC_IDM = Object.freeze({ A: 2.2, B: 3.0, S0: 6, T: 1.3, vMin: 4, minGap: 2, }); /** * Base desired speeds; each vehicle is +-8 % around its own. The reader cruises * at 18 + 0.18 * 54 = 27.7 u/s: fast cars pass on the left at ~12 u/s, the * right lane is overtaken at 8-15 u/s. */ export const TRAFFIC_SPEEDS = Object.freeze({ leftCar: 40, rightCar: 20, truck: 13, }); /** * A car borrowing the centre lane to pass drives this much over its own pace: * about the reader's cruise, so it holds station ahead while the truck drops * back, and the whole pass stays where it started. */ const OVERTAKE_BOOST = 8; /** ...and this much when the reader closes in before it can pull back in. */ const SPRINT_BOOST = 12; export const LANE_CHANGE_SECONDS = 1.4; /** * A yield the reader forces (closing fast, a flash, the deadline) is a swerve: * the corridor (|x| >= 4.47 of the 5.4 m change, smoothstep 0.71) is clear * 0.57 s in. With one 0.1 s frame of slack and the worst closing speed * (120 - 4 u/s) that is 78 + 12 m, so a car that leaves at the deadline is * clear of the corridor by -10, ahead of the nose at -6. */ export const FORCED_LANE_CHANGE_SECONDS = 0.8; export const ENTRY_LEAD_SECONDS = 0.6; export const YIELD_LEAD_SECONDS = 0.35; /** * A right-lane car enters the centre only this far ahead (rear z): 160 m * from the nose, where a car is ~15 px tall at 1440x900. It drifts ~40 m * toward the reader while it accelerates out of the queue, then holds * station, so the pass and the return happen 110-150 m ahead. */ export const CENTRE_ENTRY_Z = -150; /** ...and not so far that it is lost in the haze. */ const CENTRE_ENTRY_FAR_Z = -210; /** Still in the corridor past this z: it must leave now, or dither out. */ export const CENTRE_DEADLINE_Z = -100; /** A flash reaches a centre-lane vehicle this far ahead. */ export const FLASH_REACH_Z = -320; /** A centre vehicle the reader would reach within this many seconds yields. */ export const TIME_TO_REACH_SECONDS = 3; /** Urgent yields skip the indicator lead. */ const URGENT_SECONDS = 2; /** A centre car starts looking for its way out this early (time to reach, rear z). */ const YIELD_SOON_SECONDS = 6; export const YIELD_SOON_Z = -112; /** Past this a yield skips its indicator lead and goes at once. */ const YIELD_NOW_Z = CENTRE_DEADLINE_Z - 6; /** Headway fraction a forced yield accepts. */ const FORCED_HEADWAY = 0.35; export const MANOEUVRE_GAP_SECONDS = 4; /** Entry only when the reader is near cruise (closing speed below this). */ const ENTRY_MAX_CLOSING = 12; /** The slow leader must be within this gap and this much slower than the car wants. */ const ENTRY_LEADER_GAP = 90; const ENTRY_LEADER_DELTA = 4; /** Indicators: 1.5 Hz, 40 % duty, 80 ms edges (photosensitivity: <= 3 Hz). */ export const INDICATOR_HZ = 1.5; export const INDICATOR_DUTY = 0.4; const INDICATOR_EDGE = 0.08; /** Brake lamps: hysteresis on the IDM deceleration, minimum on time and spacing. */ export const BRAKE_ON_ACCEL = -1.2; export const BRAKE_OFF_ACCEL = -0.4; export const BRAKE_MIN_ON_SECONDS = 0.5; export const BRAKE_MIN_SPACING_SECONDS = 0.7; export const BRAKE_GLOBAL_PER_SECOND = 3; const BRAKE_RISE_SECONDS = 0.15; const BRAKE_FALL_SECONDS = 0.3; export const DITHER_SECONDS = 0.25; const SPAWN_GAP_SECONDS = 1.5; /** * The rising edge of a flash to pass, from the beam's intensity (0..1): * fires once as it climbs past FLASH_ON after resting or falling below * FLASH_OFF. The intro sweep climbs 0 -> 1 without ever falling, so it never * arms the detector; the descent to the dipped level after it does. */ export const FLASH_ON = 0.95; export const FLASH_OFF = 0.85; export function createFlashDetector(): (intensity: number) => boolean { let armed = false; let previous = Number.NEGATIVE_INFINITY; return (intensity: number): boolean => { let fired = false; if (intensity < FLASH_OFF) { if (intensity <= previous) armed = true; } else if (intensity > FLASH_ON && armed) { armed = false; fired = true; } previous = intensity; return fired; }; } /** Frame states. */ export const TRAFFIC_PENDING = 0; export const TRAFFIC_VISIBLE = 1; export const TRAFFIC_DITHERING = 2; export const TRAFFIC_BEHIND = 3; export interface TrafficDensity { readonly left: number; readonly right: number; /** Of the right lane, how many are trucks. */ readonly trucks: number; } /** Visible vehicles each chapter wants per lane. */ export const TRAFFIC_DENSITY: Readonly> = Object.freeze({ 'deep-night': Object.freeze({ left: 2, right: 2, trucks: 1 }), systems: Object.freeze({ left: 3, right: 3, trucks: 1 }), infrastructure: Object.freeze({ left: 3, right: 5, trucks: 3 }), builds: Object.freeze({ left: 3, right: 4, trucks: 2 }), 'pre-dawn': Object.freeze({ left: 2, right: 2, trucks: 0 }), destination: Object.freeze({ left: 1, right: 1, trucks: 0 }), }); /** Low tier: at most 2 / 4, one truck at a time (two in the pool). */ export const TRAFFIC_LITE_CAP: TrafficDensity = Object.freeze({ left: 2, right: 4, trucks: 1, }); export interface TrafficConfig { pool: 6 | 10 | 12; seed: number; lite: boolean; } export interface TrafficInput { /** Seconds; clamped to [0, 0.1]. */ dt: number; /** The reader's road speed, u/s. */ heroSpeed: number; chapter: EnvironmentChapter; /** A navigator jump or a fling: no manoeuvre starts. */ sweeping: boolean; /** One frame: the reader flashed the high beam. */ flash: boolean; /** The damped chapter camera x (camera-side trucks keep a near fade). */ cameraX: number; /** 0..1, parked at the destination. */ engineOff: number; /** One frame: the reader arrived in a new chapter (lite stages a pass). */ cue?: boolean; } export interface TrafficFrame { readonly count: number; /** 0 car, 1 truck (static). */ readonly kind: Uint8Array; /** 0 pending, 1 visible, 2 dithering out, 3 behind the camera (not drawn). */ readonly active: Uint8Array; readonly x: Float32Array; readonly z: Float32Array; readonly yaw: Float32Array; /** Own road speed, u/s. */ readonly speed: Float32Array; /** 0..1, ramped. */ readonly brake: Float32Array; /** -1 left, 0 off, 1 right. */ readonly indicator: Int8Array; /** 0..1 lamp level of the indicator this frame. */ readonly blink: Float32Array; /** Near-fade limit (TRAFFIC_NEAR_OPEN or TRAFFIC_NEAR_CAMERA_SIDE). */ readonly nearLimit: Float32Array; /** 0..1 screen-door fade of an emergency exit. */ readonly dither: Float32Array; } export interface TrafficEvents { /** Vehicles crossing the hero's middle either way. */ passes: number; emergencyExits: number; brakeOnsets: number; laneChanges: number; /** Seconds of model time at the last emergency exit. */ lastEmergencyAt: number; } export interface TrafficModel { step(input: TrafficInput): TrafficFrame; readonly frame: TrafficFrame; events(): TrafficEvents; reset(): void; } /** How many trucks a pool carries. */ export function trafficTruckCount(pool: number): number { return pool <= 6 ? 2 : 3; } const NO_PLAN = -1; const HERO = -2; const clamp = (value: number, min: number, max: number): number => (value < min ? min : value > max ? max : value); const smooth = (from: number, to: number, value: number): number => { const t = clamp((value - from) / (to - from), 0, 1); return t * t * (3 - 2 * t); }; /** Indicator lamp level `seconds` after it was switched on (on at once). */ export function indicatorBlink(seconds: number): number { const period = 1 / INDICATOR_HZ; const on = period * INDICATOR_DUTY; const t = seconds - Math.floor(seconds / period) * period; return smooth(0, INDICATOR_EDGE, t) * (1 - smooth(on, on + INDICATOR_EDGE, t)); } export function createTrafficModel(config: TrafficConfig): TrafficModel { const n = config.pool; const trucks = trafficTruckCount(n); const kind = new Uint8Array(n); for (let i = 0; i < n; i += 1) kind[i] = i < trucks ? TRAFFIC_TRUCK : TRAFFIC_CAR; const state = new Uint8Array(n); const home = new Uint8Array(n); const lane = new Uint8Array(n); const plan = new Int8Array(n); const indicator = new Int8Array(n); const brakeOn = new Uint8Array(n); const overtaking = new Int8Array(n); const leader = new Int8Array(n); const order = new Int8Array(n); const x = new Float64Array(n); const z = new Float64Array(n); const prevZ = new Float64Array(n); const v = new Float64Array(n); const v0 = new Float64Array(n); const accel = new Float64Array(n); const fromX = new Float64Array(n); const toX = new Float64Array(n); const laneT = new Float64Array(n); const laneSeconds = new Float64Array(n); const fadeIn = new Float64Array(n); const jitter = new Float64Array(n); const planAt = new Float64Array(n); const brakeSince = new Float64Array(n); const lastOnset = new Float64Array(n); const brakeLevel = new Float64Array(n); const indicatorSince = new Float64Array(n); const near = new Float64Array(n); const dither = new Float64Array(n); const onsets = new Float64Array(BRAKE_GLOBAL_PER_SECOND); const lastSpawn = new Float64Array(3); const frame: TrafficFrame = { count: n, kind, active: new Uint8Array(n), x: new Float32Array(n), z: new Float32Array(n), yaw: new Float32Array(n), speed: new Float32Array(n), brake: new Float32Array(n), indicator: new Int8Array(n), blink: new Float32Array(n), nearLimit: new Float32Array(n), dither: new Float32Array(n), }; const counters: TrafficEvents = { passes: 0, emergencyExits: 0, brakeOnsets: 0, laneChanges: 0, lastEmergencyAt: Number.NEGATIVE_INFINITY, }; let seed = 1; let time = 0; let hero = 18; let cameraX = 0; let engineOff = 0; let chapter: EnvironmentChapter = 'deep-night'; let primed = false; let centre = -1; let sprint = -1; /** The reader flashed the centre car: every later gap check is the forced one. */ let hurried = false; let lastManoeuvre = Number.NEGATIVE_INFINITY; let onsetIndex = 0; /** Lite pass staging: the last lane change, and the end of a chapter-entry cue. */ let lastLaneChange = 0; let stageUntil = Number.NEGATIVE_INFINITY; const random = (): number => { seed = (seed * 16807) % 2147483647; return (seed - 1) / 2147483646; }; const halfWidth = (i: number): number => TRAFFIC_KINDS[kind[i]!]!.halfWidth; const length = (i: number): number => TRAFFIC_KINDS[kind[i]!]!.length; const present = (i: number): boolean => state[i] !== TRAFFIC_PENDING; const laneOf = (value: number): number => value < -2.7 ? TRAFFIC_LANE_LEFT : value > 2.7 ? TRAFFIC_LANE_RIGHT : TRAFFIC_LANE_CENTRE; const inCorridor = (i: number): boolean => Math.abs(x[i]!) < HERO_HALF_WIDTH + halfWidth(i); const lateral = (i: number): boolean => laneT[i]! < 1; /** Where a vehicle's lane change (if any) will take it. */ const targetLane = (i: number): number => (lateral(i) ? laneOf(toX[i]!) : plan[i]! !== NO_PLAN ? plan[i]! : lane[i]!); function reset(): void { seed = (Math.floor(Math.abs(config.seed)) % 2147483646) + 1; time = 0; hero = 18; cameraX = 0; engineOff = 0; chapter = 'deep-night'; primed = false; centre = -1; sprint = -1; hurried = false; lastManoeuvre = Number.NEGATIVE_INFINITY; onsetIndex = 0; lastLaneChange = 0; stageUntil = Number.NEGATIVE_INFINITY; onsets.fill(Number.NEGATIVE_INFINITY); lastSpawn.fill(Number.NEGATIVE_INFINITY); state.fill(TRAFFIC_PENDING); plan.fill(NO_PLAN); indicator.fill(0); brakeOn.fill(0); brakeLevel.fill(0); overtaking.fill(-1); laneT.fill(1); laneSeconds.fill(LANE_CHANGE_SECONDS); fadeIn.fill(0); dither.fill(0); near.fill(TRAFFIC_NEAR_OPEN); lastOnset.fill(Number.NEGATIVE_INFINITY); for (let i = 0; i < n; i += 1) order[i] = i; counters.passes = 0; counters.emergencyExits = 0; counters.brakeOnsets = 0; counters.laneChanges = 0; counters.lastEmergencyAt = Number.NEGATIVE_INFINITY; writeFrame(); } const cameraSide = (laneIndex: number): boolean => Math.abs(cameraX) > 1.5 && Math.sign(cameraX) === Math.sign(TRAFFIC_LANE_X[laneIndex]!); const latch = (i: number): void => { near[i] = kind[i] === TRAFFIC_TRUCK && cameraSide(laneOf(x[i]!)) ? TRAFFIC_NEAR_CAMERA_SIDE : TRAFFIC_NEAR_OPEN; }; /** Swept lateral span of i: where it is and where its lane change ends. */ const spanLo = (i: number): number => Math.min(x[i]!, toX[i]!) - halfWidth(i); const spanHi = (i: number): number => Math.max(x[i]!, toX[i]!) + halfWidth(i); /** The nearest vehicle ahead whose swept span overlaps i's (or the hero). */ function findLeader(i: number): number { const lo = spanLo(i); const hi = spanHi(i); const zi = z[i]!; let best = -1; let bestZ = Number.NEGATIVE_INFINITY; for (let j = 0; j < n; j += 1) { if (j === i || !present(j)) continue; const zj = z[j]!; if (zj >= zi || zj <= bestZ) continue; if (spanHi(j) <= lo || spanLo(j) >= hi) continue; best = j; bestZ = zj; } if (HERO_TAIL_Z < zi && HERO_TAIL_Z > bestZ && hi > -HERO_HALF_WIDTH && lo < HERO_HALF_WIDTH) best = HERO; return best; } /** Room for a body of `len` / `hw` at rear z `zr` and speed `speed`, laterally over [lo, hi]. */ function clearAt(self: number, lo: number, hi: number, zr: number, len: number, speed: number, headway = 1): boolean { for (let j = 0; j < n; j += 1) { if (j === self || !present(j)) continue; if (spanHi(j) <= lo || spanLo(j) >= hi) continue; const zj = z[j]!; if (zj <= zr) { // j ahead: our nose behind its rear by a safe gap. if (zr - len - zj < (TRAFFIC_IDM.S0 + speed * TRAFFIC_IDM.T) * headway) return false; } else if (zj - length(j) - zr < (TRAFFIC_IDM.S0 + v[j]! * TRAFFIC_IDM.T + Math.max(0, v[j]! - speed) * 1.5) * headway) { // j behind: its nose behind our rear by its safe gap, more if it is closing. return false; } } return true; } /** Can vehicle i move into `laneIndex` from where it is now? */ /** * Can vehicle i move into `laneIndex` from where it is now? A yield the * reader forces accepts a tighter gap (a real driver ducking in, braking). */ function slotFree(i: number, laneIndex: number, forced = false): boolean { const target = TRAFFIC_LANE_X[laneIndex]!; const hw = halfWidth(i); const lo = Math.min(x[i]!, target) - hw - TRAFFIC_LANE_JITTER; const hi = Math.max(x[i]!, target) + hw + TRAFFIC_LANE_JITTER; return clearAt(i, lo, hi, z[i]!, length(i), v[i]!, forced ? FORCED_HEADWAY : 1); } /** Is there room to pull back in ahead of the vehicle being passed? */ function returnRoom(i: number, l: number): boolean { const front = z[l]! - length(l); const need = length(i) + 2 * (TRAFFIC_IDM.S0 + v0[i]! * TRAFFIC_IDM.T); for (let j = 0; j < n; j += 1) { if (j === i || j === l || !present(j) || laneOf(x[j]!) !== TRAFFIC_LANE_RIGHT) continue; if (z[j]! < z[l]! && front - z[j]! < need) return false; } return true; } function startLateral(i: number, laneIndex: number, seconds = LANE_CHANGE_SECONDS): void { fromX[i] = x[i]!; toX[i] = TRAFFIC_LANE_X[laneIndex]! + (laneIndex === TRAFFIC_LANE_CENTRE ? 0 : jitter[i]!); laneT[i] = 0; laneSeconds[i] = seconds; plan[i] = NO_PLAN; const direction = Math.sign(toX[i]! - fromX[i]!); if (indicator[i] !== direction) { indicator[i] = direction; indicatorSince[i] = time; } counters.laneChanges += 1; lastLaneChange = time; stageUntil = Number.NEGATIVE_INFINITY; } /** A change already under way finishes at the swerve pace (progress is kept: no jump in x). */ function hurry(i: number): void { if (lateral(i) && laneSeconds[i]! > FORCED_LANE_CHANGE_SECONDS) laneSeconds[i] = FORCED_LANE_CHANGE_SECONDS; } function signal(i: number, laneIndex: number, lead: number): void { plan[i] = laneIndex; planAt[i] = time + lead; const direction = Math.sign(TRAFFIC_LANE_X[laneIndex]! - x[i]!); if (indicator[i] !== direction) { indicator[i] = direction; indicatorSince[i] = time; } } function cancel(i: number): void { plan[i] = NO_PLAN; indicator[i] = 0; overtaking[i] = -1; if (centre === i) { centre = -1; sprint = -1; hurried = false; } } function park(i: number): void { state[i] = TRAFFIC_PENDING; plan[i] = NO_PLAN; indicator[i] = 0; brakeOn[i] = 0; brakeLevel[i] = 0; laneT[i] = 1; dither[i] = 0; fadeIn[i] = 0; overtaking[i] = -1; if (centre === i) { centre = -1; sprint = -1; hurried = false; } } function place(i: number, laneIndex: number, rear: number, fading = false): void { const k = kind[i]!; const base = k === TRAFFIC_TRUCK ? TRAFFIC_SPEEDS.truck : laneIndex === TRAFFIC_LANE_LEFT ? TRAFFIC_SPEEDS.leftCar : TRAFFIC_SPEEDS.rightCar; v0[i] = base * (0.92 + random() * 0.16); v[i] = v0[i]!; accel[i] = 0; jitter[i] = (random() * 2 - 1) * TRAFFIC_LANE_JITTER; home[i] = laneIndex; lane[i] = laneIndex; x[i] = TRAFFIC_LANE_X[laneIndex]! + jitter[i]!; fromX[i] = x[i]!; toX[i] = x[i]!; laneT[i] = 1; z[i] = rear; prevZ[i] = rear; plan[i] = NO_PLAN; indicator[i] = 0; brakeOn[i] = 0; brakeLevel[i] = 0; lastOnset[i] = Number.NEGATIVE_INFINITY; overtaking[i] = -1; dither[i] = 0; fadeIn[i] = fading ? 1 : 0; laneSeconds[i] = LANE_CHANGE_SECONDS; near[i] = TRAFFIC_NEAR_OPEN; state[i] = TRAFFIC_VISIBLE; } /** Lite: a right-lane truck in the convoy window with no car queued behind it yet. */ function convoyOpen(): boolean { for (let t = 0; t < n; t += 1) { if (!present(t) || kind[t] !== TRAFFIC_TRUCK || lane[t] !== TRAFFIC_LANE_RIGHT || lateral(t)) continue; if (z[t]! >= TRAFFIC_LITE_CONVOY_NEAR_Z || z[t]! <= TRAFFIC_LITE_CONVOY_FAR_Z) continue; let followed = false; for (let j = 0; j < n; j += 1) { if (j === t || !present(j) || kind[j] !== TRAFFIC_CAR || laneOf(x[j]!) !== TRAFFIC_LANE_RIGHT) continue; if (z[j]! > z[t]! && z[j]! - length(j) - z[t]! < CONVOY_FOLLOWER_GAP) followed = true; } if (!followed) return true; } return false; } /** * Rear z (< 0) for a car queued behind an unfollowed right-lane truck in * the convoy window; 1 while such a truck is still short of the window * (worth waiting a few seconds for); 0 when there is none. */ function convoyRear(len: number): number { const windowFar = config.lite ? TRAFFIC_LITE_CONVOY_FAR_Z : TRAFFIC_CONVOY_FAR_Z; const windowNear = config.lite ? TRAFFIC_LITE_CONVOY_NEAR_Z : TRAFFIC_CONVOY_NEAR_Z; let soon = 0; for (let t = 0; t < n; t += 1) { if (!present(t) || kind[t] !== TRAFFIC_TRUCK || lane[t] !== TRAFFIC_LANE_RIGHT || lateral(t)) continue; const zt = z[t]!; if (zt >= windowNear) continue; if (zt <= windowFar) { soon = 1; continue; } let followed = false; for (let j = 0; j < n; j += 1) { if (j === t || !present(j) || laneOf(x[j]!) !== TRAFFIC_LANE_RIGHT) continue; // (Lite: a truck behind is no passer; its own pass is already under way.) if (config.lite && kind[j] === TRAFFIC_TRUCK) continue; if (z[j]! > zt && z[j]! - length(j) - zt < CONVOY_FOLLOWER_GAP) { followed = true; break; } } if (followed) continue; return zt + len + CONVOY_GAP_MIN + random() * CONVOY_GAP_SPAN; } return soon; } /** Is any vehicle of this lane in the band around the near entry? */ function laneBandEmpty(laneIndex: number): boolean { for (let j = 0; j < n; j += 1) { if (!present(j) || laneOf(x[j]!) !== laneIndex) continue; if (z[j]! > TRAFFIC_ENTRY_NEAR_BAND_FAR && z[j]! < TRAFFIC_ENTRY_NEAR_BAND_NEAR) return false; } return true; } function target(): TrafficDensity { const wanted = TRAFFIC_DENSITY[chapter]; const parked = chapter === 'destination' && engineOff > 0.5; const cap = config.lite ? TRAFFIC_LITE_CAP : null; const left = parked ? 0 : cap ? Math.min(wanted.left, cap.left) : wanted.left; const right = parked ? 0 : cap ? Math.min(wanted.right, cap.right) : wanted.right; const truckCount = parked ? 0 : Math.min(right, cap ? Math.min(wanted.trucks, cap.trucks) : wanted.trucks); scratchDensity.left = left; scratchDensity.right = right; scratchDensity.trucks = truckCount; return scratchDensity; } const scratchDensity = { left: 0, right: 0, trucks: 0 }; function countVisible(laneIndex: number, k: number): number { let count = 0; for (let i = 0; i < n; i += 1) if (state[i] === TRAFFIC_VISIBLE && home[i] === laneIndex && kind[i] === k) count += 1; return count; } function pendingOf(k: number): number { for (let i = 0; i < n; i += 1) if (state[i] === TRAFFIC_PENDING && kind[i] === k) return i; return -1; } /** One group (lane, kind) spawns at most one vehicle per call. */ /** `queueOnly`: the call carries a staging extra, which may only join a truck's queue. */ function spawn(group: number, laneIndex: number, k: number, want: number, initial: boolean, queueOnly = false): void { if (countVisible(laneIndex, k) >= want) return; if (!initial && time - lastSpawn[group]! < SPAWN_GAP_SECONDS) return; const i = pendingOf(k); if (i < 0) return; const len = TRAFFIC_KINDS[k]!.length; const hw = TRAFFIC_KINDS[k]!.halfWidth; const base = k === TRAFFIC_TRUCK ? TRAFFIC_SPEEDS.truck : laneIndex === TRAFFIC_LANE_LEFT ? TRAFFIC_SPEEDS.leftCar : TRAFFIC_SPEEDS.rightCar; const lx = TRAFFIC_LANE_X[laneIndex]!; const lo = lx - hw - TRAFFIC_LANE_JITTER; const hi = lx + hw + TRAFFIC_LANE_JITTER; let rear: number; if (initial) { // Spread the opening road through the visible span. const seen = countVisible(laneIndex, k); const slots = Math.max(1, want); rear = -90 - ((seen + 0.5) / slots) * 500 - (laneIndex === TRAFFIC_LANE_LEFT ? 40 : 0) - random() * 30 - (k === TRAFFIC_TRUCK ? 15 : 0); } else if (base * 1.08 > hero + 0.5) { if (queueOnly && countVisible(laneIndex, k) >= want - 1) return; rear = TRAFFIC_EXIT_NOSE_Z + len - 0.01; } else if (k === TRAFFIC_CAR && laneIndex === TRAFFIC_LANE_RIGHT && (rear = convoyRear(len)) !== 0) { // A truck is about to reach the window: wait for it (rear > 0)... if (rear > 0) return; // ...or join its queue in the haze: a pass waiting to happen. if (!clearAt(i, lo, hi, rear, len, TRAFFIC_SPEEDS.truck)) return; place(i, laneIndex, rear, true); v[i] = TRAFFIC_SPEEDS.truck; lastSpawn[group] = time; return; } else if (queueOnly && countVisible(laneIndex, k) >= want - 1) { // The staging extra is for the queue only: no ordinary entry on its account. return; } else if (laneBandEmpty(laneIndex)) { // The far stretch is empty: screen-door in at the edge of the haze. rear = TRAFFIC_ENTRY_NEAR_Z - random() * TRAFFIC_ENTRY_NEAR_SPAN; if (!clearAt(i, lo, hi, rear, len, base * 1.08)) return; place(i, laneIndex, rear, true); lastSpawn[group] = time; return; } else { rear = TRAFFIC_ENTRY_FAR_Z; } if (!clearAt(i, lo, hi, rear, len, base * 1.08)) return; place(i, laneIndex, rear); if (!initial && rear > TRAFFIC_LATCH_Z) latch(i); lastSpawn[group] = time; } /** Lite: a pass is wanted (chapter-entry cue armed, or a lane-change drought), and the road is running. */ function staging(): boolean { if (!config.lite || engineOff > 0 || chapter === 'destination') return false; return time < stageUntil || time - lastLaneChange > TRAFFIC_LITE_DROUGHT_SECONDS; } /** Vehicles present in (or bound for) the right lane, ahead of the camera. */ function rightCount(): number { let count = 0; for (let i = 0; i < n; i += 1) if (state[i] === TRAFFIC_VISIBLE && home[i] === TRAFFIC_LANE_RIGHT) count += 1; return count; } /** Lite staging: a truck screen-doors in at the haze when none is on its way to the overtake band. */ function stageTruck(): void { if (time - lastSpawn[1]! < SPAWN_GAP_SECONDS) return; for (let t = 0; t < n; t += 1) { if (!present(t) || kind[t] !== TRAFFIC_TRUCK || laneOf(x[t]!) !== TRAFFIC_LANE_RIGHT) continue; // One already in (or just short of) the convoy window will do; one deep in the haze is too late. if (z[t]! >= CENTRE_ENTRY_Z || z[t]! <= TRAFFIC_LITE_CONVOY_FAR_Z - TRAFFIC_LITE_STAGE_REACH) continue; // ...and only if someone will pass it: a car closing on it, or a free car to queue in the window. if (z[t]! < TRAFFIC_LITE_CONVOY_NEAR_Z && pendingOf(TRAFFIC_CAR) >= 0) return; for (let c = 0; c < n; c += 1) { if (!present(c) || kind[c] !== TRAFFIC_CAR || laneOf(x[c]!) !== TRAFFIC_LANE_RIGHT || lateral(c)) continue; if (z[c]! <= z[t]! || v0[c]! - v[t]! < ENTRY_LEADER_DELTA) continue; // Closing in time: within reach of it by the time it is at the entry band. const seconds = Math.max(0, CENTRE_ENTRY_Z - TRAFFIC_LITE_STAGE_BAND - z[t]!) / Math.max(1, hero - v[t]!); if (z[c]! - length(c) - z[t]! - (v0[c]! - v[t]!) * seconds < TRAFFIC_LITE_STAGE_FOLLOW) return; } } const i = pendingOf(TRAFFIC_TRUCK); if (i < 0) return; const { length: len, halfWidth: hw } = TRAFFIC_KINDS[TRAFFIC_TRUCK]!; const lx = TRAFFIC_LANE_X[TRAFFIC_LANE_RIGHT]!; let rear = TRAFFIC_LITE_STAGE_Z + random() * TRAFFIC_LITE_STAGE_SPAN; if (pendingOf(TRAFFIC_CAR) < 0) { // No car left to queue behind it: slot the truck just ahead of a right-lane car already out there. let lead = -1; for (let c = 0; c < n; c += 1) { if (!present(c) || kind[c] !== TRAFFIC_CAR || laneOf(x[c]!) !== TRAFFIC_LANE_RIGHT || lateral(c)) continue; const vc = v[c]!; const ahead = z[c]! - length(c) - TRAFFIC_IDM.S0 - vc * TRAFFIC_IDM.T - Math.max(0, vc - TRAFFIC_SPEEDS.truck * 1.08) * 1.5 - 4; if (ahead > TRAFFIC_LITE_STAGE_NEAR_Z || ahead < TRAFFIC_LITE_STAGE_Z - TRAFFIC_LITE_STAGE_REACH) continue; if (lead < 0 || ahead > rear) { lead = c; rear = ahead; } } if (lead < 0) return; rear -= random() * 10; } if (!clearAt(i, lx - hw - TRAFFIC_LANE_JITTER, lx + hw + TRAFFIC_LANE_JITTER, rear, len, TRAFFIC_SPEEDS.truck * 1.08)) return; // Leave the passer room to cut back in ahead of it (as returnRoom will ask), or the pass never starts. const room = TRAFFIC_KINDS[TRAFFIC_CAR]!.length + 2 * (TRAFFIC_IDM.S0 + TRAFFIC_SPEEDS.rightCar * 1.08 * TRAFFIC_IDM.T); for (let j = 0; j < n; j += 1) { if (j === i || !present(j) || laneOf(x[j]!) !== TRAFFIC_LANE_RIGHT) continue; if (z[j]! < rear && rear - len - z[j]! < room) return; } place(i, TRAFFIC_LANE_RIGHT, rear, true); lastSpawn[1] = time; } function spawnAll(initial: boolean): void { const want = target(); // Lite staging may add one right-lane vehicle over the chapter's want (5 at most). const stage = !initial && staging() && want.right > 0 && rightCount() <= TRAFFIC_LITE_CAP.right; if (stage) stageTruck(); const extra = stage && rightCount() <= TRAFFIC_LITE_CAP.right ? 1 : 0; // Groups: left cars, right trucks, right cars. const rounds = initial ? 6 : 1; for (let r = 0; r < rounds; r += 1) { // While staging, trucks enter only through stageTruck, and the staged truck's follower has first call on a free car. if (stage) spawn(2, TRAFFIC_LANE_RIGHT, TRAFFIC_CAR, want.right - want.trucks + extra, initial, true); // ...and the left lane waits for it while one is still to be queued. if (!stage || !convoyOpen()) spawn(0, TRAFFIC_LANE_LEFT, TRAFFIC_CAR, want.left, initial); if (!stage) { spawn(1, TRAFFIC_LANE_RIGHT, TRAFFIC_TRUCK, want.trucks, initial); spawn(2, TRAFFIC_LANE_RIGHT, TRAFFIC_CAR, want.right - want.trucks, initial); } } } function sortOrder(): void { // Front (most negative rear z) first; pending vehicles last. Insertion sort, n <= 12. for (let a = 1; a < n; a += 1) { const item = order[a]!; const key = present(item) ? z[item]! : Number.POSITIVE_INFINITY; let b = a - 1; while (b >= 0) { const other = order[b]!; const otherKey = present(other) ? z[other]! : Number.POSITIVE_INFINITY; if (otherKey <= key) break; order[b + 1] = other; b -= 1; } order[b + 1] = item; } } function desired(i: number): number { const centreBound = lane[i] === TRAFFIC_LANE_CENTRE || targetLane(i) === TRAFFIC_LANE_CENTRE; if (!centreBound || overtaking[i]! < 0) return v0[i]!; // Hurried out by the reader: finish the pass rather than dawdle beside the truck. return v0[i]! + (sprint === i ? SPRINT_BOOST : OVERTAKE_BOOST); } function accelerations(): void { const { A, B, S0, T } = TRAFFIC_IDM; const root = 2 * Math.sqrt(A * B); for (let i = 0; i < n; i += 1) { if (!present(i)) continue; const want = desired(i); const speed = v[i]!; // Above its own pace a vehicle coasts (no brake lamps), it does not stamp on the brake. let a = speed <= want ? A * (1 - (speed / want) ** 4) : -0.8; const l = findLeader(i); leader[i] = l; if (l !== -1) { const leaderRear = l === HERO ? HERO_TAIL_Z : z[l]!; const leaderSpeed = l === HERO ? hero : v[l]!; const gap = Math.max(0.5, z[i]! - length(i) - leaderRear); const sStar = S0 + Math.max(0, speed * T + (speed * (speed - leaderSpeed)) / root); a -= A * (sStar / gap) ** 2; } accel[i] = clamp(a, -12, A); } } function overtakeDone(i: number): boolean { const l = overtaking[i]!; if (l < 0 || !present(l)) return true; return z[i]! < z[l]! - length(l); } function manoeuvres(sweeping: boolean, flash: boolean): void { if (centre >= 0) { const c = centre; const closing = hero - v[c]!; const gap = HERO_NOSE_Z - z[c]!; const reach = closing > 0.1 ? gap / closing : Number.POSITIVE_INFINITY; const urgent = reach < TIME_TO_REACH_SECONDS || z[c]! > CENTRE_DEADLINE_Z; // The reader speeding up, closing in or the deadline near: leave at the first comfortable gap. // (The reader's pace against the car's own, not its queued speed: a car // just out from behind a truck is still accelerating.) const soon = reach < YIELD_SOON_SECONDS || z[c]! > YIELD_SOON_Z || hero - desired(c) >= ENTRY_MAX_CLOSING; if (flash && z[c]! > FLASH_REACH_Z) hurried = true; const flashed = hurried; const leaving = lateral(c) && laneOf(toX[c]!) !== TRAFFIC_LANE_CENTRE; if (state[c] !== TRAFFIC_VISIBLE || leaving) { // Dithering out, or already on its way back out: a reader now closing fast turns the rest into a swerve. if (leaving && (urgent || flashed)) hurry(c); } else if (plan[c] === TRAFFIC_LANE_CENTRE) { // Signalling to pull out: a reader closing in (or a flash) calls it off. if (urgent || flashed || engineOff > 0) { cancel(c); } else if (time >= planAt[c]!) { if (slotFree(c, TRAFFIC_LANE_CENTRE)) { startLateral(c, TRAFFIC_LANE_CENTRE); lane[c] = TRAFFIC_LANE_CENTRE; } else { cancel(c); } } } else if (plan[c] !== NO_PLAN) { // Signalled a yield: go when the lead is up (at once if it got urgent). if (time >= planAt[c]! || reach < URGENT_SECONDS || z[c]! > YIELD_NOW_Z) { const out = plan[c]!; const forced = urgent || flashed; const pace = forced ? FORCED_LANE_CHANGE_SECONDS : LANE_CHANGE_SECONDS; if (slotFree(c, out, forced)) startLateral(c, out, pace); else if (out !== TRAFFIC_LANE_LEFT && forced && slotFree(c, TRAFFIC_LANE_LEFT, true)) { home[c] = TRAFFIC_LANE_LEFT; startLateral(c, TRAFFIC_LANE_LEFT, pace); } else if (forced || soon) sprint = c; } } else if (urgent || flashed || soon || (!sweeping && overtakeDone(c))) { // In the centre lane, or moving in (a reversal is allowed): leave. const forced = urgent || flashed; const out = slotFree(c, home[c]!, forced) ? home[c]! : (forced || soon) && slotFree(c, TRAFFIC_LANE_LEFT, forced) ? TRAFFIC_LANE_LEFT : -1; sprint = out < 0 && (forced || soon) ? c : -1; if (out >= 0) { home[c] = out; if (reach < URGENT_SECONDS || z[c]! > YIELD_NOW_Z || lateral(c)) { startLateral(c, out, forced ? FORCED_LANE_CHANGE_SECONDS : LANE_CHANGE_SECONDS); } else signal(c, out, YIELD_LEAD_SECONDS); } } // The emergency exit: still in the corridor at the deadline and not on its way out. if ( state[c] === TRAFFIC_VISIBLE && inCorridor(c) && z[c]! > CENTRE_DEADLINE_Z && !(lateral(c) && laneOf(toX[c]!) !== TRAFFIC_LANE_CENTRE) ) { state[c] = TRAFFIC_DITHERING; dither[c] = 0; counters.emergencyExits += 1; counters.lastEmergencyAt = time; } return; } if (sweeping || engineOff > 0 || time - lastManoeuvre < MANOEUVRE_GAP_SECONDS) return; for (let k = 0; k < n; k += 1) { const i = order[k]!; if (state[i] !== TRAFFIC_VISIBLE || kind[i] !== TRAFFIC_CAR || home[i] !== TRAFFIC_LANE_RIGHT) continue; if (lane[i] !== TRAFFIC_LANE_RIGHT || lateral(i) || plan[i] !== NO_PLAN) continue; const zi = z[i]!; if (zi >= CENTRE_ENTRY_Z || zi <= CENTRE_ENTRY_FAR_Z) continue; const l = leader[i]!; if (l < 0 || laneOf(x[l]!) !== TRAFFIC_LANE_RIGHT) continue; if (zi - length(i) - z[l]! > ENTRY_LEADER_GAP || v0[i]! - v[l]! < ENTRY_LEADER_DELTA) continue; // The reader near cruise (against the car's own pace; it is queued now)... if (hero - v0[i]! >= ENTRY_MAX_CLOSING) continue; // ...and not about to reach it once it is up to its passing pace. const closing = hero - (v0[i]! + OVERTAKE_BOOST); if (closing > 0.1 && (HERO_NOSE_Z - zi) / closing < 10) continue; if (!slotFree(i, TRAFFIC_LANE_CENTRE)) continue; if (!returnRoom(i, l)) continue; centre = i; overtaking[i] = l; lastManoeuvre = time; signal(i, TRAFFIC_LANE_CENTRE, ENTRY_LEAD_SECONDS); return; } } function integrate(dt: number): void { const { vMin, minGap } = TRAFFIC_IDM; for (let k = 0; k < n; k += 1) { const i = order[k]!; if (!present(i)) continue; v[i] = Math.max(vMin, v[i]! + accel[i]! * dt); z[i] = z[i]! + (hero - v[i]!) * dt; if (lateral(i)) { laneT[i] = Math.min(1, laneT[i]! + dt / laneSeconds[i]!); x[i] = fromX[i]! + (toX[i]! - fromX[i]!) * smooth(0, 1, laneT[i]!); if (laneT[i]! >= 1) { x[i] = toX[i]!; lane[i] = laneOf(x[i]!); indicator[i] = 0; if (lane[i] !== TRAFFIC_LANE_CENTRE) { overtaking[i] = -1; if (centre === i) { centre = -1; sprint = -1; hurried = false; } } } } // Never through the vehicle ahead (leaders are already integrated). const l = findLeader(i); if (l !== -1) { const leaderRear = l === HERO ? HERO_TAIL_Z : z[l]!; const minZ = leaderRear + length(i) + minGap; if (z[i]! < minZ) { z[i] = minZ; v[i] = Math.min(v[i]!, l === HERO ? Math.max(vMin, hero) : v[l]!); } } } } function brakeOnsetAllowed(): boolean { return time - onsets[onsetIndex]! >= 1; } function lamps(dt: number): void { for (let i = 0; i < n; i += 1) { if (!present(i)) continue; const a = accel[i]!; if (brakeOn[i] === 0) { if (a < BRAKE_ON_ACCEL && time - lastOnset[i]! >= BRAKE_MIN_SPACING_SECONDS && brakeOnsetAllowed()) { brakeOn[i] = 1; brakeSince[i] = time; lastOnset[i] = time; onsets[onsetIndex] = time; onsetIndex = (onsetIndex + 1) % BRAKE_GLOBAL_PER_SECOND; counters.brakeOnsets += 1; } } else if (a > BRAKE_OFF_ACCEL && time - brakeSince[i]! >= BRAKE_MIN_ON_SECONDS) { brakeOn[i] = 0; } const goal = brakeOn[i]!; const rate = dt / (goal > brakeLevel[i]! ? BRAKE_RISE_SECONDS : BRAKE_FALL_SECONDS); brakeLevel[i] = goal > brakeLevel[i]! ? Math.min(goal, brakeLevel[i]! + rate) : Math.max(goal, brakeLevel[i]! - rate); } } function wrap(dt: number): void { for (let i = 0; i < n; i += 1) { if (!present(i)) continue; const zi = z[i]!; const nose = zi - length(i); const mid = zi - length(i) / 2; const before = prevZ[i]!; prevZ[i] = zi; if (state[i] !== TRAFFIC_BEHIND && (before - length(i) / 2 - 2) * (mid - 2) < 0) counters.passes += 1; // Rear crossed the latch line toward the camera this step. if (before < TRAFFIC_LATCH_Z && zi >= TRAFFIC_LATCH_Z && state[i] === TRAFFIC_VISIBLE) latch(i); // Last resort: nothing ever drives through the reader's car. if (state[i] !== TRAFFIC_BEHIND && inCorridor(i) && nose < HERO_TAIL_Z + 1 && zi > HERO_NOSE_Z - 1) { counters.emergencyExits += 1; counters.lastEmergencyAt = time; park(i); continue; } if (state[i] === TRAFFIC_DITHERING) { dither[i] = dither[i]! + dt / DITHER_SECONDS; if (dither[i]! >= 1) park(i); continue; } if (fadeIn[i]! > 0) fadeIn[i] = Math.max(0, fadeIn[i]! - dt / FADE_IN_SECONDS); if (zi < TRAFFIC_FAR_Z || zi > TRAFFIC_BEHIND_Z) { park(i); } else if (nose > TRAFFIC_EXIT_NOSE_Z) { state[i] = TRAFFIC_BEHIND; } else if (state[i] === TRAFFIC_BEHIND) { // Coming back past the reader from beneath the camera. state[i] = TRAFFIC_VISIBLE; latch(i); } } } function writeFrame(): void { for (let i = 0; i < n; i += 1) { frame.active[i] = state[i]!; frame.x[i] = x[i]!; frame.z[i] = z[i]!; frame.speed[i] = v[i]!; frame.brake[i] = state[i] === TRAFFIC_PENDING ? 0 : brakeLevel[i]!; frame.indicator[i] = indicator[i]!; frame.blink[i] = indicator[i] === 0 || state[i] === TRAFFIC_PENDING ? 0 : indicatorBlink(time - indicatorSince[i]!); frame.nearLimit[i] = near[i]!; frame.dither[i] = state[i] === TRAFFIC_DITHERING ? Math.min(1, dither[i]!) : state[i] === TRAFFIC_VISIBLE ? fadeIn[i]! : 0; frame.yaw[i] = lateral(i) ? -Math.sign(toX[i]! - fromX[i]!) * 0.05 * Math.sin(Math.PI * laneT[i]!) : 0; } } reset(); return { frame, step(input: TrafficInput): TrafficFrame { const dt = Number.isFinite(input.dt) ? clamp(input.dt, 0, 0.1) : 0; if (Number.isFinite(input.heroSpeed)) hero = clamp(input.heroSpeed, 0, 240); if (Number.isFinite(input.cameraX)) cameraX = input.cameraX; if (Number.isFinite(input.engineOff)) engineOff = clamp(input.engineOff, 0, 1); if (Object.prototype.hasOwnProperty.call(TRAFFIC_DENSITY, input.chapter)) chapter = input.chapter; time += dt; if (config.lite && input.cue === true) stageUntil = time + TRAFFIC_LITE_STAGE_SECONDS; if (!primed) { primed = true; spawnAll(true); } sortOrder(); accelerations(); manoeuvres(input.sweeping === true, input.flash === true); integrate(dt); lamps(dt); wrap(dt); spawnAll(false); writeFrame(); return frame; }, events: () => counters, reset, }; }