diff --git a/fktry/src/ui/index.ts b/fktry/src/ui/index.ts index c28deb7..e19173b 100644 --- a/fktry/src/ui/index.ts +++ b/fktry/src/ui/index.ts @@ -13,11 +13,14 @@ import { createFax } from './fax'; import { createHotkeys } from './hotkeys'; import { createInspector } from './inspector'; import { createRoster, entityAt, syncRoster } from './pick'; +import { createScreenView } from './screenview'; import { createSelection } from './selection'; import { injectStyle } from './style'; import { createTechPanel } from './techpanel'; import { createTopStrip } from './topstrip'; import { createToasts } from './toasts'; +import { createTuner } from './tuner'; +import { COPY } from './voice'; export function createUI(): UI { let machines = new Map(); @@ -27,11 +30,25 @@ export function createUI(): UI { let fax: ReturnType; let toasts: ReturnType; let tech: ReturnType; + let tuner: ReturnType; + let screenView: ReturnType; /** Our own copy of the entity list; safe to read between frames. See pick.ts. */ const roster = createRoster(); let paused = false; + // ---- onboarding + ceremony state (per session; none of it is sim truth) ---- + /** The opening's one hint. Fires on the first brownout ever, then never again. */ + let mercyShown = false; + /** Dust piles we've already warned about, so a pile isn't a recurring nag. */ + const warnedPiles = new Set(); + + function revealTuner() { + tuner.reveal(); + // The dock lands in the toasts' corner; move them up so it can't print over the dial. + toasts.setStandoff(true); + } + function machineName(entityId: number): string { for (let i = 0; i < roster.length; i++) { const r = roster.entries[i]; @@ -48,12 +65,25 @@ export function createUI(): UI { const sel = createSelection(bus); top = createTopStrip(data); build = createBuildBar(data, sel); - inspector = createInspector(data, bus); + // v5: closing the inspector — by X, by Esc, or because its unit was demolished — + // must stop the renderer highlighting it. + inspector = createInspector(data, bus, () => sel.clearInspect()); fax = createFax(data); toasts = createToasts(); tech = createTechPanel(data, bus); + tuner = createTuner(); + screenView = createScreenView(document.getElementById('screen')); - root.append(top.el, build.el, inspector.el, fax.el, tech.el, toasts.el); + // A held tool and an open inspector are mutually exclusive: picking a machine or + // arming remove supersedes the inspect selection, so shut the panel to match. + sel.onChange(() => { + if (sel.isArmed() && inspector.isOpen()) inspector.close(); + }); + + root.append( + top.el, build.el, inspector.el, fax.el, tech.el, + tuner.el, screenView.el, toasts.el, + ); // ------------------------------------------------------------------ keyboard const keys = createHotkeys(); @@ -63,10 +93,14 @@ export function createUI(): UI { keys.bind('r', () => sel.rotate()); keys.bind('x', () => sel.toggleRemove()); keys.bind('t', () => tech.toggle()); + // Tuner keys are inert until the dock is revealed, so they don't shadow anything + // during the pre-discovery game. + for (const k of ['arrowleft', 'arrowright', 'b', 'm']) keys.bind(k, () => tuner.onKey(k)); keys.bind('escape', () => { - // One key, several jobs, in the order the player expects: put the tool down - // first (wrecking ball included), then shut whatever is open. - if (sel.isArmed()) sel.clear(); + // One key, several jobs, in the order the player expects: close the thing most + // recently in front first, then put the tool down, then shut panels. + if (screenView.isOpen()) screenView.close(); + else if (sel.isArmed()) sel.clear(); else if (tech.isOpen()) tech.close(); else inspector.close(); }); @@ -97,8 +131,12 @@ export function createUI(): UI { if (!tile) return; const hit = entityAt(roster, machines, tile); - if (hit === null) inspector.close(); - else inspector.open(hit); + if (hit === null) { + inspector.close(); + } else { + inspector.open(hit); + sel.inspect(hit); // v5: renderer highlights the unit we're showing + } }); // ?uidemo — build SIM's reference factory on boot. main.ts defines __fktryDemo @@ -122,9 +160,30 @@ export function createUI(): UI { fax.update(snap); tech.update(snap); + // The fossil may already be dug up in a loaded save, with no event to announce it. + if (!tuner.isRevealed() && snap.relics?.some((r) => r.found)) revealTuner(); + for (const ev of events) { - toasts.push(ev, machineName); if (ev.kind === 'commissionDone') fax.stamp(); + + if (ev.kind === 'relicFound') revealTuner(); // ceremony toast comes from voice.ts + + // One hatch warning per pile. `spawned` is edge-triggered so this is usually + // already true; the guard keeps a re-emitting sim from turning it into a nag. + if (ev.kind === 'wildlife' && ev.wildlife === 'dust-pile') { + if (ev.on === 'cleared') warnedPiles.delete(ev.id); + else if (warnedPiles.has(ev.id)) continue; + else warnedPiles.add(ev.id); + } + + toasts.push(ev, machineName); + + // The alarm alone teaches nothing. Once per session, on the very first brownout, + // say what to do about it. + if (ev.kind === 'brownout' && ev.on && !mercyShown) { + mercyShown = true; + toasts.pushMessage(COPY.firstBrownoutMercy, 'mercy'); + } } toasts.update(performance.now()); },