# bgm-commands Command execution for [bgm](./bgm-format.md) board games, built into [`@tts/tabletop`](./bgm-tabletop.md). A command is a unit of scripted interaction — focus the camera, wait for a tap, move a part, show a caption — that runs against the tabletop state store and render layer. This doc covers **command execution**: the async lifecycle, run contexts, and tap interaction. The message layer above this — how commands are *declared* and *fired* (triggers, orchestrators, the message queue) — is specified in [`bgm-engine.md`](./bgm-engine.md). Commands are async functions registered with the engine's handler registry; `@tts/tabletop` provides the concrete commands that mutate the tabletop store and render layer. ## 1. async commands A command is an async function that returns a result. Every command ends in one of three states, emitted as a message discriminated on the type suffix (see `bgm-engine.md` §3): - `:done` — completed normally. - `:cancel` — interrupted (a newer command superseded it, the user skipped, the surface was disabled). **Not a failure.** - `:error` — genuinely failed (asset missing, bad path, a thrown exception). `cancel` is distinct from `error`: a superseded or skipped command stops cleanly, while a broken command surfaces loudly. The runtime treats them differently — a script that is superseded unwinds without alarming the player, but an `error` is reported. ```ts type CommandResult = | { type: `${Name}:done`; data: R } | { type: `${Name}:cancel` } | { type: `${Name}:error`; error: Error }; ``` ## 2. run contexts Each command invocation creates its own **run context**: the unit of cancellation and the carrier of command-specific state. ```ts interface CommandRun { id: string; command: Command; status: 'running' | 'ok' | 'cancel' | 'error'; data: unknown; // command-specific state, e.g. a pending tap target cancel(): void; done: Promise; } ``` A command **owns its own state and its own waiting**; the runtime only orchestrates. Its job is to start a run, track its status, cancel it when superseded, and react to its terminal state. This keeps commands self-contained and testable in isolation. ## 3. fire-and-forget vs self-managed waiting Commands fall into two categories: - **Fire-and-forget** (`focus`, `highlight`, `caption`) — start and return `ok` immediately (or when their tween settles). The runtime does not block on them. - **Self-managed waiting** (`wait: tap`, a dialog) — the command resolves its own promise when its condition is met. The runtime just awaits it. Fire-and-forget commands still get a run context and a cancel path. A `focus` tween superseded by a newer `focus` must be cancellable, or two cameras fight. "Fire-and-forget" means the runtime doesn't await it, not that it has no lifecycle. **Supersede groups** cancel a running command when another in the same group starts. A `focus` command belongs to a `camera` group, so a second `focus` cancels the first. A superseded command's `signal` is aborted, and it emits `:cancel`. A command is an async function taking the `RunContext` (with its `args`): ```ts type Command = (ctx: RunContext & { args: Args }) => Promise; ``` The engine wraps it: it builds the context from the message, runs the function, and emits `:done` on resolve, `:cancel` on abort, `:error` on throw. ## 4. tap interaction Only tap interaction is supported. A tap on a part is detected and reported to the command layer as a `TapEvent`. Parts may declare **trigger points** — named, circular regions the author wants to be tappable. ```ts interface TriggerPoint { id: string; position: [number, number]; // part-local frame, mm radius: number; // mm } interface TapEvent { part: string; // package:type#id position: [number, number]; // part-local frame, mm trigger: TriggerPoint | null; // nearest within radius, or null } ``` Rules: - Trigger points are authored in the **part's local frame** (mm, relative to the part's origin), not world space. A part moves, rotates, and flips (facing), so a world-space point would break the moment it moves. The tap point is transformed into the part's local frame at tap time. - Distance is measured in the part's plane. The reported trigger point is the nearest one within its `radius`; ties go to the first declared. - **Every tap on the part is reported**, with the nearest trigger point (or `null` when none is in range). The command decides how to react — resolve, reject with a "wrong spot" shake, or ignore. The runtime stays dumb; the command owns the UX. Commands subscribe to the tap stream via the context and unsubscribe on cancel, so a cancelled `wait: tap` never leaks a handler. ## 5. run context The context a command receives is the handle to everything it can affect. The engine defines the base `RunContext` (see `bgm-engine.md` §5): `signal` (cancellation), `emit`, `wait`, and `enableTrigger`/`disableTrigger`. Tabletop extends it with the handles commands need to mutate the board: ```ts interface TabletopRunContext extends RunContext { pkg: Package; store: TabletopStore; // movePart, setPart, enableSurface, ... onTap(handler: (e: TapEvent) => void): () => void; // returns unsubscribe // camera, highlight, and overlay handles are added as those subsystems land } ``` ## 6. where trigger points come from The tap detector reads trigger points from a runtime map keyed by part id; it does not care where they are declared. Declaration (on the part definition, in a setup, or in a script) is the deferred "how to declare" half and lives in `bgm-format.md`. ## Open decisions - **Where commands are declared** — the `script` role and its schema (`bgm-format.md`), deferred. - **Animation** — a general "ease toward target placement" layer (preferred) vs explicit per-move tweens. - **Camera** — `CameraControls` (drei) vs hand-rolled. - **Triggering** — does a setup reference a script to auto-run, or is a script a separate page the player picks? - **Narration** — pre-recorded audio assets per script, or TTS at runtime?