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>
59 lines
1.8 KiB
TypeScript
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()!);
|
|
},
|
|
};
|
|
}
|