feat(new8/batch1): 两款新 RPG《不眠频率》radio +《回声地窟》dungeon 全套(多agent流水线:策划→资产→开发→测试→玩家R1→修复→R2)

- 程序章程 research/new8/00-program.md(8款概念/回合协议/验收门/进度账)
- 设计:docs/design/15-radio.md+voice-ids(132句,6状态9抉择4结局,电话滤波资产侧带通)/16-dungeon.md+voice-ids(125句,脉冲回声/7层seed生成/三档战斗/元进度遗产)
- 资产:TTS 970文件(GPU flock串行)+ Mureka BGM 8首 $0.24(台账 mureka_log.md)+ 程序合成 sfx(含预pan侧墙/开口、phone_ring铃声、amb_cave呼吸层、turn_tick)+ 终检 check_all_assets 8游戏全绿
- radio:game.js+playtest 221断言(4结局矩阵/pendingChoice盲玩公平/RING两级看门狗/菜单循环/存档schema)
- dungeon:game.js+playtest 1610断言(seed全网格回声一致性/学期零死/格挡80%/死亡归因真话/元进度/菜单冻结/上滑5段状态链)
- 引擎修复:audio.js _playVoice 加载竞态补杀 token(R1真机71分钟语音风暴根因)+ 同id 3s窗口≥5次熔断保险丝 + gesture.js 长按boost事件门(+66行,9套件回归全绿)
- 大厅:新增 radio/dungeon 卡片 + GAMES 数组
- harness:sessions/radio.mjs+dungeon.mjs(主线+压力双剧本)+ transcribe 注册 + voice_text.json 刷新(8游戏704句)+ 13号文档 §20 登记
- R1 存档:4人设评审×2游戏 + 真机试玩报告×2(radio 5/5愿意玩;dungeon 0/4→确诊风暴)+ synthesis×2 + 转写8份
- R2 修复:radio F1-F5(看门狗补挂/rep_low改词重录/真铃声/ASK续玩语义/链末跳过保护)+ dungeon D0-1..D1-6(风暴根治/回声真实性/密度下限+呼吸层/导航最短路/菜单关闭反馈/金币档同账本)+ 实现核对登记
This commit is contained in:
xsl
2026-09-02 18:52:37 +08:00
parent 3537610130
commit d870a75a26
1143 changed files with 24335 additions and 17 deletions
+26
View File
@@ -19,6 +19,8 @@
| rhythm | Uncle_Fu | Vivian | female 沿用原解说音色 |
| space | Uncle_Fu | Serena | female 沿用原导航音色 |
| rpg | Uncle_Fu | Serena | 旁白;NPC 单音色平铺(见 rpg 章节) |
| radio | Uncle_Fu(HOST)/Dylan(PROD) | Serena(HOST)/Vivian(PROD) | 双性别行同性别双 speaker(VOICE_SPK 逐句覆盖);NPC 单音色平铺 5 音色+逐人带通(见 radio 章节) |
| dungeon | Uncle_Fu | Serena | 旁白全表统一 instruct,无 NPC |
关于 Ryan:冒烟测试样本在 `tools/samples/ryan_zh_*.wav`(3 条中文句)。Ryan 是英语母语音色(见 `env.d/tts.md`),项目已有中文发音投诉(反馈⑥),男声统一采用中文母语的 Uncle_Fu;如需换成 Ryan,改 `tools/gen_voice_dual.py` 的 `SPEAKERS` 表重跑即可。
@@ -89,6 +91,30 @@
- bgm(6,Mureka):`village battle boss ending`(R4)+ R6 新增 `menu`(菜单 pad)与 `explore`(探索轻垫乐,10 文档 §7 恢复,volume 0.35);90s 无缝循环;`amb_*` 由游戏层手动 loop 接 sfx 总线
- 再生成方法见 `web/rpg/gen/README.md`
### radio《不眠频率》(批次 1 新游戏,叙事模式,台词依据 `docs/design/15-radio-voice-ids.md`)
- voice 双性别版(male/ 与 female/ 各 103 条):**HOST 主播** 29 条(male=Uncle_Fu「低沉温和的中年男声,深夜电台主播,沉稳亲切,有磁性」/ female=Serena「温暖清晰的年轻女声,深夜电台主播,沉稳亲切,不甜腻」:开场 `b_open b_rain`、主播句 `z1_* l1_* k1_* w1_* h1_*` 主播行、心结 `d9_face d9_avoid`、结局 `e1–e4_a/b`)+ **PROD 导播/系统/向导** 74 条(male=Dylan / female=Vivian,instruct「贴近耳机的低语男/女声,年轻电台导播,冷静克制」:`gd_* sys_* prog_c1–5 rep_*`、导播行、抉择提示 `p1–p9`、尾声 `ep_*`)。同性别双 speaker 逐句覆盖见 `tools/gen_voice_dual.py` 的 `VOICE_SPK["radio"]`(同场景高频对话的两者永不同 speaker)。
- voice NPC 平铺(29 条,落 `voice/` 根,引擎平铺回退;**转码带通**见下):老周 7 条(z1_*,Uncle_Fu·苍老沙哑)、小鹿 4 条(l1_*,Serena·哭腔倔强)、糖糖 6 条(k1_*,Vivian·稚嫩童声)、陈姐 7 条(w1_*,Vivian·疲惫沙哑中年)、老韩 5 条(h1_*,Eric·沙哑粗犷司机)。
- **通话滤波(资产侧,15-radio.md §6.2)**:convert 转码时按来电者逐档 ffmpeg 带通+轻压缩(老周 500-2800 / 小鹿 400-3400 / 糖糖 450-3000 / 陈姐 420-3100 / 老韩 300-3400 Hz,均 `acompressor threshold=-20dB ratio=3`);参数在 `tools/gen_voice_dual.py` 的 `NPC_BANDPASS`,未滤波原声保留在 staging `tools/gen_voice_dual_wav/radio/npc/` 可重滤/回退。HOST/PROD 不滤波。**待人工试听**:滤波句可读性抽检,不可读句按「宁窄勿脏」回退未滤波原声(staging 重转即可)。
- sfx(2,SYN 程序合成):`ui_confirm ui_menu`(`web/radio/gen/gen_sfx.py`,全局 storyline 先例同款双音/软音)。**缺失 13 件 SA3**(HF token 未配置,本轮跳过):`onair_sting phone_ring line_connect line_drop amb_studio amb_rain_city amb_station amb_kid_room arg_muffled amb_street_rain amb_cab brake_screech tape_static`。
- bgm(4,Mureka $0.12):`onair bridge ending`(-20 LUFS)+ `menu`(menu pad,-24 LUFS,10 文档 §1.2);90.00s 无缝循环;后处理脚本 `web/radio/gen/gen_bgm.sh`。
- 合计 241 ogg + 241 m4a(每游戏音频 = 235 voice + 2 sfx + 4 bgm);台词/生成/终检走 `tools/gen_voice_dual.py --game radio` 与 `tools/check_all_assets.py`。
### dungeon《回声地窟》(批次 1 新游戏,Roguelike 模式,台词依据 `docs/design/16-dungeon-voice-ids.md`)
- voice 双性别版(male/ 与 female/ 各 125 条,无 NPC——怪物不说话):male=Uncle_Fu「低沉醇厚的男声,像探窟向导,沉稳有力」/ female=Serena「平静清晰的女声,像探窟向导,沉稳可信」,两常量全表统一。分组:序章/标题 4、教学 DRILL 19、系统/菜单/状态 35、探索事件 9、战斗 30、卡关/看门狗/纠错 14、结算/元进度 14。
- sfx(19 文件,全 SYN 程序合成,`web/dungeon/gen/gen_sfx.py`):`pulse`(声波脉冲)、材质尾音 `mat_stone/door/stair/water`、**预 pan 立体声** `side_wall_l/r side_open_l/r`(mono 源 + ffmpeg pan 派生,能量只在单声道,极性脚本断言)、移动 `step_own step_turn bump`、拾取 `coin potion gear`、战斗 `atk_swing parry hurt`、`levelup`(D 小调五声上行钟声琶音)。**缺失 24 文件 SA3**(HF token 未配置,本轮跳过):`echo_1–4 stairs_down mob_step_{small,bug,heavy}_l/c/r tel_{small,bug,heavy}(_l/r 派生) atk_hit enemy_die`(SA3 单声道源 + pan 派生工艺见 16-dungeon.md §9.2)。
- bgm(4,Mureka $0.12):`menu explore battle boss`(全部 -20 LUFS,16-dungeon.md §9.1);90.00s 无缝循环;battle/boss 的 bpm 仅为 prompt 特征词、无节拍网格玩法,未跑 bpm_precise;后处理脚本 `web/dungeon/gen/gen_bgm.sh`。
- 合计 273 ogg + 273 m4a(250 voice + 19 sfx + 4 bgm);台词/生成/终检走 `tools/gen_voice_dual.py --game dungeon` 与 `tools/check_all_assets.py`。
### 批次 1 资产账(2026-09-01)
- radio 132 句(双性别 103 ×2 + NPC 平铺 29)= 235 voice 资产;dungeon 125 句 ×2 = 250。两游戏合计 970 文件(ogg+m4a)。
- GPU 串行(flock):radio male/female/npc 与 dungeon male/female 共 5 轮,`--check` 全绿(无时长离群/短句)。
- Mureka 8 任务 $0.24(台账见 `tools/mureka_log.md` 2026-09-01 节),全部一次成功。
- 音色分配表新增:radio(双性别 HOST=Uncle_Fu/Serena + PROD=Dylan/Vivian 双配对 + NPC 平铺 5 音色)、dungeon(Uncle_Fu/Serena)。
- sfx 真峰值(逐声道解码回读)全部 ≤ -3dBFS、|直流| ≤ 0.001、无边界爆豆(源 8ms cos² 淡入淡出 + 垫音 + hp30 工艺,`web/<game>/gen/gen_sfx.py` 内建 QC)。
## 响度与爆音标准(R6 起)
- sfx one-shot:峰值归一 **-3dBFS**(真峰值经 ogg 解码回读校验,见 round6-report §1);首尾 8ms cos² 淡入淡出;去直流。
+14
View File
@@ -744,3 +744,17 @@
- **引擎回归**:gesture.js 改动 opt-in(immediateTap 默认 false 零行为
变化);6 个 playtest 全绿(escape 140 / rpg 188 / simon / space 152 /
storyline / rhythm 141)+ guide.test 122 断言全绿。
## 20. 新游戏登记:《不眠频率》(radio) 与《回声地窟》(dungeon)(new8 批次 1)
两款新 RPG 的断言清单与实现核对**住各自设计文档**(断言条目随轮次在原地维护):
- radio:[15-radio.md](15-radio.md) §测试(playtest 清单)+ 文末「实现核对」(193 断言逐条 ✅/合并/舍弃)。
要点:4 结局矩阵路径、pendingChoice 盲玩公平、C3 埋线索/C4 消费差分、rep 剧本验算、
RING 看门狗两级不同句(gd_ring1/gd_ring2)、模态菜单循环-确认-关闭不丢节点、中断复述、存档 schema。
- dungeon:[16-dungeon.md](16-dungeon.md) §测试 + 文末「实现核对」(1277 断言逐条)。
要点:固定 seed 网格复现+不变式、回声调度全扫描(echo_1..4 延迟/音量/材质/pan)、DRILL 九步、
学期零死、格挡 80%/逃跑必成挨一下/Boss 拒逃、死亡归因真话(3 死因矩阵)、元进度遗产解锁、
菜单暂停冻结、上滑 5 段状态链。
R1 起每轮的增量断言(修复回归)在各自设计文档的轮次小节追加,不在本文件重复。
+230
View File
@@ -0,0 +1,230 @@
# 15b · 《不眠频率》(radio) 语音台词表(voice ids,录音依据)
> **给音频总监的录制说明**
> - **双性别组**(主播 HOST + 导播/系统 PROD,103 句):落 `web/radio/audio/voice/female|male/<id>.{ogg,m4a}`,两性别 id 集合完全一致。
> - **HOST(主播=玩家)**:female = `Serena`,instruct 常量 `温暖清晰的年轻女声,深夜电台主播,沉稳亲切,不甜腻`;male = `Uncle_Fu`,instruct 常量 `低沉温和的中年男声,深夜电台主播,沉稳亲切,有磁性`。
> - **PROD(导播/系统音/向导 gd_*)**:female = `Vivian`,instruct 常量 `贴近耳机的低语女声,年轻电台导播,冷静克制`;male = `Dylan`,instruct 常量 `贴近耳机的低语男声,年轻电台导播,冷静克制`。
> - 区分度设计:主播(播音腔,对外广播)与导播(贴耳低语,只进你耳机)共用 speaker 池但**不同 speaker 配对**——female 侧主播 Serena / 导播 Vivian、male 侧主播 Uncle_Fu / 导播 Dylan,同场景高频对话的两者永不同 speaker。
> - **NPC 平铺组**(29 句):单音色、**不做双性别**,平铺落 `web/radio/audio/voice/<id>.{ogg,m4a}`(引擎平铺回退命中),转码时按 §通话滤波 加带通。NPC 共 5 人(≤5 约束),同一角色 speaker+instruct 恒定,区分度风险逐个标注:
> | 角色 | speaker | instruct 常量 | 区分度风险 |
> |---|---|---|---|
> | 老周(75 岁走丢老人) | `Uncle_Fu` | `苍老沙哑的男声,七十五岁,疲惫害怕,带着雨夜的颤抖` | **中**:与 male 主播同 speaker,靠年龄感 instruct + 电话滤波(500-2800Hz)区分,请试听确认 |
> | 小鹿(想退票的年轻人) | `Serena` | `年轻女声,倔强,带着哭腔,疲惫` | **中**:与 female 主播同 speaker,靠哭腔 instruct + 电话滤波区分,请试听确认 |
> | 糖糖(失眠的孩子) | `Vivian` | `稚嫩童声,八岁小女孩,困倦,胆怯` | **低**:与 female 导播同 speaker,童声 vs 成年低语,且从不同场 |
> | 陈姐(争吵的夫妻·妻) | `Vivian` | `疲惫沙哑的中年女声,四十五岁,强撑着平静` | **低**:与糖糖/导播同 speaker 不同 instruct,三者从不同场,请试听确认 |
> | 老韩(雨夜司机) | `Eric` | `沙哑粗犷的中年男声,货车司机,困倦强撑` | **低**:male 侧唯一 Eric |
> - **通话滤波(转码侧)**:NPC 全部走带通(参数见 15-radio.md §6.2:老周 500-2800 / 小鹿 400-3400 / 糖糖 450-3000 / 陈姐 420-3100 / 老韩 300-3400,均 + 轻压缩);HOST/PROD 不滤波。试听不可读的句子回退未滤波原声(宁窄勿脏)。
> - **称呼规则**:来电者对主播的称呼一律中性(「主播」「您」),**全表禁用**「闺女/小哥/叔叔/阿姨/大姐头」类性别化称呼(主播性别随用户设置变化);「大姐/姑娘」仅可作来电者的**自称或被导播描述**。
> - **数字规避**:序号/口碑全部分档整句(prog_c1..c5、rep_*);句内数字(十一点半、三公里、四十分钟前)写死在整句里,禁止运行时拼接。
> - **禁语速词**:instruct 不含任何语速描述(引擎 SoundTouch 变速);NPC 句靠短句+逗号停顿实现「语速放缓」听感。
> - 管线:`tools/gen_voice_dual.py` 照录(SPEAKERS 表需新增 radio 行);多音字自查见文末注记。
## G0 组 · 系统/向导/状态(PROD 双性别 ×24)
| id | 台词全文 | 触发 |
|---|---|---|
| `gd_brief` | 单击继续节目。请选择的时候,向左或向右滑。 | 开场链最前(full 档,F8 先例) |
| `gd_continue` | 单击屏幕,继续。 | 叙事链末静默看门狗第 1 级(full 25s / standard 13s) |
| `gd_again` | 还在吗?单击继续。 | 看门狗第 2 级(50s / 30s),新句不逐字重播 |
| `gd_invalid` | 这个手势,直播间里用不上。 | 无效手势(error 震,idle-only,同局限次) |
| `gd_ring1` | 热线在闪。单击,接听。 | RING 静默 10s(第 1 级) |
| `gd_ring2` | 灯还亮着,听众在等。单击,接听。 | RING 静默 25s(第 2 级,**必须与第 1 级不同句**) |
| `sys_help` | 单击继续,双击重新听。上滑听状态,下滑开菜单。请选择时,左右滑。 | 上滑帮助 / 菜单内上滑 |
| `sys_menu` | 菜单。单击切换选项,双击确认。 | 下滑呼出模态菜单 |
| `sys_menu_resume` | 回到直播。 | 菜单项 |
| `sys_menu_restart` | 重新开始今晚。 | 菜单项 |
| `sys_menu_lobby` | 保存并回大厅。 | 菜单项(确认后跳转前播) |
| `sys_settings` | 音色、语速、背景音乐,请回大厅修改。 | 菜单内设置提示 |
| `sys_skip` | 想跳过这句,单击就行。 | 开场每局一次(skip_hint 先例) |
| `sys_continue_ask` | 检测到你的夜班记录。单击继续上次,右滑开始新的夜晚。 | 有存档进游戏 |
| `sys_new_night` | 新的夜晚,开始了。 | 新夜班开始 / 菜单重开 |
| `sys_saved` | 进度已保存。 | 存档写入后 |
| `prog_c1` | 现在是今晚第一通电话。 | 上滑状态链(按当前通号选播) |
| `prog_c2` | 现在是今晚第二通电话。 | 同上 |
| `prog_c3` | 现在是今晚第三通电话。 | 同上 |
| `prog_c4` | 现在是今晚第四通电话。 | 同上 |
| `prog_c5` | 现在是今晚第五通电话。 | 同上 |
| `rep_high` | 今晚的口碑,在听众里立住了。 | 口碑跨入 ≥5 档(BRIDGE 被动播报) |
| `rep_mid` | 今晚的口碑,不好不坏。 | 口碑回落 1-4 档 |
| `rep_low` | 今晚的口碑,还没立起来。 | 口碑 ≤0 档(零起步语义,R2 F2 改词:rep=0 开播前不再读成「砸了」;附 warning 震动) |
## G1 组 · 开场(双性别 ×4)
| id | 声部 | 台词全文 | 触发 |
|---|---|---|---|
| `b_open` | 主播 | 午夜零点,红灯亮起。欢迎回到《今夜不眠》。 | INTRO(配 sfx/onair_sting) |
| `b_rain` | 主播 | 雨下了一整晚。热线灯,马上就要亮了。 | INTRO |
| `b_rule` | 导播 | 台里的规矩就一条:先接住人,再接电话。 | INTRO(一句规矩,P4) |
| `b_goal` | 导播 | 今晚五通电话。说什么话,由你决定。 | INTRO,随后第一声铃 |
## C1 组 · 第一通·老周(走丢的老人)(双性别 13 + 老周平铺 7 = 20)
| id | 声部 | 台词全文 | 触发 |
|---|---|---|---|
| `z1_ring_in` | 导播 | 一线来电。七十多岁,雨夜,走丢了。 | 接听后线路引入(配 line_connect) |
| `z1_a` | 老周 | 主播……我,我找不到回家的路了。 | 通话主干 |
| `z1_b` | 老周 | 雨太大了。我本来,只想买副老花镜。 | 通话主干(垫 amb_rain_city) |
| `p1` | 导播 | 老人在发抖。向左滑,陪他哼那首老歌。向右滑,冷静问他路口的路牌。 | 抉择提示(播完即可选) |
| `z1_song` | 主播 | 大爷别急,这首歌我也会。咱们一句一句来。 | p1 左(称呼「大爷」指对方,中性合规) |
| `z1_songo` | 老周 | 对,对!路口修车铺,天天放这首! | p1 左·反应(正反馈:声音稳住) |
| `z1_sdone` | 导播 | 他稳住了。老歌,比路牌管用。 | p1 左·收 |
| `z1_ask` | 主播 | 大爷,路口有什么招牌?念一个给我听听。 | p1 右 |
| `z1_asko` | 老周 | 招牌……雨糊住了,我眼花,看不清啊…… | p1 右·反应(慌) |
| `z1_adone` | 导播 | 他慌了。先接住情绪,再走流程。 | p1 右·收(即时纠偏,非归因) |
| `p2` | 导播 | 雨声更密了。向左滑,留在原地陪他等巡逻队。向右滑,转接派出所。 | 抉择提示 |
| `z1_stay` | 主播 | 哪儿也不去。我陪你,这首歌还长着呢。 | p2 左 |
| `z1_stayo` | 老周 | 好,好。有人说话,我就不怕了。 | p2 左·反应 |
| `z1_staye` | 导播 | 十五分钟后,巡逻灯停在路口。全城听见老人笑了。 | p2 左·收(数字整句内嵌) |
| `z1_pol` | 主播 | 大爷,我把您转给派出所,他们马上就到。 | p2 右(p1=左 时) |
| `z1_polo` | 老周 | 好,好。谢谢你啊,谢谢。 | p2 右·反应 |
| `z1_pole` | 导播 | 转接完成,地址报得清。干净利落。 | p2 右·收(p1=左 时) |
| `z1_bad` | 老周 | 说不清……我真是,说不清了…… | p2 右(仅 p1=右 时,替代 z1_polo/z1_pole 前段) |
| `z1_bade` | 导播 | 地址没报清就转接,雨又大。这通砸了。先稳人,再办事。 | 归因句(真话律:指向玩家实际两连选) |
| `z1_ok1` | 导播 | 就这么接。记住这个手感。 | 首通成功收尾确认(全局一次,成功后无其他教学) |
## C2 组 · 第二通·小鹿(想退票的年轻人)(双性别 6 + 小鹿平铺 4 = 10)
| id | 声部 | 台词全文 | 触发 |
|---|---|---|---|
| `l1_in` | 导播 | 二线来电。姑娘,声音很轻,像怕吵到谁。 | 接听引入(「姑娘」描述来电者,合规) |
| `l1_a` | 小鹿 | 主播,退票窗口十一点半就关了。我站这儿半小时了。 | 通话主干(垫 amb_station) |
| `l1_b` | 小鹿 | 票是回去看我妈的。可我试镜又黄了,两手空空…… | 通话主干 |
| `p3` | 导播 | 向左滑,告诉她这趟车值得上。向右滑,把选择还给她。 | 抉择提示 |
| `l1_go` | 主播 | 你妈等的不是奖杯,是你进门喊的那声妈。 | p3 左 |
| `l1_goo` | 小鹿 | 呜……嗯。不退了,我现在就去检票。谢谢您。 | p3 左·反应 |
| `l1_goe` | 导播 | 二线收工。这句话,值一个头条。 | p3 左·收 |
| `l1_free` | 主播 | 退不退,都是你的自由。夜还长,慢慢想。 | p3 右 |
| `l1_freeo` | 小鹿 | 嗯……我再想想。窗口要关了,我先过去了。 | p3 右·反应 |
| `l1_freee` | 导播 | 她走了。没上车,也没退票。年轻人的夜,长了点。 | p3 右·收 |
## C3 组 · 第三通·糖糖(失眠的孩子)(双性别 10 + 糖糖平铺 6 = 16)
| id | 声部 | 台词全文 | 触发 |
|---|---|---|---|
| `k1_in` | 导播 | 三线来电。听呼吸,是个小孩。 | 接听引入 |
| `k1_a` | 糖糖 | 喂……有人吗?我睡不着。 | 通话主干(垫 amb_kid_room) |
| `k1_b` | 糖糖 | 家里灯关了。可是楼下,一直有声音。 | 通话主干(**配 sfx/arg_muffled 线索音**) |
| `p4` | 导播 | 向左滑,唱一段摇篮曲哄她睡。向右滑,轻轻问她怎么了。 | 抉择提示 |
| `k1_lull` | 主播 | 闭上眼睛。风把云朵盖在被子上,月亮替你值夜班。 | p4 左(错过线索路线) |
| `k1_lullo` | 糖糖 | 月亮也睡了……那我也睡。 | p4 左·反应 |
| `k1_lulle` | 导播 | 睡着了。可你没听见,她在怕什么。 | p4 左·收(诚实复盘,非惩罚) |
| `k1_ask` | 主播 | 睡不着的时候,心里是不是装着事?说给我听听。 | p4 右(置 heardArgument=true) |
| `k1_asko` | 糖糖 | 爸爸妈妈摔门了。是不是……我做错了什么呀。 | p4 右·反应 |
| `p5` | 导播 | 孩子的声音在抖。向左滑,告诉她这不是她的错。向右滑,答应她明天都会好。 | 抉择提示(仅 p4=右 可达) |
| `k1_truth` | 主播 | 糖糖,记住,大人的事,不是小孩的错。一个字都不是。 | p5 左 |
| `k1_trutho` | 糖糖 | 嗯……那我睡了。他们明天,会一起给我切蛋糕吗? | p5 左·反应 |
| `k1_truthe` | 导播 | 蛋糕。记下来——四线的来电,可能跟这个家有关。 | p5 左·收(flag 预告) |
| `k1_lie` | 主播 | 睡吧。明天一睁眼,什么都会好起来的。 | p5 右(善意谎言) |
| `k1_lieo` | 糖糖 | 真的吗?拉钩。晚安…… | p5 右·反应 |
| `k1_liee` | 导播 | 你许了个兑不了的现。孩子睡了,谎留下了。 | p5 右·收(诚实复盘) |
## C4 组 · 第四通·陈姐(争吵的夫妻)(双性别 13 + 陈姐平铺 7 = 20)
| id | 声部 | 台词全文 | 触发 |
|---|---|---|---|
| `w1_in` | 导播 | 四线来电。户外风声大,是位大姐。 | 接听引入(垫 amb_street_rain) |
| `w1_a` | 陈姐 | 主播,我就想找个人说说。我家那口子,今晚要出远门的活儿。 | 通话主干 |
| `w1_b` | 陈姐 | 明天孩子生日!蛋糕都订好了,他人要今晚走! | 通话主干 |
| `p6` | 导播 | 摔门声你听见了。向左滑,替孩子给她带句话。向右滑,劝她回家把话说开。 | 抉择提示(固定文本,两条件路线共用) |
| `w1_say` | 主播 | 四十分钟前,有个孩子问我们:爸妈摔门,是不是她的错。 | p6 左·**条件 heardArgument=true** |
| `w1_sayo` | 陈姐 | 什么?她还没睡?这么晚了,她还醒着? | w1_say·反应 |
| `w1_saye` | 导播 | 她攥着电话,很久没出声。风都停了。 | w1_say·收 |
| `w1_say2` | 主播 | 今天有位小朋友,还在等爸妈回家切蛋糕。 | p6 左·**条件 heardArgument=false** |
| `w1_say2o` | 陈姐 | 蛋糕……是他订的。他嘴上不说,心里都记着。 | w1_say2·反应 |
| `w1_say2e` | 导播 | 话递到了。就差最后一脚。 | w1_say2·收 |
| `p7` | 导播 | 她的呼吸平了。向左滑,留在直播里,让全城等这个门铃。向右滑,换垫乐,私下陪她说完。 | 抉择提示(仅 p6=左 可达) |
| `w1_air` | 主播 | 陈姐,别挂。让他接电话——全城都在听。 | p7 左(留在直播) |
| `w1_airo` | 陈姐 | 喂!回来吧!蛋糕要化了,孩子等着呢! | p7 左·反应(对电话那头喊) |
| `w1_aire` | 导播 | 电话那头,传来跑步声。今晚的高光时刻。 | p7 左·收 |
| `w1_priv` | 主播 | 这里只有我们俩了。想说就说,想哭就哭。 | p7 右(换垫乐私聊) |
| `w1_privq` | 陈姐 | 呜……其实他订完蛋糕就跟我道歉了。是我,把门摔得太响。 | p7 右·反应 |
| `w1_prive` | 导播 | 灯牌是你亲手关的,故事还是完整的。稳。 | p7 右·收 |
| `w1_home` | 主播 | 吵架隔着电话吵不赢。回家吧,蛋糕别放到坏了。 | p6 右(本通结束,无 p7) |
| `w1_homeo` | 陈姐 | 嗯……我这就往回走。谢谢你,听我瞎嚷嚷。 | p6 右·反应 |
| `w1_homee` | 导播 | 她往回走了。成不成,看他们自己的了。 | p6 右·收 |
## C5 组 · 第五通·老韩(雨夜司机·高潮)(双性别 8 + 老韩平铺 5 = 13)
| id | 声部 | 台词全文 | 触发 |
|---|---|---|---|
| `h1_in` | 导播 | 五线来电。引擎声没停过——他在开车! | 接听引入(垫 amb_cab) |
| `h1_a` | 老韩 | 主播,别挂。陪我说话,我的眼皮,沉得很。 | 通话主干 |
| `h1_b` | 老韩 | 跑完这趟,闺女的学费就齐了。雨刷都快刮不动了。 | 通话主干(「闺女」指其女儿,合规) |
| `p8` | 导播 | 方向盘比话筒危险。向左滑,陪他聊聊闺女。向右滑,让他立刻进服务区。 | 抉择提示 |
| `h1_talk` | 主播 | 说说她。开学第一天,她跟你说的头一句话,是什么? | p8 左 |
| `h1_talko` | 老韩 | 她说,爸,我在学校给你赚了朵小红花。哈哈,哈哈…… | p8 左·反应 |
| `h1_danger` | 导播 | 车道偏了——听! | 危机拍(**前 2s 起 brake_screech 渐强 + danger 签名同窗**) |
| `h1_dango` | 老韩 | 嘶——好险!前车急刹,我擦着护栏过去了。彻底醒了! | 危机拍·化解 |
| `h1_talke` | 导播 | 吓醒的,也算醒。这一趟,聊到家。 | p8 左·收 |
| `h1_stop` | 主播 | 听我的,前面三公里有服务区。学费急,命更急。 | p8 右 |
| `h1_stopo` | 老韩 | 行,听你的。进了服务区,我给你们放首歌听。 | p8 右·反应 |
| `h1_stope` | 导播 | 他下了高速。单子黄了,人齐了。 | p8 右·收 |
| `h1_echo` | 导播 | 对了。头通电话那位大爷,就是这片被巡逻队找到的。这城市的夜,是接着的。 | 插播回响(**条件 bond_zhou=true**,queue) |
## D9 组 · 签收抉择(心结)(双性别 6)
| id | 声部 | 台词全文 | 触发 |
|---|---|---|---|
| `g9_in` | 导播 | 五线收工。垫乐起——零点档,还剩最后一段。 | C5 后 BRIDGE→签收(配 bgm/bridge) |
| `p9` | 导播 | 留言机红灯在闪,是老主播三年前没播完的那句晚安。向左滑,替他播出去。向右滑,照常收播。 | 抉择提示(39 字,全表最长句) |
| `d9_face` | 主播 | 师父,三年了。这句晚安,我替您播。各位,晚安。 | p9 左(knot=解;配 tape_static) |
| `d9_facee` | 导播 | 带子里其实只有电流声。但空着的那一秒,你说满了。 | p9 左·收(老主播不配音,电流声留白) |
| `d9_avoid` | 主播 | 各位,今晚到这里。明晚零点,今夜不眠,不见不散。 | p9 右 |
| `d9_avoide` | 导播 | 留言机的红灯,又多亮了一夜。 | p9 右·收(开环钩子) |
## EP 组 · 结局与尾声回响(双性别 19;结局 8 + 回响 11)
| id | 声部 | 台词全文 | 触发 |
|---|---|---|---|
| `e1_a` | 主播 | 天亮了,热线灯还亮着。你们的晚安,是这座城市的心跳。 | 结局 e1(rep≥5 且直面)主句 |
| `e1_b` | 主播 | 这把话筒,我总算接得住了。明晚零点,接着不眠。 | e1 收(钩子) |
| `e2_a` | 主播 | 今晚的热线烧得发烫。全城的失眠者,都被接住了。 | 结局 e2(rep≥5 且回避)主句 |
| `e2_b` | 主播 | 只是路过留言机的时候,你还是没敢看它。红灯灭得很慢。 | e2 收(钩子) |
| `e3_a` | 主播 | 口碑掉了一夜。可那句迟到三年的晚安,今晚说出去了。 | 结局 e3(rep<5 且直面)主句 |
| `e3_b` | 主播 | 一个人的频道,也是频道。师父,收到了吗? | e3 收(钩子) |
| `e4_a` | 主播 | 今夜不眠,静音收播。有的夜班,谁也接不住谁。 | 结局 e4(rep<5 且回避)主句 |
| `e4_b` | 主播 | 留言机还在闪。你没看它,它也没怪你。明晚再说吧。 | e4 收(钩子;含 e4 复盘,不另配归因句) |
| `ep_lead` | 导播 | 收播了。导播间替你记了一笔账——今晚的人都怎样了。 | 结局链后尾声引入 |
| `ep_zhou_ok` | 导播 | 老周:孙子替他买了老花镜。回信说,谢谢那首歌。 | 尾声(bond_zhou=1) |
| `ep_zhou_no` | 导播 | 老周在便利店屋檐下坐到天亮。没出事,也没人听见他的歌。 | 尾声(bond_zhou=0) |
| `ep_deer_ok` | 导播 | 小鹿上车了。她妈后来打电话说,那声妈,比奖杯值钱。 | 尾声(bond_deer=1) |
| `ep_deer_no` | 导播 | 小鹿退了票。检票口的灯,记得那趟没上成的车。 | 尾声(bond_deer=0) |
| `ep_kid_ok` | 导播 | 糖糖家的门,这个月没再摔过。她说这是主播魔法。 | 尾声(bond_kid=1) |
| `ep_kid_no` | 导播 | 糖糖后来又打来过一次。还是那句:楼下在吵。 | 尾声(bond_kid=0) |
| `ep_wife_ok` | 导播 | 陈姐家切了蛋糕。那句「回来吧」,是我这晚听过最好的声音。 | 尾声(bond_wife=1) |
| `ep_wife_no` | 导播 | 陈姐在公车站坐到了末班车。那通电话,成了没寄出的信。 | 尾声(bond_wife=0) |
| `ep_han_ok` | 导播 | 老韩到家了。后视镜上,多了朵小红花。 | 尾声(bond_han=1) |
| `ep_han_no` | 导播 | 老韩在服务区睡了一觉。学费齐了,那首歌没放成。 | 尾声(bond_han=0) |
---
**合计:132 句**(GPU 预算 ≤170 ✓)
- 双性别 103 句(HOST 21 + PROD 82)×2 性别 = 206 文件;
- NPC 平铺 29 句(老周 7 / 小鹿 4 / 糖糖 6 / 陈姐 7 / 老韩 5)= 29 文件;
- 合计 235 条音频 ×2 格式(ogg+m4a)= **470 文件**。
- 每结局专属台词齐备:e1/e2/e3/e4 各 2 句(ep 组前 8 行)+ 尾声回响 11 句共用。
- 每局实走抉择 8-9 个(p5 仅 p4=右、p7 仅 p6=左 可达),抉择提示 p1..p9 共 9 个。
## 多音字 / 发音风险自查注记
| 风险词 | 处理 |
|---|---|
| 重听 | 全表写「重新听」(sys_help),无「重听」字样 |
| 切(qiē/qiè) | 「切垫乐」写「换垫乐」(p7);「切蛋糕」「切了蛋糕」为正确常用读法,保留 |
| 挣(zhēng/zhèng) | 「挣钱」写「赚了朵小红花」(h1_talko) |
| 巷(xiàng/hàng) | 全表不用「巷」,写「路口」 |
| 答录机(dā lù jī) | 全表用「留言机」,规避 |
| 颤(chàn) | 台词侧用「发抖」(z1_a 触发/语境、instruct 描述不受影响,instruct 不进语音) |
| 哄(hōng/hǒng) | 「哄她睡」为 hǒng 常用读法,保留,试听确认 |
| 宿/血/露/降/率/传(zhuàn 义) | 全表未使用 |
| 了(le/liǎo)、还(hái/huán)、得(de/děi) | 常规句式常见读法,逐句人工试听项(z1_staye「听见了」、l1_goe 等) |
| 数字 | 句内数字全部写死(十一点半/十五分钟/三公里/四十分钟前/零点),序号用分档整句(prog_c1..c5) |
## 台词侧自查
- 单句 ≤40 字:全表最长 = p9(37 字),次长 sys_help(26 字)✓;NPC 句平均 ≤18 字(P4 语速感靠短句+逗号,不用 instruct 语速词)✓。
- NPC ≤5 人、speaker+instruct 恒定、区分度风险逐个标注 ✓。
- 称呼合规:来电者对主播只用「主播/您/谢谢你」,性别化称呼仅指向来电者自身或第三方 ✓。
- 每个结局(e1..e4)专属主句+钩子各 2 句齐备,尾声救/错过双线齐备 ✓。
- 归因真话律:负向收尾句(z1_bade/k1_lulle/k1_liee/ep_*_no/e4_b)全部指向玩家实际选择,无假归因句 ✓。
- 向导 gd_* 6 句(gd_brief/gd_continue/gd_again/gd_invalid/gd_ring1/gd_ring2)符合 14 号 §6 台词规范(一句一义、无重复句式、单句 ≤15 字)✓。
+477
View File
@@ -0,0 +1,477 @@
# 15 · 《不眠频率》(radio) — 深夜电台热线叙事 RPG 设计文档
> **给开发的实现要点摘要**
> 1. 叙事模式(声命线范式):节点链 onComplete 串行 + seqToken 打断作废 + pendingChoice 缓存;复制 `web/storyline/` 结构起步,状态机用 6 宏状态(INTRO/RING/ONAIR/BRIDGE/ENDING + MENU 覆盖层)。
> 2. 手势零豁免:全部走全局默认映射(单击=继续/接听/跳句、双击=重听、左右滑=抉择、上滑=状态、下滑=模态菜单 §5.2 约定、长按=引擎快进)。
> 3. 台词依据 [15-radio-voice-ids.md](15-radio-voice-ids.md)(132 句:双性别 103 + 平铺 NPC 29);NPC 人声电话滤波在**资产侧** ffmpeg 带通(storyline n03_msg 先例),引擎零改动。
> 4. 向导层:storyline 同款看门狗节奏 + RING 专用两级新句提示;`gd_ring1/gd_ring2` 为本游戏新增 gd_* 资产。
> 5. 章程任务 N1(research/new8/00-program.md):单局 15-25 分钟、9 抉择点、4 结局、分支局部化。
## 1. 概述
- **一句话**:你是午夜档电台热线主播「今夜不眠」,直播间里一晚接进五个来电——你说什么话、接不接、转不转接,改变五个陌生人的夜晚,也改变你自己。
- **题材(声音即世界)**:全部叙事靠电话音质感(每位来电者一种滤波声)、电台垫乐、直播间环境音呈现——来电者永远不露面,只有声音;「声音即存在」与纯音频载体天然同构。
- **单局**:一个完整夜班(一条抉择路径)15-22 分钟;五个来电共享主干,分歧点局部化在每通电话内部;9 个抉择点(单局实走 8-9 个),4 结局。
- **核心循环**:接听 → 听 → 抉择(左=走心介入 / 右=克制程序)→ 结果与口碑结算 → 幕间喘息 → 下一通。跨来电积累「今晚状态」(听众口碑 + 各来电好感),收播前一个抉择决定你自己的心结是否解开。
## 2. 状态机
### 2.1 状态列表
| 状态 | 内容 | BGM | guide.state |
|---|---|---|---|
| `INTRO` | 开场链:台标口播 + 一句规矩 + 今晚目标 | `bgm/onair` | explore |
| `RING` | 热线接入:铃声循环,等待单击接听 | `bgm/onair`(不切) | 无新签名(turn cue 承担) |
| `ONAIR` | 通话叙事:对白链 + 抉择点 + 结果 | `bgm/onair` | explore;抉择点=choice |
| `BRIDGE` | 幕间:口碑跨档播报(如有)+ 喘息垫乐,自动链 | `bgm/bridge` | settle |
| `ENDING` | 结局链 + 尾声回响(epilogue),结算态 | `bgm/ending` | settle |
| `MENU` | 模态覆盖层(任意叙事态下滑呼出,暂停一切链) | `bgm/menu` | menu |
### 2.2 转移图(文本)
```
┌─────────────── 结算单击=新夜班 ───────────────┐
▼ │
INTRO ──链完──> RING(c1) ──单击接听──> ONAIR(c1) ──链完──> BRIDGE ──┤→ RING(c2)
│铃声循环/看门狗两级,无超时惩罚 │ …(c3,c4 同型)…
└────────────────────────────────────────────────┘
ONAIR(c5) ──链完──> BRIDGE ──链完──> SIGNOFF
│
└──> ◆p9 抉择 ──> d9 结果链 ──> ENDING(e1..e4 + ep 回响链)
任意叙事态(INTRO/RING/ONAIR/BRIDGE/ENDING):下滑 → MENU(模态)
MENU:单击=循环切换选项 → 双击=确认执行 → 上滑=帮助 → 下滑/「回到直播」项=关闭回原状态(重播当前节点)
```
- INTRO/RING/ONAIR/BRIDGE/ENDING 均为节点链驱动(storyline playNode 模式);`SIGNOFF` 是 ONAIR 的最后一个节点组(◆p9),不单设状态。
- BRIDGE 自动链 ≤12s:口碑跨档则播 rep_*(播报真实档位,账目真实),随后直接进下一通 RING。
- MENU 打开时 `guide.disarm()` + 取消一切看门狗/自动链;关闭后重播当前节点(局面复述,storyline closeMenu 先例)。
### 2.3 每状态手势映射(6+2 全覆盖,零豁免)
| 手势 | INTRO | RING | ONAIR | BRIDGE | ENDING | MENU |
|---|---|---|---|---|---|---|
| 上滑 | 帮助+状态,随后重播当前段 | 状态播报(第几通+口碑档),留在 RING | 帮助+状态,随后重播当前节点 | 帮助+状态,自动链继续 | 帮助+状态(结局名+口碑) | 帮助(sys_help) |
| 下滑 | 模态菜单 | 模态菜单 | 模态菜单 | 模态菜单 | 模态菜单 | 关闭菜单 |
| 左滑 | 无效 → error 震+gd_invalid | 无效 → gd_invalid | 抉择=左(走心) | 无效 → gd_invalid | 无效 → gd_invalid | 无效 → error 震 |
| 右滑 | 无效 → gd_invalid | 无效 → gd_invalid | 抉择=右(克制);存档询问态=新夜班 | 无效 → gd_invalid | 无效 → gd_invalid | 无效 → error 震 |
| 单击 | 播放中=跳过本句;链末=进 RING | **接听** | 播放中=跳过本句(p* 不可跳);链末=继续 | 跳过本句(自动链) | **再来一局**(新夜班) | 循环切换选项 |
| 双击 | 重听当前段 | 重听上一通话的收尾句 | 重听当前节点台词(含抉择提示) | 重听幕间链 | 重听结局链 | **确认执行**(菜单约定覆盖重听) |
| 长按 | 引擎接管:旁白临时 1.7×(12 号 §2.2,叙事类不豁免) | 同左 | 同左 | 同左 | 同左 | 同左(快进菜单语音) |
| 长按松开 | 引擎接管:恢复 settings.speed | 同左 | 同左 | 同左 | 同左 | 同左 |
- 豁免登记:**无**。六个基础手势全部符合全局宪法默认语义(单击=确认/继续/**接听**,宪法 §5.1 原文即含「接听」);菜单走 §5.2 R3 模态约定(单击循环+双击确认),不引用 rpg 探索态豁免。
- 抉择 prompt(voice/p1..p9)播放中单击不可跳、左右滑缓存 `pendingChoice` 播完即生效(storyline 盲玩公平先例)。
- 无效手势零静默:所有「无效」格均有 error 震 + gd_invalid(onceKey 同局限次,F1.4 idle-only:语音在播时跳过语音只发震)。
## 3. 抉择树
### 3.1 全局骨架(来电共享主干,分歧局部化)
```
INTRO: b_open → b_rain → b_rule → b_goal ──> 铃①
【C1 老周·走丢的老人】教学通 · 2 抉择 ──> BRIDGE ──> 铃②
【C2 小鹿·想退票的年轻人】单抉择 · 呼吸口 ──> BRIDGE ──> 铃③
【C3 糖糖·失眠的孩子】2 抉择 · 埋 heardArgument ──> BRIDGE ──> 铃④
【C4 陈姐·争吵的夫妻】2 抉择 · 消费 heardArgument ──> BRIDGE ──> 铃⑤
【C5 老韩·雨夜司机】高潮 · 1 抉择 + 危机脚本拍 ──> 签收 ◆p9(心结)──> ENDING
```
分支纪律:**每通电话内部分支、通话末尾收敛**到导播收尾句 → BRIDGE;跨来电只传三个数据(rep 数值、bond 好感、heardArgument 布尔),不生成组合节点。最坏路径与最好路径节点数相同。
### 3.2 分通结构(◆=抉择点,〔〕=条件差分)
```
【C1 老周】
铃① → z1_ring_in → z1_a → z1_b → ◆p1
├左 哼老歌: z1_song → z1_songo → z1_sdone ─┐
└右 问路牌: z1_ask → z1_asko → z1_adone ───┤
▼
◆p2
├左 陪他等: z1_stay → z1_stayo → z1_staye ─┐
└右 转接: z1_pol → z1_polo → z1_pole ────┤ (仅 p1=右 时 p2=右 改走 z1_bad → z1_bade 归因)
▼
z1_ok1(首通确认,全局一次)→ BRIDGE
【C2 小鹿】
铃② → l1_in → l1_a → l1_b → ◆p3
├左 劝上车: l1_go → l1_goo → l1_goe ──┐
└右 还自由: l1_free → l1_freeo → l1_freee ─┴→ BRIDGE
【C3 糖糖】
铃③ → k1_in → k1_a → k1_b(垫 arg_muffled 争吵声)→ ◆p4
├左 哄睡: k1_lull → k1_lullo → k1_lulle ──────────────┐(错过线索,本通结束)
└右 问她: k1_ask → k1_asko → ◆p5 │
├左 真话: k1_truth → k1_trutho → k1_truthe ────────┤(heardArgument=true)
└右 谎言: k1_lie → k1_lieo → k1_liee ──────────────┴→ BRIDGE
【C4 陈姐】
铃④ → w1_in → w1_a → w1_b → ◆p6
├左 替孩子带话:〔heardArgument ? w1_say 组 : w1_say2 组〕→ ◆p7
│ ├左 留在直播: w1_air → w1_airo → w1_aire ─┐
│ └右 换垫乐私聊: w1_priv → w1_privq → w1_prive ─┤
└右 劝她回家: w1_home → w1_homeo → w1_homee ──────────┴→ BRIDGE(右路本通结束,无 p7)
【C5 老韩】高潮
铃⑤ → h1_in → h1_a → h1_b → ◆p8
├左 陪聊闺女: h1_talk → h1_talko → 【危机拍: danger 签名+brake_screech(前置2s)】
│ → h1_danger → h1_dango → h1_talke ─┐
└右 服务区: h1_stop → h1_stopo → h1_stope ────────┤
〔bond_zhou=true 时插播 h1_echo 回响句〕 ▼
BRIDGE → g9_in → ◆p9
├左 直面(播出老主播的晚安): d9_face → d9_facee → knot=解
└右 回避(照常收播): d9_avoid → d9_avoide
▼
ENDING: e*_a → e*_b(按 rep×knot 四选一)→ ep_lead → ep_<caller>×5(按 bond 选播)
```
### 3.3 分支局部化说明
- 跨来电状态只有三个标量:`rep`(数值)、`heardArgument`(布尔)、`bond`(5 个 0/1)。没有任何「来电 i 的分支 × 来电 j 的分支」组合节点。
- 唯一的跨来电内容差分:C4 的 w1_say/w1_say2 两组(由 heardArgument 选播)与 C5 的 h1_echo 插播句(由 bond_zhou 选播)——都是**节点内选播**,不改树形。
- 树规模:132 句台词、约 60 个节点,任一路径实走 8-9 个抉择点(C3 左路/ C4 右路各少一个)。
### 3.4 四结局达成条件
| 结局 | 条件 | 主题 | 开环钩子 |
|---|---|---|---|
| e1 天亮之前 | rep ≥ 5 且 p9=左(直面) | 城市有了心跳,话筒接住了 | 「明晚零点,接着不眠」 |
| e2 热线常亮 | rep ≥ 5 且 p9=右(回避) | 满座夜班,留言机红灯未看 | 红灯「灭得很慢」 |
| e3 一个人的频道 | rep < 5 且 p9=左(直面) | 夜班难,但晚安说出去了 | 「师父,收到了吗」 |
| e4 静音 | rep < 5 且 p9=右(回避) | 谁也接不住谁的夜班 | 「明晚再说吧」 |
- 尾声回响(P2 回响感):epilogue 对 5 个来电者各播一句 bond ? 救到的后续 : 错过的后续(ep_zhou/deer/kid/wife/han_ok/no),玩家每晚的救与错过都被点名。
## 4. 数值设计
### 4.1 变量
| 变量 | 类型 | 初值 | 说明 |
|---|---|---|---|
| `rep`(听众口碑) | int | 0 | 抉择与结果积累,收播时结算档位 |
| `knot`(心结) | bool | false | 仅由 p9=左 置 true |
| `bond_zhou/deer/kid/wife/han` | 0/1 | 0 | 只影响尾声回响选播,不影响结局 |
| `heardArgument` | bool | false | p4=右 置 true,C4 消费 |
| `nights` / `endings{}` | int / map | — | 重玩计数与结局收集(存档展示用) |
### 4.2 抉择计分表(左=走心 / 右=克制)
| 抉择 | 左(走心) | 右(克制) | 备注 |
|---|---|---|---|
| p1 哼歌/问路 | +1 | 0 | 左路老周稳住(首个正反馈) |
| p2 陪等/转接 | +2(p1=左)/ +1(p1=右) | +1(p1=左)/ **-1(p1=右,砸)** | 唯一负分组合,触发归因句 |
| p3 劝上车/还自由 | +2 | +1 | 两个选项都是体面的(教学后建立信任) |
| p4 问她/哄睡 | +1,heardArgument=true | 0 | 左=倾听,右=敷衍即错过线索 |
| p5 真话/善意谎言 | +1 | 0 | p5 仅 p4=右 可达 |
| p6 替孩子带话/劝回家 | +2(heardArgument)/ +1(否则) | +0(本通结束) | |
| p7 留在直播/换垫乐 | +2 | +1 | p7 仅 p6=左 可达 |
| p8 陪聊/服务区 | +1(含危机拍) | 0 | 两路都活着(P4 零死亡) |
| p9 直面/回避 | knot=解 | knot=未解 | 不动 rep |
### 4.3 阈值与导向
- 收播结算:`rep ≥ 5` = 高口碑(e1/e2),`rep < 5` = 低口碑(e3/e4)。
- 理论上限 12、全克制路径 0;「多数走心 + 容忍 1-2 次失误」落在 5-8 档——阈值 5 把「今晚是否立住」压在玩家自己的重量上,而非单点失误。
- 口碑跨档被动播报(BRIDGE):进入 ≥5 区间播 rep_high、跌回 1-4 播 rep_mid、≤0 播 rep_low——分档整句、账目真实(A4 资源账目律)。
- 归因真话律落地:每个负向结果节点(z1_bade、k1_lulle、k1_liee、e4 链)的导播收尾句必须指向玩家的**实际选择**(「地址没报清就转接」「你没听见她在怕什么」),禁止「运气不好」类假归因;成功路径不播任何补救教学(A5)。
## 5. 音频三层映射
### 5.1 BGM(状态→曲目,Mureka instrumental,4 首 ≈ $0.12)
| 状态 | 曲目 | 切换 | Mureka prompt 草稿(英文,seamless loop) |
|---|---|---|---|
| INTRO/RING/ONAIR | `bgm/onair` | 开局淡入 800ms,全夜班不切;语音 duck 引擎自理 | "Midnight radio broadcast music bed, warm lo-fi neo-soul chords, soft vinyl crackle, gentle night-drive groove with brushed drums, intimate late-night host mood, no vocals, seamless loop, 60-90 seconds" |
| BRIDGE | `bgm/bridge` | crossfade 800ms 进;进下一通 RING 淡回 onair 600ms | "Radio interlude pad, sparse felt piano notes over warm analog drone, distant rain mood, lonely but safe, no drums, no vocals, seamless loop, 60-90 seconds" |
| MENU | `bgm/menu` | crossfade 500ms 进;关菜单淡回原状态曲 800ms | "Late-night radio studio menu pad, warm minor pad with faint tape hiss, static harmony, very sparse, no vocals, seamless loop, 60-90 seconds" + 共性后缀(10 号 §8) |
| ENDING | `bgm/ending` | p9 结果链播完后淡入 1500ms | "Dawn sign-off theme, soft piano and warm strings, hopeful sunrise after a long night shift, gentle resolution, no vocals, seamless loop, 60-90 seconds" |
- `bgm=0` 时全部静默(引擎约定);menu pad 与 onair 同调性优先。
### 5.2 SFX 清单(Stable Audio 3,one-shot 真峰值 ≤-3dBFS;amb 循环由游戏层 buffer.loop 实现,escape amb 先例)
| id | 用途 | Stable Audio prompt(英文) |
|---|---|---|
| `sfx/onair_sting` | 红灯亮起台标 sting(INTRO/重开) | "Short broadcast on-air light sting, warm chime with vintage radio click, 1 second" |
| `sfx/phone_ring` | 热线铃(RING 循环体,每遍 ≥4s 间隔) | "Old studio telephone bell, two-strike bright metallic ring, clean" |
| `sfx/line_connect` | 接听咔哒+线路接通音 | "Telephone line connect click and short dial tone fade, subtle" |
| `sfx/line_drop` | 挂断/转接忙音 | "Telephone hang-up click with short busy tone, soft" |
| `sfx/amb_studio` | 直播间底噪 loop(调音台嗡+空调) | "Quiet radio studio room tone, console hum and soft air conditioning, seamless loop" |
| `sfx/amb_rain_city` | C1 老人侧:街道雨夜 loop | "Heavy rain on city street at night, distant traffic hiss, seamless loop" |
| `sfx/amb_station` | C2 小鹿侧:深夜车站 loop | "Empty train station hall at night, distant echo and rolling suitcase, seamless loop" |
| `sfx/amb_kid_room` | C3 糖糖侧:儿童房 loop | "Quiet child bedroom at night, soft lamp buzz, muffled house silence, seamless loop" |
| `sfx/arg_muffled` | C3 线索音:闷住的争吵+摔门(信息承载,k1_b 时起) | "Muffled argument behind a door, dull thud of door slam, indistinct voices, 4 seconds" |
| `sfx/amb_street_rain` | C4 陈姐侧:公车站雨 loop | "Rain at an empty bus stop at night, distant wet road traffic, seamless loop" |
| `sfx/amb_cab` | C5 老韩侧:车内 loop(引擎+雨刷节奏) | "Inside a moving truck cab at night, steady engine hum, rhythmic windshield wipers, rain outside, seamless loop" |
| `sfx/brake_screech` | C5 危机拍:急刹+护栏摩擦 | "Sudden brake screech on wet asphalt, brief tire friction, 2 seconds" |
| `sfx/tape_static` | p9 直面:留言机电流声 | "Old cassette tape static and tape hiss with a click stop, 3 seconds" |
| `sfx/ui_confirm` | 菜单/接听确认(程序合成兜底) | —(程序合成,同全局先例) |
| `sfx/ui_menu` | 菜单呼出(程序合成兜底) | —(程序合成) |
- C3/C4 的 amb 是**信息层**:arg_muffled 在 k1_b 起播 2-3 次,玩家听没听见由 p4 抉择兑现(声音即世界)。
- 预警窗纪律:h1_danger 前 2s 先起 brake 渐强(L2 前置预警),danger 签名与 brake 同窗。
### 5.3 Voice 划分(详见 15-radio-voice-ids.md)
| 组 | 声部 | 落位 | 数量 |
|---|---|---|---|
| 主播(你)HOST | 双性别(female=Serena / male=Uncle_Fu + HOST instruct 常量) | `voice/female|male/` | 21 句 |
| 导播/系统/向导 PROD | 双性别(female=Vivian / male=Dylan + PROD instruct 常量) | `voice/female|male/` | 82 句 |
| 来电者 NPC ×5 | 平铺单音色(老周=Uncle_Fu / 小鹿=Serena / 糖糖=Vivian / 陈姐=Vivian / 老韩=Eric),**不做双性别** | `voice/` 根(平铺回退命中)+ 电话滤波转码 | 29 句 |
- NPC ≤5 个约束满足;玩家自己的声音跟随全局 `settings.voice`(男/女主播皆可代入),导播耳语是向导人设(L4 角色化系统声)。
- 合计 132 句(≤170 GPU 预算);音频文件 = 103×2 + 29 = 235 条(×2 格式 = 470 文件)。
## 6. 来电音质感设计(声音即世界的实现)
### 6.1 引擎能力边界(对照 web-engine-api.md)
| 能力 | 引擎现状 | 方案 |
|---|---|---|
| voice 运行时 EQ/滤波 | ❌ voice 链 = SoundTouch → gain → voiceBus,无滤波插入点 | **资产侧转码滤波**(storyline n03_msg/n05_bcast 的 convert.sh 带通先例):ffmpeg `highpass + lowpass + 压缩器` 逐来电者定档 |
| voice 逐句声像/空间定位 | ❌ voice 单声道总线 | 不使用;空间感交给 sfx amb(sfx 资产内 pan,escape amb 先例) |
| sfx 循环 | sfx 即发即弃,无 loop 选项 | 游戏层 `buffer.loop=true` 或 onEnded 重触发(escape amb_loop 先例) |
| 语音 duck 垫乐 | ✅ 内置(duck 20%,300ms) | 直接依赖 |
| 运行时混响/失真(「信号变差」差分) | ❌ | 降级:用 amb 响度强弱 + 台词停顿节奏表达,不做动态滤波 |
| 变速不变调 | ✅ SoundTouch(settings.speed + 长按 1.7×) | 直接依赖 |
### 6.2 每位来电者的声音签名(资产侧 ffmpeg 参数)
| 来电者 | 滤波档 | 质感设计 | 背景 amb(sfx 层) |
|---|---|---|---|
| 老周 | highpass 500 + lowpass 2800 + 轻压缩 | 老旧手机,窄而颤抖 | amb_rain_city |
| 小鹿 | highpass 400 + lowpass 3400 | 车站手机,干净偏冷 | amb_station |
| 糖糖 | highpass 450 + lowpass 3000 | 家用座机,小而近 | amb_kid_room + arg_muffled |
| 陈姐 | highpass 420 + lowpass 3100 | 户外手机,风噪 | amb_street_rain |
| 老韩 | highpass 300 + lowpass 3400 | 车载蓝牙,宽而轰 | amb_cab |
- **可读性底线**(L2 反模式「写实但不可读」):滤波保持在语音可懂频带内,宁窄勿脏;QC 试听不可读的句子回退**未滤波原声**——叙事不破,只损失质感(显式降级方案,不阻塞管线)。
- 主播/导播人声**不滤波**(直播间级音质);「电话里的人」与「身边的人」靠这一层音质差立住,无需任何视觉。
## 7. 向导层接入
### 7.1 状态签名 earcon / turn cue
| 时机 | 向导动作 | 说明 |
|---|---|---|
| INTRO 进入 | `guide.state('explore')` | 叙事态签名 |
| 每个抉择点 p* 起播 | `guide.state('choice')` | 选项句本身即 turn cue(播完当刻 choiceArmed 即真,W2 契约零插播) |
| RING 铃响 | `guide.turn({haptic:true})`(无语音) | 铃声+turn 签名=「轮到你接听」;首次铃前 b_goal 已口播规矩 |
| BRIDGE 进入 | `guide.state('settle')` | 喘息/结算签名 |
| ENDING 进入 | `guide.state('settle')` | 同上 |
| MENU 打开 | `guide.state('menu')` | 菜单签名 |
| C5 危机拍 | `guide.state('danger')` | 全局唯一 danger 使用点(刹车前 2s,与 brake_screech 同窗) |
| 无效手势 | `guide.error({voice:'gd_invalid', onceKey})` | error 震 + full 档语音,idle-only(F1.4) |
- earcon 词表:只用全项目统一 8 音(menu/explore/battle/choice/settle/danger/turn/nudge),**零新增**;battle 本游戏不用。开场台词自然教 3 个最高频签名(铃=接听、短促提示音=轮到你选择、上滑/下滑口令);settle/menu 首遇即学;逐签名口播教学列 v2 候选(14 号 §4 同款)。
### 7.2 看门狗参数(秒;任何手势经 guide.disarm 复位;guide=off 全不挂)
| 等待点 | full 档 | standard 档 | 级别内容 | 礼仪 |
|---|---|---|---|---|
| RING 无接听 | [10, 25] | [10, 25](onEscalate 游戏层补播) | ① gd_ring1「热线在闪。单击,接听。」② gd_ring2「灯还亮着,听众在等。」 | **两级不同句**(绝不逐字重播);两级用尽只剩铃声循环,永不惩罚 |
| ONAIR/INTRO/BRIDGE 叙事链末等待 | [25, 50] | [13, 30](同 storyline P2-3 先例) | ① gd_continue ② gd_again | storyline F1.1 同款,两级后彻底静默 |
| 抉择点 p* 播完无操作 | [25, 50] | [25, 50] | 重播**当前 p* 提示句本身**(复述选项=合法帮助,13 号 §16 N3 先例) | 两级后静默;空点单击=重挂计时 |
| ENDING 结算等待 | [25, 50] | [13, 30] | gd_continue / gd_again | 结算单击即重开,不阻拦 |
- 向导插播一律 `queue:true`,绝不顶掉在播对白;error/watchdog 类语音 idle-only(在播则跳过且过期丢弃,F1.4)。
- 单句语音看门狗(资产缺失兜底):30s 超时且 `isVoicePlaying()===false` 才自动续链,长句续约不掐(storyline N1 先例照抄)。
## 8. 存档设计
### 8.1 进度 data 结构(PUT /api/progress {game:'radio'})
```json
{
"schema": 1,
"updatedAt": "2026-09-01T12:00:00Z",
"summary": "第3通·失眠的孩子 · 口碑+2 · 结局1/4",
"phase": "ONAIR",
"node": "k1_asko",
"rep": 2,
"knot": false,
"heardArgument": true,
"bond": { "zhou": 1, "deer": 1, "kid": 0, "wife": 0, "han": 0 },
"endings": { "e2": true },
"nights": 3
}
```
- 写入时机:每通电话 RING 进入、每个抉择结算后、ENDING 进入、菜单「保存并回大厅」。
- 断点续玩:登录/本地读到存档 → `sys_continue_ask`(单击=继续上次 / 右滑=新夜班);继续 = 恢复 phase+node 并**重播当前节点链** = 一句话局面复述(L5:恢复成本游戏承担);重进 ≤5s 可玩。
- summary 字符串格式:进行中 `第N通·<来电者> · 口碑±X · 结局K/4`;已收播 `已收播·<结局名> · 结局K/4`(大厅卡片直接显示,人话)。
## 9. playtest 断言清单(登记 13 号文档 radio 节,格式同 §1)
**关键路径**(E1 全程,≤60 次手势):
1. 启动遮罩点按 → INTRO 链(b_open 起,voice 层有播放记录)→ 链完自动进 RING(c1),turn 事件与铃声断言。
2. RING 单击接听 → ONAIR(c1):p1 左 → p2 左 → z1_ok1 → BRIDGE(rep 断言 +1+2=3)→ RING(c2)。
3. 依序走 p3 左 / p4 右 / p5 左 / p6 左 / p7 左 / p8 左(断言每步后 rep 与 §4.2 一致;p4 右后 `heardArgument === true` 且 C4 播 w1_say 组)。
4. p9 左 → knot=true → ENDING(e1):结局链 + ep_lead + 5 条 ep_*_ok → 结算态单击重开回 INTRO。
**边界断言**:
- **四结局可达**:剧本 B(路径同上但 p9 右)→ e2;剧本 C(p1右 p2左 p3右 p4右 p5右 p6右 p8右 p9左,rep=3)→ e3;剧本 D(p1右 p2右 p3右 p4左 p6右 p8右 p9右,rep=0)→ e4;各局 rep 终值与设计表一致。
- **双击重听**:ONAIR/ENDING 播完态双击 → 当前节点链同 id 序列重播;重播期间单击=跳本句(nodeId 不变),播完回原等待态。
- **菜单返回不丢节点**:ONAIR 中下滑开菜单 → 上滑听帮助 → 双击「回到直播」→ phase/node 不变、当前节点重播;菜单内单击循环顺序 = [回到直播, 重新开始今晚, 保存并回大厅];确认回大厅 → sys_menu_lobby 播出且 PUT/localStorage 有最新 data。
- **中断复述**:杀进程重进(存档 node=k1_asko)→ sys_continue_ask → 单击 → 重播该节点链;上滑播 sys_help+prog_c*+rep_* 链(顺序断言)。
- **看门狗**:RING 快进 10s → gd_ring1、25s → gd_ring2(断言第二级 id ≠ 第一级 id,绝不逐字重播;两级后仅铃声);ONAIR 链末 25s/50s → gd_continue/gd_again;p* 播完 25s/50s → 当前 p* 重播两次后静默;`settings.guide='off'` 时全程零定时器零向导事件。
- **pendingChoice**:p* 播放中左右滑 → 播完立即生效(不丢、不重复)。
- **p* 不可跳**:p* 播放中单击 → 不跳句(PROMPT_RE 门)。
- **idle-only**:向导 gd_* 在对白在播时被跳过且不再补播(F1.4 契约);queue 句排在当前句后(在播句不被截断)。
- **归因真话**:剧本 D 中 z1_bade 播出(含真实归因);剧本 A 全程无任何归因/补救句(成功不教学)。
- **资产缺失**:屏蔽任意 voice 资产 → 30s 看门狗自动续链到结局,不卡死。
- **数字规范**:prog_*/rep_* 全部整句,转写审计无运行时拼接碎片。
- **坏音质降级**:滤波资产缺失时原声可播(回退路径不断链)。
**失败即不通过**:任何等待 >5s 无声(静默死局);无效手势零反馈;结局计数与 §3.4 不符。
## 10. 难度 / 节奏曲线
| 段 | 内容 | 目标时长 | 设计目标 |
|---|---|---|---|
| 开场 | 台标 + 一句规矩 + 第一声铃 | 1.5-2 min | 首输入 <60s(链 ~30s 后铃响即接听);规矩一句讲完(P4) |
| C1 老周 | 教学通:2 个二选一,条条有出路 | ~3.5 min | **首个正反馈 ≤3 min**(p1 左 → 老周声音稳住 + 导播确认);唯一 -1 组合不扣资源(学习期零惩罚) |
| C2 小鹿 | 单抉择重话术 | ~2.5 min | 节奏呼吸口;建立「两个选项都体面」的信任 |
| C3 糖糖 | 双抉择 + amb 线索埋设 | ~3.5 min | 环境音首次承载剧情(声音即世界);错过线索可被尾声点名 |
| C4 陈姐 | 最复杂:2 抉择 + 条件差分 | ~4 min | 情感高潮;选项句最长(p7 ≤40s 播完) |
| C5 老韩 | 高潮:危机拍(danger+急刹,前置 2s 预警) | ~4 min | 紧张峰值但零死亡、零倒计时(P4);h1_echo 兑现 C1 回响 |
| 签收+结局 | p9 + 结局链 + 5 句回响 | ~3 min | 心结抉择 + epilogue 回响感(P2);开环钩子拉下一局 |
- 合计 ≈ 21-22 min(15-25 min 达标);每 30-60s 一个反馈颗粒(导播短句/结果句/口碑档),无沉默尴尬期(P3)。
- 难度曲线 = 信息复杂度(1 抉择 → 条件差分 → 双方对峙 → 生死时速),操作复杂度恒定(永远二选一)——P4「规则一遍听懂」由恒定操作面保证。
## 11. 自检清单
| # | 硬约束 | 落地 | ✓ |
|---|---|---|---|
| 1 | FTUE 首输入 <60s | INTRO 链 ~30s 后第一声铃即接听(§10) | ☐ |
| 2 | FTUE 首个正反馈 ≤3min | p1 左 outcome ~2.5min(z1_songo+z1_sdone) | ☐ |
| 3 | 学习期零惩罚 | C1 全组合不扣资源;RING 不接听无超时惩罚 | ☐ |
| 4 | 失败三问归因 + 归因真话律 | z1_bade/k1_liee/e4 链指向实际选择;§4.3 禁假归因 | ☐ |
| 5 | 手势宪法(6 手势默认语义,零豁免) | §2.3 全覆盖;单击=确认/继续/接听、菜单走 §5.2 模态约定 | ☐ |
| 6 | 长按豁免显式登记 | 无豁免:长按=引擎快进(12 号叙事类不豁免),§2.3 登记在案 | ☐ |
| 7 | 向导层:状态签名/turn cue/看门狗 | §7.1/§7.2;RING 两级**不同句**,绝不逐字重播 | ☐ |
| 8 | 插播一律 queue 不顶对白;纠错 idle-only 过期丢弃 | §7.2(F1.4 契约) | ☐ |
| 9 | 提示永不逐字重复 | gd_ring1≠gd_ring2;gd_continue→gd_again;抉择点复述 p* 为 13 号 §16 先例 | ☐ |
| 10 | 成功后不灌补救教学 | 归因句只挂负向结果(§9 断言) | ☐ |
| 11 | 单次播报 ≤3 选项 | 全部抉择恒 2 选项 | ☐ |
| 12 | 并发声流 ≤2 | voice+amb 两流;无第二 voice 通道 | ☐ |
| 13 | earcon ≤7 个、显式教一次、语义靠语音 | 只用统一 8 音中的 6 个(battle 不用),零新增;选项内容全语音 | ☐ |
| 14 | 台词一句一文件整句、≤40 字、数字分档整句、无运行时拼接 | voice-ids 全表 ≤39 字;prog/rep 分档整句 | ☐ |
| 15 | 多音字规避(重新听)、同游戏同 instruct 常量、无语速词 | voice-ids 头部注记与 instruct 常量表 | ☐ |
| 16 | P1:可打断/重听、status key、菜单规整、不当白痴 | skipVoice/doubletap/上滑状态链/模态菜单;成功零重复教学 | ☐ |
| 17 | P2:头 3 分钟见效、结局回响 | C1 即时改变对方情绪;ep_* 救/错过点名 | ☐ |
| 18 | P3:单手免视、一句话复述局面、3min 反馈颗粒 | 全手势免视;重进重播当前节点;BRIDGE 口碑播报 | ☐ |
| 19 | P4:规则一遍懂、高容错、无高压倒计时、NPC 语速感 | 一句规矩;零死亡零倒计时;NPC 句 ≤18 字+逗号停顿(不用 instruct 语速词,禁令遵守) | ☐ |
| 20 | 抉择树 4 结局可走通 | §9 剧本 A-D 四条实路径 + rep 断言 | ☐ |
| 21 | 单局 15-25 min | §10 曲线 ≈21-22 min | ☐ |
| 22 | 分支不爆炸 | 跨来电仅 3 标量;树 ~60 节点固定深度(§3.3) | ☐ |
| 23 | 台词 ≤170 句、NPC ≤5 | 132 句 / 5 个来电者(voice-ids 账目) | ☐ |
| 24 | 引擎边界诚实 | §6.1 能力表:做不到的写明资产侧/降级方案 | ☐ |
---
## 实现核对(测试 agent 登记)
> 断言实现:`web/radio/test/playtest.mjs`,193 条,`tools/node/bin/node web/radio/test/playtest.mjs`
> 退出码 0 全绿。会话剧本:`web/harness/sessions/radio.mjs`(`session` 主线完整弧线 +
> `sessionStress` 乱按压力),经 `tools/node/bin/node web/harness/transcribe.mjs radio
> [--session sessionStress]` 跑通(transcribe.mjs 已登记 radio/支持 `--session` 选剧本)。
> 回归:六游戏 playtest + guide.test 全绿不受影响。
### §9 关键路径逐条
| # | 断言 | 状态 |
|---|---|---|
| 1 | 遮罩点按 → INTRO 链(b_open 起)→ 链完自动进 RING(c1),turn 事件与铃声 | ✅已断言(INTRO 四句逐句计数 + turn 事件 + phone_ring 循环) |
| 2 | 单击接听 → p1 左 → p2 左 → z1_ok1 → BRIDGE(rep=1+2=3)→ RING(c2) | ✅已断言 |
| 3 | p3 左/p4 右/p5 左/p6 左/p7 左/p8 左逐点 rep 与 §4.2 一致;p4 右 heardArgument=true 且 C4 播 w1_say 组 | ✅已断言 |
| 4 | p9 左 → knot=true → ENDING(e1) + ep_lead + 5 条 ep_*_ok → 结算单击重开回 INTRO | ✅已断言(含手势预算 55≤60) |
### §9 边界断言逐条
| 断言 | 状态 |
|---|---|
| 四结局可达(剧本 B→e2 / C→e3 / D→e4,rep 终值与设计表一致) | ✅已断言(另补剧本 E:全左但 p4 左 → heardArgument=false → C4 播 w1_say2 组、rep=9,补齐 w1_say/w1_say2 双差分覆盖;剧本 C/D 额外断言带话差分不播与 ep_deer_no) |
| 双击重听:ONAIR 播完态/ENDING 播完态同 id 序列重播,重播期间单击=跳句 | ✅已断言 |
| 菜单返回不丢节点;单击循环顺序 [回到直播, 重新开始今晚, 保存并回大厅];确认回大厅播 sys_menu_lobby 且 localStorage 有最新 data | ✅已断言(含报名 queue 零截断顺序断言) |
| 中断复述:存档 node=k1_asko → sys_continue_ask → 单击恢复 → 重播节点链;上滑 sys_help→prog_c3→rep_mid 顺序 | ✅已断言(另补 ASK 右滑=新夜班) |
| 看门狗:RING 10s/25s 两级不同句、两级后仅铃声;ONAIR 链末 25s/50s gd_continue/gd_again;p* 25s/50s 复述两级后静默;ENDING 25s/50s;guide=off 全程零定时器零向导语音 | ✅已断言(full 档事件钩子计数,choice 签名 ×9) |
| pendingChoice:p* 播放中滑动感存、播完即生效(不丢不重复) | ✅已断言 |
| p* 不可跳(PROMPT_RE 门) | ✅已断言 |
| idle-only:对白在播时纠错跳过且不占 onceKey(F1.4);queue 句不顶在播句 | ✅已断言(skipped 事件 + 补播对照 + 菜单报名顺序) |
| 归因真话:剧本 D 播 z1_bade;剧本 A/E 全程零归因/补救句 | ✅已断言(含 k1_lulle/k1_liee 挂点) |
| 资产缺失:屏蔽 voice → 30s 看门狗续链到结局不卡死 | ✅已断言 |
| 数字规范:prog_*/rep_*/gd_ring* 分档整句字面量、无运行时拼接 | ✅已断言(静态审计) |
| 坏音质降级:滤波资产缺失回退原声 | 合并(回退是资产侧转码工序,代码无分支;NPC 性别目录→平铺回退由静态审计覆盖) |
| 失败即不通过(>5s 无声死局 / 无效手势零反馈 / 结局计数与 §3.4 不符) | ✅已断言(无效手势 error 震逐次计数;四结局矩阵即计数核对) |
### 本轮修复的实现 bug(web/radio/game.js)
1. **ONAIR 叙事链末不等待(规格 vs 实现最大偏差)**:`_onNodeSeqDone` 无条件 `advance(node.next)`,
节点表的 `auto:true` 标志(INTRO 链/危机拍/签收链/p9 结果链)从未被读取——与 §2.3「链末=继续」、
§7.2 ONAIR 链末看门狗行、§9「ONAIR 链末 25s/50s」及文件头注释均不符。修复:仅 `node.auto` 或
`@x` 过场转移自动衔接,其余 ONAIR 叙事节点链末等待单击并挂 gd_continue/gd_again 看门狗。
2. **longpress 误报无效**:gesture.js 把 longpress/longpressend/tapcancel 派发进 onGesture,RING/
BRIDGE/ENDING/MENU/ASK 的 default 分支把它们当无效手势报 gd_invalid/error 震——§2.3 长按行是
「引擎接管快进」,全状态合法。修复:仅 left/right(ASK 另含 up/down/doubletap)报无效。
3. **菜单单击循环的引擎队列竞态**:`audio.play(item, {queue:true})` 在引擎 `_finishVoice` 中先跑
onComplete(游戏链播下一句)后 shift 队列,排队报名会把 sys_menu→sys_menu_resume 链播句顶掉。
修复:菜单报名改游戏侧 `_menuQueue` + `_drainMenuQueue()` 排水,当前报名播完接播、零截断。
4. **RING 双击首通不报无效**:INTRO 链节点完成会写 `lastCloser`,首通双击重听了 b_goal 而未按开发
注记「首通前无上一通 → 无效手势零静默」报错。修复:`callN <= 1` 时报无效。
5. **ENDING 上滑缺口碑**:§2.3 ENDING 上滑=「帮助+状态(结局名+口碑)」,原实现只播 sys_help+结局
主句。修复:链尾补 `TIER_VOICES[tierOf(rep)]`。
6. **stop() 后中断链复活**:被 stopVoice 掐断的 `_playSeq` 链可经 30s 单句看门狗「资产缺失」路径续链,
已拆除实例会走到 enterRing 留下无人接听的永久铃声定时器。修复:stop() 先 `seqToken++` 作废在途链。
7. **newNight 不清抉择武装**:菜单重开/结算重开到新夜班首链起播前,choiceArmed/pendingChoice 残留,
窗口期手势会在旧节点上误抉择。修复:newNight() 显式清零。
8. **_ringOnce 双链防护**:closeMenu(RING)/_resume 直接调 _ringOnce 不清旧计时器,可能双铃声链。
修复:_ringOnce 先 clearTimeout 旧 ringTimer。
### 规格歧义 / 实现取舍(登记不改设计)
- §4.2 表「p4 问她/哄睡」选项列序与 §3.2 树形(左=哄睡/右=问她)相反,实现按 §3.2 + p4 提示句原文
(game.js 已注记);断言按 §9 口径(p4 右 → heardArgument=true,+1)。
- `sys_saved`(15b G0 组,触发=存档写入后)未接线:自动存档每局 ≥7 次,逐次播报会持续打断节目流;
菜单「保存并回大厅」已有 sys_menu_lobby 确认句。列遗留(如需可挂菜单「重新开始今晚」确认后)。
- INTRO 上滑=「帮助+状态」:开播前无通号/口碑可报,实现只播 sys_help 后重播当前段(RING 起状态链完整)。
- 存档 data 在 §8.1 字段外增加 `call`/`ending`/`lastCloser`(断点续玩与 RING 双击需要,超集兼容)。
- ASK 右滑新夜班的 `nights` 按新实例计数(不继承被拒绝存档的 nights),与结算重开 +1 口径不一致,
无游戏性影响,列遗留。
### 资产生产缺口(列遗留,本轮不修)
- §5.2 SFX 15 件中 **12 件 Stable Audio 3 条目未产出**(`gen/gen_sfx.py` 头注:HF token 未配置跳过):
onair_sting / phone_ring / line_connect / line_drop / amb_rain_city / amb_station / amb_kid_room /
arg_muffled / amb_street_rain / amb_cab / brake_screech / tape_static。缺失走引擎 console.error
静默降级(§6.1 降级方案),玩法与断言不受阻;playtest 静态审计锁定「缺失集合 ⊆ 该清单」,
资产补齐后断言自动收敛。已产出:ui_confirm / ui_menu(程序合成)+ 132 句 voice + 4 首 bgm
(ogg+m4a 双格式零缺失,见静态审计)。
## R2 修复回归(round1 synthesis F1-F5 + V1/V2,2026-09-02)
> 证据源:research/new8/radio/round1/{synthesis,live-player}.md。断言全部落在
> `web/radio/test/playtest.mjs`「R2 修复回归」块 + 主路径/看门狗/静态审计改断;
> **playtest 断言总数 R1=193 → R2=221(+28)**,全绿。
| 项 | 修复(文件 → 行为) | 回归断言(✅=已断言) |
|---|---|---|
| F1 (P1) | `game.js`:`onGesture` 入口 `guide.disarm()` 拆掉等待态看门狗后,无效手势/引擎接管手势分支按态立即重挂——ONAIR/INTRO 链末(`_inStoryWait()` 与 `_onNodeSeqDone` 等待分支同口径)、ASK 空闲、RING 空闲(`_rearmRingWatchdog`)、ENDING 结算态;BRIDGE 本无 guide 看门狗(restTimer 为游戏层),不涉及 | ✅ 链末等待中发无效手势,gd_continue 25s 整准点、gd_again 50s 整准点(旧实现 34.2s 零事件);✅ RING 无效手势后 gd_ring1 仍 10s 准点;✅ ASK 无效手势后看门狗仍 25s 准点 |
| F2 (P4/P2) | 台词表两处同步改词(先 `docs/design/15-radio-voice-ids.md` 后 `tools/gen_voice_dual.py`,两表一致):`rep_low`「今晚的口碑,有点砸了。」→「今晚的口碑,还没立起来。」(零起步语义,与 rep_high「在听众里立住了」同尺度;rep=0 开播前不再读成「砸了」);重生成 `voice/{male,female}/rep_low.{ogg,m4a}`(m=1.92s / f=2.32s,`--only rep_low` → `--convert` → `check_all_assets` radio 242+242 全过:双格式配对、零 0 字节、全可解码、性别目录 id 一致) | ✅ 静态审计锁定 rep_low 字面量存在;live 转写复验归 R2 协议第 2 步(重新生成转写) |
| F3 (P2) | `gen/gen_sfx.py` 增加 `phone_ring` 程序合成:440+480Hz 柔和双音一响 1.8s + 静默 3.4s = 5.2s 循环周期烘进文件(≥4s);无缝 loop 工艺 = 逐声道去直流、**不做首尾淡化**(`save_loop`)、解码回读校验真峰值 ≤-3dBFS(ogg -3.87 / m4a -3.84dBFS)+ 循环头/尾样本 ≤-60dBFS 边界静音;落 `audio/sfx/phone_ring.{ogg,m4a}`。`game.js` RING 接循环垫:引擎 sfx 层无 loop/stop → 游戏侧经 ctx 起循环 BufferSource 持句柄(`_ringSrc`,guide earcon 同款直用总线),接听/菜单/重开 `_stopRing` 即停;移除定时重触发(`ringTimer`/`ringJitterMs`/`RING_INTERVAL_MS`) | ✅ 进 RING 循环垫起播且挂起态虚拟时钟可空闲(零重触发定时器,可复现);✅ 单击接听循环垫即停不拖尾;✅ RING 无效手势不拆铃声垫;✅ 两级铃看门狗用尽后循环垫仍在(永不惩罚);✅ 静态审计 phone_ring 出待补清单(Stable Audio 待补 12→11 件) |
| F4 (P2) | `game.js`:断点续玩恢复后的首句播完再武装跳句——`_resume()` 置 `_resumeGuard`,`_playSeq` 消费为 `_skipLockId`(恢复链首句),该句播完/链完/被新链顶掉即解锁;ONAIR 与 ENDING(live 陷阱场景)两态 tap 生效 | ✅ 恢复后首句(w1_say / e4_a)播完前单击不跳句;✅ 首句播完后跳句重新武装(w1_sayo / e4_b 被正常跳过,链照常走) |
| F5 (P2) | `game.js`:链末句跳过保护——`_isTailSentence()`:当前句为本节点末句且节点把控制权交出(链末等待单击 / `@x` 过场转移 / 直入抉择提示)时屏蔽「跳过本句」;自动链中段(b_open/b_rain、危机拍)与 w1_say 组前两句跳句自由保留。与 F1 同提交复核链末状态机:`_inStoryWait` 与 `_onNodeSeqDone` 口径一致 | ✅ 交接抉择提示前的末句(w1_saye)单击不可跳、完整播出后照常交接 p7;✅ @ending 过场前末句(d9_facee,live 被吃掉的点题句)单击不可跳、完整播出后自动进 ENDING(e1) |
| V1 | 复核结论:pendingChoice 生效时序无 bug(W2 契约实现正确),按裁定补显式断言关案 | ✅ p* 播放中连点不跳句、不清缓存、不提前生效(rep 未动);✅ 播中再滑覆盖缓存(最后一次为准);✅ 生效时序 = p1 播完 → ui_confirm 结算音 → 结果句;✅ 缓存恰好生效一次(rep=1、缓存清空、不丢不重复) |
| V2 | 复核结论:菜单逻辑无缺失(三项循环 + 回绕正确),按裁定补全项循环断言关案。注:菜单为 3 项(sys_menu + sys_menu_resume/restart/lobby,15-radio.md §实现核对「菜单单击循环顺序」口径)——任务书所写「4 项/sys_menu_save」对应菜单标题句 + 3 菜单项;「保存并回大厅」确认句即 `sys_menu_lobby`(无 sys_menu_save 资产) | ✅ 单击循环两个整周期逐项可达(resume→restart→lobby→resume→restart→lobby,第三项不再缺席)、6 次回绕模 3 无漂移;✅ 双击确认「保存并回大厅」→ sys_menu_lobby 播出 + leaving 置位 + 存档落盘 |
V3(arg_muffled/brake_screech 等 11 件 SA3 资产)维持「等待 HF token」,接线代码已备,不在本轮。
### 遗留(R2 收尾时需协同)
- `web/harness/voice_text.json` 的 `rep_low` 期望文本仍是旧词「有点砸了」(harness 归属协议/
转写侧,本轮改动手清单之外):R2 协议第 2 步重新生成转写前需同步为新词「还没立起来」,
否则 live 复测该句会 MISMATCH。
- dungeon 款全量回归本轮 FAIL:另一 agent 并行改动 `web/engine/{audio,gesture}.js` 与
`web/dungeon/**` 进行中(失败点为 dungeon 网格/侧墙与 tut 旁白断言,与 radio 无关);
radio与其余 6 款全绿。
+179
View File
@@ -0,0 +1,179 @@
# 16b · 《回声地窟》语音台词表(voice ids,录音依据)
> **给音频总监的录制说明**
> - **全部旁白,无 NPC**:本作怪物不说话,无角色音(主设计 §1/§9.3)。125 句双性别。**female = Serena,male = Uncle_Fu**(沿用 R1 分配惯例)。落 `web/dungeon/audio/voice/female|male/<id>.{ogg,m4a}`。
> - female instruct 常量:`平静清晰的女声,像探窟向导,沉稳可信`
> - male instruct 常量:`低沉醇厚的男声,像探窟向导,沉稳有力`
> - 两常量全表统一(含教学/战斗/结算),禁止句级变体。
> - 数字规避:层数(7 句)/血量(4 档)/金币(4 档)/面朝(4 句)/威胁(4 句)/战利(3 档)/死因(3 档)/元进度(3 档)全部分档整句,无运行时拼接。
> - 多音字自查:全表无「血」「重(chóng/zhòng 歧义句)」「爪」「挨」「壳」「栅」;「重听」一律写「重新听」;「重新」为规范 sanctioned 常用词(sys_menu_help / sys_menu_restart)保留。「回声回来越快」已去掉助词「得」。余留风险 2 处已标注:`btl_bat` 的「蝠」(读 fú,试听异常则整句改「盲眼蝙蝠扑了过来」);`unlock_potion` 的「行囊」(xíng náng 固定词,试听确认)。
> - 向导/卡关通道句(gd_*、stair_*、nav_*):trigger 见表;接线纪律按 14 号文档——watchdog/纠错句 idle-only、onceKey 同局限次、向导插播一律 queue:true;开发轮次须将本表 gd_* 条目登记进 14 号文档增补。
> - 管线:`tools/gen_voice_dual.py` 照录(--game dungeon);生成后 `--convert` 落盘 + `tools/check_all_assets.py` 终检(双格式配对、双性别集合一致)。
## A 组 · 序章/标题(旁白 ×4)
| id | 台词全文 | 触发/语境 |
|---|---|---|
| `intro_1` | 你在黑暗中醒来。这里是回声地窟。 | 新周目开场 1 |
| `intro_2` | 伸手不见五指,声音就是你的眼睛。 | 新周目开场 2 |
| `sys_continue_ask` | 检测到探窟记录。单击继续,右滑重新开始。 | 有存档进游戏(TITLE) |
| `sys_new_run` | 新的探窟,开始了。 | 新周目开始 |
## T 组 · 教学演练 DRILL(旁白 ×19)
| id | 台词全文 | 触发/语境 |
|---|---|---|
| `tut_d0` | 先学会用耳朵看。单击,发出声波脉冲。 | D1 前(首次脉冲引导) |
| `tut_echo_come` | 听,回声回来了。 | D1 首次回声后 |
| `tut_front_rule` | 回声回来越快,墙就越近。 | D2(正前距离规则) |
| `tut_side_rule` | 两侧的回声,发闷是墙,空旷是通路。 | D3(侧向规则) |
| `tut_walk` | 按住屏幕不放,你就往前走。 | D4 |
| `tut_turn` | 左滑右滑,可以转身。 | D5 |
| `tut_replay` | 双击,再发一次脉冲,不要钱。 | D6(重发免费) |
| `tut_status` | 上滑,随时听自己的状态。 | D7 |
| `tut_menu` | 下滑,打开菜单。 | D7 |
| `tut_mob` | 前面有东西!放心,它伤不到你。 | D8 遭遇教学怪前(真话:ATK0) |
| `tut_parry` | 听到它的动静,双击格挡。 | D8 前摇前 |
| `tut_parry_ok` | 挡住了! | D8 格挡成功(成功只给新短句) |
| `tut_parry_miss` | 没挡住也别怕,它伤不到你。 | D8 格挡失败 |
| `tut_attack` | 现在单击,挥拳打它。 | D8 你的回合 |
| `tut_kill` | 打中了,就是这样。 | D8 击杀 |
| `tut_stairs` | 铁器的回声,就是下潜的楼梯。 | D9 |
| `tut_stairs_on` | 楼梯就在脚下。单击,下潜。 | 首次踏上楼梯格 |
| `tut_done` | 教学完了。地窟很深,小心走。 | DRILL 完成 |
| `tut_skip` | 已跳过教学。单击脉冲,先听听四周。 | 下滑跳过教学 |
## S 组 · 系统/菜单/状态(旁白 ×35)
| id | 台词全文 | 触发/语境 |
|---|---|---|
| `sys_saved` | 进度已保存。 | 存档写入后 |
| `sys_menu` | 菜单。单击切换,双击确认。 | 下滑呼出菜单 |
| `sys_menu_resume` | 返回探索。 | 菜单项 |
| `sys_menu_save` | 保存并回大厅。 | 菜单项 |
| `sys_menu_help` | 重新听帮助。 | 菜单项 |
| `sys_menu_restart` | 重新开始本周目。 | 菜单项 |
| `help_explore` | 单击脉冲,按住前进,左右滑转身。双击再听,上滑状态,下滑菜单。 | 帮助(探索) |
| `help_battle` | 敌人前摇声响时双击格挡,单击挥击。打不过就按住屏幕逃跑。 | 帮助(战斗) |
| `settings_hint` | 音色、语速、背景音乐,请回大厅修改。 | 菜单内 |
| `st_floor_1` | 这是第一层。 | 上滑状态链 / 进层 |
| `st_floor_2` | 这是第二层。 | 同上 |
| `st_floor_3` | 这是第三层。 | 同上 |
| `st_floor_4` | 这是第四层。 | 同上 |
| `st_floor_5` | 这是第五层。 | 同上 |
| `st_floor_6` | 这是第六层。 | 同上 |
| `st_floor_7` | 这是第七层。 | 同上 |
| `st_hp_1` | 你毫发无损。 | 血量 >75% |
| `st_hp_2` | 你受了点伤。 | 50~75% |
| `st_hp_3` | 你伤势不轻。 | 25~50% |
| `st_hp_4` | 你命悬一线。 | ≤25%(danger earcon + warning 震,降档才播) |
| `st_gold_1` | 口袋空空。 | 元进度总金币 <50 |
| `st_gold_2` | 有些积蓄。 | 50~200 |
| `st_gold_3` | 收获可观。 | 200~600 |
| `st_gold_4` | 腰包鼓鼓。 | >600 |
| `st_face_e` | 你面朝东。 | 上滑状态链(绝对方位) |
| `st_face_s` | 你面朝南。 | 同上 |
| `st_face_w` | 你面朝西。 | 同上 |
| `st_face_n` | 你面朝北。 | 同上 |
| `st_threat_none` | 附近很安静。 | 上滑威胁段(相对方位,只报左右) |
| `st_threat_left` | 左边有脚步声。 | 左侧 5 格内有怪 |
| `st_threat_right` | 右边有脚步声。 | 右侧 5 格内有怪 |
| `st_threat_both` | 两边都有动静。 | 两侧皆有 |
| `st_best_1` | 你还没到过第三层。 | TITLE 元进度·最深层数档 1 |
| `st_best_2` | 你到过中层。 | 档 2(最深 3-5 层) |
| `st_best_3` | 你到过最深层。 | 档 3(最深 ≥6 层) |
## E 组 · 探索事件(旁白 ×9)
| id | 台词全文 | 触发/语境 |
|---|---|---|
| `evt_coin_1` | 捡到几枚金币。 | 金币堆·小 |
| `evt_coin_2` | 捡到一小把金币。 | 金币堆·中 |
| `evt_coin_3` | 捡到一大袋金币。 | 金币堆·大 |
| `evt_potion` | 一瓶药水,收进腰带。 | 拾取药水(腰带空) |
| `evt_potion_full` | 腰带满了,药水留在原地。 | 已有药再拾(药不消失) |
| `evt_weapon` | 捡到更快的短刃,出手更狠了。 | 武器掉落(自动装备) |
| `evt_armor` | 换上更硬的护甲,更抗打了。 | 护甲掉落(自动装备) |
| `evt_stairs_lock` | 楼梯被大东西堵住了,先赢它。 | 窟主未清时踏上楼梯格 |
| `evt_descend` | 你顺着楼梯,往下潜。 | 下潜(DESCEND) |
## B 组 · 战斗(旁白 ×30)
| id | 台词全文 | 触发/语境 |
|---|---|---|
| `btl_bat` | 盲眼蝠扑了过来! | 遭遇盲眼蝠 |
| `btl_rat` | 窟鼠窜了出来! | 遭遇窟鼠 |
| `btl_mush` | 蘑菇怪拦住了你! | 遭遇蘑菇怪 |
| `btl_crab` | 窟主石甲蟹,就在你面前。 | 遭遇窟主(L3) |
| `btl_crawler` | 绕行者贴上来了,小心背后! | 遭遇绕行者(教学绕位威胁) |
| `btl_spider` | 洞蛛落地了! | 遭遇洞蛛 |
| `btl_leech` | 窟主腐沼蟒,挡住了楼梯。 | 遭遇窟主(L6) |
| `btl_guard` | 石像卫醒了! | 遭遇石像卫 |
| `btl_king` | 窟中之王,就在深处。 | 遭遇终 Boss(L7) |
| `btl_parried` | 挡下了大半! | 格挡成功(减伤 80%) |
| `btl_behind` | 它在你背后,转身再挡。 | 未面对攻击源挨打(归因真话) |
| `btl_swish` | 挥空了,它不在你面前。 | 挥击落空(归因真话) |
| `btl_no_turn` | 它贴着你不放,不用转身。 | 缠斗怪时转身(每场 1 次,之后仅 error 震) |
| `btl_kill` | 它倒下了。 | 击杀 |
| `btl_loot_1` | 战利不多。 | 战利档 1 |
| `btl_loot_2` | 小有收获。 | 战利档 2 |
| `btl_loot_3` | 大有收获。 | 战利档 3 |
| `btl_lvup` | 你的拳脚更利落了。 | 每击杀 4 只攻击升档 |
| `btl_flee` | 你甩掉了它,被抓了一下。 | 逃跑成功(真话:撤退挨一下) |
| `btl_flee_boss` | 退路塌了,跑不掉! | Boss 战逃跑拒绝(真话) |
| `btl_half_crab` | 石甲蟹的甲裂了! | 窟主半血(阶段转换信号) |
| `btl_half_leech` | 腐沼蟒暴怒了! | 窟主半血 |
| `btl_half_king` | 王怒了,脚步更重了! | 终 Boss 半血(二阶段) |
| `die_intro` | 你倒下了。 | 死亡链首句 |
| `die_cause_1` | 你被小怪拖垮了。 | 死因:普通怪连续失血(真话) |
| `die_cause_2` | 你没挡住那记重击。 | 死因:重击未格挡(真话) |
| `die_cause_3` | 你背后的偷袭,要了命。 | 死因:背刺(真话) |
| `die_adv_1` | 下次,前摇声响时就双击。 | 建议:全程格挡数为 0 |
| `die_adv_2` | 打不过,就按住屏幕逃跑。 | 建议:逃跑 0 次且死于连续战 |
| `die_adv_3` | 身后有动静,先转身。 | 建议:死于背刺 |
## G 组 · 卡关/看门狗/纠错(旁白 ×14)
| id | 台词全文 | 触发/语境 |
|---|---|---|
| `gd_pulse` | 单击脉冲,听听四周。 | 探索看门狗第 1 级(10s,full 档,idle-only) |
| `gd_turn` | 轮到你了。单击挥击。 | 战斗回合看门狗第 1 级(15s) |
| `gd_invalid` | 这个操作在这里用不上。 | 无效手势纠错(onceKey 同局限 1 次,之后仅 error 震) |
| `stair_hint_a` | 找找铁器的回声,那是楼梯。 | 卡关 L1 暗示(60s,楼梯未找到) |
| `stair_hint_b` | 楼梯有大东西守着,先赢它。 | 卡关 L1 暗示(窟主未清) |
| `stair_dir_e` | 楼梯在东边。 | 卡关 L2 明示(120s,绝对方位) |
| `stair_dir_s` | 楼梯在南边。 | 同上 |
| `stair_dir_w` | 楼梯在西边。 | 同上 |
| `stair_dir_n` | 楼梯在北边。 | 同上 |
| `nav_e` | 往东。 | 卡关 L3 带路(180s,导航模式逐步播) |
| `nav_s` | 往南。 | 同上 |
| `nav_w` | 往西。 | 同上 |
| `nav_n` | 往北。 | 同上 |
| `nav_done` | 到了,就是这里。 | 带路抵达楼梯 |
## M 组 · 结算/元进度(旁白 ×14)
| id | 台词全文 | 触发/语境 |
|---|---|---|
| `meta_gold_1` | 这趟没带回什么。 | 死亡/通关结算·金币入账档 1 |
| `meta_gold_2` | 这趟带回一些金币。 | 档 2 |
| `meta_gold_3` | 这趟满载而归。 | 档 3 |
| `settle_stronger` | 下个周目,你会更强。 | 结算钩子句(开环) |
| `settle_win_1` | 你捧起了地窟之心,通关了! | 通关 1 |
| `settle_win_2` | 黑暗的尽头,是光。 | 通关 2 |
| `settle_again` | 单击,再探一次。 | 结算尾(重开入口 ≤2s) |
| `unlock_lead` | 攒够了新的遗产。 | 新周目开场·有新解锁时 |
| `unlock_potion` | 解锁行囊药,开局自带一瓶。 | 遗产播报(90 金) |
| `unlock_vigor` | 解锁体魄,起始更强壮。 | 遗产播报(150 金) |
| `unlock_veil` | 解锁护符,每层头一下减半。 | 遗产播报(220 金) |
| `unlock_echo` | 解锁回声强化,侧向听得更远。 | 遗产播报(300 金) |
| `unlock_blade` | 解锁武器,开局带着短刃。 | 遗产播报(450 金) |
| `unlock_none` | 金币还不够,继续攒。 | TITLE 元进度·距下一解锁尚远 |
---
**合计:125 句 ×2 性别 = 250 条音频**(×2 格式 = 500 文件;无 NPC 平铺件)。死亡结算链 =
`die_intro → die_cause_X → die_adv_Y → st_floor_N(复用)→ meta_gold_X → settle_stronger → settle_again`;
通关链 = `settle_win_1 → settle_win_2 → meta_gold_X → unlock 段 → settle_again`;
上滑局面复述链 = `st_floor_N → st_hp_X → st_gold_X → st_face_X → st_threat_X`。
主设计触发逻辑对照 [16-dungeon.md](16-dungeon.md) §4-§11;资产规模核对见其 §9。
+577
View File
@@ -0,0 +1,577 @@
# 16 · 《回声地窟》完整设计(dungeon / Echo Cavern)
> **给开发者的实现要点摘要**
> 1. 听声辨位 Roguelike:网格地窟 + 面朝转向。**单击=声波脉冲**(回声"看"地形:正前距离 4 档×材质 4 种,左右侧 墙/开口 2 档,预 pan 立体声资产),**按住=前进、左右滑=转身**(豁免登记见 §2),双击=重发脉冲(免费重听)、上滑=状态、下滑=菜单(三件套不豁免)。战斗是 rpg 式回合制(前摇双击格挡),Boss 战加入绕位(脚步声报方位、转身对准才打得到)。
> 2. 方位全程降维 ≤4:探索报前/左/右三向(身后不报,转身即查),战斗相对方位只有面前/左/右/背后。回声=静态地形、脚步=动态怪物、播报=绝对方位——三套信息由同一 `grid` 单源生成,机制上不可能互相矛盾。
> 3. 单局=一命一局,7 层满通约 24 分钟;死了重开但金币进元进度("遗产"5 项永久强化,死亡不清档)。学习期零惩罚:L1 怪 ATK=0、L2 ATK=1,学期内不可能死;药水自动喝;撞墙无伤;无任何倒计时。
> 4. 随机性全走种子 PRNG(mulberry32,`opts.seed` 可注入,playtest 传固定 seed 复现);回声/脚步全部用「预生成 sfx 资产 + setTimeout 调度 + volume」实现,不用引擎外实时节点(playtest 虚拟时钟可断言)。
> 5. 台词表 [16-dungeon-voice-ids.md](16-dungeon-voice-ids.md)(125 句 ×2 性别,无 NPC——怪物不说话);本作向导句 gd_* 需在开发轮次登记进 14 号文档增补(§6 纪律:queue:true、idle-only、onceKey)。
> 6. 规模对标 escape+rpg 折中:单文件 game.js 预计 900-1100 行,bgm 4 + sfx ~34 + voice 125×2。
---
## 1. 概述
- **一句话**:你是盲眼探窟者,单击发出声波脉冲、用回声"看"每次都不同的地窟,听脚步辨怪,逐层下潜,死了带着金币重来、越死越强。
- **题材**:黑暗洞窟探险。核心命题「伸手不见五指,声音就是你的眼睛」——题材与纯音频互为 justification,与《剑与声》的"耳即眼"呼应但玩法完全不同:rpg 是选项列表 RPG,本作是**空间探索型 roguelike**(方法论 §4.1:状态层重点=方位参数映射+撞墙归因+目标方向信标)。
- **为什么适合纯音频**:回声定位本来就是"用声音看空间"——视觉游戏的 minimap 在这里天然就是一段回声;黑暗=无画面是叙事设定而非技术妥协。移动/转身/距离全部可离散化(≤4 方位×4 距离档),不需要连续角度(文献结论:方位降维 ≤4)。
- **单局**:一命一局,出生到死亡/通关。7 层,满通约 24 分钟,普通玩家死于 4-6 层(约 14-19 分钟)。死亡结算 ≤15s 必答归因三问,金币入元进度,**死亡不清档位进度**。
- **与现有 6 游戏的关系**:空间感承接 escape(网格+带路提示),回合战斗承接 rpg(前摇格挡),演练教学承接 space(D 步逐项实操),元进度为本作新增系统(8 款新模式探索点的核心增量)。
## 2. 手势映射与豁免登记
### 2.1 探索态(EXPLORE / DRILL / DESCEND)
| 手势 | 作用 | 反馈 |
|---|---|---|
| 单击 | **发出声波脉冲**(核心动词,1.2s 冷却防连打糊声流) | sfx/pulse + 调度回声组(§3) |
| 按住(longpress,450ms 起) | **前进**:按住朝面朝方向连续走(每格 0.5s),松开(longpressend)即停;撞墙自动停 | 每格 step_own + swipe 震;撞墙 bump + 停,**无伤** |
| 左滑 | 左转 90°(面朝逆时针 N→W→S→E) | step_turn + swipe 震 |
| 右滑 | 右转 90° | step_turn + swipe 震 |
| 双击 | **重发脉冲**(免费重听回声,不消耗任何资源、无冷却惩罚) | 同单击(宪法"双击=重听"的本作落地:线索=回声) |
| 上滑 | 状态播报链:层 → 血量档 → 金币档(元进度总账)→ 面朝 → 周围威胁 | 5 段链播 |
| 下滑 | 模态菜单(§2.4) | menu earcon |
### 2.2 战斗态(BATTLE)
| 手势 | 作用 | 反馈 |
|---|---|---|
| 单击 | 挥击:怪在你面前才命中;不在(Boss/绕位怪换位后)= 挥空 | atk_swing→命中 atk_hit / 挥空 + `btl_swish`(归因真话) |
| 双击 | **格挡**:仅敌方前摇音窗口内(1.2s)有效;面对攻击来源减伤 80%,未面对 0%(背后挨刀) | parry / hurt + hit 震 |
| 左滑/右滑 | 转身(只对绕位怪/Boss 有意义;缠斗怪时无效) | step_turn / 纠错(§10) |
| 按住 | **尝试逃跑**:普通怪必成、但撤退挨一下(真实掉血);Boss 战拒绝 | `btl_flee` / `btl_flee_boss` |
| 上滑 | 战斗状态:我方血量档 → 敌方血量档 →(Boss/绕位战)它的当前方位 | 3 段链播 |
| 下滑 | 模态菜单(战斗暂停,回合制无实时流,无需 count-in) | menu earcon |
### 2.3 标题态(TITLE)/ 结算态(SETTLE)
| 手势 | TITLE | SETTLE |
|---|---|---|
| 单击 | 继续(有存档)/ 开始探窟(无存档) | 再来一局(新周目,重掷 seed) |
| 右滑 | 放弃当前周目,重掷新周目(元进度保留) | —(无效,纠错) |
| 双击 | 重听 continue_ask / 元进度播报 | 重听结算链 |
| 上滑 | 元进度播报:金币档 → 最深层数档 → 遗产清单(已解锁各一句)→ `unlock_none`(不够时) | 重听元进度段 |
| 下滑 | 菜单(帮助/返回两项) | 菜单 |
| 按住/左右滑 | 无效(error 震 + `gd_invalid`) | 同左 |
### 2.4 模态菜单(遵守 00 文档 §5.2 全球约定,无豁免)
下滑呼出 → 单击循环切换(每切播项名)→ 双击确认 → 上滑=帮助 → 下滑=关闭。打开即暂停(战斗回合制天然可暂停)。选项:返回探索(战斗态同)→ 保存并回大厅 → 重新听帮助 → 重新开始本周目。
### 2.5 豁免登记(必须显式)
| # | 豁免条目 | 理由(先例) |
|---|---|---|
| E1 | 单击=脉冲(非"确认/继续") | 核心动词必须是最快手势;脉冲是"检查"类非即时操作,300ms tap 延迟无碍(escape 单击=检查同款) |
| E2 | 左右滑=转身、按住=前进(探索态四向滑+长按被玩法占用) | 网格移动玩法(escape 四向滑=移动先例);上下滑让位给不可豁免的状态/菜单三件套,前进改绑长按——"按住就走"零认知负荷(P4),单手免视(P3) |
| E3 | 对局期 `audio.setRateBoostEnabled(false)`(探索/战斗/教学),菜单/结算恢复 true | 长按=移动是潜在玩法输入,长按快进必须豁免(rhythm/space 对局期豁免先例) |
| E4 | 战斗态下滑=菜单保留,但菜单在战斗内语义为暂停 | 回合制,菜单即暂停,无并发压力;不沿用 rpg"战斗态下滑=逃跑"豁免(本作逃跑绑长按,更贴"转身就跑"直觉) |
教学(DRILL)中逐条讲清 E1/E2/E4 的偏离(tut_* 句),符合 §5.4"偏离必须在开场交互教学中讲清"。longpressend 仅作停走信号,不承载第二语义;不开启 immediateTap(无 tapcancel 路径)。
## 3. 核心动词:声波脉冲与回声定位
### 3.1 声学编码(全在引擎能力内)
| 维度 | 编码 | 实现 |
|---|---|---|
| 正前距离 | 4 档:贴身(1格)/近(2格)/中(3-4格)/远(5+格) | 脉冲后 setTimeout 调度 `echo_1..4`,延迟 0.25/0.50/0.80/1.10s,volume 0.9/0.7/0.5/0.35 |
| 正前材质 | 4 种:石墙/木门/铁梯(楼梯)/水 | 回声到达后 +120ms 播材质尾音 `mat_stone/door/stair/water`(0.2s 短音色) |
| 左右侧 | 2 档:墙(闷)/开口(空旷回灵) | 脉冲后 +0.35s 同时播 `side_wall_l/r` 或 `side_open_l/r`(**预 pan 立体声资产**:能量只在一个声道,引擎 play() 无实时 pan,space whoosh_left/right 同工艺) |
| 身后 | **不报** | 刻意降维:回声只照面前和两侧,转身即查(教学句 tut_side_rule 讲明规则;不报≠信息矛盾) |
- 节点说明:**不用引擎外实时 WebAudio 节点**(playtest mock 只 mock bufferSource/gain)。全部=预渲染 sfx 资产 + `play()` volume + setTimeout 调度——虚拟时钟下回声调度可精确断言。
- 降级方案:若 SA3 立体声分离度不达标,侧向回声改 SYN 程序合成(直接写立体声 AudioBuffer 的 L/R 通道数据,管线能力内);材质尾音全部 SYN 可兜底。
- 频带分离防糊:己方脚步/怪物脚步=低频(100-300Hz),脉冲+回声=中频(800Hz-2kHz),BGM 垫底——探索期并发声流 ≤2(你的动作反馈 + 怪物脚步)恒成立。
- 脉冲冷却 1.2s:防连打;双击重发不受额外惩罚(P1"回声可重听且免费")。冷却期单击/双击 → 仅 error 震(无语音惩罚)。
### 3.2 三套信息自洽(P1 专项,机制级保证)
| 信息 | 声源 | 内容 | 坐标系 |
|---|---|---|---|
| 回声 | 脉冲触发 | 静态地形(墙/门/楼梯/水 + 距离) | 相对(前/左/右) |
| 怪物脚步 | 怪物移动即播(每步,间隔 1.5-2.5s) | 动态威胁(左右 pan + 距离档音量;背后=中央音量减半,转身后恢复=方位反馈) | 相对(左/前/右/后) |
| 播报 | 上滑/事件 | 层数/血量/金币/面朝(绝对)/威胁(相对左右) | 面朝=绝对,其余=相对 |
三者全部由同一 `game._floor.grid` 与怪物位置**单源查询**生成,无独立状态;模板句固定(方位句 4 句、威胁句 4 句)。回声永不报怪物、脚步永不报地形——语义域零重叠。威胁播报只报左右不报背后(同回声降维,规则在教学讲明)。
## 4. 状态机
### 4.1 状态列表
`TITLE / DRILL / EXPLORE / BATTLE / MENU(overlay) / DESCEND / SETTLE`。MENU 是挂起 overlay(记录返回态),其余为正交主态。
### 4.2 转移图
```
start ──> TITLE ──单击(有档)──> EXPLORE(恢复存档+局面复述)
│ │单击(无档) │
│ ├─────> DRILL(仅首次) ──完成/下滑跳过──> EXPLORE(L1)
│ └右滑=重掷新周目 ──────────────┘(已学过教学,直达)
v
EXPLORE <──────────────┐
│ 走进怪格/怪走进格 │ 胜利(原地)
v │
BATTLE ────────────────┘
│ HP≤0
v
SETTLE(死亡) <── EXPLORE(第7层终Boss胜=通关SETTLE)
│单击
v
TITLE(元进度入账后)
EXPLORE ──踏上楼梯格+单击──> DESCEND ──> EXPLORE(floor+1)
EXPLORE/BATTLE ──下滑──> MENU ──返回──> 原状态
```
DESCEND 演出:stairs_down 轰隆 + `evt_descend` + 下一层生成 + `st_floor_N`,约 5s。
### 4.3 每状态手势映射总表
| 手势 | TITLE | DRILL | EXPLORE | BATTLE | MENU | SETTLE |
|---|---|---|---|---|---|---|
| 单击 | 继续/开始 | 步骤作答 | 脉冲 | 挥击 | 循环切项 | 再来一局 |
| 双击 | 重听 | 步骤作答(D6) | 重发脉冲 | 格挡(前摇窗口) | 确认执行 | 重听结算 |
| 左滑 | error | 步骤作答(D5) | 左转 | 转身(绕位怪有效) | 上一项 | error |
| 右滑 | 重掷新周目 | 步骤作答(D5) | 右转 | 转身(绕位怪有效) | 下一项 | error |
| 上滑 | 元进度 | 状态(教学后可用) | 状态链 | 战斗状态链 | 帮助 | 重听元进度段 |
| 下滑 | 菜单 | 跳过教学 | 菜单 | 菜单(暂停) | 关闭 | 菜单 |
| 长按 | error | 步骤作答(D4) | 前进 | 尝试逃跑 | — | error |
| 长按松开 | — | 停 | 停 | — | — | — |
无效手势一律 error 震;full 档语音纠错 `gd_invalid`(onceKey 同局限 1 次,之后仅 error 震)。idle-only:纠错/看门狗语音在播时跳过(引擎 guide 已实现)。
## 5. 教学编排(DRILL,首局内嵌于第 1 层)
每步=一句短旁白→一次实操→反馈;做错重教同一步(**新短句**,不逐字重播)≤2 次后自动演示通过(不卡流程,space 先例)。教学寄生局内:DRILL 就在 L1 地图起点房内进行,D8 的教学怪是 L1 两只怪之一。全程可下滑跳过(`tut_skip`),跳过标志存进度,二周目不再进 DRILL。
| 步 | 旁白 | 等待动作 | 通过条件/反馈 |
|---|---|---|---|
| D0 | intro_1+intro_2+tut_d0 | 单击 | confirm 震 |
| D1 | — | 单击 | pulse+回声组 + `tut_echo_come`。**首输入≤60s 达成点(≈t+15s)** |
| D2 | tut_front_rule | 单击继续 | 纯听规则句 |
| D3 | tut_side_rule | 单击继续 | 纯听规则句 |
| D4 | tut_walk | 按住走 ≥2 格 | 脚步声(撞墙也算过,bump+`那边是墙`由既有 bump 路径播) |
| D5 | tut_turn | 左滑+右滑各 1 | 转身声 |
| D6 | tut_replay | 双击 | echo 重播 + `tut_echo_come` 复用 |
| D7 | tut_status + tut_menu | 上滑(状态链播)→ 下滑开菜单→再关 | 两键各一次 |
| D8 | tut_mob → 走向教学怪触发战斗(盲眼蝠 ATK=0)→ tut_parry →(前摇)双击 | 格挡成/败都继续:成功 `tut_parry_ok`,失败 `tut_parry_miss` | 然后 tut_attack → 单击挥击至击杀 `tut_kill`。**首正反馈≤3min 达成点** |
| D9 | tut_stairs | 脉冲听到铁器回声后自行走到楼梯 | 踏上楼梯格 `tut_stairs_on` → 单击下潜 → EXPLORE |
D9 后不再教学;上滑=状态链随时可查。生成约束(教学局 L1,优先于随机):起点房前方 2 格内保证有墙(D1 回声演示)、起点房门口固定一只盲眼蝠(D8)、房内 1 处金币(拾取体验)。
FTUE 核对:遮罩点按→D0 三句(~8s)→D1 首次脉冲 ≈ t+15s(<60s ✓);首个正反馈=教学战胜利 ≈ t+2min(≤3min ✓);学习期 L1-2 怪 ATK 0/1(死亡不可能 ✓)。
## 6. 地窟生成
### 6.1 seed 用法
```
rng(floor) = mulberry32((seed ^ (floor * 0x9E3779B9)) >>> 0) // 每层独立子种子
```
- `opts.seed` 可注入(playtest 传 42);正式局 `seed = Math.floor(Math.random()*1e9)`(开局掷一次,存入 run)。同 seed → L1..L7 全部可复现。
- 生成步骤(每层):① 尺寸定表(§6.2)→ ② 放 3-6 个矩形房间(2×2~3×4,不重叠)→ ③ L 型走廊按 MST 顺序连通全房间 → ④ 起点房=第 1 房中心,楼梯房=BFS 距起点最远房间中心,楼梯格=其中心 → ⑤ 宝物撒入房间格 → ⑥ 怪物放在 BFS 距起点 ≥3 格的格位(开局安全区)。
- 复现断言口径:同 seed 两次构造 → grid/怪物/宝物/楼梯逐格相等(测试读 `game._floor` 重算不变式,**不写魔法坐标**)。
### 6.2 层规格与难度曲线
| 层 | 尺寸 | 怪物(型×数) | 宝物 | 目标用时 | 累计 |
|---|---|---|---|---|---|
| 1 教学 | 7×7 | 盲眼蝠×2(ATK0) | 金币×1 | 3.0 | 3.0 |
| 2 | 7×7 | 窟鼠×3 | 金币×1+药水×1 | 3.0 | 6.0 |
| 3 窟主 | 9×9 | 蘑菇怪×3 + 窟主石甲蟹 | 金币×1+短刃 | 3.5 | 9.5 |
| 4 | 9×9 | 绕行者×2+洞蛛×2 | 金币×1+皮甲 | 3.5 | 13.0 |
| 5 | 11×11 | 洞蛛×3+绕行者×2 | 金币×1+药水×1 | 3.5 | 16.5 |
| 6 窟主 | 11×11 | 洞蛛×2+绕行者×2 + 窟主腐沼蟒 | 金币×1+阔刃 | 3.5 | 20.0 |
| 7 终局 | 11×11 | 石像卫×3 + 窟中之王 | 金币×1 | 4.0 | 24.0 |
- 窟主守楼梯格:未击败时踏上楼梯格播 `evt_stairs_lock`(楼梯不可用);击败后解锁。终 Boss 击败=通关。
- 敌型见 §7.2 表。每层开场氛围:`st_floor_N` + 层 BGM 不变(探索曲统一),深度感靠 `evt_descend` 与怪物构成。
## 7. 遭遇与战斗
### 7.1 遭遇
- 全**明雷**:怪物脚步声持续可闻(rpg 暗雷教训——纯音频里突袭没有信息缓冲)。你走进怪格或怪走进你格 → `btl_start_<mob>` + battle earcon + BGM 切换。
- 遭遇瞬间自动"面对"怪物(播报名即=它在面前)。怪移动的脚步:每步播 `mob_step_<class>_<l|c|r>`,pan 按相对方位(左/前/右),**背后用中央资产音量减半**;距离档音量 ≤2 格 0.9 / 3-4 格 0.6 / 5-8 格 0.35 / 更远不播。距离 ≤2 格加 heartbeat 震(威胁前置预警窗,方法论 L2)。
### 7.2 回合时序(实现契约)
```
进 BATTLE → 玩家回合(guide.turn) → 单击挥击/长按逃跑/转身 → 结算
→ 若怪活:怪行动(绕位怪先移动:脚步报新方位;缠斗怪原地)→ 攻击前摇 1.2s(tel_* 音)
→ 前摇内双击=格挡判定(面对攻击源:×0.2 / 未面对:×1.0)→ 受击/格挡音+震
→ 玩家 HP≤25% 且腰带有药 → 自动喝药(potion 音+播报)
→ 回到玩家回合。HP≤0 → 死亡归因链 → SETTLE
```
- 缠斗怪(蝠/鼠/菇/蛛):不绕位,恒正面,左右滑无效(`btl_no_turn` 纠错,每场 1 次)。
- 绕位怪(绕行者/石像卫):每 2 回合移动到 左/右/背后 之一(脚步 pan 报位),攻击前摇随其方位(tel 派生 pan 变体);`btl_behind` 在背后挨刀时播(归因真话)。
- Boss(石甲蟹/腐沼蟒/窟中之王):绕位+高血,半血播 `btl_half_*` + ATK+1(王二阶段)。Boss 战长按逃跑拒绝(`btl_flee_boss`)。
- 格挡判定与 rpg 一致绑双击(规避 tap 300ms 延迟);窗口外双击=error 震不生效。
### 7.3 逃跑
长按 = 尝试逃跑:普通怪/绕位怪**必成**(无 RNG,P4 友好),撤退挨一下(掉血=怪 ATK,真实账目),落回探索态、怪留在原格;拉开距离后脚步声自然远去。Boss 拒绝。逃跑无次数限制(容错拉满,代价=掉血+丢进攻回合)。
### 7.4 掉落与元进度
- 战斗胜利:金币入账 + `btl_loot_1..3`(按单场所得 3 档)+ 概率掉药水/装备(§6.2 宝物表为拾取点,怪掉落仅金币)。
- 每击杀 4 只 atkLv+1,播 `btl_lvup`("这层我变强了"的即时成长,P2 钩子)。
- **元进度(遗产)**:金币永续累计(meta.gold),死亡/通关结算入账,不清档。5 项解锁(下一局开场自动生效+播报):
| 遗产 | 价格(金币) | 效果 |
|---|---|---|
| unlock_potion 行囊药 | 90 | 开局自带 1 瓶腰带药 |
| unlock_vigor 体魄 | 150 | 起始 maxHp 20→26 |
| unlock_veil 护符 | 220 | 每层首次受击减半 |
| unlock_echo 回声强化 | 300 | 侧向回声加报距离档(墙闷/开口各 2 档→按远近换 2 组资产音量) |
| unlock_blade 武器 | 450 | 开局自带短刃(atk+1) |
- 经济核算:一局 40-120 金(怪 3-15/只 + 堆 10-30 + 窟主 50-80)→ 约 2-5 局解锁一项,死亡有可见成长(P2「下周目你会更强」)。
## 8. 数值表
### 8.1 玩家与敌人
```
maxHp = 20 (+vigor 6);atk = 2 + atkLv(0..4, 每4杀+1) + 武器(0..2)
dmg = max(0, atk - 敌def) ;敌打你 = max(0, 敌atk - 甲) × 格挡系数(0.2/1.0)
格挡钳制:擦伤 min 0;L1-2 怪 ATK 0-1 → 学期零惩罚
```
| 敌型 | 层 | HP | ATK | 行为 | 脚步音色类 | 掉金币 |
|---|---|---|---|---|---|---|
| 盲眼蝠 | 1 | 4 | 0 | 缠斗 | small | 3 |
| 窟鼠 | 2 | 6 | 1 | 缠斗 | small | 5 |
| 蘑菇怪 | 3 | 8 | 2 | 缠斗 | bug | 8 |
| 窟主·石甲蟹 | 3 | 20 | 3 | Boss 绕位 | heavy | 50 |
| 绕行者 | 4 | 10 | 2 | 绕位 | bug | 10 |
| 洞蛛 | 5 | 12 | 3 | 缠斗 | bug | 12 |
| 窟主·腐沼蟒 | 6 | 26 | 4 | Boss 绕位 | heavy | 80 |
| 石像卫 | 7 | 16 | 4 | 绕位 | heavy | 15 |
| 窟中之王 | 7 | 36 | 5→6 | Boss 绕位 | heavy | 通关 |
装备(拾取即自动装备,仅播报不管理):短刃 atk+1(L3 起)、阔刃 atk+2(L6 起)、皮甲 敌伤-1(L4 起)。药水:腰带限 1 瓶,HP≤25% 自动喝(回 40% maxHp,向上取整)。
平衡校验:L1 atk2 打蝠 2 刀;L3 atk3+短刃=4 打蟹 5 刀、蟹打裸装 3/下 vs 20HP+药 → 不格挡危险、会格挡稳过;L7 atk2+3+2=7 打王 6 刀、王打 5-6/下 vs 26HP → 必须格挡+药,Boss 战有真实技巧收益。
## 9. 音频三层
### 9.1 BGM(4 首,Mureka,-20 LUFS,ogg+m4a,无缝循环)
| id | 状态映射 | Mureka prompt(英文) |
|---|---|---|
| bgm/menu | TITLE / MENU / SETTLE(结算 volume 0.5) | "Dark cavern ambient pad, deep sub drone with distant water drips, calm and mysterious, no drums, no vocals, seamless loop" |
| bgm/explore | DRILL / EXPLORE / DESCEND | "Dark cave exploration ambient, low ominous drone, sparse echoing sonar-like pings, slow dripping rhythm, tense but calm, no vocals, seamless loop" |
| bgm/battle | BATTLE 普通怪 | "Tense dungeon battle music, pounding tribal drums 95 bpm, dissonant low stabs, claustrophobic urgency, no vocals, seamless loop" |
| bgm/boss | BATTLE 窟主/终 Boss | "Epic dark cavern boss theme, heavy drums 115 bpm, low brass swells, rising dread, no vocals, seamless loop" |
切换全部走引擎 crossfade;语音 duck 20% 引擎自动。danger 不切 BGM(guide danger earcon + heartbeat 震 + st_hp_4 承担)。
### 9.2 SFX(~34 个;SA3=Stable Audio 3,SYN=程序合成;全部双格式、真峰值 ≤-3dBFS)
| 类别 | id | 方式 | prompt 草稿 / 说明 |
|---|---|---|---|
| 脉冲 | `pulse` | SYN | 180→60Hz 正弦扫频+噪声瞬态,0.15s,中频突出 |
| 正前回声 | `echo_1..4` | SA3 | "close stone wall echo slap-back, bright, short, dry"(2:"mid distance cave echo, moderate tail" 3:"far cave echo, darker longer" 4:"very far cavern echo, long dark tail, huge space") |
| 材质尾音 | `mat_stone` `mat_door` `mat_stair` `mat_water` | SYN | 闷石敲/空心木咚/金属当啷/水滴湿滑,各 0.2-0.25s |
| 侧向回声 | `side_wall_l/r` `side_open_l/r` | SYN | 立体声文件:能量只在 L 或 R 声道;闷哒 vs 空旷回灵(SA3 不达标时程序写 L/R 通道兜底) |
| 移动 | `step_own` `step_turn` `bump` | SYN | 单步/拖步转身/撞墙闷响 |
| 拾取 | `coin` `potion` `gear` | SYN | 金币叮当/喝药咕咚/换装金属轻响 |
| 转层 | `stairs_down` | SA3 | "descending old metal stairs, deep cavern reverb" |
| 怪物脚步 | `mob_step_small_l/c/r` `mob_step_bug_l/c/r` `mob_step_heavy_l/c/r` | SA3 ×3 + ffmpeg pan 派生 ×6 | "small beast skittering steps, dry";"insect chitin crawling clicks";"heavy armored thudding steps"——各生成 1 条单声道,派生左/中/右三份 |
| 攻击前摇 | `tel_small` `tel_bug` `tel_heavy`(+派生 l/r) | SA3 | 小怪扑击嘶声/虫肢节摩擦/重怪蓄力重踏低吼 |
| 战斗 | `atk_swing` `atk_hit` `parry` `hurt` `enemy_die` | SYN/SA3 | 挥拳风声(SYN)/击中甲壳钝响/格挡亮叮(SYN)/受击闷击(SYN)/倒地(SA3) |
| 升档 | `levelup` | SYN | 上行钟声琶音(内容音,允许) |
monsters 脚步 pan 派生工艺:`ffmpeg -i in.ogg -af "stereotools=mlev=0.7:sbal=-.8" out_l.ogg` 等三向派生,省 SA3 任务数(SA3 实际 ~14 个)。
### 9.3 voice
125 句 ×2 性别(Serena / Uncle_Fu),无 NPC(怪物不说话,遭遇靠 `btl_start_*` 报名)。分组、台词、触发全表见 [16-dungeon-voice-ids.md](16-dungeon-voice-ids.md)。数字全分档整句(层 7 句、血量 4、金币 4、面朝 4、威胁 4、战利 3、死因 3、元进度 3),无运行时拼接。
## 10. 向导层接入(engine/guide.js,本作不新增 earcon 词表)
### 10.1 状态签名(进入状态时播)
| 状态 | guide.state() | 时点 |
|---|---|---|
| TITLE / MENU | `menu` | 进标题、开菜单 |
| DRILL / EXPLORE / DESCEND | `explore` | 进教学、进探索、下楼完成 |
| BATTLE | `battle` | 遭遇即播(与 btl_start_* 同点) |
| SETTLE | `settle` | 死亡/通关结算进入 |
| danger | `danger` | 血量首次 ≤25%(每层 1 次)+ Boss 半血 |
choice 签名本作不使用(无抉择态;菜单是 menu)。菜单回绕不播 wrap 句(菜单 ≤4 项,循环成本低,省台词预算)。
### 10.2 turn cue 与看门狗
| 等待点 | arm | at | say(full 档;L2 级一律纯 nudge,绝不逐字重播) |
|---|---|---|---|
| BATTLE 玩家回合(每回合) | guide.turn | — | turn earcon + confirm 震 |
| BATTLE 玩家回合无操作 | watchdog | [15s, 30s] | `gd_turn` / null |
| EXPLORE 静止 | watchdog | [10s, 25s] | `gd_pulse` / null |
| DRILL 每步等待 | watchdog | [8s] | 无语音,纯 nudge(教学句不重播) |
### 10.3 卡关三级提示(EXPLORE,escape 先例;"无操作时长"计时,任何有效手势重置)
进展事件=拾取/战斗胜利/踏上楼梯格/击败窟主/下潜。无进展 60/120/180s →:
| 级 | 内容 | 附带 |
|---|---|---|
| L1 暗示 | 楼梯未找到 → `stair_hint_a`;窟主未清 → `stair_hint_b` | warning 震 |
| L2 明示 | `stair_dir_e/s/w/n`(绝对方位;与上滑面朝句同坐标系,自洽) | warning 震 |
| L3 带路 | 进入导航模式:每步播 `nav_e/s/w/n`,抵达播 `nav_done`;此后每 60s 重复直至进展 | warning 震 |
提示到点遇语音在播 → 入待播队列(escape round2 礼仪),玩家操作即作废未播级别。纠错句 idle-only + 过期丢弃(引擎已实现)。
## 11. 存档设计
```json
{
"schema": 1, "game": "dungeon", "updatedAt": 0,
"summary": "第3层 · 伤势不轻 · 金币268 · 遗产2/5",
"meta": { "gold": 268, "unlocks": ["potion", "vigor"], "bestFloor": 5, "runs": 7, "kills": 23 },
"run": {
"seed": 42, "floor": 3, "pos": [5, 2], "face": "E",
"hp": 11, "atkLv": 2, "weapon": 1, "armor": 0, "potion": 1,
"veilUsedFloor": false, "killsRun": 9, "looted": [], "cleared": [], "parries": 4
}
}
```
- **summary 生成规则**:`第{floor}层 · {血量档名} · 金币{meta.gold} · 遗产{unlocks.length}/5`;死亡后 run=null,summary=`深处归来 · 金币{gold} · 遗产{n}/5`。
- 保存时机:拾取/战斗结算/下潜/菜单"保存并回大厅"/死亡结算。localStorage + PUT 服务器(00 文档 §4)。
- 读取:有 run → TITLE 播 `sys_continue_ask`,单击继续 → 恢复 `run` + 重放该层 seed 生成 + **局面复述链**(st_floor+st_hp+st_face+st_threat,≤5s 可操作);右滑 = 重掷新周目(run 清空重掷 seed,meta 保留)。无 run 单击 = 新周目(meta.unlocks 生效播报后进 DRILL/EXPLORE)。
- 归因真话依据:`parries`(全程格挡数)、lastHit 记录(击杀者类型/是否背刺/是否未格挡)在死亡结算选 `die_cause_*` 与 `die_adv_*`,句必须与真实行为对应。
## 12. playtest 断言清单(登记 13 号文档新增 §dungeon;mock 机制对齐 web/rpg/test/playtest.mjs)
**关键路径**(seed=42,首周目 L1→L3 窟主,≤260 手势):
1. 启动 → TITLE(无存档)→ 单击 → DRILL:D0..D9 逐步断言 voice 序(tut_d0→…→tut_done)与 drillStep 递进;D1 断言 pulse 与 echo 播出;D4 长按断言 pos 前进 ≥2;D8 乱按/不格挡断言 HP 不降(教学怪 ATK=0);下滑跳过教学路径断言(二周目不再进 DRILL)。
2. **seed 复现**:`opts.seed=42` 两次构造 → `_floor.grid`、楼梯/怪物/宝物坐标逐格相等;L1 尺寸 ===7;起点安全区(怪 BFS 距离 ≥3)。
3. **脉冲回声信息正确**:起点面朝 'N',按 grid 计算正前墙距 → 断言 echo_N 于脉冲后对应延迟±50ms 播出、volume 档正确、材质尾音 id 正确;左右侧向 side_* id 与 grid 一致;遍历 seed=42 的 L1 全网格 ×3 面向抽查 ≥30 组(回声编码 === grid 真值);**双击重发**断言资源零消耗且回声 id 与上次一致(未移动时)。
4. 拾取:走到金币格 → coin 音 + meta.gold 增加且 st_gold 档位相应跳档;药水满腰带 → evt_potion_full 且药留在原地。
5. 战斗路径(L2 窟鼠):进入 BATTLE → 回合流转(玩家回合 turn→敌前摇→结算循环);前摇内双击 → 敌伤 ×0.2(HP 差值断言);窗口外双击 → error 震、全额伤;长按逃跑 → 必成且 HP 真实减 ATK、怪留原格;击杀 → loot 档句 + meta.gold 入账。
6. 下潜:踏上楼梯格 → sys 提示句 → 单击 → DESCEND → floor=2 且 grid 由 floorSeed 重生成(再断言复现)。
7. **死亡与元进度**:L3 窟主战挂机至死 → 归因链序 `die_intro → die_cause_*(与 lastHit 真实来源一致,Boss 重击 → die_cause_2)→ die_adv_*(按 parries=0 → die_adv_1)→ st_floor_3`;断言 meta.gold = 死亡前+本局所得、`bestFloor` 更新;单击 → TITLE → 新周目 seed≠42、meta.unlocks 按金币阈值播 unlock_* 且起始 maxHp 生效(vigor)。
8. 菜单/帮助:探索下滑 → 模态菜单(单击循环播项名、双击确认、上滑=help、下滑=关闭);"保存并回大厅" → PUT 断言 data.summary 命中生成规则。
9. **中断复述**:存档重进 → sys_continue_ask → 单击继续 → 落在存档点 → 自动局面复述链(st_floor→st_hp→st_face→st_threat 顺序断言);上滑同链可重放。
**边界断言**:
- 学习期零惩罚:L1 全怪 ATK===0、L2 ≤1(读表断言);L1 全程乱按 HP 不变。
- 撞墙:长按顶墙 → bump + pos 不变 + HP 不变。
- 方位自洽:任意格转身后重脉冲 → 左右 side_* 与 grid 随面朝交换;身后方向从不产生回声资产。
- 怪物脚步:怪每步播 mob_step_<class>_<pan>,pan/音量档与相对方位、距离一致(含背后=中央减半口径)。
- 绕位怪背刺:未转身挨打 → 全额伤 + `btl_behind`;转身后格挡 → ×0.2。
- 缠斗怪左右滑 → `btl_no_turn`(每场 1 次)+ error 震。
- 自动喝药:HP≤25% 且有药 → HP 回升档 + potion 音;无药时不播不炸。
- 卡关三级:无操作快进 60/120/180s → stair_hint_* → stair_dir_* → nav_* 链;任意进展事件后重置分级;提示在语音在播时排队不顶句。
- 看门狗:探索 10s/25s、战斗回合 15s/30s 逐级触发且第 2 级纯 nudge 无语音。
- 脉冲冷却 1.2s:冷却期单击/双击 → error 震、无 echo。
- 屏蔽任意 voice 资产:流程不卡死(看门狗兜底推进)。
- guide=off:全静、零定时器(全局既定契约)。
- 资产齐套:voice 双性别目录 id 集合一致、sfx/bgm 双格式(assetExists 口径)。
**失败即不通过**:死亡归因句与真实死因不符(归因假话);同 seed 两次生成不一致;教学怪造成 HP 损失;双击重发扣资源;回声编码与 grid 矛盾。
## 13. 节奏曲线与单局核算
- 每层节奏:进层签名句(5s) → 探索(脉冲-移动循环,2-3 分钟)→ 1-3 场战斗(每场 40-60s,窟主 90s)→ 拾取 1-3 处 → 楼梯。每 30-60s 一个奖励点(金币/击杀升档/装备),满足方法论 L5。
- 满通 24.0 分钟(§6.2 累计列);普通玩家死于 L4-6 → 单局约 14-19 分钟;二周目起(遗产加持)更深。首局若速死(L2-3,~8 分钟)由元进度钩子拉重开——roguelike 死亡结算 ≤15s 且必带「下周目更强」,重开入口单击 ≤2s(A4/L5)。
- 无任何倒计时(P4);高手速度感=长按连走+免听完整回声直走(P2),脉冲冷却不限制移动。
## 14. 自检清单
| # | 硬约束 | 自检 |
|---|---|---|
| 1 | FTUE 首输入 <60s | D1 首次脉冲 ≈t+15s ✓(§5) |
| 2 | 首个正反馈 ≤3min | D8 教学战胜利 ≈t+2min;L1 首金币 ≈t+2.5min ✓ |
| 3 | 学习期零惩罚 | L1 怪 ATK=0、L2 ≤1,学期内死亡不可能;撞墙无伤;格挡失败无额外惩罚 ✓ |
| 4 | 失败三问归因 + 真话律 | 死亡链 die_cause_*/die_adv_* 按 lastHit 与 parries 真实选择;挥空/背刺/逃跑挨打均有真话句 ✓(§12 失败即不通过项锁定) |
| 5 | 手势宪法三件套不豁免 | 上滑=状态、下滑=菜单、双击=重听(重发脉冲) 全状态保留 ✓;移动/脉冲偏离已登记 E1-E4 且教学讲清 ✓ |
| 6 | 向导层 | 签名 5 态(不新增 earcon);turn cue 每回合;看门狗 [10/25][15/30]s 第 2 级纯 nudge;卡关 60/120/180 三级不报答案(带路只给方向);纠错 idle-only ✓(§10) |
| 7 | 方法论反模式 | 提示永不逐字重播(L2 级纯音频);成功后只给新短句肯定;单次播报 ≤3 选项(菜单逐项);并发声流 ≤2(频带分离+冷却);语义靠语音、earcon 用引擎统一词表 ✓ |
| 8 | 方位降维 ≤4 | 回声前/左/右(身后不报);战斗面前/左/右/背后;无连续角度 ✓ |
| 9 | 台词规范 | 一句一文件整句;全部 ≤25 字(<40 上限);数字全分档整句;"重听"写"重新听"、"重发"写"再发一次";同游戏同 instruct 常量;无语速词 ✓(voice-ids 注记) |
| 10 | P1 视障评委 | 双击免费重发脉冲;status key 全状态可用;方位/威胁模板句固定;归因真话;可打断/长按快进/2.2 档语速引擎既有;绝不当白痴(不简化深度:绕位/Boss/元进度保留)✓ |
| 11 | P3 通勤 | 单手免视全手势可达;上滑一句话复述局面(层/血量/面朝/威胁);任意时刻菜单存档退出;死亡不清 meta ✓ |
| 12 | P4 工人 | 教学逐字跟练+失败 2 次自动过;容错高(ATK0 怪/自动喝药/必成逃跑);无倒计时;零新手势(复用 swipe/tap/longpress)✓ |
| 13 | P2 大学生 | 元进度结算「下周目你会更强」;击杀升档+装备即时成长;长按连走速度感;死亡重开入口 ≤2s ✓ |
| 14 | seed 可复现 | floorSeed 派生公式 + opts.seed 注入 + 复现断言(§6.1/§12.2),与 playtest 种子 PRNG/强制队列/虚拟时钟机制兼容 ✓ |
| 15 | 三套信息自洽 | 单源 grid 查询;回声=静态地形/脚步=动态怪物/播报=绝对面朝,模板句固定,语义域零重叠 ✓(§3.2) |
| 16 | 引擎能力边界 | 无实时 pan(预 pan 资产);无引擎外节点(setTimeout+volume 调度);tap 300ms 延迟规避(格挡绑双击);30MB 预算内 ✓ |
---
## 实现核对(测试 agent,2026-09-02)
> 测试载体:`web/dungeon/test/playtest.mjs`(mock 层+虚拟时钟照 `web/rpg/test/playtest.mjs`,
> 1277 断言全绿,`tools/node/bin/node web/dungeon/test/playtest.mjs` 退出码 0);
> 会话剧本 `web/harness/sessions/dungeon.mjs`(`session` 主线弧线 + `sessionStress` 乱按压力,
> 经 `web/harness/transcribe.mjs dungeon [--session sessionStress]` 跑通,全程零资产缺失、零死锁)。
### §12 关键路径
| # | 断言 | 结果 |
|---|---|---|
| 1 | 启动→TITLE→DRILL:D0..D9 逐步 voice 序(intro_1/2+tut_d0→…→tut_done)与 drillStep 递进;D1 pulse+echo;D4 长按 pos ≥2;D8 乱按 HP 不降;下滑跳过+二周目不再进 DRILL | ✅(场景 2;D2 错手势重教、D3 连错 2 次自动通过、D8 乱按 80 手势 HP=20 一并覆盖) |
| 2 | seed=42 两次构造 grid/怪/宝/楼梯逐格相等;L1 尺寸 7;起点安全区 BFS≥3(教学局非门怪 ≥4) | ✅(场景 1:7 层全查 + 实流 `_floor` 与纯函数一致) |
| 3 | 回声编码 === grid 真值:全地板格 ×4 面向扫描(≥30 组),echo_N 延迟/音量、材质尾音 +120ms、侧向 +350ms 预 pan 资产 L/R;双击重发零消耗且编码一致 | ✅(场景 2:104 组合全扫,代表组精确到 0ms;身后不报=恰两路侧向) |
| 4 | 拾取:coin 音+meta.gold+金币档跳档(45+堆≥55→st_gold_2);药水满腰带 evt_potion_full 且药留原地 | ✅(场景 4) |
| 5 | 战斗:单击命中、缠斗 btl_no_turn 每场 1 次、前摇内双击 ×0.2(钩子 atk=10 → 伤 2)、窗口外/不格挡全额、长按逃跑必成挨一下怪留原格、击杀 loot 档+meta.gold | ✅(场景 5) |
| 6 | 下潜:踏上楼梯 sys 提示→单击→DESCEND→floor=2 且 grid 重生成复现 | ✅(场景 2) |
| 7 | 死亡与元进度:归因链序+真话、meta.gold 入账、bestFloor/runs、遗产购置播报、新周目 seed 重掷+继承(vigor 26/potion 1) | ✅(场景 6a/6b/6c 三种死因矩阵) |
| 8 | 菜单:单击循环/双击确认/上滑帮助/下滑关闭;「保存并回大厅」→ sys_saved → `'../'`;summary 生成规则 | ✅(场景 7 + 场景 2 D7) |
| 9 | 中断复述:sys_continue_ask→单击→st_floor→st_hp→st_face→st_threat 有序;上滑同链可重放 | ✅(场景 8) |
### §12 边界断言
| 断言 | 结果 |
|---|---|
| 学习期零惩罚:L1 全怪 ATK=0、L2 ≤1(读 `_battle.atk`);L1 乱按 HP 不变 | ✅ |
| 撞墙:bump + pos 不变 + HP 不变 | ✅ |
| 方位自洽:转身后重脉冲左右 side 随面朝交换(含全扫描 4 面向) | ✅ |
| 怪物脚步:pan/音量档(0.9/0.6/0.35)、背后中央减半(1 格 0.45、6 格 0.175)、>8 格不播、≤2 格 heartbeat | ✅ |
| 绕位怪:每 2 回合换位(右→后→左确定性轮换)、脚步报位、背刺 btl_behind+全额、转身追击命中、转身后格挡 ×0.2 | ✅ |
| 缠斗怪左右滑 → btl_no_turn(每场 1 次)+ error 震 | ✅ |
| 自动喝药:HP≤25% 有药 → 回 40% maxHp(ceil)+ potion 音+档播报;Boss 半血 ATK+1 + btl_half_*;Boss 逃跑拒绝 btl_flee_boss | ✅ |
| 卡关三级 60/120/180s:stair_hint_a→stair_dir_*→nav_* + 导航每步播方向 + nav_done;进展重置分级(重置后从 L1 重新触发);语音在播入队不顶句、操作作废、未播级别可重新触发 | ✅(场景 9) |
| 看门狗:探索 10s gd_pulse、战斗 15s gd_turn(guide=full);DRILL 8s 纯 nudge 无语音 | ✅(场景 10) |
| 脉冲冷却 1.2s:冷却期单击仅 error 震、无第二次回声 | ✅ |
| 屏蔽任意 voice:遭遇报名/下潜旁白被屏蔽 → guard 兜底推进不卡死 | ✅(场景 11) |
| guide=off:30s 空闲零播放、零定时器 | ✅ |
| 资产齐套:voice 双性别 125 句集合一致、sfx/bgm 双格式、game.js 静态引用展开全存在、运行期零缺失 | ✅ |
| E3:对局期 setRateBoostEnabled(false)(DRILL/EXPLORE/BATTLE/DESCEND)、TITLE/MENU/SETTLE 恢复 true | ✅(rateLog 断言) |
| E4:菜单=暂停——探索游走冻结、战斗前摇剩余窗口冻结、恢复恰结算一次(无重复回合) | ✅(场景 7;前摇内下滑原本不可达,见 bug ⑧) |
**§12 失败即不通过项**:死亡归因真话(cause/adv 与 `_deathInfo.lastHit` 一致,三种死因矩阵验证)、同 seed 两次生成一致、教学怪零伤害、双击重发零消耗、回声编码与 grid 一致——全部锁定 ✅。
### 与设计文档的偏差(记录不大改)
1. **归因句粒度**:台词表死因仅 3 档(小怪拖垮/Boss 重击/背刺),无「指认具体怪 A」整句资产;真话律落地为 `_deathInfo.lastHit.type` 与 cause 档位一致(playtest 断言 `lastHit.type==='crab'/'rat'/behind` ↔ die_cause_2/1/3)。资产表内不存在更细句子,不算偏差。
2. **战斗上滑第 2 段**:§2.2「敌方血量档」以 `btl_half_*`(半血阶段句)近似——台词表无 foe_hp_* 分档资产。
3. **§5 D4「撞墙也算过」**:实现额外把 D4 自动面向东侧 2 格走道+边界,保证 ≥2 格可达成(生成约束注释已写明)。
4. **L1 安全区**:非门怪 BFS≥4(严于规格 ≥3),避免 D4/D8 演练路径撞上第二只怪,注释已写明。
5. **die_adv 兜底**:挡过且跑过(parries>0 && flees>0)死于非背刺 → die_adv_1(建议句非过去事实句,规格未覆盖该象限,取最无风险句)。
6. **格挡成功播 `btl_parried`**:规格表 B 组 trigger=「格挡成功(减伤 80%)」,原实现该资产死引用,已接线(见 bug ②)。
### 修过的 bug(web/dungeon/game.js)
1. **归因真话律**:`run.parries++` 原在双击瞬间计(背对双击也计),歪曲 die_adv 选句依据——改为仅结算判成「面对且格挡成功」时计数。
2. **`voice/btl_parried` 死资产**:格挡成功(减伤 80%)从不播报——接线进 `_enemyResolve` 反馈链。
3. **卡关提示「未播级别计入 shown」**:入待播队列即标已提示,被玩家操作作废后该级永不重触发(违反 §10.3/escape round2 礼仪)——改为真正播出才计入,另加待播去重。
4. **终局层 L3 带路死路**:`_navRouteVoices`/`_navSpeakStep` 只查 `stairs`,L7 无楼梯致导航哑火——回退 `stairs || throne`(与 L2 明示同口径)。
5. **SETTLE 双击=重听结算链**:原只重播 `settle_again` 尾句——存 `_settleChain` 整链重放(§2.3)。
6. **DRILL D1 无看门狗**:`_drillEnter(1)` 无分支——补 `_drillWatchdog(1)`(§10.2 教学每步 [8s])。
7. **DRILL D8 上滑=做错**:§4.3 状态键「教学后可用」(D7 已教)——D8 补 up→状态链。
8. **E4 豁免半实现**:战斗前摇(telegraph)内下滑走 error 分支、菜单打不开,`_menuPause` 冻结机制不可达——前摇内补 `down→_openMenu()`,战斗任意时刻可暂停。
### 遗留 TODO
- 教学期 L1 游走静默:DRILL 内非教学怪不游走(`_startMobs` 仅 EXPLORE 调用),设计未明确,维持现状(D9 后下潜即恢复)。
- 木门/水材质(mat_door/mat_water):生成器不产出,资产与编码路径已备(`_echoScan` 注释),待未来层型。
- `meta.runs` 不统计 TITLE 右滑放弃的周目(仅死亡/通关计入);规格未明确。
- 会话剧本主线弧在随机图极小概率把窟主打死(玩家成长超预期时)——剧本有「没死就继续走」兜底分支,不影响零死锁承诺。
### R2 修复回归(2026-09-02,D0-1..D0-3 + D1-1..D1-6 结构修复轮)
> 证据源:research/new8/dungeon/round1/(synthesis + live-player + p1-blind)。本轮只修结构不加新内容;
> 引擎侧按产品裁定允许改动(web/engine/audio.js、gesture.js)。回归:8 套 playtest + guide.test 全绿
> (dungeon 1277→**1610** 断言;escape 140 / radio 221 / rhythm 141 / rpg 188 / simon 132 / space 155 /
> storyline 82 / guide 122,0 失败,radio 在并行修复中仍全绿)。
#### D0-1 语音抢占死循环(P0)
- **根因(引擎级竞态,非 DRILL 泄漏本身)**:`WebAudioEngine._playVoice` 的「顶掉当前对白」检查在
`await this._load(id)` **之前**,而 `this._voice = token` 在其后无条件赋值——同一 tick 双调度语音
(教学 D1:反馈句 `tut_echo_come` 裸 `_speak` + `_drillPass` 立即 `_speakGuarded` 下一句指令)在加载
竞速后,输家的 token 被静默顶号:**ScriptProcessorNode 永远无人 disconnect(僵尸节点空转抽 buffer)**,
其 endTimer 亦因 `_finishVoice` 的 token 校验提前 return 而永不清理。R1 live 证据完全吻合:t=130.2s
D1 首脉冲后 tut_echo_come 僵尸节点持续抽读,探针按「voice buffer 被抽读」记段 → 71 分钟 35,745 次
「播放」99.7% 被「掐死」、nav_n↔tut_echo_come 互掐 33,297 次;跳教学二周目 0 抢占(无 D1 双调度)。
- **修**:① `audio.js _playVoice` 在 await 之后、赋值之前补杀当前 token(顶掉语义在加载完成后复核,
后到者胜、败者必被杀);② 游戏 D1/D6 反馈句并入 `_speakGuarded` 推进链(消灭同 tick 双 play 的源头),
`_drillDone`/`_drillSkip` 收尾显式 `guide.disarm()`;③ **全线风暴保险丝**(`audio.js play()` voice 入口):
同一 voiceId 3s 窗口内被调度 ≥5 次 → 丢弃后续并 console.error(每 id 一次)。
- **断言**:`保险丝:同 id 3s 窗口内前 4 次调度放行 / 第 5 次起丢弃 / 其他 id 不受累 / 窗口滑出恢复 /
只作用于 voice 层`(真实 WebAudioEngine 单元断言,场景 12A);`D1: 反馈句与下一步指令句串行`(场景 2);
`纯探索 60s 零操作语音调度有上界(≤3 句;R1 风暴同期 >2000 句)`(场景 12C)。
#### D0-2 回声信息真实性(P0)
- **根因**:① 侧向 `_sideScan` 把「整条侧射线到边界全通」才判开口——房间/走廊地型恒为假 → side_open
两局 40+ 脉冲 0 次、能走的方向报墙;② 站在楼梯格上脉冲走射线扫描(楼梯在脚下不在面前)→ mat_stair
0 兑现,教学「铁器=楼梯」成谎言(P1 专项:165 脉冲 0 铁器,含站在楼梯上 t+142.2)。
- **修**:回声与 grid 单源重接——侧向第一格地板=开口(§3.1「墙=闷/开口=空旷」2 档口径,墙保留射线距离);
`_echoScan` 站楼梯格 → 贴身(dist1)铁器回声。
- **断言**:场景 2 D9 全扫描(全地板格 × 4 面向真脉冲)truthScan 同步更新;场景 12B **全网格一致性**:
seed=42 L1..L7 每格 × 4 面向 `_echoScan`/`_sideScan` 与测试侧独立真值逐格相等(≥200 格)+
`全网格存在侧向开口(R1 全程 0 次 → 现扫到 N 格次)` + 各层站楼梯=贴身铁器。转写复核:主线局
mat_stair/side_open 均稳定出现。
#### D0-3 探索内容真空(P0)
- **① 密度下限**:`_fillMainPathDensity`——start→楼梯(终局=王座)最短路(BFS 距离梯度回溯,确定性)
每 ≤12 格必有内容遭遇,超限段中点补本层已有普通怪型(不加新怪种,≤8 只,安全区口径不变)。
断言:场景 1 各层 `主路径内容密度(最长真空 ≤12 格)` + 构成 ⊇ 规格表。
- **② 探索呼吸层**:新 SYN 资产 `sfx/amb_cave`(gen_sfx.py:45-320Hz 带通空气声 + 4 整周期/10s 呼吸
LFO + 两粒远处水滴;10s 无缝循环源:去直流、环回交叉淡化、**不首尾淡化**,解码回读 ogg -4.20dBFS /
m4a -4.00dBFS、|直流|≤0.001、循环接缝 -25dB(rel peak);20 文件 ×2 格式全过 QC)。接线:EXPLORE 静默
≥20s(无手势无人声)播 amb_cave(volume 0.35,20s 节奏不连播)。guide=off 契约不变(环境垫属内容层,
场景 4 的零播放断言已按此口径更新并加 amb 专项断言)。
- **③ 下潜演出**:游戏代码本身完好(playtest 场景 2 一直断言 stairs_down→evt_descend→st_floor_N 通过);
R1 转写零演出为**剧本伪影**:(a) navigate 按 1 格/按建模,但 harness `waitVirtual(120)` 在虚拟时钟会
越过 +500ms 步进计时器(advance 至少推进到下一到期定时器)致每按多走 1+ 格、永远踩不准楼梯格;
(b) D9 用 `pulse:false` 违反「玩家靠耳朵」红线且无到站重试。修:navigate 立即松开(恰 1 格)+ `until:
'stairs'` 到站口径 + 逐层验证的下潜循环(tapDescend 只在真踩楼梯时出手,DRILL D9 同样受理)。
复核:主线转写两次运行弧线完全一致(两次下潜全套演出:stairs_down→evt_descend→st_floor_N)。
- **④ 卡关计时器**:计时与分级 R1 live 已证完好(64s/120s/180s 准时);转写 350s 未触发系剧本持续手势
重置 lastAct(§10.3 口径「任何有效手势重置」)+ 上句的导航伪影。断言:场景 12C `卡关 L1 暗示 60s 自然
触发(纯探索、无 hook)`。
#### D1 级修复
- **D1-1 菜单关闭无回执**:live 复核实为「关了但零反馈,下次下滑又重开」——路由本身无恙。修:`_closeMenu`
补状态签名 earcon(回 EXPLORE/DRILL=explore、BATTLE=battle、SETTLE=settle、TITLE=menu)。断言:场景 10
`菜单关闭播回程状态签名(menu → explore)`。
- **D1-2 D9 看门狗兜底**:`_drillWatchdog9()`(8s nudge、30s 升级播 stair_hint_a——自寻路步必须有救援);
D9 每次手势后重挂(onGesture 的 disarm 会清掉它,而 D9 的脉冲/转身不推进步骤)。断言:场景 10 两级触发
+ 升级句。
- **D1-3 导航指墙 + 蘑菇软封锁**:① 带路改为**最短路第一步方向**(`_navFirstStepDir`:从目标反向 BFS 取
梯度下降邻格),逐步校正;同向 ≥1.5s 才提醒(改向必播)——既不指墙也不撞全线保险丝(导航句同 id 连发
会计熔断)。断言:场景 9 `带路方向与 grid 最短路一致 + 第一步必可通行 + 节流单测(同向连呼 3 次只播 1)`。
② 蘑菇/所有游走怪**相邻即扑击接战**(`_mobStep` 移动后与玩家正交相邻 → `_encounter`),杜绝 R1 L3
「贴身绕圈、堵格不开战」35 分钟软封锁。断言:场景 12C 相邻怪必然开战。
- **D1-4 战斗结束 BGM**:教学战胜利/逃跑回 DRILL 的两条路径原先不切回 bgm/explore(只有菜单关闭路径恢复
——R1 转写 t=35.8 起战斗鼓点泡全程的组成因素之一)。两条路径补 `bgm/explore` 淡入。断言:场景 5 胜利/
逃跑后 `lastBgmEntry === bgm/explore`。
- **D1-5 金币档**:SETTLE(`_die`/`_settleWin`)原用 run 金币自设阈值(<20/<60),TITLE 用
`_goldBand(meta.gold)`(<50/≤200/≤600)——同一句 id 两套口径(live:SETTLE meta_gold_3 vs TITLE
meta_gold_2)。统一为同一分档函数同一账本(遗产购置后的 meta.gold)。断言:场景 6a `SETTLE 金币档 =
_goldBand(meta.gold)`。
- **D1-6 三件**:① 长按 boost 闪跳——E3 豁免期快进本就不生效,但 gesture.js 仍外发
`soundgame:longpress(+end)` 窗口事件(仪器记为 boost on→off 闪跳):`setRateBoostEnabled` 现同步经
`setLongpressBoostEventsEnabled` 抑制窗口事件(游戏手势回调不受影响)。② D7「菜单被系统自动收」经 live
事件流复核为风暴伪影(菜单开场链 sys_menu→首项名被僵尸节点反复重放,被听成自动执行 sys_menu_resume)
——D0-1 修复即愈,无额外代码;playtest 场景 2 已有「菜单保持打开直到玩家下滑关闭」断言。③ 战斗回合
15s 无操作播合成回执 `sfx/turn_tick`(880Hz 木鱼族,gen_sfx.py,-3.87dBFS)——standard 档无 gd_turn
语音也有「还到你」的确认。断言:场景 10 turn_tick + 引擎豁免事件抑制(rateLog 既有断言不受影响)。
#### 伪影修正(web/harness/sessions/dungeon.mjs)
- 「元进度不兑现」关案依据 + 三处剧本脱轨根因:① `waitVirtual(120)` 虚拟时钟越表多走(见 D0-3③);
② tapDescend 不受理 DRILL(D9 最后一击点不响);③ 单发 navigate+tapDescend 无重试,任何被怪打断即脱轨
——逐层验证下潜循环修复。另修 navigate 中「走位途中被扑→长按按到战斗里=瞬间逃跑」(R1 live btl_flee×9
的剧本同款)。复核:主线转写 2 次弧线逐句一致(含归因链与金币档);压力转写 2 次均零死锁走到死亡-重开。
#### R2 新增/变更断言口径登记
- 场景 1:怪物构成由「逐字相等」放宽为「⊇ 规格表 + 补怪限本层怪种 ≤8」(密度下限所需)。
- 场景 4:`guide=off 30s 零播放`改为「零语音/零向导播放,唯 amb_cave 按呼吸层口径除外」(内容层 vs
向导层切分)+ 呼吸层节奏/重置断言。
- 场景 9:L3 带路从「整段先东西后南北 + 每步必播」改为「最短路首步 + 改向必播/同向 1.5s 节流」
(§10.3 L3 口径的听感与保险丝友好修订)。
- 台词/资产表:无新增 voice;新增 sfx `amb_cave`、`turn_tick`(SYN,双格式,QC 数字见上)。