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
+140 -3
View File
@@ -62,6 +62,8 @@ SPEAKERS = {
# abyss 默认=DECK 调度(多数行);DIVER 行按 VOICE_SPK["abyss"] 逐句覆盖(radio 同型)
"abyss": {"male": "Dylan", "female": "Vivian"},
"detective": {"male": "Uncle_Fu", "female": "Serena"},
# hunt 旁白双性别(23-hunt-voice-ids.md 头注);bot 喊话固定音色在 HUNT_NPC
"hunt": {"male": "Uncle_Fu", "female": "Serena"},
}
# instruct 对:(male, female)。None 表示两性别共用同一 instruct。
@@ -1262,7 +1264,125 @@ DETECTIVE_NPC = [
("fo_5", "Serena", "冷静干练的女声,验尸官,临床腔,短句平直", "鞋底是干的。当夜,死者没出过后门。"),
]
NPCS = {"rpg": RPG_NPC, "radio": RADIO_NPC, "abyss": ABYSS_NPC, "detective": DETECTIVE_NPC}
# ---------------------------------------------------------------- hunt
# 《围猎》批次 3(docs/design/23-hunt-voice-ids.md):旁白/系统音 60 句 ×2 性别
# (全表同一 instruct 常量/性别,禁语速词;数字全分档整句无拼接;「重听」写「重新听」)
# + bot 喊话 24 条固定单音色平铺(HUNT_NPC)+ 键盘变体 V 组 11 对(KEY_LINES["hunt"])。
# 异例(23-hunt.md §15 裁决 7):喊话走 sfx 层带 pan,convert 落盘 audio/sfx/ 而非
# audio/voice/(生成管线、峰值归一 0.89 与 voice 一致;不滤波)。
# H 组头注「×9」为文档笔误,实表 8 句(end_win/end_lose/grade_×4/end_rank/end_again),
# 旁白合计 4+11+6+12+3+3+1+8+4+6+2 = 60 ✓。
_HUNT = _pair("低沉醇厚的男声,像守夜猎人的领队,沉稳有力",
"清亮沉稳的女声,像守夜猎人的领队,冷静中带着鼓舞")
HUNT_LINES = [
# ---- A 组 · 开场/房间 ----
("intro_1", _HUNT, "夜幕降临,野兽在山林里游荡。"),
("intro_2", _HUNT, "你们是守夜的猎人,营地就在身后。"),
("room_wait", _HUNT, "房间已开好,等同伴到齐。单击,立即开局。"),
("room_start", _HUNT, "狩猎开始,先听声音的方向。"),
# ---- B 组 · 教学 TEACH ----
("tut_rule", _HUNT, "野兽会从八个方向围上来。声音在哪边,就往哪边滑。"),
("tut_attack", _HUNT, "听,有狼在靠近。朝声音的方向,滑一下。"),
("tut_kill_ok", _HUNT, "好,杀掉了。就是这样。"),
("tut_callout", _HUNT, "单击一下,呼喊支援,队友会来帮你。"),
("tut_callout_ok", _HUNT, "听到没,同伴回应你了。"),
("tut_skill", _HUNT, "双击,旋风斩,清掉身边一圈。"),
("tut_skill_cd", _HUNT, "旋风斩有八秒冷却。冷却中双击,重新听威胁。"),
("tut_status", _HUNT, "上滑,听营地状态。下滑,打开菜单。"),
("tut_breach", _HUNT, "野兽冲进营地,营地就会受损。撑过五波就赢。"),
("tut_done", _HUNT, "教学完了。真正的围猎,现在开始。"),
("tut_skip", _HUNT, "已跳过教学,开打。"),
# ---- C 组 · 波次 ----
("wave_band1", _HUNT, "第一波来了,别慌。"),
("wave_band2", _HUNT, "第二波,比刚才多。"),
("wave_band3", _HUNT, "第三波,有大家伙混在里面。"),
("wave_band4", _HUNT, "第四波,更快,更多。"),
("wave_band5", _HUNT, "最后一波,全来了!"),
("wave_clear", _HUNT, "这一波,守住了。"),
# ---- D 组 · 状态链(耐久档 → 波次档 → 猎人数 → 击杀档) ----
("hp_band1", _HUNT, "营地固若金汤。"),
("hp_band2", _HUNT, "营地受了些损伤。"),
("hp_band3", _HUNT, "营地损毁过半。"),
("hp_band4", _HUNT, "营地快守不住了。"),
("st_wave_a", _HUNT, "还在头几波。"),
("st_wave_b", _HUNT, "打到中段了。"),
("st_wave_c", _HUNT, "最后一波了。"),
("team_3", _HUNT, "三名猎人在守。"),
("team_4", _HUNT, "四名猎人全在。"),
("kill_band0", _HUNT, "你还没开张。"),
("kill_band1", _HUNT, "你小有斩获。"),
("kill_band2", _HUNT, "你战果累累。"),
# ---- E 组 · 威胁重听(只报数量档,方位由声像承载) ----
("threat_none", _HUNT, "近处暂时没有野兽。"),
("threat_few", _HUNT, "有野兽贴近了,听方向出手。"),
("threat_many", _HUNT, "四周全是动静,快出手!"),
# ---- F 组 · 警报(可顶状态/播报类) ----
("breach", _HUNT, "有野兽冲进营地了!"),
("breach_heavy", _HUNT, "巨兽撞进营地,损伤很重!"),
("danger_low", _HUNT, "营地危险了,一只都不能再漏!"),
# ---- G 组 · 技能 ----
("skill_ready", _HUNT, "旋风斩准备好了。"),
# ---- H 组 · 结算 ----
("end_win", _HUNT, "天亮了,营地还在。"),
("end_lose", _HUNT, "营地守不住了,撤退!"),
("grade_s", _HUNT, "评级,S!完美的围猎。"),
("grade_a", _HUNT, "评级,A。漂亮。"),
("grade_b", _HUNT, "评级,B。守得还行。"),
("grade_c", _HUNT, "评级,C。险胜。"),
("end_rank", _HUNT, "成绩已记入猎手榜。"),
("end_again", _HUNT, "单击,再猎一局。"),
# ---- I 组 · 断线/重连 ----
("net_lost", _HUNT, "与营地断了联系,正在重连。"),
("net_back", _HUNT, "联系恢复了,接着打。"),
("net_to_bot", _HUNT, "有猎人离队,老猎手顶上。"),
("net_gone", _HUNT, "回不去那场围猎了,回大厅。"),
# ---- J 组 · 菜单/帮助 ----
("sys_menu", _HUNT, "菜单。单击切换,双击确认。"),
("sys_menu_resume", _HUNT, "回到猎场。"),
("sys_menu_help", _HUNT, "重新听帮助。"),
("sys_menu_home", _HUNT, "退出房间,回大厅。"),
("help_main", _HUNT, "朝声音的方向滑,就是出击。单击呼喊支援,双击旋风斩。上滑状态,下滑菜单。"),
("settings_hint", _HUNT, "音色、语速、背景音乐,请回大厅修改。"),
# ---- K 组 · 向导/纠错(idle-only) ----
("gd_invalid", _HUNT, "这个操作现在用不上。"),
("gd_idle", _HUNT, "听声音的方向,滑过去。"),
]
# bot 喊话固定单音色平铺(id, speaker, instruct, 台词)→ web/hunt/audio/sfx/<id>.*
# 角色 speaker+instruct 恒定(23-hunt-voice-ids.md N 组表):阿岩=Uncle_Fu /
# 小筝=Serena / 老周=Eric;座 3 真人呼喊无角色声,用 sfx/call_whistle(裁决 6)。
HUNT_NPC = [
# 阿岩(座 0,Uncle_Fu)
("npc_yan_greet", "Uncle_Fu", "憨厚壮实的中年男声,猎户,话少踏实", "我来了,这边有我。"),
("npc_yan_help_call", "Uncle_Fu", "憨厚壮实的中年男声,猎户,话少踏实", "我这侧吃紧,来个人搭把手!"),
("npc_yan_help_ack", "Uncle_Fu", "憨厚壮实的中年男声,猎户,话少踏实", "撑住,我来了。"),
("npc_yan_cover", "Uncle_Fu", "憨厚壮实的中年男声,猎户,话少踏实", "这边交给我。"),
("npc_yan_kill", "Uncle_Fu", "憨厚壮实的中年男声,猎户,话少踏实", "拿下!"),
("npc_yan_breach", "Uncle_Fu", "憨厚壮实的中年男声,猎户,话少踏实", "漏了一个,都小心。"),
("npc_yan_skill", "Uncle_Fu", "憨厚壮实的中年男声,猎户,话少踏实", "都闪开,看我的!"),
("npc_yan_wave", "Uncle_Fu", "憨厚壮实的中年男声,猎户,话少踏实", "新一波,稳住。"),
# 小筝(座 1,Serena)
("npc_zheng_greet", "Serena", "轻快爽利的年轻女声,猎手,干脆利落", "小筝到位,开工啦。"),
("npc_zheng_help_call", "Serena", "轻快爽利的年轻女声,猎手,干脆利落", "这边太多了,谁来帮一下!"),
("npc_zheng_help_ack", "Serena", "轻快爽利的年轻女声,猎手,干脆利落", "别慌,我来了!"),
("npc_zheng_cover", "Serena", "轻快爽利的年轻女声,猎手,干脆利落", "这边我罩着。"),
("npc_zheng_kill", "Serena", "轻快爽利的年轻女声,猎手,干脆利落", "搞定!"),
("npc_zheng_breach", "Serena", "轻快爽利的年轻女声,猎手,干脆利落", "哎呀,漏了一个!"),
("npc_zheng_skill", "Serena", "轻快爽利的年轻女声,猎手,干脆利落", "看我清场!"),
("npc_zheng_wave", "Serena", "轻快爽利的年轻女声,猎手,干脆利落", "又来一波,打起精神。"),
# 老周(座 2,Eric)
("npc_zhou_greet", "Eric", "沙哑沉稳的老年男声,老猎手,带点调侃", "老周我守着,放心。"),
("npc_zhou_help_call", "Eric", "沙哑沉稳的老年男声,老猎手,带点调侃", "我这边压不住了,来人。"),
("npc_zhou_help_ack", "Eric", "沙哑沉稳的老年男声,老猎手,带点调侃", "听见了,这就到。"),
("npc_zhou_cover", "Eric", "沙哑沉稳的老年男声,老猎手,带点调侃", "这边交给我吧。"),
("npc_zhou_kill", "Eric", "沙哑沉稳的老年男声,老猎手,带点调侃", "嗯,解决了。"),
("npc_zhou_breach", "Eric", "沙哑沉稳的老年男声,老猎手,带点调侃", "啧,溜进去一只。"),
("npc_zhou_skill", "Eric", "沙哑沉稳的老年男声,老猎手,带点调侃", "让让,老骨头活动活动。"),
("npc_zhou_wave", "Eric", "沙哑沉稳的老年男声,老猎手,带点调侃", "听,又来了。"),
]
NPCS = {"rpg": RPG_NPC, "radio": RADIO_NPC, "abyss": ABYSS_NPC, "detective": DETECTIVE_NPC,
"hunt": HUNT_NPC}
# ---------------------------------------------------------------- 键盘变体(P1 路线 a)
# 「生效输入模式=键盘」的玩家听按键措辞:向左滑→按左方向键(四向同理,WASD 不进
@@ -1481,6 +1601,20 @@ KEY_LINES = {
("cf_confirm", "就这句?空格,定下来。"),
("settle_hook", "同一场雨里,还有别的案子。按空格,接新案。"),
],
# hunt:V 组 11 对(23-hunt-voice-ids.md 文末;措辞「方向键/空格/回车」同 dungeon 规范)。
"hunt": [
("room_wait", "房间已开好,等同伴到齐。按空格,立即开局。"),
("tut_rule", "野兽会从八个方向围上来。声音在哪边,就按哪边方向键。"),
("tut_attack", "听,有狼在靠近。朝声音的方向,按方向键。"),
("tut_callout", "按空格,呼喊支援,队友会来帮你。"),
("tut_skill", "按回车,旋风斩,清掉身边一圈。"),
("tut_skill_cd", "旋风斩有八秒冷却。冷却中按回车,重新听威胁。"),
("tut_status", "按上方向键,听营地状态。按下方向键,打开菜单。"),
("sys_menu", "菜单。按空格切换,按回车确认。"),
("help_main", "朝声音的方向按方向键,就是出击。按空格呼喊支援,按回车旋风斩。上方向键状态,下方向键菜单。"),
("end_again", "按空格,再猎一局。"),
("gd_idle", "听声音的方向,按方向键。"),
],
}
@@ -1558,6 +1692,7 @@ GAMES = {
"dungeon": DUNGEON_LINES,
"abyss": ABYSS_LINES,
"detective": DETECTIVE_LINES,
"hunt": HUNT_LINES,
}
@@ -1821,13 +1956,15 @@ def convert(games):
subprocess.run(["ffmpeg", "-y", "-v", "error", "-i", src,
"-ar", "44100", "-c:a", "aac", "-b:a", "128k", base + ".m4a"], check=True)
total += 1
# NPC 平铺(staging <game>/npc/ → audio/voice/<id>.*;radio 按 NPC_BANDPASS 带通)
# NPC 平铺(staging <game>/npc/ → audio/voice/<id>.*;radio 按 NPC_BANDPASS 带通)。
# 异例:hunt 喊话走 sfx 层带 pan(23-hunt.md §8 R2 / §15 裁决 7),落 audio/sfx/。
npc_layer = "sfx" if game == "hunt" else "voice"
for fid, _sp, _ins, _t in NPCS.get(game, []):
src = os.path.join(STAGING, game, "npc", fid + ".wav")
if not os.path.exists(src):
print(f"[convert] WARN missing {game}/npc/{fid}.wav")
continue
base = os.path.join(ROOT, "web", game, "audio", "voice", fid)
base = os.path.join(ROOT, "web", game, "audio", npc_layer, fid)
af = npc_af(game, fid)
af_args = ["-af", af] if af else []
subprocess.run(["ffmpeg", "-y", "-v", "error", "-i", src, *af_args,