feat(hunt): 《围猎》2-4人实时联机纯音频协作守卫——纯音频MMORPG第一阶段切片

- 设计:docs/design/23-hunt.md + voice-ids(84 id,166条)+ 13号断言登记,§15记录10+1条设计裁决
- 服务器:零依赖WebSocket(ws.js)+ 房间管理/服务端权威战斗/bot AI(hunt-room.js),断线30s宽限resume回槽、超时转bot;修复package.json模块类型误判的既有破坏
- 客户端:web/hunt/ 全套(教学T0-T4/听觉视野裁剪R1-R8/结算排行榜/guide接入),playtest 211断言
- 引擎:audio.js 后向兼容 opts.pan(StereoPannerNode),web-engine-api.md 同步
- 资产:166条TTS语音(QC无离群)+ 13件合成sfx(真峰值≤-3dBFS)+ 2首Mureka BGM,终检全绿
- 平衡:bot调参至挂机局71%失守/C、3bot局71% A/B(hunt-balance-scan.mjs 24种子扫描)
- 接线:大厅卡片、profiles.js hunt桶、README/roadmap MMO路线条目

验证:hunt-sim 16/16 + api-smoke 12/12 + hunt playtest 211/211 + 既有10游戏与引擎回归零回退 + 真服务器WS端到端冒烟通过
This commit is contained in:
xsl
2026-09-13 11:08:01 +08:00
parent 31372615b2
commit 005851d01a
392 changed files with 6826 additions and 19 deletions
+39
View File
@@ -795,3 +795,42 @@ R1 起每轮的增量断言(修复回归)在各自设计文档的轮次小
结算链 800ms 手势死窗(判定出口/NIGHT/d9/e*);键盘 _k 变体 11 对按 inputMode 选播;pendingChoice/p* 不可跳/
摩尔斯首播不可跳;中断复述(含 DECODE 态恢复链 prog_t→book 档→gd_tune2);存档 schema(summary 带
暴露档名)。
---
## 24. hunt《围猎》(联机切片;设计 [23-hunt.md](23-hunt.md),台词表 23b)
> 本作是首个联机游戏:断言分两侧——**服务器侧**(`web/server/test/` 新增 hunt-room 单测,直接 import 房间模块 + 假 WS 连接,固定 seed 复现)与**客户端侧**(`web/hunt/test/playtest.mjs`,mock 引擎 + mock transport 注入 ev/snap)。协议字段以 23 号文档 §6 为唯一契约。测试钩子可读 game 实例字段与房间实例字段;手势只经引擎回调注入。
**关键路径**(首局教学房 → 5 波 → 结算 → 排行榜,≤150 次手势):
1. 启动遮罩点按 → 音频解锁(此前零播放)→ TITLE;无存档单击 → 断言客户端发出 `join { room:"quick", teach:true, ver:1 }`。
2. **教学房**:注入 `joined`(phase:'teach',slot 0,roster 含 2 bot,seed=42)→ T0(intro_1→intro_2→tut_rule 有序)→ 单击 → T1(tut_attack + 注入 spawn 狼 N eta12)→ 先错向滑 W(断言 `act attack dir:W` → 注入 `ev miss` → atk_swish + error 震 + gd_idle 重教,怪物不受伤)→ 向 N 滑 → 断言发出 `act attack dir:N` → 注入 `ev kill` → tut_kill_ok;T2 单击 → `act callout` 发出 → 注入 bot help_ack(断言 `sfx/npc_yan_help_ack` 播放且 pan=该 bot 防区中心)→ tut_callout_ok;T3 双击 → `act skill` → 注入 `ev skill cleared=[2]` → tut_skill_cd;T4 上滑 → 状态链 4 段有序(hp_band→st_wave→team_3→kill_band)→ tut_breach→tut_done → 注入 `ev wave 1` 进 WAVE。**教学狼突入零惩罚**:注入教学波 breach → dur 不变。
3. **bot 补位**:非首局(tutDone=true)→ LOBBY(room_wait 播)→ 单击发 `act start` → 开局 roster ev 断言猎人总数 = max(3, 人类数)、bot 槽 char 齐全、bot greet 逐只间隔 ≥1.5s。
4. **5 波推进**(seed=42 服务器侧快进 tick):每波 spawn 事件数/型别/逼近时长与 23 §3.4 表逐波一致;方位分配 ∈ 各座位防区;狼击杀(滑向 → act → ev kill)断言归属 slot 与 kills 账;漏一只狼 → ev breach,dur -1,`breach` 播报 + hit 震;熊漏 → dur -2 + `breach_heavy`。
5. **终局**:注入 `ev end { win:true, dur:8, grade:"A", kills }` → 结算链有序(end_win→grade_a→kill_band_*→end_rank→end_again)→ 断言 PUT `/api/progress { game:"hunt" }` 且 `data.summary` 命中生成规则(`最佳评级A · 击杀14 · 营地余8 · 共N局`)、best 按"评级>耐久>击杀"序更新;单击 → 重新进匹配。失守路径(dur 归零):end_lose 首句、无 grade_*、无 end_rank、不写 best、runs+1。
**边界断言**:
- **断线转 bot**:关连接 30s(服务器侧快进)→ roster ev 该槽 bot=true 且 char 沿用 → 该槽开始产出 act(tick 内断言);剩余人类 ≥3 时改为槽位移除(roster 缩短)。
- **重连回槽**:断开 15s 内带 `resume` 重连 → joined 同 slot/seed/char + 紧随 snap 全量 → 客户端怪表与 snap 一致、net_back 播报、战斗续接;超 30s → err `room_gone` → net_gone → LOST。
- **旋风斩冷却**:放出后 8s 内双击 → 无 act 发出、播 threat_*(按内圈只数选档);冷却完成 → skill_ready(queue:true 不顶句);伪造 `act skill` 越冷却 → 服务器 err `skill_cd`。
- **出击冷却 2.5s**:冷却内滑 → 仅 error 震、零 act;伪造连发 → 服务器只裁决首发,其余 err `cooldown`。
- **支援呼喊 bot 补位**:呼喊后 10s 内,服务器侧断言补位 bot 的下一次 act.dir ∈ 呼喊者防区(偏置 ×2 生效),且 `help_ack` 恰播一次;呼喊节流 5s(连点只出一条 callout ev)。
- **听觉裁剪上限**:eta>6s 怪零 loop;任意时刻 loop ≤3 路且为 eta 最小 3 只(注入 5 只内圈怪断言);threat_* 选档只计 eta≤4 只数;状态链/威胁句无任何逐方位枚举(台词表结构保证)。
- **拥挤控制(R1-R8)**:第 5 波峰值剧本下断言——同刻 voice ≤1;同刻 `npc_*` ≤1 且 10s 窗 ≤2 条;旁白在播时喊话延迟 ≤2s 或丢弃;one-shot 定位音同刻 ≤2;breach/danger_low/wave 播报零丢失。
- **snap 兜底**:客户端随机丢弃 50% ev,仅凭 snap(2Hz)收敛 → 怪表与服务器真值一致(id 集合 + eta 偏差 ≤0.5s 校正)。
- **菜单不暂停(E3)**:菜单开着注入 breach → dur 照扣、菜单不收;菜单 3 项固定、单击循环/双击确认/下滑关闭。
- **学期零惩罚**:教学波 breach 不扣 dur;T1 做错 2 次自动放过不卡死。
- **结算死窗**:end 到达前后 800ms 内注入 tap → 被吞(不进 TITLE/不重开);死窗后单击正常再来一局。
- **屏蔽任意 voice 资产**:结算链/警报链照走完(看门狗兜底推进),不卡死。
- **资产齐套**:voice 双性别 60 句集合一致、key 变体 11 对 ⊆ 主集、`sfx/npc_*` 24 条双格式(assetExists 口径)。
- **opts.pan 回归**:无 pan 调用路径零 panner 节点(引擎行为不变);pan=-1/0.7/1 时 panner 值正确;老 Safari 无 createStereoPanner 静默降级。
**全局通用(§7 五条全跑,另加本作条目)**:
- 启动遮罩点按前零音频播放;遮罩手势不被游戏误收(引擎双保险)。
- **guide 三档**:full=签名+语音+震;standard 无向导语音;**off=全静且零看门狗定时器**(WAVE 30s 空闲零向导播放,内容层 loop/音效除外)。
- **震动语义**:逐条对照 23 §2 表——击杀 pickup / 挥空 error / 旋风斩 success / 突入 hit / 收到呼喊 warning / 耐久 ≤2 danger / 波次 heartbeat;手势本身震动由引擎内置承担,游戏层不重复(无双震)。
- 设置边界:speed 切换下一句生效;bgm=0 全程无 bgm 加载;voice 切换下一句用对应目录。
- 服务器不可达:TITLE 单击 → net_lost → net_gone → LOST 回大厅(明示失败,不静默死屏)。
**失败即不通过**:评级与 dur 不符(归因假话);服务器接受违规 act(冷却/方位/近战窗外命中);ev 与 snap 矛盾不收敛;bot 喊话压过/打断旁白(R1/R2 破);loop >3 路;教学狼扣耐久;失守局写入 best 排行榜。
+203
View File
@@ -0,0 +1,203 @@
# 23b · 《围猎》语音台词表(voice ids,录音依据)
> **给音频总监的录制说明**
> - **旁白/系统音双性别组(60 句,预算打满)**:落 `web/hunt/audio/voice/female|male/<id>.{ogg,m4a}`,两性别 id 集合完全一致。
> - female = `Serena`,instruct 常量:`清亮沉稳的女声,像守夜猎人的领队,冷静中带着鼓舞`
> - male = `Uncle_Fu`,instruct 常量:`低沉醇厚的男声,像守夜猎人的领队,沉稳有力`
> - 两常量全表统一(含教学/警报/结算),禁止句级变体、禁语速词(引擎 SoundTouch 变速)。
> - **bot 喊话组(N 组,24 句 = 3 角色 × 8 句)**:固定单音色平铺、**不做双性别**。因喊话经 **sfx 层 + opts.pan 空间化播放**(引擎按 id 前缀分发,主设计 §8 R2 / §15 裁决 7),资产**落盘 `web/hunt/audio/sfx/npc_<char>_<line>.{ogg,m4a}`**——这是 NPC 平铺惯例的异例:生成仍走 voice 管线(TTS、峰值归一 0.89 沿用 voice 策略),仅落盘目录不同。角色 speaker+instruct 恒定:
>
> | 角色 | char | speaker | instruct 常量 | 区分度风险 |
> |---|---|---|---|---|
> | 阿岩 | yan | `Uncle_Fu` | `憨厚壮实的中年男声,猎户,话少踏实` | **中**:与 male 旁白同 speaker——缓解:sfx 层 pan 空间分离 + 1-2s 短喊话语境 + instruct 差异(abyss 先例口径) |
> | 小筝 | zheng | `Serena` | `轻快爽利的年轻女声,猎手,干脆利落` | **中**:与 female 旁白同 speaker——缓解同上 |
> | 老周 | zhou | `Eric` | `沙哑沉稳的老年男声,老猎手,带点调侃` | 低:与两侧旁白零重叠 |
>
> - **数字规避**:耐久 4 档 / 波次 3 档(st_wave_*)+ 5 整句(wave_band1-5)/ 在场猎人 2 句 / 击杀 3 档 / 威胁 3 档 / 评级 4 整句——全部分档整句,无运行时拼接。「八秒冷却」「五波」等数字写死在整句内(一次生成,非拼接)。
> - **称呼中性**:旁白称「你/你们」,无性别化称呼;bot 句不含绝对方位(方位由 pan 承载,主设计 §15 裁决 3/7——任务书示例句"东侧交给我!"据此落地为"这边交给我")。
> - **多音字自查**:全表无「血」「爪」「挨」「壳」「弹」「重(chóng/zhòng 歧义句)」;「重听」一律写「重新听」(tut_skill_cd)。已标注风险 3 处:「还」(hái:营地还在/守得还行/还没开张——asset-manifest 既有风险项,三处均 hái 语境,试听确认);「啧」(zé,npc_zhou_breach 叹词,试听异常则整句改「哼,溜进去一只。」);评级字母 S/A/B/C(rhythm「评级,S!」先例,试听确认字母读音)。
> - **触发纪律**(开发轮次对照主设计):警报类(breach/breach_heavy/danger_low)可顶掉状态/播报类,教学/结算链不可顶;skill_ready 与状态链一律 `queue:true`;gd_* idle-only + onceKey;bot 喊话调度上限见主设计 §7.4/§8 R2。
> - **键盘变体(V 组,11 对)**:操作提示句在 `gesture.inputMode()==='key'` 时改播 `voice/<gender>/key/<id>.*` 变体,变体缺失回退主资产;与 `tools/gen_voice_dual.py` 的 `KEY_LINES` 逐字一致(两表一致铁律:改一处必改另一处)。
> - 管线:`tools/gen_voice_dual.py` 照录(SPEAKERS 新增 hunt 行 + HUNT_NPC 平铺表);生成后 `--convert` 落盘 + `tools/check_all_assets.py` 终检(双格式配对、双性别集合一致、key 变体 ⊆ 主集)。
## A 组 · 开场/房间(旁白 ×4)
| id | 台词全文 | 触发/语境 |
|---|---|---|
| `intro_1` | 夜幕降临,野兽在山林里游荡。 | 开场 1(TITLE 单击后/教学房 T0) |
| `intro_2` | 你们是守夜的猎人,营地就在身后。 | 开场 2 |
| `room_wait` | 房间已开好,等同伴到齐。单击,立即开局。 | LOBBY 进入(12s 倒计时自动开局) |
| `room_start` | 狩猎开始,先听声音的方向。 | 开局(ev wave 1 前) |
## B 组 · 教学 TEACH(旁白 ×11;教学波突入不扣耐久,学期零惩罚)
| id | 台词全文 | 触发/语境 |
|---|---|---|
| `tut_rule` | 野兽会从八个方向围上来。声音在哪边,就往哪边滑。 | T0(规则总述,含后半平面发闷约定由音效演示承担) |
| `tut_attack` | 听,有狼在靠近。朝声音的方向,滑一下。 | T1(教学狼 1 放出后) |
| `tut_kill_ok` | 好,杀掉了。就是这样。 | T1 首杀(成功只给新短句) |
| `tut_callout` | 单击一下,呼喊支援,队友会来帮你。 | T2 |
| `tut_callout_ok` | 听到没,同伴回应你了。 | T2(bot help_ack 播完后,呼喊-响应闭环点明) |
| `tut_skill` | 双击,旋风斩,清掉身边一圈。 | T3(教学狼 2、3 贴近后) |
| `tut_skill_cd` | 旋风斩有八秒冷却。冷却中双击,重新听威胁。 | T3 放出后(讲冷却与宪法双击语义) |
| `tut_status` | 上滑,听营地状态。下滑,打开菜单。 | T4 |
| `tut_breach` | 野兽冲进营地,营地就会受损。撑过五波就赢。 | T4 状态链播完后 |
| `tut_done` | 教学完了。真正的围猎,现在开始。 | TEACH 完成 → 进 WAVE 1 |
| `tut_skip` | 已跳过教学,开打。 | TEACH 中下滑跳过 |
## C 组 · 波次(旁白 ×6)
| id | 台词全文 | 触发/语境 |
|---|---|---|
| `wave_band1` | 第一波来了,别慌。 | ev wave 1 |
| `wave_band2` | 第二波,比刚才多。 | ev wave 2 |
| `wave_band3` | 第三波,有大家伙混在里面。 | ev wave 3(首只暴熊) |
| `wave_band4` | 第四波,更快,更多。 | ev wave 4 |
| `wave_band5` | 最后一波,全来了! | ev wave 5 |
| `wave_clear` | 这一波,守住了。 | ev waveclear(波间休整 8s 开场) |
## D 组 · 状态链(旁白 ×12;上滑 4 段链播:耐久档 → 波次档 → 猎人数 → 击杀档)
| id | 台词全文 | 触发/语境 |
|---|---|---|
| `hp_band1` | 营地固若金汤。 | 耐久 >75%(10 制:≥8) |
| `hp_band2` | 营地受了些损伤。 | 50~75%(5-7) |
| `hp_band3` | 营地损毁过半。 | 25~50%(3-4) |
| `hp_band4` | 营地快守不住了。 | ≤25%(≤2) |
| `st_wave_a` | 还在头几波。 | 波次档:第 1-2 波 |
| `st_wave_b` | 打到中段了。 | 第 3-4 波 |
| `st_wave_c` | 最后一波了。 | 第 5 波 |
| `team_3` | 三名猎人在守。 | 在场 3 猎人(含 bot) |
| `team_4` | 四名猎人全在。 | 在场 4 猎人 |
| `kill_band0` | 你还没开张。 | 个人击杀 0 |
| `kill_band1` | 你小有斩获。 | 1-9 |
| `kill_band2` | 你战果累累。 | ≥10 |
## E 组 · 威胁重听(旁白 ×3;冷却中双击 = 重听;只报数量档,方位由声像承载)
| id | 台词全文 | 触发/语境 |
|---|---|---|
| `threat_none` | 近处暂时没有野兽。 | 内圈(eta≤4s)0 只 |
| `threat_few` | 有野兽贴近了,听方向出手。 | 内圈 1-2 只 |
| `threat_many` | 四周全是动静,快出手! | 内圈 ≥3 只 |
## F 组 · 警报(旁白 ×3;警报类,可顶状态/播报类)
| id | 台词全文 | 触发/语境 |
|---|---|---|
| `breach` | 有野兽冲进营地了! | ev breach(狼,-1);节流 ≥3.5s |
| `breach_heavy` | 巨兽撞进营地,损伤很重! | ev breach(熊,-2);节流同上 |
| `danger_low` | 营地危险了,一只都不能再漏! | 耐久首次 ≤2(每局 1 次,配 danger 震) |
## G 组 · 技能(旁白 ×1)
| id | 台词全文 | 触发/语境 |
|---|---|---|
| `skill_ready` | 旋风斩准备好了。 | 旋风斩冷却完成(queue:true,每轮冷却 1 次) |
## H 组 · 结算(旁白 ×9;进 END 盖 800ms 手势死窗)
| id | 台词全文 | 触发/语境 |
|---|---|---|
| `end_win` | 天亮了,营地还在。 | 胜利结算链首句 |
| `end_lose` | 营地守不住了,撤退! | 失守结算首句(耐久归零) |
| `grade_s` | 评级,S!完美的围猎。 | 胜利评级 S(耐久 ≥9) |
| `grade_a` | 评级,A。漂亮。 | 评级 A(7-8) |
| `grade_b` | 评级,B。守得还行。 | 评级 B(4-6) |
| `grade_c` | 评级,C。险胜。 | 评级 C(1-3) |
| `end_rank` | 成绩已记入猎手榜。 | 胜利局排行榜写入后(失守局不播) |
| `end_again` | 单击,再猎一局。 | 结算链尾句(重开入口) |
结算链:胜利 = `end_win → grade_* → kill_band_*(自己击杀档)→ end_rank → end_again`;失守 = `end_lose → kill_band_* → end_again`。
## I 组 · 断线/重连(旁白 ×4)
| id | 台词全文 | 触发/语境 |
|---|---|---|
| `net_lost` | 与营地断了联系,正在重连。 | WS 断开(本地静默重连开始,≤30s) |
| `net_back` | 联系恢复了,接着打。 | resume 回槽成功(joined + snap 到达) |
| `net_to_bot` | 有猎人离队,老猎手顶上。 | roster 变更:他人槽位转 bot(非自己) |
| `net_gone` | 回不去那场围猎了,回大厅。 | 重连失败/房已灭(err room_gone 或 30s 超时)→ LOST |
## J 组 · 菜单/帮助(旁白 ×6;菜单不暂停,E3 豁免)
| id | 台词全文 | 触发/语境 |
|---|---|---|
| `sys_menu` | 菜单。单击切换,双击确认。 | 下滑呼出模态菜单 |
| `sys_menu_resume` | 回到猎场。 | 菜单项 1 |
| `sys_menu_help` | 重新听帮助。 | 菜单项 2 |
| `sys_menu_home` | 退出房间,回大厅。 | 菜单项 3(= 发 leave) |
| `help_main` | 朝声音的方向滑,就是出击。单击呼喊支援,双击旋风斩。上滑状态,下滑菜单。 | 菜单内上滑 / 帮助项 |
| `settings_hint` | 音色、语速、背景音乐,请回大厅修改。 | 菜单内设置提示 |
## K 组 · 向导/纠错(旁白 ×2;idle-only,queue:true)
| id | 台词全文 | 触发/语境 |
|---|---|---|
| `gd_invalid` | 这个操作现在用不上。 | 无效手势纠错(onceKey 同局 1 次,之后仅 error 震) |
| `gd_idle` | 听声音的方向,滑过去。 | WAVE/LOBBY 看门狗第 1 级(10s);TEACH 错向重教复用 |
---
**旁白合计:60 句 ×2 性别 = 120 条音频**(×2 格式 = 240 文件;预算 ≤60 打满,无冗余位)。
## N 组 · bot 喊话(24 句 = 3 角色 × 8;固定单音色,落盘 `audio/sfx/`,见头注)
> 触发映射(服务端纪律见主设计 §7.4:全局 10s ≤2 条,每 bot 20s 节流,help_call/help_ack 优先):
> `greet`=开局补位完成逐只间隔 ≥1.5s;`help_call`=bot 防区压力呼救 / **真人单击呼喊(用其座位角色声)**;
> `help_ack`=响应呼喊(空闲 bot 一只);`cover`=补位落定 / 转 bot 接管;`kill`=杀熊或三连杀(15s 节流);
> `breach`=本防区漏怪(20s 节流);`skill`=bot 放旋风斩;`wave`=第 2 波起轮转一只鼓劲。
| id | 台词全文 | 触发/语境 |
|---|---|---|
| `npc_yan_greet` | 我来了,这边有我。 | 阿岩 · 开局/补位 |
| `npc_yan_help_call` | 我这侧吃紧,来个人搭把手! | 阿岩 · 呼救(或座 0 真人呼喊) |
| `npc_yan_help_ack` | 撑住,我来了。 | 阿岩 · 响应呼喊 |
| `npc_yan_cover` | 这边交给我。 | 阿岩 · 补位/接管 |
| `npc_yan_kill` | 拿下! | 阿岩 · 杀熊/三连杀 |
| `npc_yan_breach` | 漏了一个,都小心。 | 阿岩 · 本防区突入 |
| `npc_yan_skill` | 都闪开,看我的! | 阿岩 · 旋风斩 |
| `npc_yan_wave` | 新一波,稳住。 | 阿岩 · 波次鼓劲 |
| `npc_zheng_greet` | 小筝到位,开工啦。 | 小筝 · 开局/补位 |
| `npc_zheng_help_call` | 这边太多了,谁来帮一下! | 小筝 · 呼救(或座 1 真人呼喊) |
| `npc_zheng_help_ack` | 别慌,我来了! | 小筝 · 响应呼喊 |
| `npc_zheng_cover` | 这边我罩着。 | 小筝 · 补位/接管 |
| `npc_zheng_kill` | 搞定! | 小筝 · 杀熊/三连杀 |
| `npc_zheng_breach` | 哎呀,漏了一个! | 小筝 · 本防区突入 |
| `npc_zheng_skill` | 看我清场! | 小筝 · 旋风斩 |
| `npc_zheng_wave` | 又来一波,打起精神。 | 小筝 · 波次鼓劲 |
| `npc_zhou_greet` | 老周我守着,放心。 | 老周 · 开局/补位 |
| `npc_zhou_help_call` | 我这边压不住了,来人。 | 老周 · 呼救(或座 2 真人呼喊) |
| `npc_zhou_help_ack` | 听见了,这就到。 | 老周 · 响应呼喊 |
| `npc_zhou_cover` | 这边交给我吧。 | 老周 · 补位/接管 |
| `npc_zhou_kill` | 嗯,解决了。 | 老周 · 杀熊/三连杀 |
| `npc_zhou_breach` | 啧,溜进去一只。 | 老周 · 本防区突入(「啧」风险见头注) |
| `npc_zhou_skill` | 让让,老骨头活动活动。 | 老周 · 旋风斩 |
| `npc_zhou_wave` | 听,又来了。 | 老周 · 波次鼓劲 |
**bot 喊话合计:24 条单音色音频**(×2 格式 = 48 文件;座 3 真人呼喊无角色声,用 `sfx/call_whistle` 哨音,主设计 §15 裁决 6)。
---
## V 组 · 键盘变体(key,11 对;`gesture.inputMode()==='key'` 时选播,措辞规范同 dungeon/bridge)
| id | 键盘台词全文(触屏原句见上方对应 id) |
|---|---|
| `room_wait` | 房间已开好,等同伴到齐。按空格,立即开局。 |
| `tut_rule` | 野兽会从八个方向围上来。声音在哪边,就按哪边方向键。 |
| `tut_attack` | 听,有狼在靠近。朝声音的方向,按方向键。 |
| `tut_callout` | 按空格,呼喊支援,队友会来帮你。 |
| `tut_skill` | 按回车,旋风斩,清掉身边一圈。 |
| `tut_skill_cd` | 旋风斩有八秒冷却。冷却中按回车,重新听威胁。 |
| `tut_status` | 按上方向键,听营地状态。按下方向键,打开菜单。 |
| `sys_menu` | 菜单。按空格切换,按回车确认。 |
| `help_main` | 朝声音的方向按方向键,就是出击。按空格呼喊支援,按回车旋风斩。上方向键状态,下方向键菜单。 |
| `end_again` | 按空格,再猎一局。 |
| `gd_idle` | 听声音的方向,按方向键。 |
**变体合计:11 对 ×2 性别 = 22 条**(×2 格式 = 44 文件;落 `voice/<gender>/key/`)。
---
**全集账目**:旁白 60×2 + bot 24 + key 变体 11×2 = **166 条音频**(×2 格式 = 332 文件)。
主设计触发逻辑对照 [23-hunt.md](23-hunt.md) §2/§5/§7/§8/§11/§12;协议事件(ev 种类 → 播报映射)对照其 §6.3。
+500
View File
@@ -0,0 +1,500 @@
# 23 · 《围猎》完整设计(hunt / Night Hunt)
> **给开发者的实现要点摘要**
> 1. **2-4 人实时联机纯音频协作守卫**,项目"纯音频 MMORPG"路线的**第一阶段最小验证切片**(§1.3)。本作只验证三件事:实时同步(零依赖 WebSocket + 服务端权威)、音频信道拥挤控制(§8,全文重点)、空间音频协作(方位=pan+防区分工)。其余一切从简。
> 2. 圆形猎场,猎人位于营地中心。野兽从 8 罗盘方位边缘逼近(8-12s),**四向滑=出击**(滑向映射 90° 弧,击杀弧内内圈最近一只,冷却 2.5s/人);突入中心 = 共享营地耐久 -1(巨兽 -2,初始 10);单击=呼喊支援(队友收到你防区方位的空间化喊话 + warning 震,bot 补位);双击=旋风斩(清内圈全部,冷却 8s;**冷却中双击=重听威胁播报**,宪法双击语义由此保留)。上滑=状态链,下滑=菜单(**不暂停**,联机实时豁免 E3)。
> 3. 服务端权威:怪物生成/移动/命中/冷却/评级全在 `web/server/` 裁决,客户端只发意图(join/act/leave)。bot 由服务器驱动,与真人同一套事件协议。房间 3-4 座,开局空位由 bot 补至 ≥3 猎人;断线 30s 宽限重连回槽,超时转 bot。
> 4. 协议全文 §6(两条传输消息 + 三类服务端消息,JSON 字段表逐条给出);零依赖 WS 握手与帧解析规范 §6.1(Node 原生 http upgrade + 手写帧,不引 npm)。
> 5. 拥挤控制硬规则 R1-R8(§8.1):旁白单句队列(voice 层)、bot 喊话 sfx 层同刻 ≤1、怪物移动 loop ≤3 路(最近 3 只)、非语音信息全部 <1s 短音效、听觉视野裁剪只报内圈(<4s 抵达)。
> 6. **引擎改动唯一一处(后向兼容)**:`engine/audio.js` 的 sfx/voice 播放各加可选 `opts.pan`(-1..1,StereoPannerNode 接入对应总线),缺省行为逐字节不变(§9)。怪物移动 loop 走游戏层句柄(abyss `_startLoop` 先例 + 游戏层自建 panner),不占引擎改动面。
> 7. 台词表 [23-hunt-voice-ids.md](23-hunt-voice-ids.md):旁白/系统音 **60 句 ×2 性别**(male=Uncle_Fu / female=Serena,预算打满)+ bot 喊话 **3 角色 ×8 句 = 24 条固定单音色**(阿岩=Uncle_Fu / 小筝=Serena / 老周=Eric;因走 sfx 层带 pan,**落盘 `audio/sfx/`**,异例说明见台词表头注)+ 键盘变体 11 对。数字信息全分档整句,无运行时拼接。
> 8. 设计裁决与对任务书核心案的修正记录(双击矛盾、出击冷却、防区模型、房码 UX 范围等 9 条)见 §15,**实施前必读**。断言清单住 [13-playtest-criteria.md](13-playtest-criteria.md) §24,本文 §13 只留摘要与指针。
---
## 1. 概述
### 1.1 一句话
夜幕降临,野兽从四面八方围上来——你和队友背背守住营地篝火,**听声音的方向滑过去就是一刀**,撑过五波,天亮就赢。
### 1.2 题材与纯音频相性
守夜围猎是"方位即生死"的题材:怪物唯一的接近预告就是它逼近的声像。视觉游戏里这是小地图+tower defense;纯音频里,**声像本身就是小地图**——题材与媒介互为 justification,与 dungeon「声音就是你的眼睛」同源,但本作是**实时+协作**,不是回合制探索。
### 1.3 定位:纯音频 MMORPG 路线 · 第一阶段最小验证切片
本项目长线方向是纯音频 MMORPG(持久世界、多玩家同图协作)。《围猎》是该路线的**第一个可玩切片**,刻意把范围切到最小,只验证三个技术-设计命题:
| 验证点 | 本文档落点 | 通过信号 |
|---|---|---|
| **实时同步**:零依赖 WS、服务端权威战斗、事件流+快照兜底、断线重连 | §6 协议 | 局域网 2-4 端同局,事件一致,断线 30s 内重连无感回槽 |
| **音频信道拥挤控制**:多源并发(旁白/bot 喊话/怪物 loop/定位音效)不糊 | §8 R1-R8 | 最坏并发 7 声流下威胁可辨、警报不丢(§8.4 验证口径) |
| **空间音频协作**:靠 pan 的方位分工与支援是否成立 | §3 防区 + §7 bot 补位 | 玩家能用呼喊-补位闭环分担压力,无需任何视觉 |
**明确不做(切片纪律)**:无持久世界、无跨房聊天、无跨玩家排行榜("排行榜"= 进度 API 的个人最佳桶 + 大厅卡片 summary,§12)、无房码音频录入 UX(协议保留,客户端 v1 只做快速匹配,§6.4)、无内购/社交。这些列 roadmap,不在本文档。
### 1.4 单局
5 波为一局,约 5 分钟(含 lobby ≈ 5.5 分钟;首局含教学 ≈ 6.5 分钟)。营地耐久初始 10、共享;耐久归零 = 营地失守(立即终局,突围结算,不入榜);撑过 5 波 = 胜利结算(评级 S~C + 个人击杀分档 + 排行榜写入)。
### 1.5 与现有 12 游戏的关系
实时方位感承接 space(呼啸来袭+同向反应),但改为**协作分工**;状态分档链承接 dungeon(上滑多段链);威胁裁剪承接 dungeon 脚步距离档音量规则并升级为显式"听觉视野"(§8.2);bot 喊话平铺资产承接 rpg/radio/detective NPC 先例,但首次走 **sfx 层空间化**(§8 R2);结算链死窗沿用 dungeon H1 先例(800ms 手势死窗);看门狗/向导层全照 00 §6。**两个"第一"**:第一个联机游戏、第一个用到运行时 pan(引擎 opts.pan,§9)。
### 1.6 本作三大特有风险(均有显式对策)
1. **多源并发糊耳**(5 波峰值:旁白+喊话+3 路 loop+定位音效)→ §8 规则表 + §8.4 验证断言。
2. **实时联机与纯音频教学的根本矛盾**(不能暂停真人局教学)→ 首局教学房(§5):单人+bot、教学波、学期零惩罚,教完同场接续第 1 波。
3. **协作流于形式**(都能打所有方向=不需要队友)→ 出击冷却 2.5s 限单人吞吐 + 软防区 + bot 补位闭环(§3.3、§7)。
## 2. 手势映射与豁免登记
### 2.1 战斗态(WAVE,含教学局 TEACH 的战斗段)
| 手势 | 作用 | 反馈 |
|---|---|---|
| 四向滑(上/下/左/右) | **出击**:滑向映射 90° 弧(上→{NW,N,NE}、右→{NE,E,SE}、下→{SE,S,SW}、左→{SW,W,NW}),击杀弧内内圈(eta≤4s)最近一只;冷却 2.5s | 引擎 swipe 震 + `atk_whoosh`;命中 `kill_*`+pickup 震;挥空 `atk_swish`+error 震 |
| 单击 | **呼喊支援**:全队友收到你防区方位的空间化喊话 + warning 震;bot 朝你侧补位 10s(§7.3);节流 5s/人 | 引擎 confirm 震 + 本机播自己座位角色的 help_call(中央) |
| 双击 | **旋风斩就绪** → 放出:清全场内圈(eta≤4s)全部怪物,冷却 8s;**冷却中** → 重听当前威胁播报(threat_*,宪法"双击=重听"落地) | 放出:success 震 + `skill_spin`;冷却中:引擎 confirm 震 + threat_* |
| 上滑 | 状态链:营地耐久档 → 波次档 → 在场猎人数 → 自己击杀档(4 段链播 queue:true) | hp_band_* → st_wave_* → team_* → kill_band_* |
| 下滑 | 模态菜单(**不暂停**,豁免 E3,§2.4) | menu earcon |
| 长按 | 非玩法输入:引擎旁白快进默认行为保留 | — |
- 出击冷却 2.5s 内四向滑 → 仅 error 震(无语音、不发 act;dungeon 脉冲冷却先例)。
- 单击呼喊是**非即时操作**,300ms tap 延迟无碍(escape 单击=检查先例)。
- 旋风斩就绪时刻播 `skill_ready`(queue:true,绝不顶句;每轮冷却完成只播一次)。
- **事件震动总表**:命中 pickup / 挥空 error / 旋风斩放出 success / 突入 hit / 收到呼喊 warning / 波次开始(ev wave)heartbeat / 耐久首次 ≤2 danger(每局 1 次,配 danger_low)。手势识别震动(swipe/confirm)由引擎内置承担,游戏层不重复(00 §2.3)。
### 2.2 标题/匹配/大厅等待(TITLE / MATCHING / LOBBY)
| 手势 | TITLE | MATCHING | LOBBY |
|---|---|---|---|
| 单击 | 快速匹配(首局=教学房) | —(无效,error 震) | 立即开局(act start) |
| 双击 | 重听开场/提示 | 重听 | 重听 room_wait |
| 上滑 | 个人最佳播报(有档时:best 评级+击杀档) | error 震 | 在场猎人播报(team_*) |
| 下滑 | 菜单(帮助/回大厅) | 菜单(含"取消匹配") | 菜单(退出房间回大厅) |
| 左右滑 | error 震 | error 震 | error 震 |
### 2.3 结算态(END)
单击=再来一局(重新进匹配);双击=重听结算链;上滑=重听个人最佳段;下滑=菜单(回大厅)。**结算链进入时盖 800ms 手势死窗**(dungeon H1 先例:吞掉致命事件前后在途的 ghost 手势,死窗后手势照常)。
### 2.4 模态菜单(豁免 E3:联机实时不暂停)
下滑呼出 → 单击循环 → 双击确认 → 上滑=帮助(播 `help_main`)→ 下滑=关闭(00 §5.2 交互骨架不变)。**菜单打开期间战斗照常进行**(怪物照走、突入照扣)——联机不可单方暂停,这是与 00 §5.2"菜单打开时暂停计时类玩法"的显式豁免。菜单项固定 3 项:`sys_menu_resume`(回到猎场)/ `sys_menu_help`(重新听帮助)/ `sys_menu_home`(退出房间回大厅 = 发 leave,槽位按 §6.6 处理)。无"重新开始"(联机房局不可单方重开)。菜单打开即播 `sys_menu` + 首项;菜单内另播 `settings_hint`(设置只读、回大厅修改,00 §1.2)。
### 2.5 豁免登记(显式)
| # | 豁免条目 | 理由(先例) |
|---|---|---|
| E1 | 四向滑=出击(非选择/移动) | 以滑动为核心玩法(space 四向滑=闪避同款豁免);三件套(上滑/下滑/双击重听)全部保留 |
| E2 | 双击优先=旋风斩,仅冷却中=重听威胁 | 旋风斩是即时技,必须绑零延迟双击(00 §5.4:即时判定绑滑动或双击);冷却中重听保留宪法语义 |
| E3 | 菜单打开**不暂停** | 联机实时,暂停不可能;菜单项压到 3 项、打开即播提示,把"逛菜单被咬"的代价做成可感知风险 |
| E4 | 单击=呼喊(非确认/继续) | 非即时操作,300ms 延迟无碍(escape 单击=检查先例) |
教学(TEACH)逐条讲清 E1/E2/E4(§5);不占用长按(快进保留,无需 rateBoost 豁免);不开 immediateTap。
## 3. 世界模型与数值
### 3.1 猎场与怪物
- 圆形猎场:猎人位于圆心营地,野兽从边缘(8 罗盘方位 N/NE/E/SE/S/SW/W/NW)沿直线匀速逼近圆心。逼近全程 8-12s(按波次表)。
- 怪物状态由**服务器**持有:`{ id, mkind, dir, eta }`——eta = 距突入的秒数(服务端单调时钟)。客户端不推算位置,只按事件流+快照渲染(§6.7)。
- 怪物 2 型:
| 型 | mkind | 逼近 | 突入扣耐久 | 声音 |
|---|---|---|---|---|
| 饿狼 | `wolf` | 8-10s | -1 | `howl_wolf` 生成呼啸 + `loop_wolf` 移动 |
| 暴熊 | `bear` | 11-12s | **-2** | `howl_bear` 低吼 + `loop_bear` 重脚步 |
- **距离分档**(听觉视野,§8.2):外圈 eta>6s(仅生成呼啸一声);中圈 4-6s(移动 loop 可闻,低音量);内圈 eta≤4s(可击杀的近战窗,loop 升档,计入威胁播报)。
### 3.2 出击与技能(服务端裁决,§6.5)
- **出击**:`act attack dir` → 弧 = dir±45°(3 个罗盘向)→ 弧内活着且 eta≤4.0 的怪中取 eta 最小者击杀;无 → `miss`。冷却 2.5s/座(服务器计时,违规 `err cooldown`)。
- **旋风斩**:`act skill` → 清除全场 eta≤4.0 全部怪(不分方向)。冷却 8s/座(违规 `err skill_cd`)。
- **呼喊**:`act callout` → 广播 `ev callout`;bot 补位(§7.3)。节流 5s/座。
- 击杀归属 = act 来源槽位(结算个人击杀账)。
### 3.3 防区(软分工)
- 每座位固定**主防区**(连续罗盘弧)与防区中心(喊话 pan 用):
| 猎人数 | 座位 0 | 座位 1 | 座位 2 | 座位 3 |
|---|---|---|---|---|
| 3 | [N,NE,E],中心 NE | [SE,S,SW],中心 S | [W,NW],中心 WNW | — |
| 4 | [N,NE],中心 NNE | [E,SE],中心 ESE | [S,SW],中心 SSW | [W,NW],中心 WNW |
- 防区**不是硬限制**:任何玩家可打任何方向(弧映射见 §2.1)。防区用于:① bot 目标选择偏置(§7.2);② 呼喊/喊话的 pan 方位来源;③ 刷怪剧本的方位均衡(§3.5)。
- 座位角色(voice 身份):座 0=阿岩(yan)、座 1=小筝(zheng)、座 2=老周(zhou);**座 3 仅 4 人局存在,无专属角色**(呼喊=`call_whistle` 哨音,见 §15 裁决 6)。真人"扮演"座位角色:你的呼喊用你座位角色的预录声,断线转 bot 后声音身份无缝延续。
### 3.4 波次表(5 波,服务器剧本)
| 波 | 狼 | 熊 | 狼逼近 s | 熊逼近 s | 刷怪间隔 s | 波时长约 |
|---|---|---|---|---|---|---|
| 1 | 4 | 0 | 10.0 | — | 3.0 | ~21s |
| 2 | 6 | 0 | 9.5 | — | 2.5 | ~24s |
| 3 | 7 | 1 | 9.0 | 12.0 | 2.2 | ~29s |
| 4 | 8 | 1 | 8.5 | 11.5 | 1.8 | ~28s |
| 5 | 10 | 2 | 8.0 | 11.0 | 1.5 | ~30s |
- 波间休息 8s(`wave_clear` → 休整 → 号角 + 下一波播报)。波内怪物匀速逼近不止步,一波必在有限时间内出清(杀或漏),无需超时兜底。
- 每波开始播对应整句 `wave_band1`…`wave_band5`(台词表 C 组;先号角 `horn_wave` 后播报)。
- 全程怪物总数 39,耐久预算 10(熊 -2)。平衡目标:3 bot 水准下 solo 余耐久 6-9(评级 A/B),真人协作越好评级越高。
- 局时长核算:战斗 ≈ 132s + 休息 32s + lobby ≤12s ≈ **3 分钟战斗 + 首尾 ≈ 5 分钟** ✓(任务书"约 5 分钟")。
### 3.5 刷怪方位与种子
- 房间 seed 开局掷一次(`joined.seed`),mulberry32 派生:`rng(wave) = mulberry32((seed ^ (wave * 0x9E3779B9)) >>> 0)`。
- 每波方位分配:把本波怪物按座位数轮转发牌到各座位防区内的随机方位(同一方位同刻最多 2 只在场,防叠声);同 seed → 同 spawn 剧本(服务器单测可复现断言)。
- 服务端战斗模拟 tick = 100ms;快照 2Hz(§6.3)。
### 3.6 耐久与评级
| 结算时剩余耐久 | 评级 |
|---|---|
| ≥9 | S |
| 7-8 | A |
| 4-6 | B |
| 1-3 | C |
| 0(波内归零) | 失守(立即终局,无评级,不入榜) |
评级由服务器算好在 `ev end.grade` 下发(客户端不算分,防双源)。
**终局播报链**:胜利 = `end_win → grade_s/a/b/c → kill_band_*(自己击杀档)→ end_rank → end_again`;失守 = `end_lose → kill_band_* → end_again`(失守不播评级、不播 end_rank、不写 best,§12)。进 END 盖 800ms 手势死窗(§2.3)。
## 4. 状态机(客户端)
### 4.1 状态列表
`TITLE / MATCHING / LOBBY / TEACH / WAVE / MENU(overlay) / END / LOST`。MENU 为挂起 overlay(记录返回态,**不暂停战斗**);LOST = 连接不可恢复(回大厅出口)。
### 4.2 转移图
```
TITLE ──单击──> MATCHING ──joined──> LOBBY ──act start / 12s 到 / 满4人──> (ev wave 1)
│ 首局(tutDone=false) → join teach=true → joined.state.phase='teach' → TEACH
MATCHING ──连接失败──> LOST(net_lost→net_gone) ──单击──> TITLE
TEACH ──教学完成 / 下滑跳过──> WAVE(wave 1)
WAVE ──ev end──> END ──单击──> MATCHING(再来一局)
WAVE/LOBBY ──下滑──> MENU ──返回──> 原态(战斗不暂停)
WAVE ──WS 断开──> 本地静默重连 ≤30s(net_lost) ──joined(resume)──> WAVE(net_back)
└──超时/房已灭──> LOST(net_gone)
```
### 4.3 每状态手势映射总表
| 手势 | TITLE | MATCHING | LOBBY | TEACH | WAVE | MENU | END |
|---|---|---|---|---|---|---|---|
| 四向滑 | error | error | error | 步骤作答/出击 | 出击 | error | error |
| 单击 | 快速匹配 | error | 立即开局 | 步骤作答/呼喊 | 呼喊 | 循环切项 | 再来一局 |
| 双击 | 重听 | 重听 | 重听 | 步骤作答/旋风斩 | 旋风斩/重听威胁 | 确认执行 | 重听结算链 |
| 上滑 | 最佳播报 | error | team_* | 状态链(T4 起) | 状态链 | 帮助 | 重听最佳段 |
| 下滑 | 菜单 | 菜单(取消匹配) | 菜单 | **跳过教学** | 菜单(不暂停) | 关闭 | 菜单 |
| 长按 | 快进 | 快进 | 快进 | 快进 | 快进(非玩法) | — | 快进 |
无效手势一律 error 震;full 档语音纠错 `gd_invalid`(onceKey 同局 1 次)。看门狗语音 idle-only。
## 5. 教学编排(TEACH,首局教学房)
**矛盾与解法**:实时联机局不能暂停教学 → 首次进入(进度 `tutDone` 为假)走**教学房**:单人 + 2 bot(教学段 bot AI 挂起)、先行一波**教学波**(3 狼,逼近 12s,学期零惩罚:教学波突入**不扣耐久**),教学完成或下滑跳过后,同一房间直接接续第 1 波(bot 激活)。教学即真联机局——这本身就是切片验证点(首局就跑了完整的 WS/事件流路径)。
| 步 | 旁白 | 等待动作 | 通过/反馈 |
|---|---|---|---|
| T0 | `intro_1` + `intro_2` + `tut_rule` | 单击 | confirm 震 |
| T1 | tut_attack(服务器放教学狼 1,方位 N) | 朝声音方向滑 | kill → tut_kill_ok;挥空/错向 → gd_idle 重教(≤2 次后自动放过该狼) |
| T2 | tut_callout | 单击呼喊 | callout ev → bot `npc_*_help_ack` 响应演示(教学 bot 必答)→ tut_callout_ok(呼喊-响应闭环点明) |
| T3 | tut_skill(服务器放教学狼 2、3,贴近 eta 5s) | 双击旋风斩 | cleared=2 → tut_skill_cd(讲冷却与冷却中双击=重听) |
| T4 | tut_status | 上滑(状态链) | 链播完 → tut_breach → tut_done → 进 WAVE 1 |
| 任意 | — | 下滑 | tut_skip → 进 WAVE 1(`tutDone` 写入进度) |
- 教学波零惩罚:教学狼突入不扣耐久(明说规则前不漏真话);T1 首输入 ≈ t+20s(<60s ✓);首个正反馈 = T1 首杀 ≈ t+40s(≤3min ✓)。
- 看门狗:每步 8s nudge 纯震(教学句不重播),T1/T3 卡 30s 自动放过当前狼推进(space 先例),不卡流程。
- 二周目起 TITLE 单击直达快速匹配(tutDone 存进度,§12)。
## 6. 多人协议(两个开发 agent 的实现契约)
### 6.1 传输:零依赖 WebSocket(server.js 增补)
- 端点:`GET /ws/hunt`,与静态/REST 同一 HTTP 服务器、同一端口。`http.createServer` 的 `'upgrade'` 事件接管:校验 `Upgrade: websocket`、`Sec-WebSocket-Key`,回 `101` + `Sec-WebSocket-Accept = base64(sha1(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))`(node:crypto,无 npm)。
- 帧解析(手写,约 100 行,建议独立 `web/server/ws.js` 便于单测):客户端→服务器帧**必须带 mask**(不带 → close 1002);支持 opcode 0(continuation 拼装)/1(text)/8(close)/9(ping→回 pong)/10(pong);payload 7/16/64 位长度全支持;单条消息上限 **4KB**(超限 close 1009);二进制帧 close 1003。服务器→客户端帧不 mask。
- 活性:服务器每 15s ping,45s 无 pong 判死(等同断线走 §6.6);每连接应用消息速率帽 20 条/s(超限 err `rate`,再犯 close)。
- 发送背压:每连接一个待发队列;`snap` 可丢(新顶旧),`ev/joined/err` 不丢;队列 >200 → close 1013。
- 每消息 = 1 条 text 帧 = 1 个 JSON 对象,必有 `t` 字段。协议版本 `ver: 1`(join 携带,不符 err `bad_msg`)。
### 6.2 客户端→服务器消息(3 条)
**join**(连接后首条,5s 内不发则 close):
| 字段 | 类型 | 含义 |
|---|---|---|
| `t` | `"join"` | — |
| `ver` | number | 协议版本,恒 1 |
| `room` | string | `"quick"` = 快速匹配;或 4 位房码(大写无歧义字符 `23456789ABCDEFGHJKMNPQRSTUVWXYZ`) |
| `name` | string | 显示名(1-24 字符,取自档案;服务端截断兜底) |
| `teach` | bool? | true = 请求教学房(首局;服务端强制单人房+教学波) |
| `resume` | string? | 重连凭证(`joined.resume` 原值);携带时按 §6.6 回槽 |
**act**(对局意图;服务器按到达序裁决):
| 字段 | 类型 | 含义 |
|---|---|---|
| `t` | `"act"` | — |
| `kind` | `"attack" \| "callout" \| "skill" \| "start"` | start 仅 LOBBY(立即开局) |
| `dir` | `"N"\|"NE"\|"E"\|"SE"\|"S"\|"SW"\|"W"\|"NW"` | attack 必填(客户端四向滑只发 N/E/S/W 四基向,弧解析在服务端;bot 可发 8 向) |
| `seq` | number | 客户端单调递增(去重/乱序检测;乱序不拒,仅日志) |
| `at` | number | 客户端发出时刻 ms(仅诊断用,裁决以服务端到达序为准) |
**leave**:`{ "t": "leave" }` —— 主动退出(菜单"回大厅"),槽位立即按 §6.6 处理(无 30s 宽限)。
### 6.3 服务器→客户端消息(joined / ev / snap / err)
**joined**(join 受理;resume 重连亦发):
| 字段 | 类型 | 含义 |
|---|---|---|
| `t` | `"joined"` | — |
| `room` | string | 实际房码 |
| `slot` | number | 你的座位号 0-3 |
| `resume` | string | 重连凭证(16 hex,每槽位签发一次,连接存活期内不变) |
| `char` | `"yan"\|"zheng"\|"zhou"\|null` | 座位角色(座 3 恒 null) |
| `sector` | string[] | 你的主防区方位集(§3.3) |
| `seed` | number | 房间种子(复现用) |
| `state` | object | `{ phase: "lobby"\|"teach"\|"wave"\|"end", wave: number, dur: number }` |
| `roster` | array | 全花名册:`[{ slot, name, bot, char, sector }]`(bot=true 时 name=角色名) |
**ev**(事件流,即时下发;`ts` = 房间单调时钟 ms(开局为 0)):
| kind | 附加字段 | 含义 |
|---|---|---|
| `spawn` | `id, mkind("wolf"\|"bear"), dir, eta` | 怪物生成(eta=逼近全程秒数) |
| `kill` | `id, slot, dir, mkind` | 出击命中击杀(归属 slot) |
| `miss` | `slot, dir` | 挥空(弧内无可打怪) |
| `skill` | `slot, cleared: number[]` | 旋风斩放出,cleared=被清怪 id 表 |
| `breach` | `id, mkind, dir, dur` | 突入:耐久新值 dur(wolf -1 / bear -2 已结算) |
| `wave` | `wave` | 第 wave 波开始(1-5) |
| `waveclear` | `wave` | 第 wave 波出清 |
| `callout` | `slot` | 求援呼喊(真人单击或 bot 压力呼救;接收端播该座角色 help_call + warning 震) |
| `botline` | `slot, line` | bot 喊话(line ∈ `greet/help_ack/cover/kill/breach/skill/wave`;客户端按 char+line 播 `sfx/npc_<char>_<line>`) |
| `roster` | `roster` | 花名册变更(加入/离开/断线/转 bot;形状同 joined.roster) |
| `end` | `win, dur, grade("S"\|"A"\|"B"\|"C"\|null), kills` | 终局;kills=`{ "0": 5, ... }` 按 slot 的个人击杀 |
**snap**(2Hz 兜底快照):
| 字段 | 类型 | 含义 |
|---|---|---|
| `t` | `"snap"` | — |
| `ts` | number | 房间单调时钟 ms |
| `phase` / `wave` / `dur` | — | 同 joined.state |
| `mobs` | array | 在场怪物全量 `[{ id, mkind, dir, eta }]`(eta 为 ts 时刻值) |
| `slots` | array | `[{ slot, alive, bot, cd, skillCd }]`(cd/skillCd=剩余冷却秒,重连恢复冷却 UI/判定用) |
**err**:`{ "t": "err", "code": "cooldown"|"skill_cd"|"bad_dir"|"not_ingame"|"room_full"|"room_gone"|"bad_msg"|"rate", "msg": "人话描述" }`(cooldown/skill_cd 为正常违规拒绝,客户端不应惊扰玩家——客户端本地已先拦)。
### 6.4 房间管理与匹配
- 快速匹配:找 `phase='lobby'` 且有空人类座的房 → 进;没有 → 建房(房码 4 位随机)。**客户端 v1 只发 `room:"quick"`**;显式房码 join 协议完整、服务端实现并单测,客户端录入 UX 列 roadmap(§15 裁决 5)。
- LOBBY 上限 12s:到点、任一人类 `act start`、或满 4 人 → 开局(播 `room_start`,随后 `ev wave 1` + `wave_band1`)。开局时空位由 bot 补至 **max(3, 人类数)** 猎人:1 人→2 bot、2 人→1 bot、3 人→0 bot(3 座)、4 人→0 bot(4 座)。第 4 座仅 4 人局启用。
- join 只在 lobby 受理;对局中 join → err `room_full`(v1 不做中途加入)。
- 教学房:join `teach:true` 且本地 tutDone=false → 强制单人房(§5 剧本);教学波完直接续 WAVE 1。
### 6.5 服务端权威战斗(hunt-room.js,建议独立模块便于单测)
- 房间结构:`{ seed, phase, slots[≤4], mobs: Map, wave, dur, tickTimer, snapTimer }`;tick 100ms:eta 递减、突入结算、波剧本推进;snap 500ms。
- 裁决规则(§3.2 数值的权威实现):弧映射、eta≤4.0 近战窗、冷却 2.5/8s、呼喊节流 5s——全部只在服务端生效;客户端可做本地预扣(冷却 UI),但以 err/ev 为准。
- bot = 特殊 slot(bot:true,无连接):同一套裁决管线,由 §7 AI 产生 act。
- **时钟**:ts 为房间单调 ms。客户端在 joined/snap 到达时记 `offset = ts - performance.now()`,渲染用 `nowServer = performance.now() + offset`;局域网无需 RTT 补偿(逼近时长 8-12s,误差淹没在听觉分辨率内)。
### 6.6 断线与重连
- 连接断开(或 45s 无 pong)→ 槽位标记断线(roster 广播 alive=false),**30s 宽限**:槽位静默(无 act 产出,猎人"愣在原地"——这是真实的协作代价)。
- 宽限内带 `resume` 重连 → 回槽:重发 joined(同 slot/seed/char)+ 紧随 snap 全量,客户端无缝续渲染(net_back)。
- 宽限超时:剩余人类 ≥3 → 槽位移除(roster);否则槽位转 bot(roster,bot=true,char 沿用座角色,补位 bot 播 `npc_*_cover`,其余客户端播 `net_to_bot`)。转 bot 不可逆(该局内)。
- 最后一个人类断开:房间保留至其 30s 宽限耗尽后销毁(无人类即灭,bot 不续命)。房灭后迟到 resume → err `room_gone`(客户端 net_gone → LOST)。
- 终局 end 后房间保留 30s(各端写排行榜)随后 close 1000。
### 6.7 客户端渲染模型(client agent 契约)
- 本地怪表 = ev 增删 + snap 校正:每 snap 对账——本地多出的 id 删、缺的按 snap 补、eta 偏差 >0.5s 校正。事件流为主、快照兜底,二者矛盾以 snap 为准。
- 每 500ms 重估渲染集:eta>6s 静默;≤6s 按 eta 升序取前 3 只起 loop(§8 R3);loop 音量按档(6-4s:0.30 / 4-2.5s:0.55 / <2.5s:0.80);pan 按方位角映射(§8.3)。
- 断网期间:本地怪表按最后已知 eta 继续衰减渲染(怪物照爬——戏剧张力保留),恢复后以 snap 校正。
## 7. Bot 设计(服务端驱动)
### 7.1 角色与声音资产
| 座位 | 角色 | char | speaker | instruct 常量 | 性格 |
|---|---|---|---|---|---|
| 0 | 阿岩 | yan | Uncle_Fu | `憨厚壮实的中年男声,猎户,话少踏实` | 憨厚 |
| 1 | 小筝 | zheng | Serena | `轻快爽利的年轻女声,猎手,干脆利落` | 轻快 |
| 2 | 老周 | zhou | Eric | `沙哑沉稳的老年男声,老猎手,带点调侃` | 沙哑 |
每角色 8 句固定短句(事件触发,全集见台词表 N 组):`greet / help_call / help_ack / cover / kill / breach / skill / wave`。**固定单音色平铺、不做双性别**(rpg NPC 先例);因经 sfx 层带 pan 播放,资产落 `audio/sfx/npc_<char>_<line>.{ogg,m4a}`(§15 裁决 7)。
### 7.2 战斗 AI(tick 500ms,确定性)
- 出击:冷却就绪且存在目标方向时出手。方向压力 `P(d) = Σ(1/eta)`(d 内活怪,仅计 eta≤4);主防区方向 ×1.5 偏置;取 max P 方向发 `act attack`(dir=该方向精确罗盘向)。**拟人反应带**:只在最优目标 eta ≤ 2.0 + rng()×1.0 时才出手,且每次出手决策带 80% 空转概率(rng 走房间 seed,复现可断言),避免" eta 一进 4 秒就秒"的机械感(数值为 §15 裁决 10 调参后口径)。
- 旋风斩:全场 eta≤2.5 的怪 ≥3 只且技能就绪,且 60% 概率想起 → `act skill`(裁决 10)。
- 呼救:主防区压力 ≥3(≥3 只 eta≤6)且 20s 节流 → `act callout`。
- 难度校准:bot 不失误(挥空不存在——只打有目标的弧),但受 2.5s 冷却、反应带与空转概率上限;3 bot 局实测评级 A/B 为主(§3.4 平衡目标,多种子扫描见 §15 裁决 10),给真人留出"比 bot 强"的空间。
### 7.3 补位(呼喊响应)
收到 `ev callout`(真人或 bot 发出)后:**空闲 bot**(当前 500ms 无待发 act 者,多取座位号小者)播 `help_ack`,且其后 10s 内目标选择偏置改为呼喊者防区方向 ×2.0(覆盖主防区偏置)——即"朝你侧补位"。补位 bot 首次在新防区击杀后再无怪时播 `cover`。呼喊本身不改变 bot 的冷却/反应带(公平)。
### 7.4 喊话调度(与 §8 R2 联动的服务端纪律)
- botline 触发源:greet(开局补位完成,逐只间隔 ≥1.5s)/ cover(转 bot 接管、补位落定)/ kill(杀熊或三连杀,15s 节流)/ breach(本防区漏怪,20s 节流)/ skill(放旋风斩)/ wave(第 2 波起,轮转一只 bot)。
- 服务端对 botline/callout 全局节流:**10s 窗口 ≤2 条**,超出丢弃(喊话是氛围,不是信息主通道);help_call/help_ack 优先级最高(呼救语义不可被氛围句顶掉)。
## 8. 音频信道拥挤控制(本文档重点章节)
### 8.1 硬规则表 R1-R8
| # | 规则 | 数值 |
|---|---|---|
| R1 | **旁白/系统音单句队列**(引擎 voice 层既有):同刻 1 句,链播 onComplete 串行;警报类(breach/breach_heavy/danger_low)可顶掉状态/播报类(上滑链、skill_ready),教学/结算链不可顶 | 同刻 voice=1 |
| R2 | **bot 喊话走 sfx 层**(带 pan,与旁白并发):同刻在播 ≤1 条;待播队列深 ≤1(新到顶掉未播的过期句);旁白在播时延迟投递 ≤2s,超时丢弃;服务端全局 10s ≤2 条(§7.4) | 同刻喊话=1 |
| R3 | **怪物移动 loop ≤3 路**:只渲染 eta 最小的 3 只(500ms 重估),其余静默;loop 经游戏层句柄 + 自建 StereoPannerNode(abyss `_startLoop` 先例,不占引擎改动面) | 同刻 loop≤3 |
| R4 | **定位 one-shot ≤2 并发**(howl/击杀/突入/旋风斩):同类间隔 ≥0.15s;波次生成潮的呼啸队列深 ≤2,超出丢弃(怪近后 loop 已接管信息,呼啸可弃) | 同刻 one-shot≤2 |
| R5 | **非语音信息全部 <1s 短音效**:号角 0.9s、join_chime 0.4s、call_whistle 0.6s、kill 0.5s 量级;语音通道只给人声 | — |
| R6 | **听觉视野裁剪**(§8.2):威胁播报只计内圈(eta≤4);状态链不报具体方位(方位由 loop pan 承载,语音永不逐只报方位——8 向 × 距离档的语音枚举被明确禁止) | — |
| R7 | **电平分层**:voice 层播时 BGM 自动 duck 20%(引擎既有);bot 喊话(sfx 层)不触发 duck → 游戏层统一 vol 0.55 压一档,防压旁白;loop 音量档上限 0.80 | — |
| R8 | **最坏并发核算**:旁白 1 + 喊话 1 + loop 3 + one-shot 2 = **7 声流**;频带分离(loop 低频 100-300Hz / 人声 300Hz-3kHz / howl 中高 1-3kHz / BGM 垫底),引擎限幅器兜底。峰值场景(第 5 波 + 警报 + 呼喊)不得丢警报、不得叠爆 | ≤7 声流 |
### 8.2 听觉视野裁剪规则
8 方位 × 3 距离档:
| 档 | eta | 可听内容 | 可被打 |
|---|---|---|---|
| 外圈 | >6s | 仅生成呼啸一声(one-shot,pan+距离音量 0.4) | 否 |
| 中圈 | 4-6s | 移动 loop(vol 0.30;受 R3 路数上限约束) | 否 |
| 内圈 | ≤4s | loop 升档(0.55/0.80);计入 threat_* 威胁播报与 danger 评估 | **是(近战窗)** |
- threat 口径:`threat_none`(内圈 0 只)/ `threat_few`(1-2 只)/ `threat_many`(≥3 只)——**只报数量档,不报方位**;方位信息永远由 loop/呼啸的 pan 实时承载(这本身就是切片要验证的"空间音频协作"命题)。
- 播报裁剪:状态链/威胁播报里禁止出现逐方位枚举句;台词表不含此类 id(结构性杜绝)。
### 8.3 方位 → pan 映射(全作统一)
方位角 az(N=0°、E=90°、S=180°、W=270°):`pan = sin(az)`(N/S=0、NE/SE=±0.7、E/W=±1、NW/SW=∓0.7)。**前/后歧义**(StereoPanner 不能分前后):后半平面(90°<az<270°)的 loop/呼啸音量 ×0.6("身后发闷",dungeon"背后=中央减半"先例);教学 tut_rule 讲明此约定。呼喊/喊话的 pan 取座位防区中心方位(§3.3)。
### 8.4 验证口径(harness/测试 agent 记录点)
- 逐事件记录:任一时刻 voice 在播句数、sfx 层人声句数、loop 路数、one-shot 并发数——断言不超 R1-R4 上限。
- 警报可达性:第 5 波峰值剧本下,breach/danger_low/wave 播报零丢失(可被顶掉的状态类句不计)。
- 实机听测 checklist(roadmap 真机轮):戴耳机分辨"左前狼 + 正右熊 + 旁白警报"三源。
## 9. 引擎改动(唯一一处,后向兼容)
`web/engine/audio.js`:
1. `_playSfx(id, { volume = 1, pan } = {})`:`typeof pan === 'number' && isFinite(pan)` 且 `pan !== 0` 时,clamp 到 [-1,1],链路改为 `source → gain → StereoPannerNode(pan) → sfxBus`;pan 缺省或 0 → **原路径逐字节不变**(不建 panner 节点)。`ctx.createStereoPanner` 不存在(老 Safari)→ 静默忽略 pan(不炸、不报错音)。
2. `_playVoice` 同款:opts.pan 存在时把 panner 插在 voice gain 与 voiceBus 之间;缺省不变。(**本作旁白不使用 voice pan**——对称能力一次给足,后续游戏可用;见 §15 裁决 7。)
3. 回归断言(harness 增补):全部既有游戏 play() 调用(无 pan)行为不变;pan=0 与缺省等价;pan=-1/1 时 panner 值正确。
本作使用面:bot 喊话、生成呼啸、击杀/突入/技能音全部 one-shot 带 pan;怪物 loop 走游戏层句柄(§8 R3)。
## 10. 音频三层资产
### 10.1 BGM(2 首,Mureka,-20 LUFS,ogg+m4a,90s 无缝循环)
| id | 状态映射 | prompt 草稿(英文) |
|---|---|---|
| `bgm/menu` | TITLE / MATCHING / LOBBY / END(结算 vol 0.5) | "Night camp ambient pad, distant crickets and low wind, warm fireplace crackle subtle, calm anticipation, no vocals, seamless loop" |
| `bgm/hunt` | TEACH / WAVE | "Tense night hunt drums, low tribal percussion 100 bpm, ominous strings pulses, cold wind, no vocals, seamless loop" |
切波不改 BGM(波次张力靠号角+怪密度);失守/胜利走引擎 crossfade 到 menu。语音 duck 20% 引擎自动。
### 10.2 SFX(13 + 24 喊话;SYN=程序合成,SA3=Stable Audio 3;双格式、真峰值 ≤-3dBFS)
| id | 方式 | 说明 |
|---|---|---|
| `atk_whoosh` | SYN | 挥击风声 0.3s(带 pan=出击方位) |
| `atk_swish` | SYN | 挥空空荡风声 0.3s(归因真话:没打到) |
| `kill_wolf` / `kill_bear` | SA3 | 狼哀嚎倒地 0.5s / 熊闷重倒地 0.7s(带 pan) |
| `skill_spin` | SA3 | 旋风斩气旋 0.8s(中央,大动态) |
| `breach_thud` | SA3 | 营地被撞闷响+木栅裂 0.6s(中央) |
| `horn_wave` | SA3 | 波次号角 0.9s(中央) |
| `join_chime` | SYN | 队友加入 0.4s(双音上行) |
| `call_whistle` | SYN | 求援哨音 0.6s(座 3 呼喊用,带 pan) |
| `howl_wolf` / `howl_bear` | SA3 | 生成呼啸 0.8s / 低吼 1.0s(带 pan,后半平面 ×0.6) |
| `loop_wolf` / `loop_bear` | SA3 | 移动循环:狼碎步+低呜 / 熊重步+喘息(4s 无缝循环源,游戏层 loop,pan+距离档音量) |
| `npc_<char>_<line>` ×24 | TTS | bot 喊话(voice 管线生成、落盘 sfx/,峰值归一 0.89 沿用 voice 策略;游戏层 vol 0.55,§8 R7) |
### 10.3 voice(旁白/系统音)
60 句 ×2 性别(male=Uncle_Fu / female=Serena,全表同一 instruct 常量/性别),分组与全文见 [23-hunt-voice-ids.md](23-hunt-voice-ids.md)。数字全分档整句:耐久 4 档 / 波次 3 档 + 5 整句 / 在场猎人 2 句 / 击杀 3 档 / 威胁 3 档 / 评级 4 整句。键盘变体 11 对(V 组,`gesture.inputMode()` 选播)。
规模核算:voice 60×2×2fmt ≈ 12MB + npc 24×2 ≈ 2MB + key 变体 11×2×2 ≈ 2MB + sfx 13×2 ≈ 1MB + bgm 2×2 ≈ 6MB ≈ **23MB < 30MB 预算** ✓。
## 11. 向导层接入(engine/guide.js,不新增 earcon 词表)
| 状态 | guide.state() | 时点 |
|---|---|---|
| TITLE / MATCHING / LOBBY / MENU | `menu` | 进态即播 |
| TEACH / WAVE | `battle` | 开局/波开始 |
| END | `settle` | 结算进入 |
| danger | `danger` | 耐久首次 ≤2(每局 1 次,配 danger 震 + danger_low) |
- turn cue 不使用(无回合制);看门狗:WAVE/LOBBY 无操作 [10s, 25s] → `gd_idle` / null(第 2 级纯 nudge);TEACH 每步 [8s] 纯 nudge(教学句不重播)。玩家任意手势复位;guide=off 全静零定时器(既定契约)。
- 纠错:无效手势 error 震 + `gd_invalid`(onceKey 同局 1 次,之后仅震)。
- 无卡关三级提示(本作无"卡住"形态:压力来自怪物而非谜题)。
## 12. 存档与排行榜(进度 API)
```json
{
"schema": 1, "game": "hunt", "updatedAt": 0,
"summary": "最佳评级A · 击杀14 · 营地余8 · 共3局",
"best": { "grade": "A", "kills": 14, "durLeft": 8, "at": "<ISO>" },
"runs": 3, "totalKills": 31, "tutDone": true
}
```
- **写入时机**:终局结算(每局一次,胜利/失守都写 runs/totalKills;`best` 只被胜利局按 评级 > 营地余耐久 > 击杀 的序更新)。客户端在收到 `ev end` 后自行 PUT(Bearer = 档案 token;离线只写 localStorage `soundgame.progress.hunt`)。00 §4 约定遵守:data 自由格式 + **必带 summary**(大厅卡片直接显示)。
- **summary 生成规则**:`最佳评级{grade} · 击杀{kills} · 营地余{durLeft} · 共{runs}局`;无 best(全败)时 `共{runs}局 · 还没守到天亮过`。
- **排行榜形态**:本切片无跨玩家排行 API——"排行榜"= 个人最佳桶 + 大厅卡片 summary(00 §4.3 既有机制)。跨玩家榜列 roadmap。
- 读取:TITLE 有 best 时上滑播最佳段(评级+击杀档);tutDone 控制是否进教学房(§5)。
## 13. playtest 断言(摘要;全表住 13 号文档 §24)
- **关键路径**:遮罩解锁 → TITLE → 快速匹配(首局教学房 T0-T4 逐步)→ LOBBY → 开局 bot 补位(roster 断言 3 猎人)→ 5 波(spawn/kill/breach/wave 事件序列与 §3.4 表逐波核对)→ 终局结算链(grade 与 dur 一致)→ PUT progress(summary 命中生成规则)→ 再来一局。
- **边界**:断线 30s 转 bot / 宽限内 resume 回槽无缝 / 旋风斩冷却与冷却中双击=重听 / 呼喊→bot 补位(10s 内 bot act 方向落入呼喊者防区)/ 听觉裁剪(eta>6 静默、loop ≤3、threat 只计内圈)/ 攻击冷却服务端拒绝 / snap 校正 / 菜单不暂停照扣耐久。
- **失败即不通过**:评级与 dur 不符(归因假话);服务器接受违规 act;ev 与 snap 矛盾不收敛;bot 喊话压过旁白(R1/R2 破);loop >3 路;教学狼扣耐久。
- 全表与全局通用断言见 [13-playtest-criteria.md](13-playtest-criteria.md) §24。
## 14. 自检清单
| # | 硬约束 | 自检 |
|---|---|---|
| 1 | FTUE 首输入 <60s / 首正反馈 ≤3min | T1 首滑 ≈t+20s;首杀 ≈t+40s ✓(§5) |
| 2 | 学习期零惩罚 | 教学波突入不扣耐久;做错重教 ≤2 次自动放过;挥空仅 error 震 ✓ |
| 3 | 手势宪法 | 上滑=状态、下滑=菜单、双击=重听(冷却中落地)保留;E1-E4 登记且教学讲清 ✓(§2.5) |
| 4 | 向导层 | 签名 4 态不新增词表;看门狗 [10/25]s 二级纯 nudge;纠错 onceKey;guide=off 全静 ✓(§11) |
| 5 | 旁白规范 | 60 句 ≤ 预算;整句无拼接;数字全分档;同 instruct 常量;无语速词;"重新听"规范写法 ✓(台词表头注) |
| 6 | 拥挤控制 | R1-R8 可断言;最坏并发 ≤7 声流;警报零丢失口径 ✓(§8) |
| 7 | 方位降维 | 8 方位 × 3 距离档,pan 七值 + 后半平面 ×0.6;无连续角度、无逐方位语音枚举 ✓(§8.2/§8.3) |
| 8 | 协议自洽 | C→S 3 条、S→C 4 类字段表全;服务端权威+快照兜底;断线/重连/房灭路径闭合 ✓(§6) |
| 9 | 引擎边界 | 引擎改动仅 opts.pan 一处且后向兼容;loop 走游戏层先例;30MB 预算内 ✓(§9/§10.3) |
| 10 | 服务器约束 | 零依赖(手写 WS 帧);数据落盘/REST 零改动;单实例约束不恶化(房间全内存) ✓(§6.1) |
| 11 | P1 视障 | 双击免费重听威胁;上滑一键复述;方位由声像承载且规则教学讲明;无倒计时焦虑(波次自然出清) ✓ |
| 12 | P3 通勤 | 单手免视;菜单不暂停但 3 项极简;局 5 分钟随玩随放 ✓ |
| 13 | 失败归因真话 | 评级=dur 服务端直发;结算击杀档=个人真账;挥空/失守有真话反馈(atk_swish/end_lose) ✓ |
| 14 | seed 可复现 | 房间 seed + mulberry32 派生;刷怪剧本/bot 反应带全走 seed;服务器单测可复现 ✓(§3.5/§7.2) |
## 15. 设计裁决与矛盾修正记录(实施前必读)
对任务书核心案的细化/修正,逐条登记(核心形态零改动):
1. **双击矛盾修正**:任务书"双击=旋风斩,8 秒冷却;**无冷却时**双击=重听威胁播报"自相矛盾(无冷却=就绪=放旋风斩)。裁定为:**旋风斩就绪时双击=放出;冷却中双击=重听威胁播报**——既保留技能,又让宪法"双击=重听"在全时段有定义(豁免 E2 登记)。
2. **补出击冷却 2.5s/人**:任务书未给出击冷却。无冷却则单人滑动吞吐无上限,协作失去必要性(§1.6 风险③)。2.5s 使单人峰值约 1 杀/窗,三人局才有分工意义。数值写在 §3.2,服务端裁决。
3. **防区=软分工模型**:任务书"玩家位于营地中心"与"bot 朝你侧补位"并存但无站位定义。裁定为**软防区**(§3.3):不设站位硬锁(任何方向都能打),防区只做 bot 偏置、喊话 pan 来源、刷怪均衡——保留"补位"语义且不引入连续几何。
4. **四向滑 → 90° 弧映射**:任务书"四向滑映射到最近罗盘方位"在 8 方位刷怪下不可玩(斜向怪永远打不到)。裁定:滑向映射 ±45° 弧,击杀弧内内圈 eta 最小者(§2.1/§3.2);协议 act.dir 仍用 8 罗盘枚举(bot 用满 8 向),客户端只发四基向。
5. **房码 UX 范围**:音频录入 4 位房码需数字语音资产(10 条)会击穿 60 句预算。裁定:协议 join.room 完整支持房码(服务端实现+单测),**客户端 v1 只做快速匹配**;音频录码 UX 列 roadmap(届时加数字整句资产)。
6. **第 4 座无角色声音**:bot 角色预算 3×8=24(任务书定死),而 4 人局第 4 座真人的呼喊无角色可用。裁定:座 3 呼喊=`call_whistle` 哨音(带 pan,<1s,R5 合规),bot 照常响应补位;哨音本身是有效求援信号,明示于 §3.3。
7. **bot 喊话落盘 sfx/(异例)**:任务书两处表述有张力——拥挤控制章"bot 喊话走 sfx 层空间化播放"与引擎章"voice 层同样支持 pan(bot 喊话用)"。裁定:喊话必须与旁白并发(R1/R2 的核心),voice 层单流做不到 → 走 sfx 层,引擎按 id 前缀分发故资产落 `audio/sfx/npc_*`(voice 管线生成不变);voice 层 opts.pan 能力同步提供但**本作不使用**。旁白 narrator 与 bot 同 speaker 的区分度风险(阿岩×male 旁白、小筝×female 旁白)按 abyss 先例标注"中",缓解=空间 pan + instruct 差异 + 语境(bot 只讲 1-2s 短喊话)。
8. **巨兽 -2**:任务书"突入=耐久-1"为默认口径;第 3 波起加入暴熊(-2)作为重威胁,给击杀优先级一个明确信号(重 loop/重倒地音可辨),数值与播报(breach_heavy)配套,不改核心形态。
9. **失守与入榜**:任务书"评级 S~C"隐含必完 5 波。裁定:耐久波内归零=失守终局(end_lose,无评级、不写 best,runs/totalKills 照记);胜利局才按 §3.6 评级写入 best——排行榜不被败局污染,失守有真话结算。
10. **bot 强度调参(§7.2 数值修订,协议字段不变)**:初版严格按 §7.2 公式(反应带 eta≤2.2+rng×1.2、无空转、旋风斩 eta≤3≥3 必放)实测**零泄漏**——多种子仿真 3 bot 水准局余耐久恒 10(S),甚至真人全程挂机 2 bot 独守也恒 10,§3.4"3 bot 水准余 6-9"与"协作必要性"(§1.6 风险③)落空。根因是吞吐超配:3 猎人 1.2 杀/s vs 峰值到达 0.67 杀/s,冷却 2.5s 不构成约束。裁定修订为:反应带 **eta ≤ 2.0+rng×1.0**;出手决策加 **80% 空转概率**(500ms tick 内愣神,等效出手周期 ~4.5s);旋风斩 **eta≤2.5 ≥3 只且 60% 概率想起**。调后 24 种子扫描(`web/server/test/hunt-balance-scan.mjs`):挂机局 17/24 失守或余 ≤3(6 场失守,mean 3.3,C/失守口径 ✓);3 bot 水准局 17/24 余 6-9(mean 7.5,A/B 为主 ✓);真人最优仍 23/24 满耐久 S("比 bot 强"空间保留 ✓)。bot"不失误"语义保留(只打有目标的弧),弱化的是**反应速度与出手频率**(拟人迟钝),不是命中率。
+1
View File
@@ -14,6 +14,7 @@
| [03-rhythm-rebuild.md](03-rhythm-rebuild.md) + [03-rhythm-voice-ids.md](03-rhythm-voice-ids.md) | 《节拍行者》重构施工稿:跟随领奏玩法、三首完整谱面、判定契约、31 句台词表 | ⑧ |
| [04-space-rebuild.md](04-space-rebuild.md) + [04-space-voice-ids.md](04-space-voice-ids.md) | 《盲航》重构:交互式演练、同向闪避、60 秒保护期、52 句台词表 | ⑨ |
| [05-rpg.md](05-rpg.md) + [05-rpg-chapter1.md](05-rpg-chapter1.md) + [05-rpg-ch1-voice-ids.md](05-rpg-ch1-voice-ids.md) + [05-rpg-addendum.md](05-rpg-addendum.md) | 新 RPG《剑与声》:总设计 + 第一章施工稿(场景图/战斗状态机/数值/存档/资产 prompt)+ 120 句台词表 + 增补(战斗震动配合、无声前摇协议、层叠规则) | ⑩ |
| [23-hunt.md](23-hunt.md) + [23-hunt-voice-ids.md](23-hunt-voice-ids.md) | 《围猎》2-4 人实时联机协作守卫(纯音频 MMORPG 路线第一阶段切片):零依赖 WS 协议 + 服务端权威战斗、bot 行为与补位、音频拥挤控制 R1-R8、引擎 opts.pan 改动 + 84 台词 id(60 旁白 ×2 性别 + 24 bot 喊话) | 长线方向 |
## new8 系列设计文档(8 款新 RPG,research/new8/00-program.md)
+11 -2
View File
@@ -1,6 +1,6 @@
# SoundGame 后续路线图(策划,最终轮)
范围:R5(RPG 收尾)之后的事。分四块:RPG 内容扩展、storyline 打磨、真机听测、商业化前置。
范围:R5(RPG 收尾)之后的事。分五块:RPG 内容扩展、storyline 打磨、真机听测、商业化前置、纯音频 MMORPG 路线。
设计基准文档索引见 [README.md](README.md);审查结论见 round1/round2/round3-4-review。
---
@@ -112,6 +112,15 @@
6. **付费墙设计预留**(如需):进度 API 加 `entitlements` 字段的位置先留好;不要在现有 data 自由格式里埋付费标记。
7. **合规扫尾**:无年龄分级素材问题(武侠战斗无血腥音效——atk_hit prompt 已注明 not gory,保持);商店/内购若上 iOS 需走 IAP 不能用网页支付(届时再议)。
## 6. 建议顺序
## 6. 纯音频 MMORPG 路线(第一阶段切片已落地)
- **第一阶段(已落地)**:《围猎》hunt([23-hunt.md](23-hunt.md))——2-4 人实时联机协作守卫,
验证三个切片命题:零依赖 WS 实时同步 + 服务端权威战斗、音频信道拥挤控制 R1-R8、
pan 空间音频协作(呼喊-补位闭环)。
- **后续步骤**(切片纪律外事项,先列 research/ 方向调研,不在本 roadmap 排期):
持久世界与跨房聊天、跨玩家排行榜、房码音频录入 UX(协议 join.room 已留,客户端 v1 只做快速匹配)、
更大规模同图协作的拥挤模型扩展。真机听测时按 23-hunt §8.4 口径补一轮"三源分辨"听测项。
## 7. 建议顺序
1. R5 收尾(RPG 第一章上线)→ 2. §3 存量打磨小项(半天级)→ 3. 真机听测 §4(用户执行,反馈回来后定参数旋钮)→ 4. storyline §2(与听测并行)→ 5. RPG ch2/ch3 §1 → 6. 商业化 §5(决定公网化时再启动)。
+7 -1
View File
@@ -129,10 +129,16 @@ new WebAudioEngine(basePath = 'audio/', opts?)
(key →「按空格开始」,touch →「轻触开始」);显式传 `text` 完全覆盖。
- `play(id, opts?)` — 统一入口,按 id 前缀(`bgm/` `voice/` `sfx/`)分发:
- 公共:`opts.volume`(0~1,乘层音量)
- 公共(sfx/voice):`opts.pan`(可选声像 -1~1)——非 0 有限数时在 gain
与层总线之间插入 `StereoPannerNode`(clamp 到 [-1,1]);**缺省或 0 时
原路径逐字节不变**(不创建 panner 节点);老 Safari 无
`ctx.createStereoPanner` 时静默忽略 pan(不炸、不降级音质)。
方位→声像的游戏层约定见各游戏设计文档(如 hunt §8.3 `pan = sin(az)`)。
- BGM:`opts.fadeMs`、`opts.loop = true`;同一首重复 `play` 不重启
- voice:`opts.onComplete`(**自然播完**才触发;被 `stopVoice()` 或新 voice
顶掉时**不触发**,对齐 Android——用这一点实现"打断即作废整条语音链");
`opts.queue = true` 排队等当前对白播完(默认 false 直接顶掉)
`opts.queue = true` 排队等当前对白播完(默认 false 直接顶掉,排队项同样
携带 pan)
- sfx:即发即弃,天然支持并发叠加
- `prewarm(ids: string[])` — 预解码一批音频,消除首次播放的网络+解码延迟。
建议 `start()` 里把所有短音效 prewarm 一遍。