feat: night freight container yard works through the additional work chapter
This commit is contained in:
124
src/journey/yard-progress.ts
Normal file
124
src/journey/yard-progress.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* NIGHT FREIGHT: the Additional work chapter is a working container yard. A
|
||||
* rail-mounted gantry crane rides the left verge through the chapter and
|
||||
* stops once per additional project: the spreader lowers onto a stack, the
|
||||
* stack-top container's end plate lights and the crane's work lamps strike.
|
||||
* Pure helpers for the lazy scene chunk only (src/scene/world/yard.ts):
|
||||
* nothing in the initial bundle imports this module. Every placement is a
|
||||
* function of section-local progress, so scrolling back lifts the spreader,
|
||||
* dims the plate and runs the crane back.
|
||||
*/
|
||||
|
||||
/** Section-local progress of each stop (one per additional project). */
|
||||
export const YARD_STOPS = [0.1, 0.33, 0.56, 0.79] as const;
|
||||
/** The crane stands still this far either side of a stop while the spreader works. */
|
||||
export const YARD_HOLD = 0.06;
|
||||
/**
|
||||
* Rail coordinates: the crane rails run in a straight line from the camera's
|
||||
* pivot into the distance (yard.ts turns the line by the viewport aspect so it
|
||||
* stays inside the left reading margin), so a rail coordinate s is the metres
|
||||
* from the camera along that line, negative ahead; world z ~ cameraZ + s * cos(angle).
|
||||
* Where the crane enters and leaves its rails.
|
||||
*/
|
||||
export const CRANE_FROM_Z = -150;
|
||||
export const CRANE_TO_Z = -62;
|
||||
/** The stacks along the rails, one under each stop, far to near. */
|
||||
export const STACK_ZS = [-134, -112, -94, -80] as const;
|
||||
/** Containers per stack, far to near: the yard steps down toward the camera so every stack top and the crane stay in view. */
|
||||
export const STACK_TIERS = [4, 3, 2, 1] as const;
|
||||
/** Work lamps: no strike ramps faster than this, and no more than one strike per gap. */
|
||||
export const LAMP_RAMP_SECONDS = 0.35;
|
||||
export const LAMP_STRIKE_GAP_SECONDS = 0.6;
|
||||
/** High-mast floods: HID warm-up from orange to blue-white, and back on exit. */
|
||||
export const FLOOD_WARMUP_SECONDS = 1.5;
|
||||
|
||||
const clamp01 = (value: number): number => (Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0);
|
||||
const smooth = (from: number, to: number, value: number): number => {
|
||||
const t = clamp01((value - from) / (to - from));
|
||||
return t * t * (3 - 2 * t);
|
||||
};
|
||||
|
||||
/**
|
||||
* Crane z for progress p: parked over stack i within YARD_HOLD of stop i,
|
||||
* easing between stacks, from CRANE_FROM_Z at 0 to CRANE_TO_Z at 1.
|
||||
* Non-decreasing in p.
|
||||
*/
|
||||
export function craneZ(p: number): number {
|
||||
const t = clamp01(p);
|
||||
const knots: Array<[number, number, number]> = [];
|
||||
let fromP = 0;
|
||||
let fromZ = CRANE_FROM_Z;
|
||||
YARD_STOPS.forEach((stop, i) => {
|
||||
knots.push([fromP, stop - YARD_HOLD, fromZ]);
|
||||
fromP = stop + YARD_HOLD;
|
||||
fromZ = STACK_ZS[i]!;
|
||||
});
|
||||
knots.push([fromP, 1, fromZ]);
|
||||
let z = CRANE_FROM_Z;
|
||||
knots.forEach(([a, b, startZ], i) => {
|
||||
const endZ = i < STACK_ZS.length ? STACK_ZS[i]! : CRANE_TO_Z;
|
||||
if (t >= a) z = startZ + (endZ - startZ) * smooth(a, b, t);
|
||||
});
|
||||
return z;
|
||||
}
|
||||
|
||||
/** 0..1: how far the spreader is lowered (1 on the stack at a stop). */
|
||||
export function spreaderDrop(p: number | null): number {
|
||||
if (p === null) return 0;
|
||||
return Math.max(0, ...YARD_STOPS.map((stop) => 1 - smooth(0.015, YARD_HOLD, Math.abs(p - stop))));
|
||||
}
|
||||
|
||||
/** 0..1: stack i's end plate, lit as the spreader lands and from then on. */
|
||||
export function plateLit(p: number | null, index: number): number {
|
||||
const stop = YARD_STOPS[index];
|
||||
if (p === null || stop === undefined) return 0;
|
||||
return smooth(stop - 0.03, stop, p);
|
||||
}
|
||||
|
||||
/** Stops passed at progress p (plates fully lit). */
|
||||
export const yardStopsPassed = (p: number | null): number =>
|
||||
p === null ? 0 : YARD_STOPS.filter((stop) => p >= stop).length;
|
||||
|
||||
/** Moves the flood warm-up level (0 off, 1 full blue-white) toward on/off over FLOOD_WARMUP_SECONDS. */
|
||||
export function floodWarmup(level: number, on: boolean, deltaSeconds: number): number {
|
||||
const step = Math.max(0, deltaSeconds) / FLOOD_WARMUP_SECONDS;
|
||||
return clamp01(on ? level + step : level - step);
|
||||
}
|
||||
|
||||
/** A warming HID lamp: output and hue (0 sodium orange, 1 blue-white) for a warm-up level. */
|
||||
export function floodLight(level: number): { output: number; hue: number } {
|
||||
const t = clamp01(level);
|
||||
return { output: smooth(0, 0.12, t) * (0.3 + 0.7 * t), hue: smooth(0.15, 1, t) };
|
||||
}
|
||||
|
||||
export interface LampState {
|
||||
level: number;
|
||||
/** Seconds since the last strike began. */
|
||||
since: number;
|
||||
strikes: number;
|
||||
}
|
||||
|
||||
export const createLamp = (): LampState => ({ level: 0, since: Number.POSITIVE_INFINITY, strikes: 0 });
|
||||
|
||||
/**
|
||||
* A work lamp following target (0..1). A strike (a dark lamp coming on) ramps
|
||||
* over at least LAMP_RAMP_SECONDS and begins at most once per
|
||||
* LAMP_STRIKE_GAP_SECONDS, however fast the scroll: inside the gap a dark
|
||||
* lamp stays dark. Mutates and returns state.
|
||||
*/
|
||||
export function stepLamp(state: LampState, target: number, deltaSeconds: number): LampState {
|
||||
const dt = Math.max(0, deltaSeconds);
|
||||
const goal = clamp01(target);
|
||||
state.since += dt;
|
||||
if (goal > state.level) {
|
||||
if (state.level < 0.02) {
|
||||
if (state.since < LAMP_STRIKE_GAP_SECONDS) return state;
|
||||
state.since = 0;
|
||||
state.strikes += 1;
|
||||
}
|
||||
state.level = Math.min(goal, state.level + dt / LAMP_RAMP_SECONDS);
|
||||
} else {
|
||||
state.level = Math.max(goal, state.level - dt / LAMP_RAMP_SECONDS);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
Reference in New Issue
Block a user