glytch/fktry/src/ui/toasts.ts
type-two e24100c26e [ui] wildlife, the no-seam jam, and the opening's one mercy hint
Voice: dust piles read as nurseries rather than mess ("SOMETHING WILL HATCH IN
IT"), swarms announce and disperse differently, and SIM's new 'no seam' jam
finally gets the line it deserves — "THERE IS NOTHING DOWN THERE. MOVE."

The inspector grows a MOSQUITO EXPOSURE row, read from snap.wildlife by target,
so a machine that is mysteriously slow says why where the player is already
looking. It shows the swarm's severity, not a throughput promise — SIM owns the
real penalty and doesn't expose a number.

Toasts gain pushMessage for lines that aren't raw sim events. First among them:
once per session, on the very first brownout, "NOTHING IS GENERATING. THE
COMMITTEE SUGGESTS: POWER." The alarm alone taught nothing; this is the one
hint the opening needs, and it is emphatically not a nag — hatch warnings are
deduped per pile for the same reason.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 14:11:59 +10:00

59 lines
1.8 KiB
TypeScript

/**
* LANE-UI — event toasts, bottom right. Max 4 on screen, 6s each, oldest evicted.
*
* Wall-clock timing is fine here: toasts are chrome, not sim. The determinism rule in
* MASTERPLAN binds `src/sim/**`, and nothing in this file feeds back into it.
*/
import type { SimEvent } from '../contracts';
import { cls, el } from './dom';
import { eventToast } from './voice';
export interface Toasts {
el: HTMLElement;
push(ev: SimEvent, machineName: (entityId: number) => string): void;
/** A toast that isn't a raw sim event — onboarding hints, deduped warnings, ceremony. */
pushMessage(msg: string, kind: string): void;
/** Stand the stack off the bottom-right corner once the tuner dock moves in there. */
setStandoff(on: boolean): void;
/** Called once per frame to expire old lines. */
update(nowMs: number): void;
}
const TTL_MS = 6000;
const FADE_MS = 400; // must match the .fk-toast.is-out transition
const MAX = 4;
export function createToasts(): Toasts {
const root = el('div');
root.id = 'fk-toasts';
const live: Array<{ node: HTMLElement; dieAt: number }> = [];
function retire(entry: { node: HTMLElement; dieAt: number }) {
cls(entry.node, 'is-out', true);
setTimeout(() => entry.node.remove(), FADE_MS);
}
function emit(msg: string, kind: string) {
const node = el('div', `fk-toast k-${kind}`, [msg]);
root.append(node);
live.push({ node, dieAt: performance.now() + TTL_MS });
while (live.length > MAX) retire(live.shift()!);
}
return {
el: root,
push(ev, machineName) {
const msg = eventToast(ev, machineName);
if (msg) emit(msg, ev.kind);
},
pushMessage: emit,
setStandoff(on) {
cls(root, 'is-standoff', on);
},
update(nowMs) {
while (live.length && live[0].dieAt <= nowMs) retire(live.shift()!);
},
};
}