New @tts/engine package: the pure message layer that drives bgm games. MessageQueue snapshots and drains on tick; TriggerRegistry matches by type/id with runtime enable/disable; runOrchestrator suspends on wait until a matching message is processed. Engine ties them together behind the CommandHost contract that @tts/tabletop will implement.
77 lines
2.5 KiB
TypeScript
77 lines
2.5 KiB
TypeScript
/**
|
|
* Triggers — declarative reactive glue.
|
|
*
|
|
* A trigger matches a message by `type` and named params, and emits messages
|
|
* in response. It is declared as data, keyed by `role+type+id` like other
|
|
* defs, and collision-checked the same way. `match` binds named params from
|
|
* the payload (like a route's candidates); `emit` uses the command-id-as-key
|
|
* convention. Multiple triggers can match the same message — both fire.
|
|
*/
|
|
import type { Message } from './message.js';
|
|
|
|
export interface Trigger {
|
|
/** The message type this trigger matches. */
|
|
type: string;
|
|
/** Optional identity, for runtime enable/disable and collision checks. */
|
|
id?: string;
|
|
/** Named params that must equal the corresponding fields in `msg.data`. */
|
|
match?: Record<string, unknown>;
|
|
/** Messages to emit when the trigger matches. */
|
|
emit: Message[];
|
|
}
|
|
|
|
/** A trigger matches when its `type` and every `match` param line up. */
|
|
export function triggerMatches(t: Trigger, msg: Message): boolean {
|
|
if (t.type !== msg.type) return false;
|
|
if (!t.match) return true;
|
|
const data = msg.data as Record<string, unknown> | undefined;
|
|
if (!data) return false;
|
|
return Object.entries(t.match).every(([k, v]) => data[k] === v);
|
|
}
|
|
|
|
/**
|
|
* A registry of triggers, keyed by `type#id`. `register` collision-checks the
|
|
* key; `enable`/`disable` toggle a trigger at runtime (an orchestrator's
|
|
* "no more placements this turn" control). `match` returns every enabled
|
|
* trigger that matches a message.
|
|
*/
|
|
export class TriggerRegistry {
|
|
private triggers = new Map<string, Trigger>();
|
|
private enabled = new Set<string>();
|
|
|
|
register(t: Trigger): void {
|
|
const key = triggerKey(t);
|
|
if (this.triggers.has(key)) {
|
|
throw new Error(`Duplicate trigger: ${key}`);
|
|
}
|
|
this.triggers.set(key, t);
|
|
this.enabled.add(key);
|
|
}
|
|
|
|
unregister(t: Trigger): void {
|
|
const key = triggerKey(t);
|
|
this.triggers.delete(key);
|
|
this.enabled.delete(key);
|
|
}
|
|
|
|
enable(type: string, id?: string): void {
|
|
this.enabled.add(triggerKey({ type, id }));
|
|
}
|
|
|
|
disable(type: string, id?: string): void {
|
|
this.enabled.delete(triggerKey({ type, id }));
|
|
}
|
|
|
|
/** Every enabled trigger matching `msg`. */
|
|
match(msg: Message): Trigger[] {
|
|
const out: Trigger[] = [];
|
|
for (const [key, t] of this.triggers) {
|
|
if (this.enabled.has(key) && triggerMatches(t, msg)) out.push(t);
|
|
}
|
|
return out;
|
|
}
|
|
}
|
|
|
|
function triggerKey(t: Pick<Trigger, 'type' | 'id'>): string {
|
|
return t.id ? `${t.type}#${t.id}` : t.type;
|
|
} |