The banner keyed on `out || ratio > 1`, so any time draw exceeded generation the panel screamed BROWNOUT — even while the buffer tanks were quietly covering it, the sim's flag was false and THE SCREEN was calm. That state is the factory working as designed, not failing. Only the HUD was panicking. BROWNOUT is now the sim's flag and nothing else. A covered deficit gets its own amber, steady, non-flashing state: "ON RESERVE · 34s", where the seconds are stored/(draw-gen) — real arithmetic, because v3 ruled stored is bandwidth-SECONDS and gen/draw are per-second. Same maths SCREEN uses. Four states (ok / tight / reserve / brownout) now live in a pure power.ts and are pinned by tests, including the exact case that lied: gen 100, draw 134, stored 1000, brownout false -> reserve, not brownout. Also uses v4 bandwidth.capacity to turn the buffer readout into a fuel gauge (0/900) rather than a bare number. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
139 lines
5.3 KiB
TypeScript
139 lines
5.3 KiB
TypeScript
/**
|
|
* LANE-UI — the house voice.
|
|
*
|
|
* Register: OSHA-poster deadpan (lore §8). Everything horrifying is labelled like
|
|
* routine industrial equipment. Nothing here apologises, explains itself, or uses an
|
|
* exclamation mark. The player is an employee who has already read the placard.
|
|
*
|
|
* All copy in the UI comes from this file so the tone stays in one place.
|
|
*/
|
|
import type { MachineDef, SimEvent } from '../contracts';
|
|
|
|
/**
|
|
* Fallback flavor, keyed by MachineDef.id.
|
|
*
|
|
* `MachineDef.flavor` (contracts v2) is the real source and wins whenever LANE-DATA has
|
|
* filled it. These are the seven I hand-transcribed from the codex in round 1, kept as
|
|
* the fallback per orders. Unknown ids get `GENERIC_FLAVOR` — a new machine never
|
|
* crashes the panel, it just sounds like a filing cabinet until its line lands.
|
|
*/
|
|
const MACHINE_FLAVOR: Record<string, string> = {
|
|
'seam-extractor':
|
|
'Chews the payload seam and spits raw ore. Do not place hands in the seam. ' +
|
|
'The seam has no policy regarding hands.',
|
|
belt:
|
|
'Moves things. Moving things is the entire job. Back-pressure is a normal ' +
|
|
'operating condition and is not a grievance.',
|
|
demuxer:
|
|
'Cleaves raw ore into luma and chroma. One intake, four chutes, no apologies. ' +
|
|
'Contents were never yours.',
|
|
quantizer:
|
|
'Rounds coefficients to zero. The dial goes to CRIME. Material leaking from the ' +
|
|
'seams is HF DUST and is not glitter. Do not lick the machine.',
|
|
'mosh-reactor':
|
|
'A decoder sealed in a vessel and starved of anchors until it hallucinates. ' +
|
|
'The hallucination is the product. Certified vessel. Uncertified hallucination.',
|
|
'decode-asic':
|
|
'Handles standard profiles only. Cool, efficient, incurious. Generates bandwidth ' +
|
|
'by declining to be interesting.',
|
|
shipper:
|
|
'Composites shipped freight onto THE SCREEN. All deliveries final. ' +
|
|
'All sins permanent. Congratulations.',
|
|
};
|
|
|
|
const GENERIC_FLAVOR =
|
|
'Industrial equipment. Operates within tolerances. Tolerances not disclosed.';
|
|
|
|
export function machineFlavor(def: MachineDef | undefined, id: string): string {
|
|
return def?.flavor || MACHINE_FLAVOR[id] || GENERIC_FLAVOR;
|
|
}
|
|
|
|
/**
|
|
* Jam reasons, in the house voice. The sim owns the reason strings; we translate the
|
|
* ones we know and pass anything else through uppercased, so a new stall reason shows
|
|
* up honestly rather than being swallowed.
|
|
*/
|
|
const JAM_TEXT: Record<string, string> = {
|
|
'output full': 'OUTPUT BUFFER AT CAPACITY. THIS IS A YOU PROBLEM.',
|
|
starved: 'INTAKE STARVED. NOTHING IS ARRIVING.',
|
|
'no recipe': 'NO PROCESS ASSIGNED. UNIT AWAITS INSTRUCTION.',
|
|
'no power': 'INSUFFICIENT BANDWIDTH. THE MATHEMATICS ARE UNMOVED.',
|
|
};
|
|
|
|
export function jamText(reason: string): string {
|
|
return JAM_TEXT[reason] ?? reason.toUpperCase();
|
|
}
|
|
|
|
/** Toast copy for events the player should notice without reading a manual. */
|
|
export function eventToast(ev: SimEvent, machineName: (id: number) => string): string | null {
|
|
switch (ev.kind) {
|
|
case 'jammed':
|
|
return `${machineName(ev.entity)}: ${jamText(ev.reason)}`;
|
|
case 'brownout':
|
|
return ev.on
|
|
? 'BUFFER UNDERRUN. THIS SECTOR IS NOW A STILL LIFE.'
|
|
: 'BUFFER RESTORED. RESUME SINNING.';
|
|
case 'scram':
|
|
return ev.on
|
|
? `THERMAL SCRAM. ${machineName(ev.entity)} HAS EXCUSED ITSELF.`
|
|
: `${machineName(ev.entity)} IS COOL ENOUGH TO CONTINUE. REGRETTABLY.`;
|
|
case 'commissionDone':
|
|
return 'FAX SENT. THE COLLECTOR DOES NOT SAY THANK YOU.';
|
|
case 'removed':
|
|
return 'UNIT RECLAIMED. THE FLOOR REMEMBERS.';
|
|
case 'researched':
|
|
return 'STANDARD RATIFIED. NEW CRIMES AVAILABLE.';
|
|
default:
|
|
return null; // shipped/crafted/placed are too frequent to toast
|
|
}
|
|
}
|
|
|
|
export const COPY = {
|
|
bandwidth: 'BANDWIDTH',
|
|
brownout: 'BROWNOUT',
|
|
/** Deficit covered by the tanks. Not a fault — the machine is doing its job. */
|
|
onReserve: 'ON RESERVE',
|
|
shipped: 'SHIPPED',
|
|
paused: 'HALTED',
|
|
running: 'RUNNING',
|
|
tick: 'TICK',
|
|
|
|
inspectorEmptyRecipe: 'NO PROCESS ASSIGNED. UNIT IS BEING PAID TO EXIST.',
|
|
inspectorInput: 'INTAKE',
|
|
inspectorOutput: 'OUTPUT',
|
|
inspectorProgress: 'PROCESS',
|
|
inspectorRecipe: 'PROCESS SELECT',
|
|
inspectorNoRecipeOption: 'IDLE',
|
|
inspectorClose: 'X',
|
|
inspectorHeat: 'HEAT',
|
|
heatThrottling: 'THROTTLING',
|
|
heatScram: 'SCRAM — UNIT OFFLINE',
|
|
|
|
faxHeader: 'INCOMING FAX',
|
|
faxSubheader: 'GLITCH ACQUISITIONS — UNSOLICITED',
|
|
faxNoCommission: 'NO ACTIVE COMMISSION. THE FAX MACHINE IS RESTING.',
|
|
faxStamp: 'FAX SENT',
|
|
faxStanding: 'STANDING ORDER',
|
|
faxNext: 'NEXT IN TRAY',
|
|
/** Shown against shipments The Correction quietly buys back (M3 fiction). */
|
|
sanitising: 'SANITISING',
|
|
|
|
techHeader: 'STANDARDS COMMITTEE',
|
|
techSubheader: 'MEDIA ARCHAEOLOGY — RESEARCH DOWN, DERIVE UP',
|
|
techActive: 'IN COMMITTEE',
|
|
techDone: 'RATIFIED',
|
|
/** Shown when the sim reports no research state at all. */
|
|
researchOffline: 'RESEARCH OFFLINE. THE COMMITTEE HAS NOT CONVENED.',
|
|
requires: 'REQUIRES:',
|
|
|
|
placingHint: 'LMB PLACE · R ROTATE · ESC CANCEL',
|
|
idleHint: '1-9 SELECT · [ ] PAGE · X REMOVE · T RESEARCH · SPACE HALT · CLICK A UNIT',
|
|
|
|
removeLabel: 'REMOVE',
|
|
removeKey: 'X',
|
|
removeHint: 'REMOVE ARMED · LMB DEMOLISH · ESC STAND DOWN',
|
|
} as const;
|
|
|
|
/** Direction names for the rotation readout. Dir 0..3 = N,E,S,W clockwise. */
|
|
export const DIR_NAME = ['N', 'E', 'S', 'W'] as const;
|