feat(bgm): add surface mounting and setup surfaces
This commit is contained in:
+56
-3
@@ -193,11 +193,29 @@ tables. It does not affect part placement; placement lives in the state store
|
||||
(see §4). A surface need not cover every part — parts with no matching route on
|
||||
this surface are simply not shown.
|
||||
|
||||
A surface also declares how it is **mounted**: as the root table surface, on a
|
||||
HUD area, or as a child of another surface. `mount` is always an object, with
|
||||
`x`, `y`, and `rotation` (defaulting to `0`) anchoring it like a route. The
|
||||
`kind` selects the mount type:
|
||||
|
||||
- `table` — the root table surface (default).
|
||||
- `hud` — mounted to a HUD area, e.g. a player's hand.
|
||||
- `child` — mounted relative to a parent surface. A surface lists its
|
||||
`children` (`type#id` refs) so a surface can be repeated, like a player
|
||||
board; each child is mounted relative to its parent's anchor.
|
||||
|
||||
```yaml
|
||||
type: board
|
||||
id: harbor
|
||||
role: surface
|
||||
size: [300, 200]
|
||||
mount:
|
||||
kind: table
|
||||
x: 0
|
||||
y: 0
|
||||
rotation: 0
|
||||
children:
|
||||
- board#player
|
||||
layout:
|
||||
- route: /dock/:seat
|
||||
candidates:
|
||||
@@ -208,19 +226,49 @@ layout:
|
||||
rotation: 0
|
||||
```
|
||||
|
||||
```yaml
|
||||
type: hud
|
||||
id: hand
|
||||
role: surface
|
||||
size: [200, 100]
|
||||
mount:
|
||||
kind: hud
|
||||
area: bottom-left
|
||||
```
|
||||
|
||||
```yaml
|
||||
type: board
|
||||
id: player
|
||||
role: surface
|
||||
size: [200, 200]
|
||||
mount:
|
||||
kind: child
|
||||
x: 100
|
||||
y: 50
|
||||
rotation: 0
|
||||
```
|
||||
|
||||
### setup
|
||||
|
||||
`setup` seeds the state store. Each valid game state is a valid setup.
|
||||
`setup` seeds the state store: the enabled surfaces and the part placement.
|
||||
Each valid game state is a valid setup.
|
||||
|
||||
```yaml
|
||||
role: setup
|
||||
type: game
|
||||
id: main
|
||||
surfaces:
|
||||
- board#harbor
|
||||
- hud#hand
|
||||
setup:
|
||||
/dock/0: harbor:boat#fleet
|
||||
/deck: harbor:card
|
||||
```
|
||||
|
||||
`surfaces` lists the surfaces enabled at the start. A surface not listed is
|
||||
disabled and not rendered. When `surfaces` is omitted, all surfaces are
|
||||
enabled.
|
||||
|
||||
The value on a setup path can be either a string, or a string list.
|
||||
|
||||
The string can either be a one part string, or a type without an id.
|
||||
@@ -233,13 +281,18 @@ When id is omitted, it expands to all parts in that type during game state initi
|
||||
|
||||
### Game state
|
||||
|
||||
The board's state is a **state store**: a map from path to a **stack** of
|
||||
parts. It is the authoritative record of where every part is placed.
|
||||
The board's state is a **state store**: the set of **enabled surfaces** and a
|
||||
map from path to a **stack** of parts. It is the authoritative record of which
|
||||
surfaces are in play and where every part is placed.
|
||||
|
||||
A path is a URL path with named params, like `/dock/1`.
|
||||
|
||||
A part is identified by a `package:type#id` string.
|
||||
|
||||
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
|
||||
progresses (e.g. enabling the main board after an expansion-chooser scene).
|
||||
|
||||
### Routing
|
||||
|
||||
A route is a **visualization route**: it maps a part to a location on a
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `src/types.ts` | Roles (`Package`, `Part`, `Surface`, `Setup`, `Route`, `Stacking`, …), `SerializedPackage` (the JSON the plugin emits), `BgmError`, `DefFile`, `ParsedDef` |
|
||||
| `src/types.ts` | Roles (`Package`, `Part`, `Surface`, `Setup`, `Route`, `Stacking`, `SurfaceMount`, …), `SerializedPackage` (the JSON the plugin emits), `BgmError`, `DefFile`, `ParsedDef` |
|
||||
| `src/schemas.ts` | zod schemas + `validate*` for each role |
|
||||
| `src/markdown.ts` | Virtual def files from markdown code blocks, via **`marked`**. `file=` naming + content-hash auto-naming (`./<hash>.yaml`); multiple blocks may share a `file=` name |
|
||||
| `src/parse.ts` | yaml/json/toml → def objects (`yaml`, `smol-toml`); real-file walker (incl. `.csv`) |
|
||||
@@ -22,7 +22,7 @@ Deps: `marked`, `typed-csv`, `yaml`, `smol-toml`, `picomatch`, `zod`, `vite`, `@
|
||||
|
||||
### `games/harbor/harbor.md` — example game (new)
|
||||
|
||||
Exercises the format end-to-end: package decl, two `file=parts/tokens.yaml` blocks, a surface with `candidates: $variants` against a virtual csv block, and a setup. Same content duplicated as the vitest fixture under `packages/bgm/src/__fixtures__/harbor/`.
|
||||
Exercises the format end-to-end: package decl, two `file=parts/tokens.yaml` blocks, a table surface with `mount`/`children` and `candidates: $variants` against a virtual csv block, a child player surface, and a setup declaring its enabled `surfaces`. Same content duplicated as the vitest fixture under `packages/bgm/src/__fixtures__/harbor/`.
|
||||
|
||||
### `apps/web` — consumer (new)
|
||||
|
||||
@@ -54,4 +54,5 @@ The vite plugin itself lives in `packages/bgm/src/vite.ts` (exported from `@tts/
|
||||
- **`$variants` URL paths** — spec mentions file/URL; URLs deferred.
|
||||
- **zod `SerializedPackage` shape for the emitted JSON** — the plugin emits `SerializedPackage` objects; a zod schema for the emitted module would give runtime validation beyond the ambient `declare module`.
|
||||
- **`setup` value expansion** — `type` without `id` → all parts of that type is documented but not implemented in the loader (it's a game-state init concern; noted as future).
|
||||
- **Surface mounting is validated but not resolved** — `mount`/`children`/`surfaces` are parsed and validated, but the loader doesn't resolve child→parent relationships or enforce that a setup's `surfaces`/a surface's `children` reference existing surfaces. That's a game-state/rendering concern (see `docs/bgm-tabletop.md`).
|
||||
- Docs for the loader itself (this file is the start).
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# bgm-tabletop
|
||||
|
||||
a r3f based interactive component library to work with [bgm](./bgm-format.md) board games. will be used somewhere in the `web` app's bgm inspector routes.
|
||||
|
||||
## 1. stack
|
||||
|
||||
`react` - react, react router, tailwindv4
|
||||
`r3f` - r3f, drei, postprocessing
|
||||
`zustand` - for state management
|
||||
|
||||
## 2. states
|
||||
|
||||
source-of-truth game state:
|
||||
|
||||
```ts
|
||||
{
|
||||
surfaces: Record<string, boolean>, // enabled per surface id
|
||||
paths: Record<string, string[]>, // path -> part list
|
||||
}
|
||||
```
|
||||
|
||||
**assumption:** each piece on the board has a unique id, even tokens of the same type. so each entry in a path's list is a unique piece id, and a piece id never appears twice in the same path. this makes the render list keyed by piece id stable and unambiguous.
|
||||
|
||||
derived surface render state: game state + surface routes => map of piece id to `{ surface, route, candidate, index, stackSize }` for rendering on a surface. keys of this map makes a stable render list.
|
||||
|
||||
- `route` - the matched route.
|
||||
- `candidate` - the matched candidate for a `:param` route, carrying its anchor `x`/`y`/`rotation`. absent for routes without candidates.
|
||||
- `index` - the piece's position in its path's stack.
|
||||
- `stackSize` - the number of pieces on the path.
|
||||
|
||||
the render map is per enabled surface: a piece may appear on more than one enabled surface (e.g. an expansion path and the main board), and each is rendered independently.
|
||||
|
||||
## 3. components
|
||||
|
||||
- `SetupLoader` side effect only component that seeds the game state with setup (enabled surfaces + part placement).
|
||||
- `WorldSurfaceView` mounts a surface to world space.
|
||||
- `HudSurfaceView` mounts a surface to hud space.
|
||||
- `PartPlacement` a stable per-part component that positions a part on a surface location. uses the stacking hook (below) to apply the route's stacking strategy.
|
||||
- `PartView` used in `PartPlacement`, creates a mesh from part definition. a standalone component library, so it reuses geometry/shape code from `@tts/mesh` rather than the web app's viewers.
|
||||
|
||||
## 4. stacking
|
||||
|
||||
the format's stacking strategy (curve / limit / align / steps, see `bgm-format.md` §4) is implemented as a hook, e.g. `useStacking(route.stacking, index, stackSize)`, returning the offset/rotation to apply to a piece. `PartPlacement` consumes it.
|
||||
|
||||
## 5. usage
|
||||
|
||||
- we will inspect individual parts with `PartView` in the web app's part inspection route.
|
||||
- as a library, the public surface is the components above: mount a surface with `WorldSurfaceView`/`HudSurfaceView`, seed state with `SetupLoader`, and let `PartPlacement`/`PartView` render the pieces. the web app is one consumer; the library should not assume the web app's routes or store.
|
||||
- a surface is mounted only when enabled; a disabled surface is not rendered.
|
||||
Reference in New Issue
Block a user