[ui] wire round 5: tuner reveal, screen enlarge, notices, camera hint

The HUD root gains the tuner and the enlarge overlay, the T/arrows/B/M keys,
and the small amount of session state the onboarding needs: the mercy toast
fires once ever, hatch warnings are remembered per pile.

The fossil reveals the dock from either direction — the relicFound event, or a
loaded save where relics[].found is already true and no event is coming. The
dock lands in the toast stack's corner, so the toasts stand off above it rather
than printing over the dial.

Esc now closes the most-recently-fronted thing first: enlarge, then the held
tool, then panels.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-18 14:12:42 +10:00
parent d6d6833b78
commit 0bf0e5a076

View File

@ -13,11 +13,14 @@ import { createFax } from './fax';
import { createHotkeys } from './hotkeys'; import { createHotkeys } from './hotkeys';
import { createInspector } from './inspector'; import { createInspector } from './inspector';
import { createRoster, entityAt, syncRoster } from './pick'; import { createRoster, entityAt, syncRoster } from './pick';
import { createScreenView } from './screenview';
import { createSelection } from './selection'; import { createSelection } from './selection';
import { injectStyle } from './style'; import { injectStyle } from './style';
import { createTechPanel } from './techpanel'; import { createTechPanel } from './techpanel';
import { createTopStrip } from './topstrip'; import { createTopStrip } from './topstrip';
import { createToasts } from './toasts'; import { createToasts } from './toasts';
import { createTuner } from './tuner';
import { COPY } from './voice';
export function createUI(): UI { export function createUI(): UI {
let machines = new Map<string, MachineDef>(); let machines = new Map<string, MachineDef>();
@ -27,11 +30,25 @@ export function createUI(): UI {
let fax: ReturnType<typeof createFax>; let fax: ReturnType<typeof createFax>;
let toasts: ReturnType<typeof createToasts>; let toasts: ReturnType<typeof createToasts>;
let tech: ReturnType<typeof createTechPanel>; let tech: ReturnType<typeof createTechPanel>;
let tuner: ReturnType<typeof createTuner>;
let screenView: ReturnType<typeof createScreenView>;
/** Our own copy of the entity list; safe to read between frames. See pick.ts. */ /** Our own copy of the entity list; safe to read between frames. See pick.ts. */
const roster = createRoster(); const roster = createRoster();
let paused = false; 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<number>();
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 { function machineName(entityId: number): string {
for (let i = 0; i < roster.length; i++) { for (let i = 0; i < roster.length; i++) {
const r = roster.entries[i]; const r = roster.entries[i];
@ -48,12 +65,25 @@ export function createUI(): UI {
const sel = createSelection(bus); const sel = createSelection(bus);
top = createTopStrip(data); top = createTopStrip(data);
build = createBuildBar(data, sel); 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); fax = createFax(data);
toasts = createToasts(); toasts = createToasts();
tech = createTechPanel(data, bus); 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 // ------------------------------------------------------------------ keyboard
const keys = createHotkeys(); const keys = createHotkeys();
@ -63,10 +93,14 @@ export function createUI(): UI {
keys.bind('r', () => sel.rotate()); keys.bind('r', () => sel.rotate());
keys.bind('x', () => sel.toggleRemove()); keys.bind('x', () => sel.toggleRemove());
keys.bind('t', () => tech.toggle()); 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', () => { keys.bind('escape', () => {
// One key, several jobs, in the order the player expects: put the tool down // One key, several jobs, in the order the player expects: close the thing most
// first (wrecking ball included), then shut whatever is open. // recently in front first, then put the tool down, then shut panels.
if (sel.isArmed()) sel.clear(); if (screenView.isOpen()) screenView.close();
else if (sel.isArmed()) sel.clear();
else if (tech.isOpen()) tech.close(); else if (tech.isOpen()) tech.close();
else inspector.close(); else inspector.close();
}); });
@ -97,8 +131,12 @@ export function createUI(): UI {
if (!tile) return; if (!tile) return;
const hit = entityAt(roster, machines, tile); const hit = entityAt(roster, machines, tile);
if (hit === null) inspector.close(); if (hit === null) {
else inspector.open(hit); 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 // ?uidemo — build SIM's reference factory on boot. main.ts defines __fktryDemo
@ -122,9 +160,30 @@ export function createUI(): UI {
fax.update(snap); fax.update(snap);
tech.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) { for (const ev of events) {
toasts.push(ev, machineName);
if (ev.kind === 'commissionDone') fax.stamp(); 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()); toasts.update(performance.now());
}, },