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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user