Compare commits
2 Commits
fda996031f
...
2ca096f821
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ca096f821 | |||
| 2b6087de4c |
@@ -0,0 +1,561 @@
|
||||
# 声控狗叫对战 2D 浏览器游戏设计与实现计划
|
||||
|
||||
## 目标
|
||||
|
||||
基于用户提供的视频:
|
||||
|
||||
`C:\Users\DSK\Videos\一款双方比狗叫的游戏 - 1.一款双方比狗叫的游戏(Av116504192360177,P1).mp4`
|
||||
|
||||
提取其中“双方比狗叫”的核心玩法,并按照 BDD / TDD / DDD 的方法,为 Genarrative 中可运行于浏览器的 2D 游戏方案生成一份可落地设计与实现思路。实现方向遵循仓库内 `game-studio` 插件工作流,默认采用 2D Phaser + TypeScript + Vite + DOM HUD 的浏览器游戏架构。
|
||||
|
||||
本计划仅做方案设计,不直接编码。
|
||||
|
||||
## 当前上下文与输入分析
|
||||
|
||||
### 已识别视频核心画面
|
||||
|
||||
通过抽帧观察,视频中的游戏呈现出以下稳定特征:
|
||||
|
||||
- 画面是横版 2D 手绘舞台,场景包括公园、海边等固定关卡背景。
|
||||
- 双方各有一只狗作为对战角色,站在左右两侧。
|
||||
- 中央有明显倒计时,例如 `30`、`28`。
|
||||
- 顶部有红蓝双方拉锯式能量条 / 进度条。
|
||||
- 中央提示出现:`对着麦克风汪一声`、`用声音大小 + 叫声次数推动能量条!`
|
||||
- 玩家输入不是传统键鼠,而是麦克风声音。
|
||||
- 玩家需要模仿狗叫,系统根据声音大小与叫声次数推动能量条。
|
||||
- 屏幕会根据叫声出现 `BARK`、`WOOF`、`WAN`、`WANGOOF` 等拟声词与冲击波视觉反馈。
|
||||
- 回合结束时,根据能量条偏向或推进结果判定胜负。
|
||||
|
||||
### 提炼出的核心玩法
|
||||
|
||||
这是一个“声控拔河式狗叫对战”小游戏:
|
||||
|
||||
- 两名玩家 / 一名玩家对 AI 分别代表左右两只狗。
|
||||
- 每局限时 30 秒。
|
||||
- 玩家通过麦克风持续发出狗叫声。
|
||||
- 游戏实时分析音量峰值、叫声次数、叫声节奏。
|
||||
- 声音越大、叫声越密集,己方推动力越强。
|
||||
- 顶部能量条在双方推动力差值下左右移动。
|
||||
- 时间结束后,能量条偏向哪一方,哪一方获胜。
|
||||
|
||||
### 需要合理抽象的地方
|
||||
|
||||
视频中存在直播弹幕、贴图、表情包、遮挡层,这些不是游戏本体机制。本方案只吸收游戏本体核心:
|
||||
|
||||
- 双方狗狗对叫
|
||||
- 麦克风输入
|
||||
- 声音强度 + 次数判定
|
||||
- 红蓝拉锯能量条
|
||||
- 限时回合
|
||||
- 夸张拟声词与冲击波反馈
|
||||
|
||||
## game-studio 插件路线
|
||||
|
||||
根据仓库内 `.hermes/plugins/game-studio` 技能:
|
||||
|
||||
- 早期游戏工作先走 `game-studio` 总入口。
|
||||
- 2D 浏览器游戏默认选择 Phaser。
|
||||
- 架构上需要分离 simulation 与 renderer。
|
||||
- HUD / 菜单 / 设置优先使用 DOM overlay,不把密集文字塞进 canvas。
|
||||
- 玩法状态不应由 Phaser Scene 直接持有,Scene 只负责渲染、动画、相机、输入适配。
|
||||
|
||||
因此本方案采用:
|
||||
|
||||
- Runtime:Phaser 3
|
||||
- Language:TypeScript
|
||||
- Build:Vite
|
||||
- UI:React/DOM HUD overlay 或项目现有 DOM UI 层
|
||||
- Audio input:Web Audio API + MediaDevices.getUserMedia
|
||||
- Simulation:纯 TS domain/service 层
|
||||
- Renderer:Phaser Scene 读取 simulation snapshot 并播放动画/特效
|
||||
|
||||
## 游戏概念设计
|
||||
|
||||
### 游戏名建议
|
||||
|
||||
- 中文:`汪汪声浪大作战`
|
||||
- 英文代号:`bark-battle`
|
||||
- Play type ID 建议:`bark-battle`
|
||||
|
||||
### 玩家幻想
|
||||
|
||||
玩家不是通过按键战斗,而是真的对着麦克风“汪汪叫”,把自己的狗狗声浪推向对手。游戏目标是在倒计时结束前用更响、更密集、更有节奏的叫声赢得声浪拔河。
|
||||
|
||||
### 核心动词
|
||||
|
||||
- 叫:对麦克风发出狗叫声。
|
||||
- 推:通过叫声推动能量条。
|
||||
- 压制:让能量条持续向对手方向倾斜。
|
||||
- 爆发:短时间内连续高质量叫声触发冲击波。
|
||||
- 防守:对手强势时通过持续叫声把能量条拉回。
|
||||
|
||||
### 单局流程
|
||||
|
||||
1. 准备阶段
|
||||
- 展示双方狗狗、地图、麦克风权限提示。
|
||||
- 用户授权麦克风。
|
||||
- 系统检测环境噪音并校准阈值。
|
||||
|
||||
2. 倒计时阶段
|
||||
- 3、2、1 或中央 `30` 倒计时开始。
|
||||
- 玩家看到提示:`对着麦克风汪一声`。
|
||||
|
||||
3. 对战阶段
|
||||
- 每帧或固定 tick 采集麦克风音量。
|
||||
- 根据音量峰值与短促叫声次数计算本方 barkPower。
|
||||
- AI 或远端对手产生 opponentPower。
|
||||
- 能量条根据 `playerPower - opponentPower` 拉锯。
|
||||
- 狗狗张嘴动画、拟声词、冲击波按声音强度生成。
|
||||
|
||||
4. 结算阶段
|
||||
- 30 秒结束。
|
||||
- 能量条偏玩家侧则胜利,偏对手侧则失败,接近中线则平局。
|
||||
- 展示叫声次数、最大音量、平均节奏、声浪评分。
|
||||
|
||||
5. 重开 / 返回
|
||||
- 支持再来一局。
|
||||
- 支持返回玩法入口或结果页。
|
||||
|
||||
## 规则设计
|
||||
|
||||
### 关键状态
|
||||
|
||||
```ts
|
||||
type BarkBattlePhase = 'permission' | 'calibration' | 'countdown' | 'playing' | 'finished'
|
||||
|
||||
type BarkBattleSnapshot = {
|
||||
phase: BarkBattlePhase
|
||||
remainingMs: number
|
||||
energy: number // -100 到 100,负数偏对手,正数偏玩家
|
||||
player: BarkSideState
|
||||
opponent: BarkSideState
|
||||
winner: 'player' | 'opponent' | 'draw' | null
|
||||
}
|
||||
|
||||
type BarkSideState = {
|
||||
barkCount: number
|
||||
currentVolume: number
|
||||
recentPeak: number
|
||||
combo: number
|
||||
power: number
|
||||
isBarking: boolean
|
||||
}
|
||||
```
|
||||
|
||||
### 输入判定
|
||||
|
||||
#### 音量采样
|
||||
|
||||
- 使用 Web Audio API 创建 `AnalyserNode`。
|
||||
- 每个 simulation tick 读取频域或时域数据。
|
||||
- 计算 RMS 或 peak volume。
|
||||
- 根据校准后的环境噪音设置动态阈值。
|
||||
|
||||
#### 一次“叫声”的判定
|
||||
|
||||
一次有效叫声建议满足:
|
||||
|
||||
- 音量超过 `barkThreshold`。
|
||||
- 与上一次叫声峰值至少间隔 `minBarkGapMs`,避免持续噪音被无限计数。
|
||||
- 持续时长在合理范围,例如 80ms 到 1200ms。
|
||||
- 可选:频谱能量集中在中高频,不强制做复杂语音识别,MVP 先用音量 + 峰值节奏。
|
||||
|
||||
#### 推动力计算
|
||||
|
||||
```text
|
||||
playerPower = volumeScore * 0.65 + barkRateScore * 0.35 + comboBonus
|
||||
opponentPower = aiPower 或远端玩家 power
|
||||
energyDelta = (playerPower - opponentPower) * deltaTime * balanceFactor
|
||||
energy = clamp(energy + energyDelta, -100, 100)
|
||||
```
|
||||
|
||||
### AI 对手 MVP
|
||||
|
||||
若先做单机浏览器版,右侧对手可由 AI 模拟:
|
||||
|
||||
- 简单难度:周期性小叫,power 低。
|
||||
- 普通难度:有节奏地爆发,power 中等。
|
||||
- 困难难度:根据玩家领先程度自适应追赶,但不得作弊到不可赢。
|
||||
|
||||
后续可扩展为多人实时对战。
|
||||
|
||||
## BDD 行为场景
|
||||
|
||||
### 功能: 麦克风授权与准备
|
||||
|
||||
```gherkin
|
||||
功能: 狗叫对战麦克风准备
|
||||
为了让玩家能用声音参与对战
|
||||
作为浏览器玩家
|
||||
我希望游戏在开局前明确请求麦克风权限并完成环境校准
|
||||
|
||||
场景: 玩家允许麦克风权限后进入准备倒计时
|
||||
假如玩家打开狗叫对战页面
|
||||
当玩家同意浏览器麦克风授权
|
||||
那么系统应进入环境噪音校准阶段
|
||||
而且校准完成后应显示开局倒计时
|
||||
|
||||
场景: 玩家拒绝麦克风权限
|
||||
假如玩家打开狗叫对战页面
|
||||
当玩家拒绝浏览器麦克风授权
|
||||
那么系统应显示无法声控游玩的提示
|
||||
而且应提供重试授权入口
|
||||
而且不应直接开始对战
|
||||
```
|
||||
|
||||
### 功能: 声音推动能量条
|
||||
|
||||
```gherkin
|
||||
功能: 声音大小和叫声次数推动能量条
|
||||
为了复刻双方比狗叫的核心体验
|
||||
作为玩家
|
||||
我希望自己的叫声能实时推动顶部能量条
|
||||
|
||||
场景: 玩家发出一次有效狗叫
|
||||
假如游戏处于 playing 阶段
|
||||
而且麦克风输入音量超过有效叫声阈值
|
||||
当系统检测到一次新的叫声峰值
|
||||
那么玩家叫声次数应增加 1
|
||||
而且玩家狗狗应播放张嘴吠叫动画
|
||||
而且画面应出现拟声词反馈
|
||||
|
||||
场景: 玩家连续大声狗叫压制对手
|
||||
假如游戏处于 playing 阶段
|
||||
而且玩家在短时间内产生多次有效叫声
|
||||
当玩家推动力高于对手推动力
|
||||
那么顶部能量条应向玩家侧移动
|
||||
而且玩家侧声浪特效应增强
|
||||
|
||||
场景: 环境噪音低于阈值不计入叫声
|
||||
假如游戏处于 playing 阶段
|
||||
当麦克风只有低于阈值的背景噪音
|
||||
那么玩家叫声次数不应增加
|
||||
而且能量条不应因为背景噪音明显移动
|
||||
```
|
||||
|
||||
### 功能: 限时胜负结算
|
||||
|
||||
```gherkin
|
||||
功能: 狗叫对战胜负结算
|
||||
为了让单局对抗有明确目标
|
||||
作为玩家
|
||||
我希望倒计时结束后根据能量条位置判定胜负
|
||||
|
||||
场景: 倒计时结束时玩家侧占优
|
||||
假如游戏剩余时间归零
|
||||
而且能量条位于玩家侧
|
||||
当系统进入结算阶段
|
||||
那么系统应判定玩家胜利
|
||||
而且展示玩家叫声次数、最大音量和声浪评分
|
||||
|
||||
场景: 倒计时结束时双方接近平衡
|
||||
假如游戏剩余时间归零
|
||||
而且能量条处于平局阈值范围内
|
||||
当系统进入结算阶段
|
||||
那么系统应判定为平局
|
||||
而且展示再来一局入口
|
||||
```
|
||||
|
||||
### 功能: 移动端与无麦克风降级
|
||||
|
||||
```gherkin
|
||||
功能: 声控游戏移动端与无麦克风降级
|
||||
为了让不同设备玩家都能理解当前状态
|
||||
作为移动端或无麦克风环境玩家
|
||||
我希望系统给出清晰、可操作的降级路径
|
||||
|
||||
场景: 当前浏览器不支持麦克风 API
|
||||
假如玩家设备不支持 getUserMedia
|
||||
当玩家进入狗叫对战页面
|
||||
那么系统应显示设备不支持麦克风输入
|
||||
而且提供返回入口
|
||||
|
||||
场景: 移动端进入对战页面
|
||||
假如玩家使用移动端浏览器
|
||||
当玩家进入狗叫对战页面
|
||||
那么主要能量条、倒计时和狗狗角色应保持可见
|
||||
而且非关键设置应收起到菜单中
|
||||
```
|
||||
|
||||
## DDD 领域划分
|
||||
|
||||
### 领域层:bark-battle domain
|
||||
|
||||
职责:只处理玩法规则,不依赖 Phaser、DOM、Web Audio、后端。
|
||||
|
||||
建议模块:
|
||||
|
||||
- `BarkBattleSession`
|
||||
- 管理 phase、remainingMs、energy、winner。
|
||||
- `BarkDetector`
|
||||
- 根据音量样本判断是否形成一次有效叫声。
|
||||
- `EnergyTugOfWar`
|
||||
- 根据双方 power 更新能量条。
|
||||
- `BarkBattleScoring`
|
||||
- 计算最大音量、叫声次数、combo、评分。
|
||||
- `OpponentStrategy`
|
||||
- 单机 AI 对手策略接口。
|
||||
|
||||
领域规则必须可用纯单元测试验证。
|
||||
|
||||
### 应用层:use case / controller
|
||||
|
||||
职责:编排麦克风输入、simulation tick、AI 对手、结果输出。
|
||||
|
||||
建议用例:
|
||||
|
||||
- `requestMicrophonePermission()`
|
||||
- `calibrateAmbientNoise()`
|
||||
- `startBarkBattleSession()`
|
||||
- `submitAudioSample(sample)`
|
||||
- `tickBarkBattle(deltaMs)`
|
||||
- `finishBarkBattle()`
|
||||
|
||||
### 基础设施层
|
||||
|
||||
职责:浏览器 API 与引擎适配。
|
||||
|
||||
- `BrowserMicrophoneInput`
|
||||
- 封装 `navigator.mediaDevices.getUserMedia`。
|
||||
- 输出 normalized volume samples。
|
||||
- `PhaserBarkBattleScene`
|
||||
- 渲染狗狗、背景、拟声词、冲击波。
|
||||
- 不持有核心玩法规则。
|
||||
- `DomBarkBattleHud`
|
||||
- 展示倒计时、能量条、权限提示、结算面板。
|
||||
|
||||
### 表现层
|
||||
|
||||
- Phaser Canvas:地图、狗狗、声浪、粒子、拟声词。
|
||||
- DOM HUD:顶部能量条、倒计时、权限/结算/设置面板。
|
||||
|
||||
## TDD 落地顺序
|
||||
|
||||
### 第一轮:领域规则 RED-GREEN-REFACTOR
|
||||
|
||||
先写纯 TS 单元测试,不接 Phaser,不接麦克风。
|
||||
|
||||
目标测试:
|
||||
|
||||
- `BarkDetector`:超过阈值且间隔足够时计为一次叫声。
|
||||
- `BarkDetector`:持续噪音不会无限增加叫声次数。
|
||||
- `EnergyTugOfWar`:玩家 power 高于对手时 energy 向玩家侧移动。
|
||||
- `EnergyTugOfWar`:energy 被 clamp 在 -100 到 100。
|
||||
- `BarkBattleSession`:倒计时归零后进入 finished。
|
||||
- `BarkBattleSession`:根据 energy 判定 player/opponent/draw。
|
||||
|
||||
### 第二轮:应用层测试
|
||||
|
||||
- 模拟音频 sample 输入,验证 session snapshot 更新。
|
||||
- 模拟 AI 对手 power,验证能量条拉锯。
|
||||
- 模拟权限失败,验证 phase 不进入 playing。
|
||||
|
||||
### 第三轮:组件 / 集成测试
|
||||
|
||||
- HUD 根据 snapshot 显示倒计时。
|
||||
- HUD 根据 energy 渲染红蓝能量条比例。
|
||||
- 权限拒绝时显示重试入口。
|
||||
- 结算阶段显示胜负与再来一局。
|
||||
|
||||
### 第四轮:浏览器 smoke / playtest
|
||||
|
||||
- 本地启动页面。
|
||||
- 授权麦克风。
|
||||
- 对麦克风发声后看到拟声词与能量条变化。
|
||||
- 移动端宽度下主游戏画面不被 HUD 遮挡。
|
||||
|
||||
## 建议文件结构
|
||||
|
||||
如果作为独立前端玩法原型,可采用:
|
||||
|
||||
```text
|
||||
src/games/bark-battle/
|
||||
domain/
|
||||
BarkBattleSession.ts
|
||||
BarkDetector.ts
|
||||
EnergyTugOfWar.ts
|
||||
BarkBattleScoring.ts
|
||||
OpponentStrategy.ts
|
||||
application/
|
||||
BarkBattleController.ts
|
||||
BrowserMicrophoneInput.ts
|
||||
phaser/
|
||||
BarkBattleScene.ts
|
||||
BarkBattlePreloadScene.ts
|
||||
barkBattleAssets.ts
|
||||
ui/
|
||||
BarkBattleHud.tsx
|
||||
BarkBattleResultPanel.tsx
|
||||
BarkBattlePermissionPanel.tsx
|
||||
tests/
|
||||
BarkDetector.test.ts
|
||||
EnergyTugOfWar.test.ts
|
||||
BarkBattleSession.test.ts
|
||||
```
|
||||
|
||||
如果接入 Genarrative 玩法类型闭环,后续还需要按 `genarrative-play-type-integration` 扩展:
|
||||
|
||||
```text
|
||||
src/components/bark-battle-runtime/BarkBattleRuntimeShell.tsx
|
||||
src/components/bark-battle-result/BarkBattleResultView.tsx
|
||||
src/services/barkBattleRuntimeClient.ts
|
||||
packages/shared/src/contracts/barkBattle.ts
|
||||
server-rs/crates/shared-contracts/src/bark_battle.rs
|
||||
```
|
||||
|
||||
MVP 阶段建议先做浏览器单机 runtime 原型,再决定是否进入创作入口、作品发布、广场和后端持久化。
|
||||
|
||||
## UI / 视觉方向
|
||||
|
||||
### 画面
|
||||
|
||||
- 横版固定舞台。
|
||||
- 左右两只狗对峙。
|
||||
- 背景可先做公园一张图,后续扩展海边、街区等地图。
|
||||
- 狗狗用 2D sprite 或简单骨架帧动画。
|
||||
|
||||
### HUD
|
||||
|
||||
- 顶部:红蓝声浪能量条。
|
||||
- 中央:大号倒计时,只在开局和关键时间突出显示。
|
||||
- 左右:双方狗狗状态,不堆叠复杂面板。
|
||||
- 底部或角落:麦克风状态、小型重试按钮。
|
||||
- 结算:居中弹出简洁面板,显示胜负和关键数据。
|
||||
|
||||
### 动效
|
||||
|
||||
- 叫声触发狗狗张嘴。
|
||||
- 声音越大,拟声词越大,冲击波越宽。
|
||||
- combo 时触发短暂屏幕震动,但不能遮挡能量条。
|
||||
- 尊重 reduced motion,非必要动画可降级。
|
||||
|
||||
## 测试映射
|
||||
|
||||
| BDD 场景 | 测试层级 | 目标文件 | 状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| 玩家允许麦克风权限后进入准备倒计时 | application/component | `BarkBattleController.test.ts`, `BarkBattlePermissionPanel.test.tsx` | planned |
|
||||
| 玩家拒绝麦克风权限 | application/component | `BarkBattleController.test.ts`, `BarkBattlePermissionPanel.test.tsx` | planned |
|
||||
| 玩家发出一次有效狗叫 | unit | `BarkDetector.test.ts` | planned |
|
||||
| 玩家连续大声狗叫压制对手 | unit/integration | `EnergyTugOfWar.test.ts`, `BarkBattleController.test.ts` | planned |
|
||||
| 环境噪音低于阈值不计入叫声 | unit | `BarkDetector.test.ts` | planned |
|
||||
| 倒计时结束时玩家侧占优 | unit | `BarkBattleSession.test.ts` | planned |
|
||||
| 倒计时结束时双方接近平衡 | unit | `BarkBattleSession.test.ts` | planned |
|
||||
| 当前浏览器不支持麦克风 API | component | `BarkBattlePermissionPanel.test.tsx` | planned |
|
||||
| 移动端进入对战页面 | visual/smoke | Playwright 或人工 playtest 清单 | planned |
|
||||
|
||||
## 验证命令建议
|
||||
|
||||
具体命令以后续实际落地位置为准,建议包括:
|
||||
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/**/*.test.ts
|
||||
npm run test -- --run src/games/bark-battle/**/*.test.tsx
|
||||
npm run typecheck
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
若接入 Genarrative 后端或玩法配置,还需要追加:
|
||||
|
||||
```bash
|
||||
cd server-rs && cargo check -p api-server -p shared-contracts --no-default-features
|
||||
npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts
|
||||
```
|
||||
|
||||
## 实施阶段拆分
|
||||
|
||||
### Phase 0:产品与技术定稿
|
||||
|
||||
- 确认玩法 ID:`bark-battle`。
|
||||
- 确认 MVP 只做单机玩家 vs AI,不做实时多人。
|
||||
- 确认是否只做 runtime 原型,还是接入 Genarrative 创作入口。
|
||||
- 确认是否允许浏览器麦克风权限作为核心输入。
|
||||
|
||||
### Phase 1:纯领域模型
|
||||
|
||||
- 建立 bark-battle domain。
|
||||
- 按 TDD 写 `BarkDetector`、`EnergyTugOfWar`、`BarkBattleSession` 测试。
|
||||
- 实现最小规则让测试通过。
|
||||
|
||||
### Phase 2:麦克风输入适配
|
||||
|
||||
- 封装 Web Audio API。
|
||||
- 支持权限请求、权限失败、环境噪音校准。
|
||||
- 使用 mock input 完成自动化测试,真实麦克风做 smoke。
|
||||
|
||||
### Phase 3:Phaser 2D runtime
|
||||
|
||||
- 新建 Phaser Scene。
|
||||
- 绘制或占位加载公园背景、左右狗狗、声浪特效。
|
||||
- Scene 只消费 snapshot,不写规则。
|
||||
- 接入 DOM HUD。
|
||||
|
||||
### Phase 4:反馈与结算
|
||||
|
||||
- 加入拟声词、冲击波、狗狗张嘴动画。
|
||||
- 加入结算面板。
|
||||
- 加入再来一局与返回入口。
|
||||
|
||||
### Phase 5:Genarrative 集成可选项
|
||||
|
||||
若要正式接入玩法类型:
|
||||
|
||||
- 补 `shared-contracts` 中 bark-battle runtime/result DTO。
|
||||
- 补前端 service 与 runtime shell。
|
||||
- 补入口配置数据库 seed。
|
||||
- 补作品架 / 发布 / 广场链路,若需要持久化成绩或作品。
|
||||
- 按 `genarrative-play-type-integration` 执行完整闭环验证。
|
||||
|
||||
## 风险与权衡
|
||||
|
||||
### 麦克风权限风险
|
||||
|
||||
浏览器麦克风权限受 HTTPS、浏览器策略、用户设置影响。MVP 需要明确:
|
||||
|
||||
- 本地开发可在 localhost 使用。
|
||||
- 线上必须 HTTPS。
|
||||
- 权限拒绝需要可恢复。
|
||||
|
||||
### 声音识别准确性风险
|
||||
|
||||
MVP 不建议做复杂“是否真的是狗叫”的 AI 识别,否则实现成本高、误判多。建议先用:
|
||||
|
||||
- 音量阈值
|
||||
- 峰值次数
|
||||
- 节奏间隔
|
||||
- 环境噪音校准
|
||||
|
||||
后续再考虑加入频谱特征或 ML 分类。
|
||||
|
||||
### 噪音作弊风险
|
||||
|
||||
玩家可以喊叫、拍桌子或播放音频。若是娱乐派对玩法可以接受;若要竞技公平,需要后续加入:
|
||||
|
||||
- 频谱特征
|
||||
- 输入冷却
|
||||
- 异常持续噪音削弱
|
||||
- 本地/服务端反作弊策略
|
||||
|
||||
### 移动端兼容风险
|
||||
|
||||
移动端 Web Audio 可能需要用户手势激活 AudioContext。计划中需把“开始”按钮作为显式用户手势,避免自动启动失败。
|
||||
|
||||
### UI 遮挡风险
|
||||
|
||||
视频原型中的核心可读信息非常少:倒计时、能量条、狗狗、拟声词。实现时应避免把说明文案、复杂面板长期铺在画面上。
|
||||
|
||||
## 开放问题
|
||||
|
||||
1. MVP 是“玩家 vs AI”,还是需要从第一版开始支持双人同屏 / 联机?
|
||||
2. 是否要作为 Genarrative 新玩法入口完整接入,还是先做独立 runtime 原型?
|
||||
3. 是否需要记录成绩、发布作品、进入作品架和广场?
|
||||
4. 狗狗与背景素材是使用临时占位、AI 生成,还是需要复用项目既有素材系统?
|
||||
5. 是否允许游戏强依赖麦克风权限,还是必须提供键盘备用输入?
|
||||
|
||||
## 推荐下一步
|
||||
|
||||
建议下一步先执行 Phase 0 + Phase 1:
|
||||
|
||||
1. 明确 MVP 边界:单机玩家 vs AI。
|
||||
2. 写 `BarkDetector` / `EnergyTugOfWar` / `BarkBattleSession` 的 BDD 对应单元测试。
|
||||
3. 不接 Phaser、不接麦克风,先把核心规则用 TDD 跑通。
|
||||
4. 规则稳定后再接 Web Audio 与 Phaser runtime。
|
||||
BIN
.hermes/plans/frame_003.jpg
Normal file
BIN
.hermes/plans/frame_003.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
BIN
.hermes/plans/frame_010.jpg
Normal file
BIN
.hermes/plans/frame_010.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
BIN
.hermes/plans/frame_020.jpg
Normal file
BIN
.hermes/plans/frame_020.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
BIN
.hermes/plans/frame_035.jpg
Normal file
BIN
.hermes/plans/frame_035.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
@@ -0,0 +1,662 @@
|
||||
# bark-battle 2D Runtime 前端技术方案(2026-05-11)
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
本方案基于 `.hermes/plans/2026-05-11_144229-bark-battle-2d-game-bdd-ddd-tdd-plan.md`,为“汪汪声浪大作战 / bark-battle”细化前端与浏览器游戏 runtime 的技术实现路线。
|
||||
|
||||
本任务只产出技术方案,不直接实现代码。后续编码应以本文作为 runtime 层设计约束,再按 TDD 小步落地。
|
||||
|
||||
### 1.1 玩法定位
|
||||
|
||||
`bark-battle` 是一个声控拔河式 2D 浏览器小游戏:玩家对麦克风发出狗叫声,系统根据音量峰值、有效叫声次数和节奏计算本方声浪推动力,在限时 30 秒内推动顶部红蓝能量条,时间结束后按能量条偏向判定胜负。
|
||||
|
||||
### 1.2 本文范围
|
||||
|
||||
本文覆盖:
|
||||
|
||||
- Phaser + TypeScript + Vite 栈选择
|
||||
- simulation / render / HUD 边界
|
||||
- 建议目录结构
|
||||
- 核心 domain 类型
|
||||
- Web Audio 输入适配
|
||||
- Phaser Scene 切分
|
||||
- DOM HUD 设计
|
||||
- 移动端与权限降级
|
||||
- 测试与验证命令
|
||||
|
||||
本文不覆盖:
|
||||
|
||||
- 后端表结构、持久化成绩、作品发布、广场接入
|
||||
- 实时多人对战协议
|
||||
- 复杂 AI 狗叫识别模型
|
||||
- 美术素材正式生产流程
|
||||
|
||||
## 2. 技术栈选择
|
||||
|
||||
### 2.1 推荐栈
|
||||
|
||||
```text
|
||||
Runtime Renderer: Phaser 3
|
||||
Language: TypeScript
|
||||
Build: Vite
|
||||
Host UI: React / DOM overlay
|
||||
Audio Input: Web Audio API + MediaDevices.getUserMedia
|
||||
Test: Vitest + Testing Library + 浏览器 smoke / Playwright 可选
|
||||
```
|
||||
|
||||
### 2.2 选择理由
|
||||
|
||||
1. 玩法是横版 2D 舞台,核心表现是狗狗 sprite、声浪、拟声词、粒子、屏幕震动与能量条反馈,Phaser 对 2D 渲染、时间循环、sprite animation、camera、粒子和 Scene 生命周期支持成熟。
|
||||
2. 当前项目主前端已使用 TypeScript + Vite,继续复用现有构建和测试体系,避免引入独立构建链。
|
||||
3. 文字密集、权限提示、结算、设置和移动端响应式布局适合 DOM HUD;Canvas 保持负责 playfield 和动态特效。
|
||||
4. Web Audio API 可以在浏览器端完成 MVP 所需音量采样、RMS/peak 计算、环境噪音校准和输入归一化,不需要首版接入后端音频处理。
|
||||
|
||||
### 2.3 不选择其它路线的原因
|
||||
|
||||
- 不使用 Three.js / 3D:当前玩法画面是 2D 横版舞台,不需要 3D 相机、模型和材质管线。
|
||||
- 不把 HUD 全部塞入 Phaser Canvas:权限说明、重试、结算、移动端布局和可访问性更适合 DOM。
|
||||
- 不在前端实现正式业务真相:浏览器 runtime 可承载单局即时 simulation,但若后续涉及成绩、作品、排行榜、发布和奖励,必须交给后端投影/API 裁决。
|
||||
|
||||
## 3. 总体架构
|
||||
|
||||
### 3.1 分层总览
|
||||
|
||||
```text
|
||||
React Runtime Shell
|
||||
├─ DOM HUD / Panels
|
||||
│ ├─ PermissionPanel
|
||||
│ ├─ TopEnergyBar
|
||||
│ ├─ TimerChip
|
||||
│ └─ ResultPanel
|
||||
│
|
||||
├─ Application Controller
|
||||
│ ├─ permission / calibration orchestration
|
||||
│ ├─ simulation tick
|
||||
│ ├─ audio sample submission
|
||||
│ └─ snapshot publish
|
||||
│
|
||||
├─ Pure Domain / Simulation
|
||||
│ ├─ BarkBattleSession
|
||||
│ ├─ BarkDetector
|
||||
│ ├─ EnergyTugOfWar
|
||||
│ ├─ BarkBattleScoring
|
||||
│ └─ OpponentStrategy
|
||||
│
|
||||
├─ Infrastructure Adapters
|
||||
│ ├─ BrowserMicrophoneInput
|
||||
│ ├─ AudioAnalyserSampler
|
||||
│ └─ PhaserGameHost
|
||||
│
|
||||
└─ Phaser Renderer
|
||||
├─ BootScene
|
||||
├─ PreloadScene
|
||||
├─ BattleScene
|
||||
├─ FxScene / DebugScene(可选)
|
||||
└─ Asset manifest
|
||||
```
|
||||
|
||||
### 3.2 强制边界
|
||||
|
||||
1. `domain/` 不依赖 Phaser、Web Audio、DOM、React、浏览器全局对象或后端 API。
|
||||
2. `domain/` 只接收 plain data,例如时间增量、归一化音量样本、对手 power、配置参数。
|
||||
3. `application/` 负责编排权限、校准、音频输入、AI 对手、tick 和 snapshot 分发。
|
||||
4. Phaser Scene 只消费 `BarkBattleSnapshot`,把 snapshot 映射成 sprite、动画、粒子、camera 和 sound effect;不得持有核心胜负、计数和能量条规则。
|
||||
5. DOM HUD 只消费 snapshot 和少量 runtime UI 状态,负责展示、按钮和弹层;不得重复实现核心胜负规则。
|
||||
6. 若后续接入平台作品/成绩/排行榜,前端只调用后端 API 和展示投影,不在本地绕过后端生成正式结论。
|
||||
|
||||
## 4. 建议目录结构
|
||||
|
||||
首版建议以独立 runtime 原型落在 `src/games/bark-battle/`,避免提前侵入平台创作链路。
|
||||
|
||||
```text
|
||||
src/games/bark-battle/
|
||||
domain/
|
||||
BarkBattleTypes.ts
|
||||
BarkBattleSession.ts
|
||||
BarkDetector.ts
|
||||
EnergyTugOfWar.ts
|
||||
BarkBattleScoring.ts
|
||||
OpponentStrategy.ts
|
||||
__tests__/
|
||||
BarkDetector.test.ts
|
||||
EnergyTugOfWar.test.ts
|
||||
BarkBattleSession.test.ts
|
||||
BarkBattleScoring.test.ts
|
||||
|
||||
application/
|
||||
BarkBattleController.ts
|
||||
BarkBattleConfig.ts
|
||||
BarkBattleSnapshotStore.ts
|
||||
__tests__/
|
||||
BarkBattleController.test.ts
|
||||
|
||||
infrastructure/
|
||||
BrowserMicrophoneInput.ts
|
||||
AudioAnalyserSampler.ts
|
||||
MicrophonePermission.ts
|
||||
__tests__/
|
||||
AudioAnalyserSampler.test.ts
|
||||
|
||||
phaser/
|
||||
BarkBattleGameHost.ts
|
||||
scenes/
|
||||
BarkBattleBootScene.ts
|
||||
BarkBattlePreloadScene.ts
|
||||
BarkBattleScene.ts
|
||||
BarkBattleFxScene.ts
|
||||
assets/
|
||||
barkBattleAssetManifest.ts
|
||||
|
||||
ui/
|
||||
BarkBattleRuntimeShell.tsx
|
||||
BarkBattleHud.tsx
|
||||
BarkBattlePermissionPanel.tsx
|
||||
BarkBattleResultPanel.tsx
|
||||
BarkBattleMobileControls.tsx
|
||||
BarkBattleHud.css
|
||||
__tests__/
|
||||
BarkBattleHud.test.tsx
|
||||
BarkBattlePermissionPanel.test.tsx
|
||||
BarkBattleResultPanel.test.tsx
|
||||
```
|
||||
|
||||
若后续进入 Genarrative 正式玩法类型闭环,再按 `genarrative-play-type-integration` 扩展到:
|
||||
|
||||
```text
|
||||
src/components/bark-battle-runtime/BarkBattleRuntimeShell.tsx
|
||||
src/components/bark-battle-result/BarkBattleResultView.tsx
|
||||
src/services/barkBattleRuntimeClient.ts
|
||||
packages/shared/src/contracts/barkBattle.ts
|
||||
server-rs/crates/shared-contracts/src/bark_battle.rs
|
||||
```
|
||||
|
||||
首版不建议直接新增后端表或正式作品链路,除非产品明确要求成绩、发布和广场能力。
|
||||
|
||||
## 5. 核心 Domain 类型
|
||||
|
||||
### 5.1 基础枚举与数值约定
|
||||
|
||||
```ts
|
||||
export type BarkBattlePhase =
|
||||
| 'permission'
|
||||
| 'calibration'
|
||||
| 'countdown'
|
||||
| 'playing'
|
||||
| 'finished'
|
||||
| 'unsupported'
|
||||
| 'permission-denied'
|
||||
|
||||
export type BarkBattleSide = 'player' | 'opponent'
|
||||
|
||||
export type BarkBattleWinner = BarkBattleSide | 'draw' | null
|
||||
|
||||
export type BarkBattleDifficulty = 'easy' | 'normal' | 'hard'
|
||||
```
|
||||
|
||||
关键数值:
|
||||
|
||||
- `energy`: `-100..100`,正数偏玩家侧,负数偏对手侧。
|
||||
- `currentVolume`: `0..1`,音频采样归一化后的瞬时音量。
|
||||
- `recentPeak`: `0..1`,短窗口内峰值。
|
||||
- `power`: `0..1` 或 `0..100` 二选一,建议 domain 内统一 `0..1`,HUD 显示再转百分比。
|
||||
- `remainingMs`: 单局剩余毫秒。
|
||||
|
||||
### 5.2 Snapshot
|
||||
|
||||
```ts
|
||||
export type BarkBattleSnapshot = {
|
||||
phase: BarkBattlePhase
|
||||
elapsedMs: number
|
||||
remainingMs: number
|
||||
countdownMs: number
|
||||
energy: number
|
||||
player: BarkSideState
|
||||
opponent: BarkSideState
|
||||
winner: BarkBattleWinner
|
||||
result: BarkBattleResult | null
|
||||
lastEvents: BarkBattleVisualEvent[]
|
||||
}
|
||||
|
||||
export type BarkSideState = {
|
||||
side: BarkBattleSide
|
||||
barkCount: number
|
||||
currentVolume: number
|
||||
recentPeak: number
|
||||
combo: number
|
||||
power: number
|
||||
isBarking: boolean
|
||||
lastBarkAtMs: number | null
|
||||
maxVolume: number
|
||||
}
|
||||
|
||||
export type BarkBattleResult = {
|
||||
winner: BarkBattleWinner
|
||||
finalEnergy: number
|
||||
playerBarkCount: number
|
||||
playerMaxVolume: number
|
||||
playerAveragePower: number
|
||||
score: number
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 输入样本与叫声事件
|
||||
|
||||
```ts
|
||||
export type BarkAudioSample = {
|
||||
atMs: number
|
||||
volume: number
|
||||
peak: number
|
||||
rms: number
|
||||
}
|
||||
|
||||
export type BarkDetectedEvent = {
|
||||
id: string
|
||||
atMs: number
|
||||
side: BarkBattleSide
|
||||
volume: number
|
||||
strength: number
|
||||
combo: number
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 视觉事件
|
||||
|
||||
视觉事件由 domain 或 application 生成,但只包含 plain data,不包含 Phaser 对象:
|
||||
|
||||
```ts
|
||||
export type BarkBattleVisualEvent =
|
||||
| {
|
||||
type: 'bark-word'
|
||||
id: string
|
||||
side: BarkBattleSide
|
||||
atMs: number
|
||||
strength: number
|
||||
text: 'BARK' | 'WOOF' | 'WAN' | 'WANGOOF'
|
||||
}
|
||||
| {
|
||||
type: 'shockwave'
|
||||
id: string
|
||||
side: BarkBattleSide
|
||||
atMs: number
|
||||
strength: number
|
||||
}
|
||||
| {
|
||||
type: 'combo-burst'
|
||||
id: string
|
||||
side: BarkBattleSide
|
||||
atMs: number
|
||||
combo: number
|
||||
}
|
||||
```
|
||||
|
||||
Phaser 只根据这些事件播放一次性特效,并维护已消费事件 ID,避免重复播放。
|
||||
|
||||
## 6. Domain 模块职责
|
||||
|
||||
### 6.1 BarkDetector
|
||||
|
||||
职责:把连续音频样本转换为有效叫声事件。
|
||||
|
||||
输入:
|
||||
|
||||
- `BarkAudioSample`
|
||||
- 校准后的 `ambientNoiseFloor`
|
||||
- `barkThreshold`
|
||||
- `minBarkGapMs`
|
||||
- `minBarkDurationMs`
|
||||
- `maxBarkDurationMs`
|
||||
|
||||
规则建议:
|
||||
|
||||
1. 音量超过动态阈值进入 candidate 状态。
|
||||
2. 峰值回落到阈值以下或持续时长达到上限时结束 candidate。
|
||||
3. candidate 持续时间在 `80ms..1200ms` 且与上一次有效叫声间隔足够时,记为一次叫声。
|
||||
4. 长时间持续噪音不应无限计数,只能按冷却和峰值回落形成新事件。
|
||||
5. MVP 不要求识别“是否真狗叫”,先基于音量峰值、时长和间隔判断。
|
||||
|
||||
### 6.2 EnergyTugOfWar
|
||||
|
||||
职责:更新红蓝拉锯条。
|
||||
|
||||
建议公式:
|
||||
|
||||
```text
|
||||
playerPower = volumeScore * 0.65 + barkRateScore * 0.35 + comboBonus
|
||||
opponentPower = opponentStrategy.tick(...)
|
||||
energyDelta = (playerPower - opponentPower) * deltaSeconds * balanceFactor
|
||||
energy = clamp(energy + energyDelta, -100, 100)
|
||||
```
|
||||
|
||||
约束:
|
||||
|
||||
- `EnergyTugOfWar` 不知道玩家来自麦克风还是 mock input。
|
||||
- `EnergyTugOfWar` 不知道 Phaser 能量条宽度。
|
||||
- 平衡参数集中在 `BarkBattleConfig`,不要散落在 Scene 或 HUD 中。
|
||||
|
||||
### 6.3 BarkBattleSession
|
||||
|
||||
职责:管理局内 phase、计时、胜负和 snapshot。
|
||||
|
||||
状态机建议:
|
||||
|
||||
```text
|
||||
permission → calibration → countdown → playing → finished
|
||||
↘ permission-denied
|
||||
↘ unsupported
|
||||
```
|
||||
|
||||
关键规则:
|
||||
|
||||
- `countdown` 结束才进入 `playing`。
|
||||
- `playing` 时 `remainingMs` 随 tick 递减。
|
||||
- `remainingMs <= 0` 后进入 `finished`。
|
||||
- `energy > drawThreshold` 判定玩家胜利。
|
||||
- `energy < -drawThreshold` 判定对手胜利。
|
||||
- `abs(energy) <= drawThreshold` 判定平局。
|
||||
|
||||
### 6.4 OpponentStrategy
|
||||
|
||||
职责:为单机 MVP 提供对手推动力。
|
||||
|
||||
```ts
|
||||
export interface OpponentStrategy {
|
||||
tick(input: OpponentTickInput): OpponentTickOutput
|
||||
}
|
||||
```
|
||||
|
||||
普通难度建议:
|
||||
|
||||
- 周期性小叫声提供基础压力。
|
||||
- 每 3~6 秒一次短爆发。
|
||||
- 玩家大幅领先时可轻微增强,但不能追到不可赢。
|
||||
|
||||
## 7. Web Audio 输入适配
|
||||
|
||||
### 7.1 BrowserMicrophoneInput
|
||||
|
||||
职责:封装浏览器麦克风权限与音频流生命周期。
|
||||
|
||||
建议 API:
|
||||
|
||||
```ts
|
||||
export interface MicrophoneInputPort {
|
||||
isSupported(): boolean
|
||||
requestPermission(): Promise<MicrophoneSession>
|
||||
stop(): void
|
||||
}
|
||||
|
||||
export interface MicrophoneSession {
|
||||
sample(atMs: number): BarkAudioSample
|
||||
stop(): void
|
||||
}
|
||||
```
|
||||
|
||||
实现要点:
|
||||
|
||||
1. 使用 `navigator.mediaDevices?.getUserMedia({ audio: true })`。
|
||||
2. 在用户点击“开始”后创建或 resume `AudioContext`,避免移动端自动播放策略拦截。
|
||||
3. 使用 `AnalyserNode` 读取时域数据,计算 RMS 与 peak。
|
||||
4. 输出归一化样本,不把 `MediaStream`、`AudioContext`、`AnalyserNode` 泄漏到 domain。
|
||||
5. 退出、重开、页面卸载时停止 track,避免麦克风占用残留。
|
||||
|
||||
### 7.2 校准流程
|
||||
|
||||
`calibration` 阶段建议持续 `800ms..1500ms`:
|
||||
|
||||
1. 收集静默环境样本。
|
||||
2. 计算 `ambientNoiseFloor`,例如 `p75` 或均值 + 标准差。
|
||||
3. 设置动态阈值:
|
||||
|
||||
```text
|
||||
barkThreshold = clamp(ambientNoiseFloor + 0.12, 0.18, 0.55)
|
||||
```
|
||||
|
||||
4. 若环境噪音过高,HUD 给出简短提示和“继续 / 重新校准”入口,但不要把长说明常驻在画面上。
|
||||
|
||||
### 7.3 权限与错误分类
|
||||
|
||||
```ts
|
||||
export type MicrophoneFailureReason =
|
||||
| 'unsupported'
|
||||
| 'permission-denied'
|
||||
| 'not-found'
|
||||
| 'not-readable'
|
||||
| 'audio-context-blocked'
|
||||
| 'unknown'
|
||||
```
|
||||
|
||||
前端只根据错误分类展示可操作状态:重试授权、返回、或使用调试备用输入。不要把浏览器原始错误堆栈展示给玩家。
|
||||
|
||||
## 8. Phaser Scene 切分
|
||||
|
||||
### 8.1 BarkBattleBootScene
|
||||
|
||||
职责:
|
||||
|
||||
- 初始化 Phaser 全局配置。
|
||||
- 注册 scale、background color、全局事件桥。
|
||||
- 不加载重资源,不处理玩法规则。
|
||||
|
||||
### 8.2 BarkBattlePreloadScene
|
||||
|
||||
职责:
|
||||
|
||||
- 根据 `barkBattleAssetManifest` 加载背景、狗狗 sprite、声浪 FX、拟声词 bitmap / atlas、轻量音效。
|
||||
- 使用稳定 manifest key,不在 gameplay 代码中散写文件路径。
|
||||
- 加载完成后进入 `BarkBattleScene`。
|
||||
|
||||
### 8.3 BarkBattleScene
|
||||
|
||||
职责:
|
||||
|
||||
- 创建横版舞台、左右狗狗、背景层、声浪层、拟声词层。
|
||||
- 每帧读取最新 `BarkBattleSnapshot`。
|
||||
- 根据 snapshot 更新:
|
||||
- 狗狗 idle / bark / win / lose 动画
|
||||
- 声浪强度
|
||||
- camera shake
|
||||
- transient bark words
|
||||
- shockwave
|
||||
- 把可选的调试输入 action 传给 controller,但不处理麦克风和规则。
|
||||
|
||||
不得在 Scene 中实现:
|
||||
|
||||
- 叫声计数
|
||||
- 胜负判定
|
||||
- 能量条规则
|
||||
- 权限流程
|
||||
- 结算数据计算
|
||||
|
||||
### 8.4 BarkBattleFxScene(可选)
|
||||
|
||||
如果特效复杂,可拆出叠加 Scene:
|
||||
|
||||
- 专门处理拟声词、粒子、冲击波和 camera shake。
|
||||
- 通过视觉事件 ID 去重。
|
||||
- 对 `prefers-reduced-motion` 或低端设备降级。
|
||||
|
||||
首版也可以先把 FX 保持在 `BarkBattleScene` 内,但必须仍然只消费 snapshot / visual events。
|
||||
|
||||
## 9. DOM HUD 设计
|
||||
|
||||
### 9.1 HUD 层级
|
||||
|
||||
DOM HUD 建议覆盖在 Phaser Canvas 上方:
|
||||
|
||||
```text
|
||||
BarkBattleRuntimeShell
|
||||
├─ <div className="bark-battle-canvas-host" />
|
||||
└─ <BarkBattleHud snapshot={snapshot} uiState={uiState} />
|
||||
```
|
||||
|
||||
HUD 分区:
|
||||
|
||||
- 顶部:红蓝声浪能量条 + 小型剩余时间。
|
||||
- 中央:仅在倒计时、关键提示或结算时短暂展示大号状态。
|
||||
- 左右边缘:双方简洁状态,例如叫声次数 / combo chip。
|
||||
- 底部角落:麦克风状态、重试、小菜单。
|
||||
- 结算:独立居中面板,显示胜负、叫声次数、最大音量、评分、再来一局、返回。
|
||||
|
||||
### 9.2 Playfield 保护
|
||||
|
||||
遵循 game UI 约束:
|
||||
|
||||
1. 正常 playing 阶段保持中心和下中部 playfield 清爽,不常驻长文案。
|
||||
2. 不把规则说明、长控制说明、多段提示默认铺在画面上。
|
||||
3. 权限、设置、结算使用独立面板或弹层,不在当前面板下面展开一大块内容。
|
||||
4. 移动端优先保证顶部能量条、倒计时、狗狗和重试入口可见可点。
|
||||
5. 大动效不能遮挡顶部能量条和倒计时。
|
||||
|
||||
### 9.3 CSS 设计建议
|
||||
|
||||
- 使用局部 CSS class 或 CSS module,避免污染全站。
|
||||
- 使用 CSS 变量定义主题:
|
||||
- `--bark-player-color`
|
||||
- `--bark-opponent-color`
|
||||
- `--bark-panel-bg`
|
||||
- `--bark-safe-bottom`
|
||||
- 使用 `dvh` / `svh` 和 safe-area inset 处理移动端地址栏与刘海。
|
||||
- `pointer-events` 分层:HUD 容器默认 `pointer-events: none`,按钮和面板恢复 `pointer-events: auto`。
|
||||
|
||||
## 10. 移动端与权限降级
|
||||
|
||||
### 10.1 移动端输入约束
|
||||
|
||||
移动端浏览器通常要求用户手势才能启动 AudioContext。开局流程必须是:
|
||||
|
||||
```text
|
||||
玩家点击“开始” → requestPermission → 创建/恢复 AudioContext → calibration → countdown → playing
|
||||
```
|
||||
|
||||
不要在页面加载时自动请求或自动启动 AudioContext。
|
||||
|
||||
### 10.2 响应式布局
|
||||
|
||||
移动端建议:
|
||||
|
||||
- 横屏优先呈现完整舞台;竖屏可保持舞台居中并缩小 HUD。
|
||||
- 顶部能量条高度保持可读,但不要占满大面积。
|
||||
- 结算面板宽度使用 `min(92vw, 420px)`。
|
||||
- 底部按钮避开 `env(safe-area-inset-bottom)`。
|
||||
- 非关键设置折叠进小菜单。
|
||||
|
||||
### 10.3 权限失败降级
|
||||
|
||||
权限失败时:
|
||||
|
||||
- `unsupported`:展示“当前浏览器不支持麦克风输入”,提供返回入口。
|
||||
- `permission-denied`:展示简短说明和“重新授权”入口。
|
||||
- `not-found`:提示未检测到麦克风,提供返回入口。
|
||||
- `audio-context-blocked`:提示点击重试。
|
||||
|
||||
可选开发调试降级:
|
||||
|
||||
- 本地 dev 可启用键盘 mock input,例如按住空格模拟音量峰值。
|
||||
- mock input 必须标记为开发/调试能力,不作为正式竞技能力。
|
||||
|
||||
## 11. 测试策略
|
||||
|
||||
### 11.1 Domain 单元测试(优先)
|
||||
|
||||
目标:不接 Phaser、不接 DOM、不接 Web Audio。
|
||||
|
||||
建议测试:
|
||||
|
||||
- `BarkDetector`:超过阈值且间隔足够时计为一次有效叫声。
|
||||
- `BarkDetector`:持续噪音不会无限计数。
|
||||
- `BarkDetector`:低于环境噪音阈值不计入叫声。
|
||||
- `EnergyTugOfWar`:玩家 power 高于对手时 energy 向玩家侧移动。
|
||||
- `EnergyTugOfWar`:energy clamp 在 `-100..100`。
|
||||
- `BarkBattleSession`:倒计时结束进入 playing。
|
||||
- `BarkBattleSession`:剩余时间归零进入 finished。
|
||||
- `BarkBattleSession`:按 energy 和 drawThreshold 判定胜 / 负 / 平。
|
||||
|
||||
### 11.2 Application 测试
|
||||
|
||||
目标:验证输入样本、AI、tick 和 snapshot 编排。
|
||||
|
||||
建议测试:
|
||||
|
||||
- 权限允许后进入校准,再进入倒计时。
|
||||
- 权限拒绝后 phase 为 `permission-denied`,不进入 playing。
|
||||
- 提交 mock audio sample 后 snapshot 中玩家状态更新。
|
||||
- AI 对手 power 参与能量条拉锯。
|
||||
- `lastEvents` 只发布新增视觉事件。
|
||||
|
||||
### 11.3 HUD 组件测试
|
||||
|
||||
目标:验证 snapshot 到 DOM 的展示映射。
|
||||
|
||||
建议测试:
|
||||
|
||||
- playing 阶段展示倒计时和能量条。
|
||||
- energy 正负值映射到玩家 / 对手侧比例。
|
||||
- permission-denied 展示重试入口。
|
||||
- finished 展示胜负、叫声次数和再来一局。
|
||||
- 移动端 class / 结构不依赖 Phaser Canvas 才能渲染。
|
||||
|
||||
### 11.4 Phaser 集成与 smoke
|
||||
|
||||
自动化层面不强行在 Vitest 中完整启动 Phaser。建议:
|
||||
|
||||
- 用 adapter mock 测试 Scene 消费 snapshot 的纯映射函数。
|
||||
- 浏览器 smoke 验证真实 Canvas、Web Audio 和动画。
|
||||
- 若后续引入 Playwright,再做最小视觉和交互 smoke。
|
||||
|
||||
## 12. 验证命令建议
|
||||
|
||||
文档阶段只需要编码检查和 diff 检查:
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
git diff -- docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md
|
||||
```
|
||||
|
||||
后续实现 domain 后建议:
|
||||
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/domain/**/*.test.ts
|
||||
npm run typecheck
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
后续实现 HUD 后建议:
|
||||
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/ui/**/*.test.tsx
|
||||
npm run lint:eslint
|
||||
npm run typecheck
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
后续接入浏览器 runtime 后建议:
|
||||
|
||||
```bash
|
||||
npm run dev:web
|
||||
# 人工 smoke:授权麦克风 → 校准 → 发声 → 能量条变化 → 结算 → 再来一局
|
||||
```
|
||||
|
||||
若未来接入 Genarrative 正式玩法类型、后端持久化或发布链路,再追加对应契约、api-server 和 SpacetimeDB 验证;首版 runtime 原型不应提前新增这些命令作为门槛。
|
||||
|
||||
## 13. 后续落地顺序
|
||||
|
||||
建议后续实现按以下顺序推进:
|
||||
|
||||
1. 先建 `domain/` 和纯单元测试。
|
||||
2. 实现 `BarkDetector`、`EnergyTugOfWar`、`BarkBattleSession` 的最小规则。
|
||||
3. 建 `application/` controller,用 mock audio sample 跑通 snapshot。
|
||||
4. 实现 DOM HUD 的 permission / energy / timer / result 展示。
|
||||
5. 接入 `BrowserMicrophoneInput` 和校准流程。
|
||||
6. 接入 Phaser host、Scene 和 asset manifest,占位素材先跑通视觉反馈。
|
||||
7. 做移动端视口和权限失败 smoke。
|
||||
8. 产品确认后再决定是否进入正式玩法类型、作品发布和后端真相链。
|
||||
|
||||
## 14. 关键技术决策
|
||||
|
||||
1. 默认采用 Phaser 3 + TypeScript + Vite,符合 2D 浏览器游戏默认路线。
|
||||
2. 核心 simulation 放在纯 TypeScript domain,严格不依赖 Phaser / Web Audio / DOM。
|
||||
3. Web Audio 只作为输入 adapter,输出归一化 `BarkAudioSample`。
|
||||
4. Phaser Scene 是 renderer,只消费 snapshot 和 visual events,不承载规则真相。
|
||||
5. HUD 使用 DOM overlay,承载权限、能量条、倒计时、结算和移动端响应式布局。
|
||||
6. MVP 不做复杂狗叫语义识别,先用音量峰值、持续时长、冷却和环境噪音校准。
|
||||
7. MVP 建议玩家 vs AI 单机 runtime,正式成绩、排行榜、发布和奖励后续再交给后端链路。
|
||||
@@ -0,0 +1,799 @@
|
||||
# bark-battle 后端 DDD 技术方案(2026-05-11)
|
||||
|
||||
## 1. 背景、范围与非目标
|
||||
|
||||
### 1.1 背景
|
||||
|
||||
`bark-battle` / “汪汪声浪大作战”是一个浏览器 2D 声控狗叫对战玩法。玩家通过麦克风发出狗叫声,浏览器 runtime 根据音量峰值、有效叫声次数与节奏推动顶部红蓝能量条;每局默认 30 秒;结束后按能量条偏向判定胜负或平局。
|
||||
|
||||
现有前端方案 `docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md` 已覆盖 Phaser / TypeScript / Vite / Web Audio / DOM HUD 的 runtime 落地方式,并明确不覆盖后端表结构、成绩持久化、作品发布、广场接入与实时多人协议。因此需要单独补充后端 DDD 技术方案,避免前端 runtime 在接入平台作品、正式游玩埋点、成绩、排行榜和发布闭环时承接不属于表现层的业务真相。
|
||||
|
||||
本方案遵循当前 Genarrative 后端路线:`server-rs` + Axum + SpacetimeDB。DDD 边界保持为:
|
||||
|
||||
- `module-*`:纯领域规则与 use case 约束。
|
||||
- `spacetime-module`:SpacetimeDB 表、reducer、migration。
|
||||
- `spacetime-client`:SpacetimeDB 绑定调用 facade。
|
||||
- `api-server`:HTTP / SSE / BFF 门面与权限校验入口。
|
||||
- `shared-contracts`:前后端共享 DTO、请求、响应、错误码与 schema 约束。
|
||||
- `platform-*`:作品、发布、广场、埋点等平台能力的领域边界。
|
||||
|
||||
### 1.2 本文范围
|
||||
|
||||
本文覆盖后端方案,不实现代码:
|
||||
|
||||
1. bark-battle 玩法接入级别建议。
|
||||
2. 后端 DDD 分层与职责边界。
|
||||
3. shared contracts 草案。
|
||||
4. SpacetimeDB 数据模型草案。
|
||||
5. HTTP API / facade 草案。
|
||||
6. SpacetimeDB migration 与绑定生成策略。
|
||||
7. 安全、隐私与反作弊约束。
|
||||
8. TDD / 验收顺序与可执行命令。
|
||||
9. 与现有前端方案和 BDD 文档的关系。
|
||||
|
||||
### 1.3 非目标
|
||||
|
||||
MVP 明确不做:
|
||||
|
||||
- 不保存原始麦克风音频、音频片段、可还原语音内容的 waveform 或频谱明细。
|
||||
- 不做实时多人在线对战;首版只支持本地 runtime + 后端记录派生结果。
|
||||
- 不做复杂 AI 声纹识别、狗叫语义识别、身份声纹比对或真人/动物声纹分类。
|
||||
- 不由前端直接写正式成绩、排行榜或作品发布状态。
|
||||
- 不把 Phaser / Web Audio / DOM HUD 逻辑迁入后端。
|
||||
- 不在本方案中实现代码、建表或生成绑定。
|
||||
|
||||
## 2. 玩法接入级别建议
|
||||
|
||||
### 2.1 推荐首版闭环
|
||||
|
||||
建议先支持“本地 runtime + 可发布配置化作品 + 单局结果记录 / 可选排行榜”的闭环:
|
||||
|
||||
1. 创作者创建 bark-battle 草稿,配置标题、描述、狗狗主题、背景、难度、单局时长、音量阈值、AI 对手参数和排行榜开关。
|
||||
2. 发布为稳定作品 ID,`playTypeId = "bark-battle"`。
|
||||
3. 玩家从作品页或广场进入 runtime,前端获取发布态 runtime config。
|
||||
4. 玩家授权麦克风后在本地完成 30 秒声控对战。
|
||||
5. 前端提交单局 finish 请求,只上传派生指标,例如峰值、有效叫声次数、节奏命中、最终能量、客户端结果摘要等。
|
||||
6. 后端校验 work、config version、run token、时长、分数范围和权限后,生成服务端认可的 run result / score summary。
|
||||
7. 若作品开启排行榜,则写入可投影的 leaderboard 记录。
|
||||
8. 正式作品级游玩埋点统一写 `work_play_start`,其中 `scope_kind=work`,`scope_id=稳定作品 ID`,metadata 包含 `playType`、`workId`、`sourceRoute`、`userId`。
|
||||
|
||||
### 2.2 后续增强路径
|
||||
|
||||
后续再考虑多人实时:
|
||||
|
||||
- Phase 2:排行榜、挑战分享、个人历史成绩、作品统计面板。
|
||||
- Phase 3:异步影子对手 / ghost replay,但仍不保存原始音频,只保存低维派生曲线或聚合指标。
|
||||
- Phase 4:实时多人对战协议,需要独立同步模型、房间服务、延迟补偿、断线恢复与更严格反作弊;不应混入 MVP。
|
||||
|
||||
## 3. DDD 分层设计
|
||||
|
||||
### 3.1 总体分层
|
||||
|
||||
```text
|
||||
frontend/runtime
|
||||
-> api-server HTTP BFF
|
||||
-> shared-contracts DTO
|
||||
-> spacetime-client facade
|
||||
-> generated SpacetimeDB bindings
|
||||
-> spacetime-module reducers / tables / migration.rs
|
||||
-> module-bark-battle pure domain
|
||||
-> platform-work / platform-publish / platform-tracking / platform-leaderboard
|
||||
```
|
||||
|
||||
### 3.2 `module-bark-battle` 职责
|
||||
|
||||
建议新增 `server-rs/crates/module-bark-battle/`,只放纯领域规则,不依赖 Axum、SpacetimeDB SDK、HTTP、数据库绑定或前端类型。
|
||||
|
||||
职责:
|
||||
|
||||
- 校验 bark-battle 配置合法性。
|
||||
- 定义配置版本兼容规则。
|
||||
- 计算提交结果的派生分数区间与胜负判定是否自洽。
|
||||
- 计算 `ScoreSummary`、排行榜排序分数、统计指标。
|
||||
- 定义反作弊基础规则:时长范围、有效叫声次数上限、峰值范围、能量范围、提交窗口、run 状态机。
|
||||
|
||||
不职责:
|
||||
|
||||
- 不访问数据库。
|
||||
- 不处理 HTTP 请求。
|
||||
- 不生成 SpacetimeDB 表。
|
||||
- 不处理麦克风音频采样。
|
||||
|
||||
### 3.3 `shared-contracts` 职责
|
||||
|
||||
建议新增 `server-rs/crates/shared-contracts/src/bark_battle.rs`,并在前端共享契约生成流程中对齐同名 DTO。
|
||||
|
||||
职责:
|
||||
|
||||
- 定义草稿、发布态 runtime config、work summary、run start、run finish、run result、score summary、leaderboard DTO。
|
||||
- 定义 request / response / error code。
|
||||
- 保持字段命名、枚举值、时间单位和数值范围稳定。
|
||||
|
||||
约束:
|
||||
|
||||
- DTO 可以表达业务数据,但不承载领域算法。
|
||||
- 前端不得手写与后端不一致的正式契约;如果存在 TypeScript mirror,需要由契约生成或测试保证一致。
|
||||
|
||||
### 3.4 `spacetime-module` 职责
|
||||
|
||||
建议在现有 SpacetimeDB 模块边界内新增 bark-battle 表与 reducer,或按当前仓库约定新增独立模块文件。职责:
|
||||
|
||||
- 定义表:作品配置、发布版本、runtime run、score/stat、leaderboard entry。
|
||||
- 定义 reducer / procedure:保存草稿、发布版本、开始 run、结束 run、写排行榜、查询投影所需索引。
|
||||
- 维护 migration。
|
||||
|
||||
必须明确:
|
||||
|
||||
- 所有表结构变更进入 `migration.rs`。
|
||||
- SpacetimeDB 绑定通过既有生成命令生成。
|
||||
- 不手改生成物,不手改 generated bindings。
|
||||
|
||||
### 3.5 `spacetime-client` 职责
|
||||
|
||||
职责:
|
||||
|
||||
- 封装 generated bindings,向 `api-server` 提供稳定 facade。
|
||||
- 隐藏 reducer 名称、表订阅细节和绑定类型差异。
|
||||
- 将 SpacetimeDB 错误转换为后端内部错误模型。
|
||||
|
||||
不职责:
|
||||
|
||||
- 不放业务规则主逻辑。
|
||||
- 不绕过 `module-bark-battle` 做胜负和分数裁决。
|
||||
|
||||
### 3.6 `api-server` 职责
|
||||
|
||||
Axum HTTP / BFF 门面职责:
|
||||
|
||||
- 鉴权、用户上下文、work 权限、发布态读取权限。
|
||||
- 解析请求、调用 domain 校验、调用 spacetime-client facade。
|
||||
- 将内部错误映射成 HTTP status + shared error code。
|
||||
- 负责正式作品级游玩埋点入口,统一写 `work_play_start`。
|
||||
- 为前端 runtime 提供一次性 start token / run id,避免匿名 finish 直接刷榜。
|
||||
|
||||
### 3.7 `platform-*` 职责
|
||||
|
||||
- `platform-work`:稳定作品 ID、作品所有权、作品摘要、作品状态。
|
||||
- `platform-publish`:草稿到发布态版本、config version、发布可见性。
|
||||
- `platform-tracking`:统一埋点,尤其 `work_play_start`。
|
||||
- `platform-leaderboard`:若已有通用排行榜能力,bark-battle 只提供 score projection,不重复建设平台级排名系统。
|
||||
|
||||
### 3.8 `frontend/runtime` 职责边界
|
||||
|
||||
前端只做:
|
||||
|
||||
- Phaser / DOM HUD 表现。
|
||||
- Web Audio 采样、环境噪声校准和本地即时反馈。
|
||||
- 本地临时 UI 状态:权限、倒计时、动画、结算展示。
|
||||
- 调用 start / finish / leaderboard API。
|
||||
|
||||
前端不得承接正式业务真相:
|
||||
|
||||
- 不直接决定正式排行榜结果。
|
||||
- 不直接写作品发布状态。
|
||||
- 不绕过后端写成绩。
|
||||
- 不上传原始麦克风音频。
|
||||
|
||||
## 4. shared contracts 设计草案
|
||||
|
||||
以下为字段草案,具体 Rust / TypeScript 命名按仓库契约规范落地。
|
||||
|
||||
### 4.1 BarkBattleDraft
|
||||
|
||||
```text
|
||||
BarkBattleDraft {
|
||||
draftId: string
|
||||
ownerUserId: string
|
||||
playTypeId: "bark-battle"
|
||||
title: string
|
||||
description?: string
|
||||
theme: BarkBattleTheme
|
||||
runtimeConfig: BarkBattleRuntimeConfigDraft
|
||||
leaderboardEnabled: boolean
|
||||
visibility: "private" | "unlisted" | "public"
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 BarkBattleRuntimeConfig
|
||||
|
||||
```text
|
||||
BarkBattleRuntimeConfig {
|
||||
workId: string
|
||||
configVersion: number
|
||||
playTypeId: "bark-battle"
|
||||
durationMs: number // MVP 默认 30000
|
||||
energyMin: number // 默认 -100
|
||||
energyMax: number // 默认 100
|
||||
winEnergyThreshold: number // 可选:低于阈值可平局
|
||||
barkThreshold: number // 归一化 0..1
|
||||
peakWeight: number
|
||||
barkCountWeight: number
|
||||
rhythmWeight: number
|
||||
opponent: {
|
||||
difficulty: "easy" | "normal" | "hard"
|
||||
basePower: number
|
||||
variance: number
|
||||
}
|
||||
theme: {
|
||||
playerDogSkin: string
|
||||
opponentDogSkin: string
|
||||
stageId: string
|
||||
soundPackId?: string
|
||||
}
|
||||
leaderboardEnabled: boolean
|
||||
publishedAt: string
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 WorkSummary
|
||||
|
||||
```text
|
||||
BarkBattleWorkSummary {
|
||||
workId: string
|
||||
playTypeId: "bark-battle"
|
||||
title: string
|
||||
description?: string
|
||||
coverAssetId?: string
|
||||
authorUserId: string
|
||||
authorDisplayName?: string
|
||||
configVersion: number
|
||||
leaderboardEnabled: boolean
|
||||
totalPlayCount: number
|
||||
publishedAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 RunStart
|
||||
|
||||
```text
|
||||
BarkBattleRunStartRequest {
|
||||
workId: string
|
||||
configVersion: number
|
||||
sourceRoute?: string
|
||||
clientRuntimeVersion?: string
|
||||
}
|
||||
|
||||
BarkBattleRunStartResponse {
|
||||
runId: string
|
||||
runToken: string
|
||||
workId: string
|
||||
configVersion: number
|
||||
serverStartedAt: string
|
||||
expiresAt: string
|
||||
runtimeConfig: BarkBattleRuntimeConfig
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 RunFinish
|
||||
|
||||
```text
|
||||
BarkBattleRunFinishRequest {
|
||||
runId: string
|
||||
runToken: string
|
||||
workId: string
|
||||
configVersion: number
|
||||
clientStartedAt?: string
|
||||
clientFinishedAt?: string
|
||||
elapsedMs: number
|
||||
finalEnergy: number
|
||||
clientWinner: "player" | "opponent" | "draw"
|
||||
metrics: BarkBattleDerivedMetrics
|
||||
clientRuntimeVersion?: string
|
||||
}
|
||||
|
||||
BarkBattleDerivedMetrics {
|
||||
peakVolumeMax: number // 0..1
|
||||
peakVolumeAvg: number // 0..1
|
||||
validBarkCount: number
|
||||
rhythmHitCount: number
|
||||
longestCombo: number
|
||||
sampleWindowCount?: number
|
||||
calibrationNoiseFloor?: number // 0..1
|
||||
}
|
||||
```
|
||||
|
||||
### 4.6 RunResult
|
||||
|
||||
```text
|
||||
BarkBattleRunResult {
|
||||
runId: string
|
||||
workId: string
|
||||
userId?: string
|
||||
configVersion: number
|
||||
accepted: boolean
|
||||
serverWinner: "player" | "opponent" | "draw"
|
||||
finalEnergy: number
|
||||
score: number
|
||||
scoreSummary: BarkBattleScoreSummary
|
||||
leaderboardEntry?: BarkBattleLeaderboardEntry
|
||||
antiCheatFlags: string[]
|
||||
finishedAt: string
|
||||
}
|
||||
```
|
||||
|
||||
### 4.7 ScoreSummary
|
||||
|
||||
```text
|
||||
BarkBattleScoreSummary {
|
||||
score: number
|
||||
grade: "S" | "A" | "B" | "C" | "D"
|
||||
finalEnergy: number
|
||||
winMargin: number
|
||||
validBarkCount: number
|
||||
peakVolumeMax: number
|
||||
rhythmHitCount: number
|
||||
longestCombo: number
|
||||
elapsedMs: number
|
||||
}
|
||||
```
|
||||
|
||||
### 4.8 Leaderboard 可选类型
|
||||
|
||||
```text
|
||||
BarkBattleLeaderboardQuery {
|
||||
workId: string
|
||||
configVersion?: number
|
||||
period: "all" | "daily" | "weekly"
|
||||
limit: number
|
||||
cursor?: string
|
||||
}
|
||||
|
||||
BarkBattleLeaderboardEntry {
|
||||
rank?: number
|
||||
runId: string
|
||||
workId: string
|
||||
userId?: string
|
||||
displayName?: string
|
||||
score: number
|
||||
scoreSummary: BarkBattleScoreSummary
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
BarkBattleLeaderboardResponse {
|
||||
workId: string
|
||||
entries: BarkBattleLeaderboardEntry[]
|
||||
viewerBest?: BarkBattleLeaderboardEntry
|
||||
nextCursor?: string
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 数据模型草案
|
||||
|
||||
### 5.1 作品配置表
|
||||
|
||||
建议表:`bark_battle_work_config`。
|
||||
|
||||
字段草案:
|
||||
|
||||
- `work_id`:稳定作品 ID,关联平台作品。
|
||||
- `draft_id`:草稿 ID,可选。
|
||||
- `owner_user_id`:创作者。
|
||||
- `play_type_id`:固定 `bark-battle`。
|
||||
- `config_version`:发布配置版本。
|
||||
- `title`、`description`、`cover_asset_id`。
|
||||
- `runtime_config_json`:发布态 runtime 配置 JSON;字段需由 shared contracts 校验。
|
||||
- `leaderboard_enabled`。
|
||||
- `status`:`draft` / `published` / `archived`。
|
||||
- `created_at`、`updated_at`、`published_at`。
|
||||
|
||||
约束:
|
||||
|
||||
- 同一 `work_id + config_version` 不可变;新发布生成新版本。
|
||||
- runtime 请求只读发布态配置。
|
||||
|
||||
### 5.2 runtime run 表
|
||||
|
||||
建议表:`bark_battle_runtime_run`。
|
||||
|
||||
字段草案:
|
||||
|
||||
- `run_id`。
|
||||
- `run_token_hash`:保存 token hash,不保存明文 token。
|
||||
- `work_id`。
|
||||
- `config_version`。
|
||||
- `user_id`:匿名时可空或使用匿名会话 ID。
|
||||
- `source_route`。
|
||||
- `status`:`started` / `finished` / `rejected` / `expired`。
|
||||
- `server_started_at`、`server_finished_at`、`expires_at`。
|
||||
- `client_elapsed_ms`。
|
||||
- `final_energy`。
|
||||
- `client_winner`、`server_winner`。
|
||||
- `anti_cheat_flags_json`。
|
||||
- `client_runtime_version`。
|
||||
|
||||
### 5.3 score / stat 表
|
||||
|
||||
建议表:`bark_battle_score_record`。
|
||||
|
||||
字段草案:
|
||||
|
||||
- `score_id`。
|
||||
- `run_id`。
|
||||
- `work_id`。
|
||||
- `config_version`。
|
||||
- `user_id`。
|
||||
- `score`。
|
||||
- `grade`。
|
||||
- `final_energy`。
|
||||
- `valid_bark_count`。
|
||||
- `peak_volume_max`。
|
||||
- `peak_volume_avg`。
|
||||
- `rhythm_hit_count`。
|
||||
- `longest_combo`。
|
||||
- `elapsed_ms`。
|
||||
- `metrics_json`:只保存派生聚合指标。
|
||||
- `created_at`。
|
||||
|
||||
明确禁止:
|
||||
|
||||
- 不保存原始麦克风音频。
|
||||
- 不保存可还原语音的 PCM、Opus、MP3、WAV、base64 音频、逐帧 waveform。
|
||||
- 不保存高精度声纹向量。
|
||||
|
||||
### 5.4 leaderboard 表
|
||||
|
||||
若平台已有通用排行榜,优先复用平台 leaderboard 投影;否则可新增 `bark_battle_leaderboard_entry`:
|
||||
|
||||
- `entry_id`。
|
||||
- `work_id`。
|
||||
- `config_version`。
|
||||
- `run_id`。
|
||||
- `user_id`。
|
||||
- `score`。
|
||||
- `tie_breaker_energy`。
|
||||
- `tie_breaker_elapsed_ms`。
|
||||
- `created_at`。
|
||||
|
||||
排序建议:
|
||||
|
||||
1. `score` 降序。
|
||||
2. `final_energy` / `winMargin` 降序。
|
||||
3. `elapsed_ms` 更接近配置时长者优先,避免异常短局刷分。
|
||||
4. `created_at` 升序或按平台既有规则。
|
||||
|
||||
## 6. API 草案
|
||||
|
||||
路径仅为草案,落地时按 `api-server` 当前路由命名规范调整。
|
||||
|
||||
### 6.1 创建 / 保存草稿
|
||||
|
||||
```text
|
||||
POST /api/bark-battle/drafts
|
||||
PUT /api/bark-battle/drafts/{draftId}
|
||||
GET /api/bark-battle/drafts/{draftId}
|
||||
```
|
||||
|
||||
职责:
|
||||
|
||||
- 仅创作者可创建和保存。
|
||||
- 校验 `playTypeId = bark-battle`。
|
||||
- 调用 `module-bark-battle` 校验 runtime config 范围。
|
||||
|
||||
### 6.2 发布 / 获取作品
|
||||
|
||||
```text
|
||||
POST /api/bark-battle/drafts/{draftId}/publish
|
||||
GET /api/works/{workId}/bark-battle
|
||||
GET /api/bark-battle/works/{workId}/runtime-config
|
||||
```
|
||||
|
||||
职责:
|
||||
|
||||
- 发布生成稳定 `workId` 和递增 `configVersion`。
|
||||
- 获取作品只返回发布态配置与展示摘要。
|
||||
- 未发布或无权限作品返回明确错误。
|
||||
|
||||
### 6.3 start runtime
|
||||
|
||||
```text
|
||||
POST /api/bark-battle/runs/start
|
||||
```
|
||||
|
||||
请求:`BarkBattleRunStartRequest`。响应:`BarkBattleRunStartResponse`。
|
||||
|
||||
职责:
|
||||
|
||||
- 校验作品存在、发布态、可游玩。
|
||||
- 校验 config version,必要时返回最新版本。
|
||||
- 创建 `run_id` 与一次性 `run_token`。
|
||||
- 写正式作品级游玩埋点:`work_play_start`。
|
||||
|
||||
埋点要求:
|
||||
|
||||
```text
|
||||
event_key: work_play_start
|
||||
scope_kind: work
|
||||
scope_id: <稳定作品 ID>
|
||||
metadata: {
|
||||
playType: "bark-battle",
|
||||
workId: "<workId>",
|
||||
sourceRoute: "<sourceRoute>",
|
||||
userId: "<userId or anonymous>"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 finish runtime
|
||||
|
||||
```text
|
||||
POST /api/bark-battle/runs/{runId}/finish
|
||||
```
|
||||
|
||||
请求:`BarkBattleRunFinishRequest`。响应:`BarkBattleRunResult`。
|
||||
|
||||
职责:
|
||||
|
||||
- 校验 run token。
|
||||
- 校验 run 仍处于 `started` 且未过期。
|
||||
- 校验 `work_id + config_version` 与 start 时一致。
|
||||
- 校验时长、finalEnergy、metrics 范围。
|
||||
- 使用 `module-bark-battle` 生成服务端认可的 `serverWinner`、`score`、`ScoreSummary`、`antiCheatFlags`。
|
||||
- 写 `bark_battle_runtime_run` finish 状态与 `bark_battle_score_record`。
|
||||
- 如开启 leaderboard 且结果 accepted,写排行榜。
|
||||
|
||||
### 6.5 作品级游玩埋点
|
||||
|
||||
`start runtime` 内部必须触发统一埋点;不建议前端单独调用一个 bark-battle 专用埋点 API。若平台已有通用 tracking API,则 api-server 内部调用 platform tracking facade:
|
||||
|
||||
```text
|
||||
track_event(
|
||||
event_key = "work_play_start",
|
||||
scope_kind = "work",
|
||||
scope_id = workId,
|
||||
metadata = { playType, workId, sourceRoute, userId }
|
||||
)
|
||||
```
|
||||
|
||||
### 6.6 可选排行榜
|
||||
|
||||
```text
|
||||
GET /api/bark-battle/works/{workId}/leaderboard?period=all&limit=50&cursor=...
|
||||
GET /api/bark-battle/works/{workId}/leaderboard/me
|
||||
```
|
||||
|
||||
职责:
|
||||
|
||||
- 只读已接受成绩。
|
||||
- 支持分页。
|
||||
- 支持匿名用户时隐藏或弱化身份展示。
|
||||
- 若作品关闭排行榜,返回空投影或明确 disabled 状态。
|
||||
|
||||
## 7. SpacetimeDB 与 migration 策略
|
||||
|
||||
### 7.1 表 / reducer / procedure 边界
|
||||
|
||||
SpacetimeDB 侧只承载持久化、索引、reducer 原子写入和可查询投影,不承载 HTTP 鉴权或前端表现。
|
||||
|
||||
建议 reducer / procedure:
|
||||
|
||||
- `bark_battle_save_draft_config`:保存草稿配置。
|
||||
- `bark_battle_publish_work_config`:发布配置版本。
|
||||
- `bark_battle_start_run`:创建 runtime run。
|
||||
- `bark_battle_finish_run`:结束 run 并写 score。
|
||||
- `bark_battle_upsert_leaderboard_entry`:写排行榜投影。
|
||||
- `bark_battle_get_work_runtime_config`:读取发布态配置。
|
||||
- `bark_battle_get_leaderboard`:读取排行榜投影。
|
||||
|
||||
如当前架构要求 reducer 仅由 `spacetime-client` 调用,则 api-server 不直接操作 SpacetimeDB SDK。
|
||||
|
||||
### 7.2 migration.rs
|
||||
|
||||
所有表结构变更必须进入 `migration.rs`:
|
||||
|
||||
- 新增 bark-battle 表时写显式 migration。
|
||||
- 新增索引、唯一约束或版本字段时写 migration。
|
||||
- 从草稿 JSON 拆字段时写数据迁移说明。
|
||||
- 不允许只改 Rust struct 而不补 migration。
|
||||
|
||||
### 7.3 绑定生成
|
||||
|
||||
涉及 SpacetimeDB schema / reducer 变更后:
|
||||
|
||||
1. 运行仓库既有 SpacetimeDB 绑定生成命令。
|
||||
2. 检查 generated bindings 变化。
|
||||
3. `spacetime-client` 只引用生成物,不手改生成物。
|
||||
4. shared contracts 与 generated bindings 的差异通过 facade 消化,不让前端直接依赖数据库绑定。
|
||||
|
||||
明确要求:不手改生成物,不手改 generated bindings,不用临时复制粘贴类型绕过生成流程。
|
||||
|
||||
### 7.4 api-server facade
|
||||
|
||||
`api-server` 通过 `spacetime-client` facade 调用 SpacetimeDB:
|
||||
|
||||
```text
|
||||
BarkBattleService
|
||||
-> BarkBattleDomainPolicy
|
||||
-> BarkBattleSpacetimeClient
|
||||
-> generated bindings
|
||||
```
|
||||
|
||||
这样可以保证:
|
||||
|
||||
- HTTP 层易测试。
|
||||
- domain 纯函数可独立测试。
|
||||
- SpacetimeDB 绑定变更不会扩散到 route handler。
|
||||
|
||||
## 8. 安全、隐私与反作弊
|
||||
|
||||
### 8.1 隐私
|
||||
|
||||
- 不上传原始音频。
|
||||
- 不保存原始音频。
|
||||
- 不保存可还原用户声音的高精度采样曲线。
|
||||
- 只保存派生指标:峰值、均值、有效叫声次数、节奏命中、最终能量、分数、耗时。
|
||||
- 前端权限文案必须说明麦克风只用于本地玩法输入,MVP 不上传原始声音。
|
||||
|
||||
### 8.2 不信任前端胜负
|
||||
|
||||
后端不能直接信任:
|
||||
|
||||
- `clientWinner`。
|
||||
- `score`。
|
||||
- `finalEnergy`。
|
||||
- `elapsedMs`。
|
||||
- `validBarkCount`。
|
||||
|
||||
后端必须校验并重算服务端认可结果。MVP 因不上传音频,无法完全证明声音真实性,但仍需做边界反作弊。
|
||||
|
||||
### 8.3 校验规则
|
||||
|
||||
必须校验:
|
||||
|
||||
- run token 是否匹配且未使用。
|
||||
- run 是否未过期。
|
||||
- `work_id + config_version` 是否与 start 时一致。
|
||||
- 用户是否有权限游玩该 work。
|
||||
- 提交时长是否接近配置时长,例如 30 秒局允许少量网络 / 页面调度误差。
|
||||
- `finalEnergy` 是否在配置范围。
|
||||
- `peakVolumeMax`、`peakVolumeAvg` 是否在 `0..1`。
|
||||
- `validBarkCount`、`rhythmHitCount`、`longestCombo` 是否在物理合理上限。
|
||||
- `clientStartedAt` / `clientFinishedAt` 与服务端时间窗口是否合理。
|
||||
- 同一用户 / 匿名会话的频率限制。
|
||||
|
||||
### 8.4 反作弊处理
|
||||
|
||||
建议结果状态:
|
||||
|
||||
- `accepted`:写 score,可进入排行榜。
|
||||
- `accepted_with_flags`:写 score,但标记异常,默认不入榜或降低可信度。
|
||||
- `rejected`:不入榜,只记录 run 失败原因。
|
||||
|
||||
常见 flags:
|
||||
|
||||
- `elapsed_too_short`
|
||||
- `elapsed_too_long`
|
||||
- `metric_out_of_range`
|
||||
- `config_version_mismatch`
|
||||
- `token_invalid`
|
||||
- `duplicate_finish`
|
||||
- `rate_limited`
|
||||
- `impossible_bark_count`
|
||||
|
||||
## 9. TDD / 验收顺序与命令
|
||||
|
||||
本任务只写方案,不执行代码实现。后续落地建议按以下顺序:
|
||||
|
||||
### 9.1 domain 纯函数测试
|
||||
|
||||
先实现 `module-bark-battle`:
|
||||
|
||||
```bash
|
||||
cd server-rs
|
||||
cargo test -p module-bark-battle
|
||||
```
|
||||
|
||||
测试覆盖:
|
||||
|
||||
- runtime config 校验。
|
||||
- finish metrics 范围校验。
|
||||
- 胜负判定。
|
||||
- score / grade / leaderboard score 计算。
|
||||
- 反作弊 flags。
|
||||
|
||||
### 9.2 contracts 测试
|
||||
|
||||
再实现 shared contracts:
|
||||
|
||||
```bash
|
||||
cd server-rs
|
||||
cargo test -p shared-contracts bark_battle
|
||||
cargo check -p shared-contracts
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
- DTO 可序列化 / 反序列化。
|
||||
- 枚举值稳定。
|
||||
- 可选字段向后兼容。
|
||||
- TypeScript mirror 或契约生成产物与 Rust contract 对齐。
|
||||
|
||||
### 9.3 SpacetimeDB / api-server check
|
||||
|
||||
实现表、reducer、migration 与 facade 后:
|
||||
|
||||
```bash
|
||||
cd server-rs
|
||||
cargo check -p spacetime-module
|
||||
cargo check -p spacetime-client
|
||||
cargo check -p api-server
|
||||
cargo test -p api-server bark_battle
|
||||
```
|
||||
|
||||
如仓库有统一命令,以统一命令为准,例如:
|
||||
|
||||
```bash
|
||||
cd server-rs
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
- migration.rs 包含新增表与变更。
|
||||
- 绑定由生成命令产出,未手改生成物。
|
||||
- api-server route handler 只做编排,不内嵌复杂计分规则。
|
||||
|
||||
### 9.4 前端 contract 对齐
|
||||
|
||||
前端只在后端 contract 稳定后接入:
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm test -- bark-battle
|
||||
npm run build
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
- runtime start / finish 请求字段与 shared contracts 一致。
|
||||
- 前端 result panel 展示后端 `RunResult`。
|
||||
- 前端本地结算只作为即时反馈,正式结果以后端返回为准。
|
||||
|
||||
### 9.5 手工验收清单
|
||||
|
||||
- 可以创建并保存 bark-battle 草稿。
|
||||
- 可以发布成稳定作品 ID,`playTypeId = bark-battle`。
|
||||
- runtime start 返回 config、runId、runToken。
|
||||
- start 写入 `work_play_start`,scope 与 metadata 符合要求。
|
||||
- finish 不上传音频,只上传派生指标。
|
||||
- finish 返回服务端认可的 result。
|
||||
- 异常时长、重复提交、config version mismatch 会被拒绝或打 flag。
|
||||
- 排行榜关闭时不写榜;开启时只写 accepted 结果。
|
||||
|
||||
## 10. 与现有前端方案和 BDD 文档的关系
|
||||
|
||||
### 10.1 依赖文档
|
||||
|
||||
- 前端 runtime 方案:`docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md`。
|
||||
- BDD / DDD / TDD 总计划:`.hermes/plans/2026-05-11_144229-bark-battle-2d-game-bdd-ddd-tdd-plan.md`。
|
||||
- 当前后端实现基线:`docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`。
|
||||
- SpacetimeDB 表结构变更约束:`docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`。
|
||||
|
||||
### 10.2 与前端方案的对齐点
|
||||
|
||||
- 前端方案负责 Phaser / Web Audio / DOM HUD;本文负责作品、成绩、排行榜、发布和埋点。
|
||||
- 前端 `BarkBattleSnapshot` 可用于本地即时表现,但正式 `RunResult` 以后端返回为准。
|
||||
- 前端不上传原始音频,只上传 `BarkBattleDerivedMetrics`。
|
||||
- 前端本地 config 应来自后端发布态 `BarkBattleRuntimeConfig`,不能在生产游玩中使用未发布临时配置。
|
||||
- 前端 result panel 应能展示后端返回的 score、grade、antiCheatFlags 与 leaderboard entry。
|
||||
|
||||
### 10.3 与 BDD 的对齐点
|
||||
|
||||
后续 BDD 场景应覆盖:
|
||||
|
||||
- 玩家从作品页进入 bark-battle runtime。
|
||||
- 玩家授权麦克风后开始 30 秒对战。
|
||||
- 玩家完成单局后看到后端确认结果。
|
||||
- 未授权麦克风时可以看到降级说明,但不写正式成绩。
|
||||
- 作品关闭排行榜时不展示排名入口。
|
||||
- 作品开启排行榜时展示当前作品排名。
|
||||
- 重复 finish / 过期 run / 配置版本不一致时返回可解释错误。
|
||||
|
||||
### 10.4 后端落地顺序建议
|
||||
|
||||
1. 先只做 contract + domain,固定 `playTypeId = bark-battle` 与配置 schema。
|
||||
2. 再做草稿 / 发布态作品配置读写。
|
||||
3. 再做 start / finish run 与 `work_play_start` 埋点。
|
||||
4. 最后做排行榜投影。
|
||||
5. 实时多人协议另起方案,不与 MVP 混做。
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
## 文档列表
|
||||
|
||||
- [BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md](./BARK_BATTLE_BACKEND_DDD_TECHNICAL_PLAN_2026-05-11.md):冻结“汪汪声浪大作战 / bark-battle”后端 DDD 技术方案,明确 `server-rs + Axum + SpacetimeDB` 分层边界、shared contracts、作品配置、runtime run、派生成绩、排行榜、`work_play_start` 埋点、migration/绑定生成策略,以及不保存原始麦克风音频的隐私与反作弊约束。
|
||||
- [BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md](./BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md):冻结“汪汪声浪大作战 / bark-battle”2D 浏览器 runtime 技术方案,明确 Phaser + TypeScript + Vite 选型、纯 TS simulation 与 Phaser renderer/DOM HUD 边界、Web Audio 输入适配、移动端权限降级和后续测试验证命令。
|
||||
- [CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md](./CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md):冻结儿童动作识别互动玩法 Demo 固定热身关的开发落地规格,覆盖横屏展示、摄像头背景虚化、角色剪影、绿色圆环 2 秒保持、动作教学、当前会话内空间边界记录和后续关卡安全暂停规则。
|
||||
- [RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md](./RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md):记录运行态输入设备抽象层,明确鼠标、触控、mocap 等设备统一归一为通用拖拽语义,玩法组件只负责解释目标和落点。
|
||||
- [RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md](./RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md):记录 `server-rs` Cargo 依赖集中配置口径,第三方版本和 workspace 内部 crate path 统一维护在根 `server-rs/Cargo.toml`,成员 crate 只保留 feature/optional 差异。
|
||||
|
||||
Reference in New Issue
Block a user