docs: add free interaction design to bgm specs
Add the command/dialog split to the state-model and interactions docs, declare role dialog and zone facing in the format spec, and let setups declare which dialogs power open interactions.
This commit is contained in:
@@ -180,6 +180,7 @@ by their `role=` (or, for real files, their filename) for either
|
|||||||
- `part`
|
- `part`
|
||||||
- `surface`
|
- `surface`
|
||||||
- `setup`
|
- `setup`
|
||||||
|
- `dialog`
|
||||||
|
|
||||||
a valid object can either be the root or in the list of the yaml block.
|
a valid object can either be the root or in the list of the yaml block.
|
||||||
|
|
||||||
@@ -236,6 +237,10 @@ placed on the board via `setup`, and visualized by routes.
|
|||||||
- `size` — `size`. The token is scaled to fit in the box. The x/y aspect
|
- `size` — `size`. The token is scaled to fit in the box. The x/y aspect
|
||||||
ratio is kept, but not z (thickness).
|
ratio is kept, but not z (thickness).
|
||||||
- `fillet` — number in mm. Used to fillet the shape. Defaults to `0`.
|
- `fillet` — number in mm. Used to fillet the shape. Defaults to `0`.
|
||||||
|
- `facing` — the **physical affordance**: the facings the piece supports.
|
||||||
|
A card supports `[face, back, standing]`; a tile supports `[face, back]`.
|
||||||
|
Declares what's *possible*, not what's legal on a given zone (see `surface`
|
||||||
|
`layout`). Defaults to `[face]`.
|
||||||
|
|
||||||
#### example
|
#### example
|
||||||
|
|
||||||
@@ -366,6 +371,49 @@ initialization.
|
|||||||
A part's `facing` is seeded into the game state and can change at runtime; it
|
A part's `facing` is seeded into the game state and can change at runtime; it
|
||||||
only affects orientation, never the part's texture.
|
only affects orientation, never the part's texture.
|
||||||
|
|
||||||
|
A setup also declares the **interaction affordances** — which dialogs are the
|
||||||
|
tool for which open interactions on which paths:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
interactions:
|
||||||
|
- dialog: insert
|
||||||
|
on: [draw, grid] # insertion uses the `insert` dialog on these paths
|
||||||
|
- dialog: shuffle # open the shuffle dialog on any deck
|
||||||
|
```
|
||||||
|
|
||||||
|
`interactions` is a list of declarations: each names a `dialog` (a `role:
|
||||||
|
dialog` definition, see below) and the paths it applies to (`on`, matching by
|
||||||
|
path or by stack). It declares the *interaction surface*, not the legality of
|
||||||
|
the resulting command — rule scripts (later) gate legality. A dialog can be
|
||||||
|
opened by a player gesture or pushed by a rule script; only the trigger differs.
|
||||||
|
|
||||||
|
### dialog
|
||||||
|
|
||||||
|
A `dialog` is declarative content shown in the layer-3 shell. Opening and
|
||||||
|
closing it never issues a command or mutates state; it is pure UI. Its **action
|
||||||
|
buttons** issue commands — the bridge between the dialog and the rule seam.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
role: dialog
|
||||||
|
type: prompt
|
||||||
|
id: discard
|
||||||
|
title: Choose a card to discard
|
||||||
|
body: |
|
||||||
|
Select a card from your hand.
|
||||||
|
actions:
|
||||||
|
- label: Confirm
|
||||||
|
command: { move: { part: "#chosen", to: /discard } }
|
||||||
|
widget: stack # a stack-of-parts content type
|
||||||
|
```
|
||||||
|
|
||||||
|
A dialog's content is a **title**, **body**, **action buttons**, and an optional
|
||||||
|
`widget`. Supported content types include a **stack of parts** — an ordered,
|
||||||
|
scrolled view of a path's stack with an insertion cursor — which is what powers
|
||||||
|
insertion and shuffling dialogs against the state store.
|
||||||
|
|
||||||
|
`dialog` is a same-shaped definition as the others: `type`/`id` identify it
|
||||||
|
(`type#id` unique in the package), and it collides-checks like `part`/`setup`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Concepts
|
## 4. Concepts
|
||||||
@@ -384,6 +432,11 @@ A surface is enabled or disabled; a disabled surface is not rendered. Setup
|
|||||||
seeds the enabled set (see §3), and it changes at runtime as the game
|
seeds the enabled set (see §3), and it changes at runtime as the game
|
||||||
progresses (e.g. enabling the main board after an expansion-chooser scene).
|
progresses (e.g. enabling the main board after an expansion-chooser scene).
|
||||||
|
|
||||||
|
Dialogs are **not** part of the state store. The dialog stack is UI state
|
||||||
|
hosted by the shell; opening/closing a dialog never mutates the store. A dialog's
|
||||||
|
action buttons issue commands (see `role: dialog` §3), which is the one-way
|
||||||
|
bridge onto the state.
|
||||||
|
|
||||||
### Routing
|
### Routing
|
||||||
|
|
||||||
A route is a **visualization route**: it maps a part to a location on a
|
A route is a **visualization route**: it maps a part to a location on a
|
||||||
@@ -399,6 +452,15 @@ A route is an express-style URL path with named params, plus the `x`, `y`, and
|
|||||||
`rotation` of its anchor. Routes are defined in a **list**, not a map, so the
|
`rotation` of its anchor. Routes are defined in a **list**, not a map, so the
|
||||||
same route path may appear more than once:
|
same route path may appear more than once:
|
||||||
|
|
||||||
|
A route may also declare a zone `facing` — the **legal facing** on that zone.
|
||||||
|
This constrains what the placed parts may be, independent of each part's
|
||||||
|
physical affordance (see `part` `facing`). A state is legal only when the part's
|
||||||
|
facing is in both the part's affordance *and* the zone's legal set. When omitted,
|
||||||
|
the zone is unrestricted beyond the part's affordance. An MTG discard pile
|
||||||
|
requires `[face]`; a play area allows `[face, tap]`; a facedown deck requires
|
||||||
|
`[back]`. The same card is face-up in the discard but tapped in play — the
|
||||||
|
piece's affordance is unchanged, only the zone's rule differs.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
layout:
|
layout:
|
||||||
- route: /dock/:seat
|
- route: /dock/:seat
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# bgm Interactions
|
||||||
|
|
||||||
|
The free-interaction layer (layer 3 of the layering) — how a player interacts
|
||||||
|
with a bgm game that has no rules yet: a sandbox. It builds on the state model:
|
||||||
|
see [`state-model.md`](./state-model.md) for components-vs-setup, path→stack ×
|
||||||
|
facing, and the anchoring scope.
|
||||||
|
|
||||||
|
> **Status:** Design. Proposes the operation set, the command/dialog split, and
|
||||||
|
> the deck-pick-up dialog before the interaction half of `@tts/tabletop` is
|
||||||
|
> built.
|
||||||
|
|
||||||
|
## 1. The operation set is closed and tiny
|
||||||
|
|
||||||
|
Two assumptions from the state model do most of the work:
|
||||||
|
|
||||||
|
1. **Parts are never created or destroyed** — the set of parts is fixed (the
|
||||||
|
setup's parts).
|
||||||
|
2. **State is only path, stack, and facing** — there is nowhere to store a free
|
||||||
|
position in space.
|
||||||
|
|
||||||
|
Together they collapse the *entire* space of free interaction into three
|
||||||
|
operations:
|
||||||
|
|
||||||
|
1. **`move(id, path, index?)`** — relocate a part to a path, at a stack
|
||||||
|
position (default: top of stack).
|
||||||
|
2. **`setFacing(id, facing)`** — change facing, within the part's physical
|
||||||
|
affordance.
|
||||||
|
3. **reorder** — a `move` with an explicit `index`.
|
||||||
|
|
||||||
|
That's it. There is no arbitrary positioning and no free placement in 3D because
|
||||||
|
the state model has nowhere to store one. "Free" means *unconstrained over these
|
||||||
|
three operations*, not "free in space."
|
||||||
|
|
||||||
|
## 2. Free interaction and rule play are the same operations
|
||||||
|
|
||||||
|
This is the payoff. Rule-enforced play (layer 4) is **the same three
|
||||||
|
operations, gated by a legality check**:
|
||||||
|
|
||||||
|
- The interaction layer produces an **intent** — "player wants
|
||||||
|
`move(card, /discard)`."
|
||||||
|
- **Sandbox mode**: apply it directly.
|
||||||
|
- **Rule mode**: validate the intent against the setup's shape + the rule script
|
||||||
|
first; reject if illegal.
|
||||||
|
|
||||||
|
So the rule engine needs no interaction vocabulary of its own — it is a **filter
|
||||||
|
over free interaction**. `move`/`setFacing` are the shared primitives; rules
|
||||||
|
decide which are legal in the current state. Drag-and-drop and a scripted move
|
||||||
|
both funnel through the same store mutation. That is the seam between layer 3
|
||||||
|
and layer 4.
|
||||||
|
|
||||||
|
## 3. Commands vs the dialog stack
|
||||||
|
|
||||||
|
There are two channels, and they do not mix:
|
||||||
|
|
||||||
|
- **Commands** — intent to *change state* (`move`, `setFacing`). They go through
|
||||||
|
the rule seam and mutate the store.
|
||||||
|
- **The dialog stack** — transient UI contexts (the deck pick-up, a "confirm
|
||||||
|
discard," a hint prompt). Opening/closing a dialog **never issues a command**
|
||||||
|
and never touches state; it is pure UI.
|
||||||
|
|
||||||
|
This simplifies the model. The dialog stack is **UI state, hosted by the layer-3
|
||||||
|
shell, not by the game-state store** — dialogs don't belong in path/stack/facing.
|
||||||
|
|
||||||
|
How they connect — one direction only:
|
||||||
|
|
||||||
|
- **Player-initiated**: a click on a deck pushes the deck dialog; the dialog's
|
||||||
|
insert button issues a `move` command.
|
||||||
|
- **Script-initiated**: a rule script pushes the same dialog (e.g. to force a
|
||||||
|
discard) and awaits the player's `move` through it. The script's open/close
|
||||||
|
is tied to the dialog stack; it can `pushDialog`/`popDialog` without mutating
|
||||||
|
game state.
|
||||||
|
|
||||||
|
So the deck dialog is **one implementation, driven either way** — by a player
|
||||||
|
click or by a rule script. Only the *trigger* differs.
|
||||||
|
|
||||||
|
Dialogs are **authored in the manifest**, not hardcoded. A `role: dialog`
|
||||||
|
definition declares the title, body, action buttons, and an optional widget
|
||||||
|
(a stack of parts). Setups declare which dialogs are the tool for which
|
||||||
|
interactions via `interactions:` (see [`format.md`](./format.md) §3). The
|
||||||
|
rule seam stays clean because a dialog's buttons issue commands while its
|
||||||
|
open/close is stack-only.
|
||||||
|
|
||||||
|
## 4. Compound interactions: the deck pick-up dialog
|
||||||
|
|
||||||
|
A dialog is where compound, multi-step manipulation lives, because it owns the
|
||||||
|
transient sub-state that the store must not. The prime example — inserting a
|
||||||
|
card into the middle of a deck:
|
||||||
|
|
||||||
|
1. pick up the deck
|
||||||
|
2. scroll through it to find the place
|
||||||
|
3. insert the card at the cursor
|
||||||
|
4. put the deck back
|
||||||
|
|
||||||
|
(even 0: put down your held hand of cards first).
|
||||||
|
|
||||||
|
The dialog is an **alternate view of the stack**: the deck is "lifted" off the
|
||||||
|
table into the dialog (it stays on its path; the rest of the table becomes
|
||||||
|
backdrop). Its contents are shown **in order**, with an **insertion cursor**
|
||||||
|
between cards that you scroll. The cursor *is* the index: `move(id, path,
|
||||||
|
index)`'s index is discovered by scrolling the visible deck, not typed.
|
||||||
|
|
||||||
|
This is a real interaction *mode*, owned by the dialog:
|
||||||
|
|
||||||
|
- The scroll position is state.
|
||||||
|
- Everything else pauses while it's open (or the held part is kept visible).
|
||||||
|
- It only exists for **stacks**; single parts just snap onto a path.
|
||||||
|
|
||||||
|
We treat it as a **reusable pattern** — a stack-inspector dialog — not a one-off
|
||||||
|
hack for one command, so `deal`, `draw`, and `look-at-the-top` can reuse the
|
||||||
|
same lifted-deck view.
|
||||||
|
|
||||||
|
## 5. The primitives map to concrete gestures
|
||||||
|
|
||||||
|
- **Move** — pick up a part → it leaves its stack (transient "in hand"); drag →
|
||||||
|
resolve the nearest path anchor within a threshold; drop → `move` (drop on
|
||||||
|
nothing returns to origin).
|
||||||
|
- **Facing** — click cycles the part through its physical affordance
|
||||||
|
(`face → back → standing`, or the declared list).
|
||||||
|
- **Reorder / insert** — the deck dialog above.
|
||||||
|
|
||||||
|
The "in hand" state is transient and illegal, so it lives **outside the store**
|
||||||
|
(a UI-level held part); only the committed outcome (drop) mutates the store.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- **Held source for insertion** — single held card (fits the physical model; the
|
||||||
|
dialog inserts it at the cursor), vs the dialog owns the source (you cursor a
|
||||||
|
card *from* the deck to lift). Lean single-held-card, but confirm it doesn't
|
||||||
|
fight the hand step.
|
||||||
|
- **Multi-part ops** — picking up a whole stack, dealing N cards. Deferred; the
|
||||||
|
single-part primitives are the foundation.
|
||||||
|
- **Scroll window** — a window over a subset of the deck can return with the
|
||||||
|
cursor's scroll position; exact widget is a render concern, not a state one.
|
||||||
@@ -15,6 +15,7 @@ and go stale as work lands.
|
|||||||
| [`bgm/commands.md`](./bgm/commands.md) | bgm command execution: lifecycle, run contexts, tap interaction |
|
| [`bgm/commands.md`](./bgm/commands.md) | bgm command execution: lifecycle, run contexts, tap interaction |
|
||||||
| [`bgm/tabletop.md`](./bgm/tabletop.md) | The r3f tabletop component library |
|
| [`bgm/tabletop.md`](./bgm/tabletop.md) | The r3f tabletop component library |
|
||||||
| [`bgm/state-model.md`](./bgm/state-model.md) | The format's state model: components vs setup, facing, anchoring scope |
|
| [`bgm/state-model.md`](./bgm/state-model.md) | The format's state model: components vs setup, facing, anchoring scope |
|
||||||
|
| [`bgm/interactions.md`](./bgm/interactions.md) | Free interaction: the operation set, the rule seam, and the deck pick-up dialog |
|
||||||
|
|
||||||
## Status & plans (dev logs)
|
## Status & plans (dev logs)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user