docs: design command execution for tabletop scripts

Add bgm-commands.md covering async commands with ok/cancel/error
results, per-invocation run contexts, fire-and-forget vs self-managed
waiting, and tap interaction with part-local trigger points. Link it
from the tabletop design and plan docs, and record the decisions in
decisions.md.
This commit is contained in:
2026-08-10 15:41:43 +08:00
parent 9b5223686e
commit 634a99dd25
4 changed files with 203 additions and 2 deletions
+41 -1
View File
@@ -259,4 +259,44 @@ boundary.
**Alternatives considered:** A polling `CameraFit` that waited for non-empty
bounds each frame; hand-rolled camera math. Rejected — Suspense already
signals content readiness, so `Bounds` inside the boundary fits the loaded
geometry directly.
geometry directly.
## D19 — Commands are async with ok/cancel/error results
**Decision:** Scripted interaction is built on async commands. Each command
returns `ok`, `cancel` (interrupted — superseded, skipped, surface disabled),
or `error` (genuinely failed). Each invocation gets its own run context — the
unit of cancellation and the carrier of command state. Commands are either
fire-and-forget (the runtime doesn't await them) or self-managed waiting (they
resolve their own promise when a condition is met); both get a run context and
cancel path. Supersede groups cancel a running command when another in the
group starts (e.g. a `camera` group so a second focus cancels the first).
**Context:** The user wants to script interaction sequences — focus, caption,
title, highlight, tap-to-advance, move, camera away. The state store and
render layer already exist; what's missing is a way to drive them over time
and react to input. Design: [`bgm-commands.md`](./bgm-commands.md).
**Alternatives considered:** A single monolithic script interpreter. Rejected
— commands as self-contained async units are testable in isolation and let
the runtime stay a thin orchestrator.
## D20 — Tap interaction reports every tap with the nearest trigger point
**Decision:** Only tap interaction is supported. A tap on a part is reported
to the command layer as a `TapEvent` carrying the part, the tap position in
the part's local frame, and the nearest trigger point within its `radius` (or
`null` on a miss). Trigger points are authored in the part's local frame with
mm radius; distance is measured in the part's plane; ties go to the first
declared. The command decides how to react to a miss — resolve, reject, or
ignore.
**Context:** Commands need to wait on player input (`wait: tap`). Reporting
every tap with the nearest trigger point keeps the runtime dumb and lets the
command own the UX (e.g. a "wrong spot" shake). Authoring trigger points in
the part's local frame keeps them valid as the part moves, rotates, and
flips.
**Alternatives considered:** Reporting only a hit and silently dropping
misses. Rejected — a command that needs to react to a wrong tap has no way to
do so. World-space trigger points. Rejected — they break when the part moves.