polish: scene nits — live-lens yard, square stacks, deck-clear billboard, two-layer lamps, flare reach, phone traffic and exposure

This commit is contained in:
2026-10-12 18:59:33 +03:00
parent 71465fa481
commit 3602eeb3d1
23 changed files with 671 additions and 73 deletions

View File

@@ -74,6 +74,34 @@ 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
@@ -245,6 +273,8 @@ export interface TrafficInput {
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 {
@@ -381,6 +411,9 @@ export function createTrafficModel(config: TrafficConfig): TrafficModel {
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;
@@ -409,6 +442,8 @@ export function createTrafficModel(config: TrafficConfig): TrafficModel {
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);
@@ -514,6 +549,8 @@ export function createTrafficModel(config: TrafficConfig): TrafficModel {
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). */
@@ -592,24 +629,43 @@ export function createTrafficModel(config: TrafficConfig): TrafficModel {
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 >= TRAFFIC_CONVOY_NEAR_Z) continue;
if (zt <= TRAFFIC_CONVOY_FAR_Z) {
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;
@@ -656,7 +712,8 @@ export function createTrafficModel(config: TrafficConfig): TrafficModel {
}
/** One group (lane, kind) spawns at most one vehicle per call. */
function spawn(group: number, laneIndex: number, k: number, want: number, initial: boolean): void {
/** `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);
@@ -684,6 +741,7 @@ export function createTrafficModel(config: TrafficConfig): TrafficModel {
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)...
@@ -694,6 +752,9 @@ export function createTrafficModel(config: TrafficConfig): TrafficModel {
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;
@@ -710,14 +771,85 @@ export function createTrafficModel(config: TrafficConfig): TrafficModel {
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) {
spawn(0, TRAFFIC_LANE_LEFT, TRAFFIC_CAR, want.left, initial);
spawn(1, TRAFFIC_LANE_RIGHT, TRAFFIC_TRUCK, want.trucks, initial);
spawn(2, TRAFFIC_LANE_RIGHT, TRAFFIC_CAR, want.right - want.trucks, initial);
// 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);
}
}
}
@@ -995,6 +1127,7 @@ export function createTrafficModel(config: TrafficConfig): TrafficModel {
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);