feat(bgm): add surface mounting and setup surfaces

This commit is contained in:
2026-08-09 21:03:20 +08:00
parent 4e18ab5bc8
commit 599fb6a76d
8 changed files with 187 additions and 10 deletions
@@ -46,6 +46,13 @@ 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:
@@ -56,6 +63,29 @@ layout:
rotation: 0
```
```yaml file=parts/player.yaml
type: board
id: player
role: surface
size: [200, 200]
mount:
kind: child
x: 100
y: 50
rotation: 0
layout:
- route: /hand/:slot
candidates:
$variants: ./hand.csv
```
```csv file=parts/hand.csv
slot,x,y,rotation
string,number,number,number
0,0,0,0
1,0,20,0
```
```csv file=parts/seats.csv
seat,x,y,rotation
string,number,number,number
@@ -69,6 +99,9 @@ string,number,number,number
role: setup
type: game
id: main
surfaces:
- board#harbor
- board#player
setup:
/dock/0: harbor:token#wood
/deck: harbor:token#grain
+10 -3
View File
@@ -26,10 +26,12 @@ describe('collectPackages', () => {
expect(wood.face).toBe('./assets/tokens.png');
expect(wood.faceCrop).toEqual([1, 0, 5, 2]);
// One surface with a $variants-expanded candidates list.
expect([...harbor.surfaces.keys()]).toEqual(['board#harbor']);
// Two surfaces: the table board and its child player board.
expect([...harbor.surfaces.keys()].sort()).toEqual(['board#harbor', 'board#player']);
const board = harbor.surfaces.get('board#harbor')!;
expect(board.size).toEqual([300, 200]);
expect(board.mount).toEqual({ kind: 'table', x: 0, y: 0, rotation: 0 });
expect(board.children).toEqual(['board#player']);
expect(board.layout).toHaveLength(2);
const dock = board.layout[0]!;
expect(dock.route).toBe('/dock/:seat');
@@ -41,9 +43,14 @@ describe('collectPackages', () => {
expect(deck.route).toBe('/deck');
expect(deck).toMatchObject({ x: -100, y: 0, rotation: 0 });
// One setup.
const player = harbor.surfaces.get('board#player')!;
expect(player.mount).toEqual({ kind: 'child', x: 100, y: 50, rotation: 0 });
expect(player.layout).toHaveLength(1);
// One setup, declaring the enabled surfaces.
expect([...harbor.setups.keys()]).toEqual(['game#main']);
const setup = harbor.setups.get('game#main')!;
expect(setup.surfaces).toEqual(['board#harbor', 'board#player']);
expect(setup.setup).toEqual({
'/dock/0': 'harbor:token#wood',
'/deck': 'harbor:token#grain',
+11
View File
@@ -40,16 +40,27 @@ const partSchema = z.object({
fillet: z.number().optional(),
});
const surfaceMount = z.object({
kind: z.enum(['table', 'hud', 'child']),
x: z.number().optional(),
y: z.number().optional(),
rotation: z.number().optional(),
area: z.string().optional(),
});
const surfaceSchema = z.object({
type: z.string().min(1),
id: z.string().min(1),
size: surfaceSize.optional(),
mount: surfaceMount.optional(),
children: z.array(z.string()).optional(),
layout: z.array(route),
});
const setupSchema = z.object({
type: z.string().min(1),
id: z.string().min(1),
surfaces: z.array(z.string()).optional(),
setup: z.record(z.string(), z.union([z.string(), z.array(z.string())])),
});
+24 -1
View File
@@ -104,21 +104,44 @@ export interface Stacking {
steps?: number;
}
/** How a surface is mounted. `kind` selects the mount type. */
export type SurfaceMountKind = 'table' | 'hud' | 'child';
/**
* How a surface is mounted. Always an object, anchored by `x`/`y`/`rotation`
* like a route. `table` is the root table surface; `hud` mounts to a HUD
* area; `child` mounts relative to a parent surface (see `children`).
*/
export interface SurfaceMount {
kind: SurfaceMountKind;
x?: number;
y?: number;
rotation?: number;
/** HUD area, for `kind: hud`. */
area?: string;
}
/** A view over the state store, purely for visual rendering. */
export interface Surface {
type: string;
id: string;
/** Reference `[width, height]` in mm; may be scaled to fit the table. */
size?: SurfaceSize;
/** How this surface is mounted; defaults to `{ kind: 'table' }`. */
mount?: SurfaceMount;
/** Child surfaces (`type#id` refs), mounted relative to this surface. */
children?: string[];
layout: Route[];
}
export type SetupValue = string | string[];
/** Seeds the state store: a map from path to a stack of parts. */
/** Seeds the state store: the enabled surfaces and a map from path to parts. */
export interface Setup {
type: string;
id: string;
/** Surfaces (`type#id` refs) enabled at the start; omitted = all enabled. */
surfaces?: string[];
setup: Record<string, SetupValue>;
}
+1 -1
View File
@@ -67,7 +67,7 @@ describe('bgm vite plugin', () => {
// Consumers read the collections with Object.values / Object.keys.
expect(Object.values(pkg.parts).map((p: any) => p.id).sort()).toEqual(['grain', 'wood']);
expect(Object.keys(pkg.surfaces)).toEqual(['board#harbor']);
expect(Object.keys(pkg.surfaces)).toEqual(['board#harbor', 'board#player']);
expect(Object.keys(pkg.setups)).toEqual(['game#main']);
});