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

@@ -15,6 +15,10 @@ import {
INDICATOR_HZ,
TRAFFIC_ASPHALT_HALF_WIDTH,
TRAFFIC_BEHIND,
TRAFFIC_CONVOY_GAP_MAX,
TRAFFIC_LITE_CONVOY_NEAR_Z,
TRAFFIC_LITE_DROUGHT_SECONDS,
TRAFFIC_LITE_STAGE_SECONDS,
TRAFFIC_DENSITY,
TRAFFIC_ENTRY_FAR_Z,
TRAFFIC_ENTRY_NEAR_Z,
@@ -122,6 +126,8 @@ describe('traffic model: long randomised drives', () => {
hero += (target - hero) * Math.min(1, dt * 3);
if (time >= nextChapter) {
step.chapter = CHAPTERS[Math.floor(random() * CHAPTERS.length)]!;
// A chapter entry: lite stages a pass (the invariants hold through it).
step.cue = true;
nextChapter = time + 20 + random() * 40;
if (random() < 0.4) sweepUntil = time + 0.5 + random();
}
@@ -134,6 +140,7 @@ describe('traffic model: long randomised drives', () => {
step.cameraX = Math.sin(time * 0.05) * 5.6;
step.dt = dt;
const f = model.step(step);
step.cue = false;
const problem = violation(f);
if (problem) throw new Error(`seed ${seed} t ${time.toFixed(2)}: ${problem}`);
const events = model.events();
@@ -489,7 +496,8 @@ describe('traffic model: the interaction is in frame', () => {
const f = model.step(step);
for (let i = 0; i < f.count; i += 1) {
const x = f.active[i] === TRAFFIC_VISIBLE ? f.x[i]! : 9;
if (prev[i]! > 3 && x <= 3) entries.push(f.z[i]!);
// A pull-out from the right lane (not a pooled slot reappearing in another lane).
if (prev[i]! > 3 && prev[i]! !== 9 && x <= 3) entries.push(f.z[i]!);
if (Math.abs(prev[i]!) <= 3 && Math.abs(x) > 3 && x !== 9) exits.push(f.z[i]!);
prev[i] = x;
}
@@ -532,8 +540,11 @@ describe('traffic model: the interaction is in frame', () => {
seen[i] = 1;
if (t === 0) continue;
if (f.z[i]! > TRAFFIC_ENTRY_FAR_Z + 1 && f.z[i]! < TRAFFIC_EXIT_NOSE_Z - 1) {
// Nearer than the far entry: screen-doored in, still in the haze.
expect(f.z[i]!, `vehicle ${i} at ${t.toFixed(1)} s`).toBeLessThan(TRAFFIC_ENTRY_NEAR_Z + 2);
// Nearer than the far entry: screen-doored in, at the haze. Lite
// queues its convoy car (and stages its truck) a little nearer:
// the rear of a car behind a truck at the window's near edge.
const liteNear = TRAFFIC_LITE_CONVOY_NEAR_Z + TRAFFIC_KINDS[0]!.length + TRAFFIC_CONVOY_GAP_MAX;
expect(f.z[i]!, `vehicle ${i} at ${t.toFixed(1)} s`).toBeLessThan(Math.max(TRAFFIC_ENTRY_NEAR_Z, liteNear) + 2);
expect(f.z[i]!).toBeGreaterThan(TRAFFIC_ENTRY_NEAR_Z - TRAFFIC_ENTRY_NEAR_SPAN - 50);
expect(f.dither[i]!).toBeGreaterThan(0.9);
fading[i] = t;
@@ -614,3 +625,79 @@ describe('flash detector', () => {
expect(fired.length).toBe(2);
});
});
describe('traffic model: lite passes (phones)', () => {
const LIVE: EnvironmentChapter[] = ['deep-night', 'systems', 'infrastructure', 'builds', 'pre-dawn'];
it('stages a pass at each chapter entry: a car pulls out within 30 s, 20 s on average', () => {
// A truck staged at the haze takes ~15 s to reach the 150-215 m entry band.
const waits: number[] = [];
for (const seed of [1, 5, 8, 13, 21]) {
const model = createTrafficModel({ pool: 6, seed, lite: true });
const step = input();
for (let t = 0; t < 5; t += 1 / 60) model.step(step);
for (let entry = 0; entry < 8; entry += 1) {
step.chapter = LIVE[(entry + seed) % LIVE.length]!;
step.cue = true;
const before = model.events().laneChanges;
let waited = 0;
while (model.events().laneChanges === before && waited < 40) {
const f = model.step(step);
step.cue = false;
waited += 1 / 60;
const problem = violation(f);
if (problem) throw new Error(`seed ${seed} entry ${entry}: ${problem}`);
}
expect(waited, `seed ${seed} entry ${entry} (${step.chapter})`).toBeLessThan(30);
waits.push(waited);
// Read the chapter for a while before the next entry.
for (let t = 0; t < 12; t += 1 / 60) model.step(step);
}
}
expect(waits.reduce((a, b) => a + b, 0) / waits.length).toBeLessThan(20);
expect(TRAFFIC_LITE_STAGE_SECONDS).toBeGreaterThanOrEqual(15);
});
it('never goes long without a pass at a steady cruise (the drought rule)', () => {
for (const seed of [2, 5, 8, 17, 3, 11, 29, 42]) {
const model = createTrafficModel({ pool: 6, seed, lite: true });
const step = input({ chapter: 'systems' });
let last = 0;
let longest = 0;
let changes = 0;
for (let t = 0; t < 600; t += 1 / 60) {
const f = model.step(step);
const problem = violation(f);
if (problem) throw new Error(`seed ${seed} t ${t.toFixed(1)}: ${problem}`);
if (model.events().laneChanges !== changes) {
changes = model.events().laneChanges;
longest = Math.max(longest, t - last);
last = t;
}
}
longest = Math.max(longest, 600 - last);
// Lane changes count the return too: drought, then ~15 s for a staged truck to reach the band, then the pass.
// (Without staging this runs 82-88 s on these seeds.)
expect(longest, `seed ${seed}`).toBeLessThan(55);
}
expect(TRAFFIC_LITE_DROUGHT_SECONDS).toBeLessThanOrEqual(15);
});
it('staging stays inside the low-tier caps and never with the engine off', () => {
const model = createTrafficModel({ pool: 6, seed: 6, lite: true });
const step = input({ chapter: 'infrastructure' });
let maxRight = 0;
for (let t = 0; t < 300; t += 1 / 60) {
step.cue = Math.floor(t * 60) % 600 === 0;
const f = model.step(step);
let right = 0;
for (let i = 0; i < f.count; i += 1) if (f.active[i] === TRAFFIC_VISIBLE && f.x[i]! > 0) right += 1;
maxRight = Math.max(maxRight, right);
}
expect(maxRight).toBeLessThanOrEqual(5);
const parked = createTrafficModel({ pool: 6, seed: 6, lite: true });
for (let t = 0; t < 60; t += 1 / 60) parked.step(input({ chapter: 'destination', engineOff: 1, heroSpeed: 0, cue: t < 0.02 }));
expect(parked.events().laneChanges).toBe(0);
});
});