glytch/fktry/src/ui/voice.ts
type-two d017e710ad [ui] HUD foundation: voice, style, chips, hotkeys, selection
Shared plumbing for the DOM HUD, no framework:

- voice.ts: every player-facing string, so the OSHA-poster deadpan
  register lives in one place instead of being re-invented per panel.
- style.ts: injected terminal-industrial stylesheet. Panels re-enable
  pointer-events; #ui itself stays transparent to the mouse.
- chips.ts: the item chip (swatch + name + count), keyed off GameData
  so new items from LANE-DATA light up with no code change.
- hotkeys.ts: the whole keymap in one file, per lane orders.
- selection.ts: pending build selection, written through to bus._sel.
- pick.ts: tile -> entity hit-testing plus the flattened roster the UI
  keeps instead of holding a snapshot across frames (CONTRACTS says sim
  may reuse those buffers, and input handlers fire between frames).
- palette.ts: machine accent colors. Stopgap — MachineDef has no color
  field; contract request filed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 16:15:38 +10:00

98 lines
3.8 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 { SimEvent } from '../contracts';
/**
* Flavor lines for the inspector, keyed by MachineDef.id.
*
* TEMPORARY: hand-transcribed from docs/FKTRY_LORE.md §4 by LANE-UI. These belong in
* data/machines.json as a `flavor` field owned by LANE-DATA — CONTRACT REQUEST filed in
* NOTES. Unknown ids fall back to `GENERIC_FLAVOR`, so new machines never crash the
* panel, they just sound like a filing cabinet until the codex 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(defId: string): string {
return MACHINE_FLAVOR[defId] ?? GENERIC_FLAVOR;
}
/** 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)} HAS STOPPED. ${ev.reason.toUpperCase()}.`;
case 'brownout':
return ev.on
? 'BUFFER UNDERRUN. THIS SECTOR IS NOW A STILL LIFE.'
: 'BUFFER RESTORED. RESUME SINNING.';
case 'commissionDone':
return 'FAX SENT. THE COLLECTOR DOES NOT SAY THANK YOU.';
default:
return null; // shipped/crafted/placed/removed are too frequent to toast
}
}
export const COPY = {
bandwidth: 'BANDWIDTH',
brownout: 'BROWNOUT',
shipped: 'SHIPPED',
paused: 'HALTED',
running: 'RUNNING',
tick: 'TICK',
buildBarTitle: 'REQUISITION',
inspectorEmptyRecipe: 'NO PROCESS ASSIGNED. UNIT IS BEING PAID TO EXIST.',
inspectorInput: 'INTAKE',
inspectorOutput: 'OUTPUT',
inspectorProgress: 'PROCESS',
inspectorJamPrefix: 'FAULT: ',
inspectorClose: 'X',
faxHeader: 'INCOMING FAX',
faxSubheader: 'GLITCH ACQUISITIONS — UNSOLICITED',
faxNoCommission: 'NO ACTIVE COMMISSION. THE FAX MACHINE IS RESTING.',
faxStamp: 'FAX SENT',
/** Shown on the build bar when the player has something selected. */
placingHint: 'LMB PLACE · R ROTATE · ESC CANCEL',
idleHint: '1-9 SELECT · SPACE HALT · TAB MANIFEST',
/** Inspector fallback when no entity is under the cursor. */
manifestEmpty: 'NO UNITS ON RECORD. THE FLOOR IS AUDITED AND EMPTY.',
} 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;