feat: add bark battle browser prototype
This commit is contained in:
@@ -135,6 +135,7 @@ src/games/bark-battle/
|
||||
AudioAnalyserSampler.ts
|
||||
MicrophonePermission.ts
|
||||
__tests__/
|
||||
BrowserMicrophoneInput.test.ts
|
||||
AudioAnalyserSampler.test.ts
|
||||
|
||||
phaser/
|
||||
@@ -183,14 +184,34 @@ export type BarkBattlePhase =
|
||||
| 'countdown'
|
||||
| 'playing'
|
||||
| 'finished'
|
||||
| 'unsupported'
|
||||
| 'permission-denied'
|
||||
| 'unavailable'
|
||||
|
||||
export type BarkBattleSide = 'player' | 'opponent'
|
||||
|
||||
export type BarkBattleWinner = BarkBattleSide | 'draw' | null
|
||||
|
||||
export type BarkBattleDifficulty = 'easy' | 'normal' | 'hard'
|
||||
|
||||
export type BarkBattleUiState =
|
||||
| 'idle'
|
||||
| 'permission-ready'
|
||||
| 'microphone-authorized'
|
||||
| 'calibrating'
|
||||
| 'ready-countdown'
|
||||
| 'playing'
|
||||
| 'finished'
|
||||
| 'microphone-unavailable'
|
||||
|
||||
export type MicrophoneFailureReason =
|
||||
| 'unsupported'
|
||||
| 'permission-denied'
|
||||
| 'non-secure-context'
|
||||
| 'not-found'
|
||||
| 'not-readable'
|
||||
| 'audio-context-blocked'
|
||||
| 'calibration-timeout'
|
||||
| 'calibration-sample-unreadable'
|
||||
| 'unknown'
|
||||
```
|
||||
|
||||
关键数值:
|
||||
@@ -206,6 +227,9 @@ export type BarkBattleDifficulty = 'easy' | 'normal' | 'hard'
|
||||
```ts
|
||||
export type BarkBattleSnapshot = {
|
||||
phase: BarkBattlePhase
|
||||
uiState: BarkBattleUiState
|
||||
errorReason: MicrophoneFailureReason | null
|
||||
statusMessageKey: BarkBattleStatusMessageKey | null
|
||||
elapsedMs: number
|
||||
remainingMs: number
|
||||
countdownMs: number
|
||||
@@ -237,6 +261,17 @@ export type BarkBattleResult = {
|
||||
playerAveragePower: number
|
||||
score: number
|
||||
}
|
||||
|
||||
export type BarkBattleStatusMessageKey =
|
||||
| 'microphone-unsupported'
|
||||
| 'microphone-permission-denied'
|
||||
| 'microphone-non-secure-context'
|
||||
| 'microphone-not-found'
|
||||
| 'microphone-not-readable'
|
||||
| 'microphone-audio-context-blocked'
|
||||
| 'microphone-calibration-timeout'
|
||||
| 'microphone-calibration-sample-unreadable'
|
||||
| 'microphone-unknown-error'
|
||||
```
|
||||
|
||||
### 5.3 输入样本与叫声事件
|
||||
@@ -341,10 +376,11 @@ energy = clamp(energy + energyDelta, -100, 100)
|
||||
|
||||
```text
|
||||
permission → calibration → countdown → playing → finished
|
||||
↘ permission-denied
|
||||
↘ unsupported
|
||||
↘ unavailable
|
||||
```
|
||||
|
||||
`phase` 只表达 runtime 是否可继续参与局内流程;所有麦克风不可用、权限失败、非安全上下文和校准失败都统一收敛到 `phase: 'unavailable'`,再通过 `uiState: 'microphone-unavailable'` 与 `errorReason` 区分 HUD 展示和重试策略,避免把基础设施错误枚举直接扩散成 domain 阶段。
|
||||
|
||||
关键规则:
|
||||
|
||||
- `countdown` 结束才进入 `playing`。
|
||||
@@ -419,13 +455,29 @@ barkThreshold = clamp(ambientNoiseFloor + 0.12, 0.18, 0.55)
|
||||
export type MicrophoneFailureReason =
|
||||
| 'unsupported'
|
||||
| 'permission-denied'
|
||||
| 'non-secure-context'
|
||||
| 'not-found'
|
||||
| 'not-readable'
|
||||
| 'audio-context-blocked'
|
||||
| 'calibration-timeout'
|
||||
| 'calibration-sample-unreadable'
|
||||
| 'unknown'
|
||||
```
|
||||
|
||||
前端只根据错误分类展示可操作状态:重试授权、返回、或使用调试备用输入。不要把浏览器原始错误堆栈展示给玩家。
|
||||
错误来源与分层归属:
|
||||
|
||||
| 失败原因 | 主要检测位置 | controller snapshot 表达 | HUD 可区分状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| 浏览器无 `mediaDevices.getUserMedia` | `BrowserMicrophoneInput.isSupported()` | `phase: 'unavailable'`, `uiState: 'microphone-unavailable'`, `errorReason: 'unsupported'` | 设备或浏览器不支持麦克风输入,只提供返回入口,不展示可开始声控按钮 |
|
||||
| 非安全上下文 | `BrowserMicrophoneInput.isSupported()` 或 `MicrophonePermission` 预检 `window.isSecureContext` | `phase: 'unavailable'`, `errorReason: 'non-secure-context'` | 当前环境无法使用麦克风,提示使用受支持的安全环境或返回 |
|
||||
| 用户拒绝授权 | `BrowserMicrophoneInput.requestPermission()` 捕获 `NotAllowedError` / `SecurityError` | `phase: 'unavailable'`, `errorReason: 'permission-denied'` | 提供重新授权或返回入口,不进入 calibration/countdown/playing |
|
||||
| 未检测到设备 | `getUserMedia` 捕获 `NotFoundError` / `DevicesNotFoundError` | `phase: 'unavailable'`, `errorReason: 'not-found'` | 展示麦克风不可用,可重试授权或返回 |
|
||||
| 设备被占用或不可读 | `getUserMedia` 捕获 `NotReadableError` / `TrackStartError` | `phase: 'unavailable'`, `errorReason: 'not-readable'` | 展示麦克风不可用,可重试授权或返回 |
|
||||
| AudioContext 被移动端策略拦截 | 用户手势后创建 / resume `AudioContext` 失败 | `phase: 'unavailable'`, `errorReason: 'audio-context-blocked'` | 提示点击重试,不自动循环请求 |
|
||||
| 校准超时 | `BarkBattleController` 在 calibration 阶段等待样本超出 `calibrationMaxWaitMs` | `phase: 'unavailable'`, `errorReason: 'calibration-timeout'` | 展示麦克风输入不可用,提供重试校准入口 |
|
||||
| 校准样本不可读 | `AudioAnalyserSampler.sample()` 持续返回空样本、NaN 或无法读取 buffer | `phase: 'unavailable'`, `errorReason: 'calibration-sample-unreadable'` | 展示麦克风输入不可用,提供重试校准入口 |
|
||||
|
||||
前端只根据错误分类展示可操作状态:重试授权、重试校准、返回、或使用调试备用输入。不要把浏览器原始错误堆栈展示给玩家。
|
||||
|
||||
## 8. Phaser Scene 切分
|
||||
|
||||
@@ -544,10 +596,13 @@ HUD 分区:
|
||||
|
||||
权限失败时:
|
||||
|
||||
- `unsupported`:展示“当前浏览器不支持麦克风输入”,提供返回入口。
|
||||
- `unsupported`:展示“当前浏览器不支持麦克风输入”,提供返回入口,不展示开始声控按钮。
|
||||
- `non-secure-context`:展示“当前环境无法使用麦克风”,提示切换到受支持的安全环境或返回。
|
||||
- `permission-denied`:展示简短说明和“重新授权”入口。
|
||||
- `not-found`:提示未检测到麦克风,提供返回入口。
|
||||
- `not-found`:提示未检测到麦克风,提供重试授权或返回入口。
|
||||
- `not-readable`:提示麦克风被占用或暂时不可读,提供重试授权或返回入口。
|
||||
- `audio-context-blocked`:提示点击重试。
|
||||
- `calibration-timeout` / `calibration-sample-unreadable`:提示麦克风输入不可用,提供“重新校准”和返回入口。
|
||||
|
||||
可选开发调试降级:
|
||||
|
||||
@@ -578,7 +633,9 @@ HUD 分区:
|
||||
建议测试:
|
||||
|
||||
- 权限允许后进入校准,再进入倒计时。
|
||||
- 权限拒绝后 phase 为 `permission-denied`,不进入 playing。
|
||||
- 权限拒绝后 `phase` 为 `unavailable`、`errorReason` 为 `permission-denied`,不进入 playing。
|
||||
- 非安全上下文、设备未找到、设备不可读、AudioContext 被拦截时,controller snapshot 都进入 `phase: 'unavailable'`,并保留可供 HUD 区分的 `errorReason`。
|
||||
- 校准超时或样本持续不可读时,controller snapshot 使用 `errorReason: 'calibration-timeout'` 或 `calibration-sample-unreadable`,并提供重试校准动作。
|
||||
- 提交 mock audio sample 后 snapshot 中玩家状态更新。
|
||||
- AI 对手 power 参与能量条拉锯。
|
||||
- `lastEvents` 只发布新增视觉事件。
|
||||
@@ -591,7 +648,9 @@ HUD 分区:
|
||||
|
||||
- playing 阶段展示倒计时和能量条。
|
||||
- energy 正负值映射到玩家 / 对手侧比例。
|
||||
- permission-denied 展示重试入口。
|
||||
- `errorReason: 'permission-denied'` 展示重试授权入口。
|
||||
- `errorReason: 'unsupported'` 展示返回入口且不展示开始声控按钮。
|
||||
- `not-found`、`not-readable`、`non-secure-context`、`audio-context-blocked`、`calibration-timeout`、`calibration-sample-unreadable` 分别映射到可区分的简短状态文案和对应操作。
|
||||
- finished 展示胜负、叫声次数和再来一局。
|
||||
- 移动端 class / 结构不依赖 Phaser Canvas 才能渲染。
|
||||
|
||||
@@ -609,7 +668,7 @@ HUD 分区:
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
git diff -- docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md
|
||||
git diff -- docs/prd/BARK_BATTLE_BDD_2026-05-11.md docs/technical/BARK_BATTLE_2D_RUNTIME_TECHNICAL_PLAN_2026-05-11.md
|
||||
```
|
||||
|
||||
后续实现 domain 后建议:
|
||||
@@ -620,6 +679,14 @@ npm run typecheck
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
后续实现 infrastructure/application 错误状态后建议:
|
||||
|
||||
```bash
|
||||
npm run test -- --run src/games/bark-battle/infrastructure/__tests__/BrowserMicrophoneInput.test.ts src/games/bark-battle/application/__tests__/BarkBattleController.test.ts
|
||||
npm run typecheck
|
||||
npm run check:encoding
|
||||
```
|
||||
|
||||
后续实现 HUD 后建议:
|
||||
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user