/** * 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 }; }