# 02 · UI 原语 API 设计 ## 做什么 在动手写代码之前,先把 UI 组件的 props、game.ui 字段结构、数据流定义清楚,写成 `docs/ui-primitives.md` 供实现参考。 ## 遇到什么问题 1. **接口界定**:组件要通用(支持任意 boardgame.io 游戏),又不能过于抽象失去表现力。 2. **game.ui 字段怎么设计**:是松散的"约定"还是强类型的 schema? 3. **拖拽 API**:TTS 用 Lua 控制每个对象的交互。JS 端怎么抽象? 4. **玩家视角**:boardgame.io master 会按 playerID 过滤状态,UI 怎么配合? ## 怎么解决 ### 5 个设计原则 1. **声明式**:业务代码只描述"是什么",不描述"怎么画" 2. **数据驱动**:所有可见状态都来自 `G`(boardgame.io 的 state) 3. **服务器权威**:所有 game-affecting 操作都通过 `onMove` 触发 reducer 4. **玩家视角**:UI 接收的 `G` 已经是 boardgame.io 过滤后的版本 5. **可扩展**:通过 `game.ui.overrides` 注入自定义组件 ### 8 个组件 `` `` `` `` `` `` `` `` ### 6 个 zone type `hand` / `pile` / `discard` / `grid` / `area` / `free` ### 关键 schema ```ts interface UISchema { background?: string; zones: ZoneDef[]; cards?: CardTemplate; tokens?: TokenDef[]; actions?: ActionDef[]; overrides?: UIOverrides; } interface ZoneDef { id: string; type: 'hand' | 'pile' | 'discard' | 'grid' | 'area' | 'free'; position: { x: number; y: number }; size?: { w: number; h: number }; layout?: { fan?: number; spacing?: number; maxWidth?: number }; faceDown?: boolean; owner?: string | null; // 'self' | 'P0' | null(公开) collection?: string; // G 中持有 ID 数组的字段名 entityField?: string; // G 中持有 ID → 实体映射的字段名 dropMove?: string; // 接收 drop 时调用的 move dropArgs?: (cardIds, fromZoneId, toZoneId) => any[]; accepts?: string | ((card, ctx) => boolean); // 接收规则 } ``` ### 6 个待决项(已锁定 ✅) | 待决项 | 决定 | | --- | --- | | zone 坐标系统 | 固定像素 + Board 视口缩放 | | 动画策略 | CSS transition 默认 + 游戏可覆盖 | | 可访问性 | v1 仅鼠标 | | 触屏/移动端 | v1 仅桌面 | | 多选 UI | v1 仅 Shift+click | | 牌堆数量显示 | 角标显示数量 | 资产格式:SVG 内嵌字符串优先(fallback DataURL/URL)。 ## 成果 - `docs/ui-primitives.md`(15 KB,511 行) - 6 个待决项的设计默认值锁定,避免实现时反复讨论 ## 关联 - [04-ui-primitive-impl.md](./04-ui-primitive-impl.md) — 按这份设计实现 - [03-monorepo-scaffold.md](./03-monorepo-scaffold.md) — 同步开始搭骨架