Per the v3 units ruling, stored is bandwidth-seconds and draw-gen is bandwidth per second, so stored/(draw-gen) is literally how long until the lights go out. Tremor ramps over the last 8 seconds. That reads correct at every factory size for free: 1000/s with 4000 banked feels exactly as doomed as 10/s with 40. A factory in surplus is always calm, which is also what keeps the boot state (gen=draw=stored=0) sterile by construction rather than by special case. Fever averages only the machines that run hot, so one glowing decoder is not drowned by a sea of cold belts. Pure and snapshot-shaped so the sterile guarantee is a test, not a promise. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
67 lines
2.4 KiB
TypeScript
67 lines
2.4 KiB
TypeScript
/**
|
|
* Signal strain — the continuous half of THE SCREEN's vocabulary.
|
|
*
|
|
* Events give edges (brownout on/off); the snapshot gives quantities. Strain is what the
|
|
* factory feels BEFORE the seizure: the reserve draining toward empty, the decoders running
|
|
* hot. Strain is weather; the corruption ledger is climate. It stays deliberately subordinate.
|
|
*
|
|
* Pure and snapshot-shaped on purpose: no DOM, no GL, so the "healthy factory stays sterile"
|
|
* guarantee is a unit test rather than a promise.
|
|
*/
|
|
import type { SimSnapshot } from '../contracts';
|
|
|
|
export interface Strain {
|
|
/** 0..1 tremor/desync judder as the bandwidth reserve runs out */
|
|
tremor: number;
|
|
/** 0..1 slow feverish shimmer from aggregate machine heat */
|
|
fever: number;
|
|
}
|
|
|
|
export const NO_STRAIN: Strain = { tremor: 0, fever: 0 };
|
|
|
|
/**
|
|
* How much runway counts as calm. Reserve deeper than this reads as healthy; dread ramps up
|
|
* as the factory's remaining seconds fall toward zero.
|
|
*/
|
|
export const STRAIN_HORIZON_SECONDS = 8;
|
|
|
|
/** Machines idling at a trace of heat are not a fever. */
|
|
const HEAT_FLOOR = 0.01;
|
|
|
|
const clamp01 = (x: number) => Math.min(1, Math.max(0, x));
|
|
|
|
/**
|
|
* Read-only peek at the snapshot (contracts v3). The snapshot is never retained: everything
|
|
* needed collapses to two scalars here and now.
|
|
*/
|
|
export function computeStrain(snap?: SimSnapshot): Strain {
|
|
if (!snap) return NO_STRAIN;
|
|
|
|
// --- tremor: seconds of reserve remaining at the current deficit.
|
|
// Per the v3 units ruling, `stored` is bandwidth-SECONDS while gen/draw are bandwidth per
|
|
// second — so stored/deficit is literally "how long until the lights go out".
|
|
const { gen, draw, stored } = snap.bandwidth;
|
|
const deficit = draw - gen;
|
|
let tremor = 0;
|
|
if (deficit > 0) {
|
|
const secondsLeft = Math.max(0, stored) / deficit;
|
|
tremor = 1 - clamp01(secondsLeft / STRAIN_HORIZON_SECONDS);
|
|
}
|
|
// A factory in surplus is calm no matter how little it has banked — that is what keeps
|
|
// strain out of the NOTHING IS WRONG era, where gen and draw are both zero.
|
|
|
|
// --- fever: how hot are the machines that actually run hot.
|
|
// Averaging across every entity would drown a glowing decoder in a sea of cold belts.
|
|
let sum = 0;
|
|
let hot = 0;
|
|
for (const e of snap.entities) {
|
|
if (e.heat > HEAT_FLOOR) {
|
|
sum += e.heat;
|
|
hot++;
|
|
}
|
|
}
|
|
const fever = hot > 0 ? clamp01(sum / hot) : 0;
|
|
|
|
return { tremor, fever };
|
|
}
|