diff --git a/.gitignore b/.gitignore index 27629c7c..ca5e9c22 100644 --- a/.gitignore +++ b/.gitignore @@ -51,8 +51,10 @@ tools/gen_voice_dual_wav/ tools/gen_amb_wav/ tools/gen_inst_wav/ tools/gen_rpg_sfx_wav/ -# 批次 1(radio/dungeon)资产中间产物(可由 gen 脚本复现,不入库) +# 批次 1/2(radio/dungeon/abyss/detective)资产中间产物(可由 gen 脚本复现,不入库) web/radio/gen/staging/ web/dungeon/gen/staging/ +web/abyss/gen/staging/ +web/detective/gen/staging/ tools/bgm_post_wav/ __pycache__/ diff --git a/docs/audio/asset-manifest.md b/docs/audio/asset-manifest.md index 7ffb2e7d..0e4beda8 100644 --- a/docs/audio/asset-manifest.md +++ b/docs/audio/asset-manifest.md @@ -21,6 +21,8 @@ | 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 | +| abyss | Uncle_Fu(DIVER)/Dylan(DECK) | Serena(DIVER)/Vivian(DECK) | 玩家/调度同性别双 speaker(VOICE_SPK,radio 同型);NPC 平铺 4 音色+逐船员带通(见 abyss 章节) | +| detective | Uncle_Fu | Serena | 旁白全表统一 instruct;NPC 平铺 5 音色满配、不滤波(见 detective 章节) | 关于 Ryan:冒烟测试样本在 `tools/samples/ryan_zh_*.wav`(3 条中文句)。Ryan 是英语母语音色(见 `env.d/tts.md`),项目已有中文发音投诉(反馈⑥),男声统一采用中文母语的 Uncle_Fu;如需换成 Ryan,改 `tools/gen_voice_dual.py` 的 `SPEAKERS` 表重跑即可。 @@ -34,6 +36,8 @@ - rhythm(R3 起,全表统一):female(Vivian)「活力清晰的年轻女声,像节奏游戏主持人,情绪饱满但不夸张」/ male(Uncle_Fu)「低沉醇厚的男声,像节奏游戏主持人,平稳有力」 - space(R3 起,全表统一):female(Serena)「平静清晰的女声,像飞船领航员,沉稳可信」/ male(Uncle_Fu)「低沉醇厚的男声,像飞船领航员,沉稳有力」 - rpg(R4 起,旁白全表统一):female(Serena)「平静清晰的年轻女声,像在讲武侠故事」/ male(Uncle_Fu)「低沉醇厚的男声,像说书人,平静有韵味」;NPC 各角色 speaker+instruct 恒定(村长 Uncle_Fu·慈祥和缓的老者/老猎人 Eric·沙哑粗犷豪爽中带着暖意/头目 Dylan·凶狠粗鲁大声喝骂/寨主 Uncle_Fu·低沉阴狠慢条斯理) +- abyss(批次 2):DIVER female(Serena)「低沉冷静的年轻女声,深潜救援员,干练沉着,水下通讯的轻微气声」/ male(Uncle_Fu)「低沉冷静的男声,深潜救援员,干练沉着,水下通讯的轻微气声」;DECK female(Vivian)「清晰稳定的女声,母船潜水调度员,专业干脆,隔着耳机下达指令」/ male(Dylan)「清晰稳定的男声,母船潜水调度员,专业干脆,隔着耳机下达指令」;NPC 恒定(小舟 Serena·年轻女声二十三岁声呐兵缺氧后发虚强撑着干脆/老白 Uncle_Fu·沙哑爽朗中年男声五十岁轮机长忍着伤痛开玩笑/陈医官 Vivian·平静沙哑中年女声四十五岁船医专业镇定语气温和/高船长 Eric·低沉苍老男声五十八岁船长疲惫克制有老舵手的威严) +- detective(批次 2,旁白全表统一):female(Serena)「沉稳清晰的女声,专案组探长,冷静克制,像在雨夜读卷宗」/ male(Uncle_Fu)「低沉沉稳的男声,专案组探长,冷静克制,像在雨夜读卷宗」;NPC 恒定(老葛 Eric·苍老沙哑的男声守夜人慢悠悠带倦意/小顺 Dylan·年轻清亮的男声学徒怯生生的慌张诚实/周先生 Uncle_Fu·平板谨慎的男声账房字斟句酌被追问时渐慌/温太太 Vivian·快嘴泼辣的中年女声房东太太嗓门亮热心肠/秦法医 Serena·冷静干练的女声验尸官临床腔短句平直) ## 各游戏资产清单 @@ -107,6 +111,33 @@ - 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`。 - 合计 303 ogg + 303 m4a(250 voice + 49 sfx + 4 bgm;批次 1 表述 273 未计 R2 amb_cave/turn_tick 与批次 2 的 28 文件,本行已按实盘点修正);台词/生成/终检走 `tools/gen_voice_dual.py --game dungeon` 与 `tools/check_all_assets.py`。 +### abyss《深海回音》(批次 2 新游戏,叙事模式,台词依据 `docs/design/17-abyss-voice-ids.md`) + +- voice 双性别版(male/ 与 female/ 各 116 条 + key 变体各 11 条):**DIVER 潜航员(玩家)** 30 条(male=Uncle_Fu「低沉冷静的男声,深潜救援员,干练沉着,水下通讯的轻微气声」/ female=Serena「低沉冷静的年轻女声,…」:开场 `b_open`、行动句 `d1_* d2_* d3_* d4_* d9_* g9_knot`、结局 `e1–e4_a/b`)+ **DECK 母船调度/系统/向导** 86 条(male=Dylan / female=Vivian「清晰稳定的男/女声,母船潜水调度员,专业干脆,隔着耳机下达指令」:`gd_* sys_* prog_d1–4 oxy_* dry_*`、下潜/解读句 `dv1–dv4_*`、抉择提示 `p1–p9`(含单载 `p7_chen/p7_zheng`)、结算 `d*_surf/d*_supply`、尾声 `ep_*` 14 条)。同性别双 speaker 逐句覆盖见 `tools/gen_voice_dual.py` 的 `VOICE_SPK["abyss"]`(玩家与调度同场景永不同 speaker,radio HOST/PROD 同型)。 +- voice NPC 平铺(21 条,落 `voice/` 根,引擎平铺回退;**转码带通**见下):小舟 5 条(z1_* + d1_log,Serena·年轻女声 23 岁声呐兵)、老白 4 条(bai_*,Uncle_Fu·沙哑爽朗轮机长)、陈医官 5 条(chen_*,Vivian·平静沙哑船医)、高船长 7 条(gao_*,Eric·低沉苍老船长)。**区分度风险中**:小舟与 female 玩家同 speaker、老白与 male 玩家同 speaker——靠年龄感/沙哑 instruct + 电台滤波区分,待人工试听(17b 头注)。 +- **水声电台滤波(资产侧,17-abyss.md §5.3)**:convert 转码按船员逐档带通+轻压缩(小舟 420-3200 / 老白 350-3000 / 陈医官 400-3100 / 高船长 300-2800 Hz,均 `acompressor threshold=-20dB ratio=3`);参数在 `NPC_BANDPASS["abyss"]`,已验证生效(带内能量 47-62%→69-80%,>3.4kHz 高频占比 7% vs 未滤波 DECK 45%),未滤波原声保留在 staging 可重滤/回退(宁窄勿脏)。DIVER/DECK 不滤波(母船级音质)。 +- sfx(19 文件):SYN 6 件(`web/abyss/gen/gen_sfx.py`:`sonar_ping`(1.4→0.9kHz chirp 220ms **-6dBFS 设计指定电平**)、`pipe_knock`(三短三长三短固定节拍,断言可数)、`link_beep`、`ascent_chime`、`ui_confirm ui_menu`)+ SA3 13 件(`web/abyss/gen/gen_sfx_sa3.py`,seed 20260909):`echo_metal_l/_r`(**预 pan 立体声**:mono 下混 + ffmpeg pan 派生,能量只在目标声道、极性断言,dungeon side_* 工艺——引擎无运行时 pan)、`echo_debris echo_groan static_burst spark_flash valve_close landslide surf_splash bell_toll`(one-shot)+ `amb_deep amb_deck breath_loop`(18s amb 床,只去直流不淡化,游戏层 buffer.loop,RMS -26/峰值帽 -8 先例)。 +- bgm(4,Mureka $0.12):`deck dive comms ending`(全部 -20 LUFS——MENU 复用 deck 低响度淡入由游戏层控制);90.00s 无缝循环(mean -20.5~-21.9dB);dive 的 sonar-like pulse 仅为 prompt 特征词,未跑 bpm_precise;后处理脚本 `web/abyss/gen/gen_bgm.sh`。 +- 合计 298 ogg + 298 m4a(voice 275 + sfx 19 + bgm 4);台词/生成/终检走 `tools/gen_voice_dual.py --game abyss` 与 `tools/check_all_assets.py`;两表一致闭环校验脚本 `/tmp/verify_batch2.py`(重解析 17b 文档逐字 diff,一次性,未入库)。 + +### detective《雨夜专案》(批次 2 新游戏,本格推理模式,台词依据 `docs/design/18-detective-voice-ids.md`) + +- voice 旁白双性别版(male/ 与 female/ 各 96 条 + key 变体各 10 条):male=Uncle_Fu「低沉沉稳的男声,专案组探长,冷静克制,像在雨夜读卷宗」/ female=Serena「沉稳清晰的女声,…」,两常量全表统一(禁句级变体)。分组:G0 系统/菜单 11、G1 开场/教学 12、G2 状态/条目 20(`st_asked_0–6 st_doubt_0–4 item_* intro_w3`)、G3 现场勘验 6(探长自勘 `sc_*`)、G4 指证 8、G5 结案三问 18、G6 结算 9、G7 看门狗/卡关 12(`hint_a/b/c1/c2 b_hint_c1/c2 hint_fin recap_go`)、V 键盘变体 10。**句序号一律不播**(数字拼接禁令,交互侧落实)。 +- voice NPC 平铺(52 条,落 `voice/` 根,**不加滤波**——区分度靠 speaker+instruct+游戏侧签名 earcon `sig_*` 前缀,18b 头注):老葛 7 条(w1_* + acc_ge,Eric·苍老沙哑守夜人)、小顺 16 条(w2_* acc_shun b_shun_1–4 b_shun_react_1–2 b_shun_break_1–3,Dylan·年轻清亮学徒)、周先生 17 条(z_1–6 z_react_1–4 acc_zhou z_break_1–3 b_zhou_1–3,Uncle_Fu·平板谨慎账房)、温太太 7 条(wen_* + acc_wen,Vivian·快嘴泼辣房东)、秦法医 5 条(fo_*,Serena·冷静干验收官)。**区分度风险中**:周先生与 male 旁白同 speaker、秦法医与 female 旁白同 speaker——靠 sig 前缀+instruct+语境兜底,待人工试听(18b 头注)。声音演技核心 `z_react_1–4`(记岔→支吾→拒答→想走)与 `z_break_1–3`(认罪三段)情绪全在句内文本(省略号/破折号)。 +- sfx(12 文件):SYN 6 件(`web/detective/gen/gen_sfx.py`:签名 `sig_shun sig_wen sig_fore`、`contra_hit`(小二度金属相击+盖章尾,最强确认音 -4dBFS)、`contra_miss`(钝木鱼,非 error 音色)、`turn_tick`(880Hz 木鱼族,dungeon 配方自存))+ SA3 6 件(`web/detective/gen/gen_sfx_sa3.py`,seed 20260909):`paper_turn sig_ge sig_zhou sig_scene gavel rain_swell`(认罪链垫 7.2s,-10dBFS 压在 bgm/verdict 下)。签名 earcon 实测 0.78-1.28s(文档 0.3-0.5s 指引——两步+伞抖水类内容下限,超出已记录批次报告)。 +- bgm(3,Mureka $0.09):`menu board verdict`(18-detective.md §8.1 明确全部 -20 LUFS;雨声底烤进曲目省第三声流);90.00s 无缝循环(mean -20.2~-21.7dB);后处理脚本 `web/detective/gen/gen_bgm.sh`。 +- 合计 279 ogg + 279 m4a(voice 264 + sfx 12 + bgm 3);台词/生成/终检走 `tools/gen_voice_dual.py --game detective` 与 `tools/check_all_assets.py`。 + +### 批次 2 资产账(2026-09-09) + +- abyss 148 句(双性别 127 含 key 11 对 ×2 + NPC 平铺 21)= 275 voice 资产;detective 158 句(旁白 106 含 key 10 对 ×2 + NPC 平铺 52)= 264。两游戏合计 1154 文件(ogg+m4a)+ 2 cover。 +- GPU 串行(flock):abyss male/female/key/npc 与 detective male/female/key/npc + SA3 两批共 3 轮,两游戏 `--check` 全绿(无时长离群/短句)。 +- Mureka 7 任务 $0.21(台账见 `tools/mureka_log.md` 2026-09-09 批次 2 节),全部一次成功;项目累计 $1.08/$2.00。 +- 台词两表一致闭环校验:一次性脚本重解析 17b/18b 文档 vs `gen_voice_dual.py` 逐字 diff PASS(148/158 句、key 11/10 对、NPC 21/52 全对齐,账目 275/264 音频条数与文档合计一致)。 +- 音色分配表新增:abyss(双性别 DIVER=Uncle_Fu/Serena + DECK=Dylan/Vivian 双配对 + NPC 平铺 4 音色)、detective(旁白 Uncle_Fu/Serena + NPC 平铺 5 音色满配)。 +- sfx 真峰值(逐声道解码回读)全部 ≤ -3dBFS、|直流| ≤ 0.001(SYN 首轮即抓出 link_beep.m4a AAC 过冲 -2.82dBFS,迭代补 volume 修复;SA3 超限自动补偿未触发)。 +- check_all_assets 10 游戏全绿(exit=0):abyss 298/298、detective 279/279,双性别集合一致 + key 变体两性别一致且 ⊆ 主集。 + ### 批次 1 资产账(2026-09-01) - radio 132 句(双性别 103 ×2 + NPC 平铺 29)= 235 voice 资产;dungeon 125 句 ×2 = 250。两游戏合计 970 文件(ogg+m4a)。 diff --git a/research/new8/batch2-assets-report.md b/research/new8/batch2-assets-report.md new file mode 100644 index 00000000..ad98fb80 --- /dev/null +++ b/research/new8/batch2-assets-report.md @@ -0,0 +1,121 @@ +# 批次 2 资产报告:abyss《深海回音》+ detective《雨夜专案》 + +> 音频资产管线 agent 产出 · 2026-09-10 · 覆盖任务书 A(语音)/ B(BGM)/ C(音效)/ D(封面)/ E(登记)。 +> 总耗时 ≈ **55 分钟**(00:25–01:20,含 3 轮 GPU flock 串行生成、7 个 Mureka 任务、全部 QC 与文档; +> Mureka 与 GPU TTS 并行推进,互不占锁——前者纯 API)。 + +## 1. 语音(TTS,GPU flock 串行) + +| 项 | abyss | detective | +|---|---|---| +| 台词句数 | 148(双性别 127 含 key 11 对 + NPC 平铺 21) | 158(旁白 106 含 key 10 对 + NPC 平铺 52) | +| 生成文件 | (116 主 + 11 key)×2 + 21 NPC = **275 条** ×2 格式 = 550 文件 | (96 主 + 10 key)×2 + 52 NPC = **264 条** ×2 格式 = 528 文件 | +| speaker | DIVER 玩家=Uncle_Fu/Serena + DECK 调度=Dylan/Vivian(VOICE_SPK 逐句覆盖,同性别双音色,radio HOST/PROD 同型:同场景对话两者永不同 speaker) | 旁白全表 Uncle_Fu/Serena,单一 instruct 常量 | +| NPC 平铺 | 小舟 Serena / 老白 Uncle_Fu / 陈医官 Vivian / 高船长 Eric(4 音色,电台滤波见下) | 老葛 Eric / 小顺 Dylan / 周先生 Uncle_Fu / 温太太 Vivian / 秦法医 Serena(5 音色满配,**不加滤波**——18b 头注) | +| `--check` | ALL OK(无时长离群/短句) | ALL OK | + +- 台词表进入 `tools/gen_voice_dual.py`(新增 abyss/detective + SPEAKERS/VOICE_SPK["abyss"]/NPCS/NPC_BANDPASS["abyss"]/KEY_LINES 各 11/10 对),**两表一致闭环校验**:一次性脚本(/tmp/verify_batch2.py,未入库)重解析 17b/18b 两份 voice-ids 文档,与脚本内三张表(主表/key/NPC)**逐字 diff PASS**——148/158 句、key 11/10 对、NPC 21/52 全对齐,账目核对(音频条数 275/264)与文档合计一致。 +- key 变体落 `voice//key/.*`(radio 批次 1 既有引擎约定:`web/engine/audio.js` 候选链前置 `voice//key/`;文档中 `_k` 是逻辑 id,落盘无后缀进 key/ 子目录)。 +- **abyss 电台滤波(17-abyss.md §5.3)**:convert 转码按船员逐档 `highpass+lowpass+轻压缩`(小舟 420-3200 / 老白 350-3000 / 陈医官 400-3100 / 高船长 300-2800 Hz,均 acompressor threshold=-20dB ratio=3);**已验证生效**:带内能量 47-62%→69-80%,>3.4kHz 高频占比 7.1%(最窄档 gao_a)vs 未滤波 DECK 44.8%,时长与 staging 完全一致;未滤波原声保留在 `tools/gen_voice_dual_wav/abyss/npc/`(不入库)可重滤/回退。DIVER/DECK 不滤波(母船级音质)。 +- `extract_voice_text.py` 已刷新 `web/harness/voice_text.json`:**games=10,lines=991,key_variants=185**。 + +**文档内矛盾检查**(未停下项,均登记不阻塞管线): +1. 17b 头注「双性别组……126 句」与文末合计「双性别 127 句(30+97)」不符——逐行表实数 **127**(30 DIVER + 97 DECK,脚本与文档逐行一致),头注笔误,建议策划改 126→127(不影响任何资产)。 +2. 17 号文档 §7.3 写 key 变体落 `voice//_k.*`,与引擎实现(`voice//key/`,audio.js L365-366)及批次 1 先例/任务书口径不一致——**按引擎约定落盘**(零代码改动铁律),17 号文档该句待策划改写。 +3. abyss key 措辞用「左箭头/右箭头/上箭头/下箭头」(17b 原文),dungeon 用「左方向键」——两文档各自定稿措辞,照抄不改写。 + +## 2. BGM(Mureka API) + +7 首全部一次成功,无重试;**总花费 $0.21 ≤ $0.30(预算线),项目累计 $1.08/$2.00**;台账见 `tools/mureka_log.md` 2026-09-09「批次 2」节。 + +| 曲目 | task id | 选用 choice | 候选时长 | 响度 | +|---|---|---|---|---| +| abyss deck | 160001236664321 | 候选 0(160001381367809) | 192.1 / 201.1s | I=-20 | +| abyss dive | 160001429798913 | 候选 0(160001536622595) | 179.8 / 210.6s | I=-20 | +| abyss comms | 160001576402945 | 候选 1(160001689714693) | 213.7 / 168.7s | I=-20 | +| abyss ending | 160001717043203 | 候选 0(160001815478273) | 191.4 / 211.1s | I=-20 | +| detective menu | 160001918435329 | 候选 1 | 232.3 / 219.8s | I=-20(18 号 §8.1 明确) | +| detective board | 160002065039361 | 候选 0 | 216.8 / 233.6s | I=-20 | +| detective verdict | 160002209873922 | 候选 1 | 240.0 / 223.7s | I=-20 | + +- 英文 prompt 从 17 号 §5.1 / 18 号 §8.1 表格**逐字照抄**;选用原则同批次 1(拿不准选时长更接近 90s 的候选)。 +- 后处理(脚本入库 `web/{abyss,detective}/gen/gen_bgm.sh`):首个强 onset(silenceremove -35dB)→ 93s 裁剪 → 首尾 3s acrossfade(tri) 无缝循环 → loudnorm(TP=-2/LRA=11)→ 44.1kHz → ogg q3 + m4a 128k。**成品全部 90.00s**,mean -20.2 ~ -21.9dB(与在库 BGM 同量级)。 +- 响度裁定:abyss 4 首全部 I=-20——MENU 复用 deck 曲、低响度淡入由游戏层 gain 控制(§2.1),无独立 menu pad 曲,radio menu -24 先例不适用;detective 3 首按 18 号 §8.1 明确 -20。雨声底按设计烤进 detective 曲目(省第三声流)。无节奏关键曲目,未跑 bpm_precise。 +- flac 原件存 `tools/mureka_out/{abyss,detective}-*/`(不入库)。 + +## 3. 音效(SYN 程序合成 + SA3 直生成——本批 SA3 token 就绪,无缺失清单) + +### 3a. SYN(`web//gen/gen_sfx.py`,入库可复现) + +- abyss 6 件:`sonar_ping`(1.4→0.9kHz chirp 220ms,**-6dBFS 设计指定电平**,回波家族统一音头)、`pipe_knock`(三短三长三短固定节拍金属敲击,节拍精确可复现/断言可数)、`link_beep`(双音上行 0.3s)、`ascent_chime`(上行三音琶音 0.4s)、`ui_confirm ui_menu`(全局先例同款)。 +- detective 6 件:签名 `sig_shun`(布掸子两拍)/`sig_wen`(门环二连击)/`sig_fore`(翻页+钢笔划线)、`contra_hit`(小二度金属相击+盖章尾,全游戏最强确认音 -4dBFS)、`contra_miss`(钝木鱼,非 error 音色 -8dBFS)、`turn_tick`(880Hz 木鱼族,dungeon 配方自存一份)。 +- **QC(解码回读法,全部通过)**:逐声道真峰值 ≤ -3dBFS、|直流| ≤ 0.001。**铁律首轮即抓出一例真超标**:abyss `link_beep.m4a` AAC 过冲实测 -2.82dBFS(纯弦 MDCT 过冲坑,asset-pipeline.md 有案)——两脚本 encode() 升级为迭代补偿工艺(超限按实测补 volume + 直流预减,dungeon 先例),修复后 -3.67dBFS。 + +### 3b. SA3(`web//gen/gen_sfx_sa3.py`,--emit 生成 + 后处理入库) + +seed 20260909,steps 8,模型已缓存走 `HF_HUB_OFFLINE=1`;raw 为定长 latent 床(abyss 18.02s / detective 15.05s),one-shot 按活动窗自然修剪(maxs 帽),amb 床整段保留。 + +| abyss id | 时长s | ogg峰 | RMS | 结论 | +|---|---|---|---|---| +| echo_metal_l / _r | 2.76 | -4.93 | -24.5 | **预 pan 立体声**:mono 下混 + ffmpeg pan 派生,能量只在目标声道(极性断言),dungeon side_* 工艺复用(引擎无运行时 pan) | +| echo_debris | 2.78 | -7.05 | -24.5 | 碎石回波(D2) | +| echo_groan | 4.42 | -5.82 | -25.3 | 船体呻吟(D3 预警/D4,≈设计 4s) | +| static_burst | 2.58 | -8.41 | -24.5 | 通讯静电(z1_a 垫) | +| amb_deep / amb_deck / breath_loop | 18.02 | -12.1/-7.3/-8.2 | -26.8/-29.7/-28.8 | **amb 床只去直流不淡化**(游戏层 buffer.loop,radio/escape 先例),RMS -26 + 峰值帽 -8 | +| spark_flash | 2.24 | -4.13 | -23.7 | D2 危机拍(danger 同窗) | +| valve_close | 2.35 | -5.80 | -25.4 | p4 左 payoff | +| landslide | 5.37 | -5.00 | -22.9 | D4 危机拍(前置渐强) | +| surf_splash | 2.33 | -7.40 | -27.1 | 每潜出水 | +| bell_toll | 2.79 | -4.72 | -26.4 | p9 左 payoff | + +| detective id | 时长s | ogg峰 | RMS | 结论 | +|---|---|---|---|---| +| paper_turn | 1.08 | -7.20 | -31.7 | 条目切换内容反馈 | +| sig_ge / sig_zhou / sig_scene | 0.78 / 1.08 / 1.28 | -5.8~-6.8 | -29.8~-35.1 | 证人签名 earcon;**0.78-1.28s 超出文档 0.3-0.5s 指引**(两步+伞抖水类内容下限,见 §6 遗留) | +| gavel | 1.35 | -3.69 | -33.6 | 裁决仪式拍 | +| rain_swell | 7.18 | -10.77 | -26.7 | 认罪链垫(≈设计 6s + 垫音;-10dBFS 压在 bgm/verdict 下) | + +- m4a 峰值全部同过(-3.05 ~ -10.25dBFS),SA3 侧超限自动补偿未触发;|直流| 全部 ≤ 0.0001。 + +## 4. 封面(D) + +`web/{abyss,detective}/gen/gen_cover.py`(PIL,入库可复现,系统 python3 与 .venv-tts 均有 PIL 12.3.0)→ `web//cover.png` 1080x1920,批次 1 风格(深色底 + 极简主题图形 + 居中大标题 + 手势图例两行 + 底部标语),固定 seed。 +- abyss:深海近黑蓝渐变(下潜方向)+ 声呐同心圆脉冲光点 + 右下暗色回波环(「回音」具象)+ 水雪微粒;视觉审查通过(标题/图例/构图无缺陷)。 +- detective:墨蓝夜雨渐变 + 左上路灯暖光晕 + 双景深斜雨丝 + 中央翻开卷宗剪影(右页一条对不上的红线 = 矛盾具象);CDN 缓存碰撞无法二次人工目测,已用像素级断言验证(暖光 +36.7 / 红线 155px / 雨丝竖向性 1.64 / 标题亮区 50595px)。 + +## 5. check_all_assets 结果(abyss/detective 已入 GAMES + FLAT_OK + key 规则) + +``` +| 游戏 | ogg | m4a | 0字节 | 解码失败 | voice male/female id 一致 | key 一致且 ⊆ 主集 | +| abyss | 298 | 298 | 0 | 0 | ✅ 各 116(平铺 21 个 NPC 句,豁免) | ✅ 各 11 ⊆ 主集 | +| detective| 279 | 279 | 0 | 0 | ✅ 各 96(平铺 52 个 NPC 句,豁免) | ✅ 各 10 ⊆ 主集 | +结论:10 游戏全绿,exit=0(其余 8 游戏无回归) +``` + +构成:abyss = voice 275(双性别 232 + key 22 + NPC 21)+ sfx 19 + bgm 4;detective = voice 264(192+20+52)+ sfx 12 + bgm 3。两游戏合计 **1154 文件(ogg+m4a)**。 + +## 6. 缺失清单与遗留 + +1. **无缺失**:SA3 token 就绪,本批 SYN+SA3 全量直产,无批次 1 式补产轮。 +2. **人工试听项**(建议开发联调前抽检): + - abyss 电台滤波可读性(17b 标注**中风险**两组:小舟 vs female 玩家、老白 vs male 玩家同 speaker——不可读句回退未滤波原声,staging 重转即可); + - abyss NPC 多音字(17b 文末注记:「脏」zāng、「闷」mèn、d3_zheng_e「都没敢出声」等); + - detective 同 speaker 风险(18b 标注**中风险**两组:周先生 vs male 旁白、秦法医 vs female 旁白——sig 前缀兜底)与低风险三处(「得」de/「背」bèi/「处」chù); + - 声音演技段:z_react_1..4 递进与 z_break_1..3 认罪三段(情绪靠省略号/破折号引导,试听确认)。 +3. **文档笔误待策划改**:17b 头注「126 句」→ 127(§1 矛盾检查第 1 条);17 号 §7.3 key 路径表述与引擎实现不一致(第 2 条)。 +4. **签名 earcon 时长超指引**:detective sig_* 实测 0.78-1.28s vs 文档 0.3-0.5s(「两下」类签名的内容下限;若导航节奏测试发现拖沓,可回 WIN 事件窗收紧重出)。 +5. 红线遵守:GPU 全程 flock 串行(TTS abyss→detective 两轮 + SA3 一轮,无并行;Mureka 为纯 API 不占 GPU 锁);Mureka $0.21 ≤ $0.30 且逐笔记账;token/key 未入任何入库文件(env.d/{mureka,sfx}.sh 均被 .gitignore 覆盖,已验证);未改 web/engine/**、未写任何 game.js/index.html/test;`web/{abyss,detective}/gen/staging/` 已补进 .gitignore(此前规则只列了 radio/dungeon,会误入库)。 + +## 7. 产出物索引 + +| 类型 | 路径 | +|---|---| +| 台词表(入库) | `tools/gen_voice_dual.py`(abyss/detective 表 + SPEAKERS + VOICE_SPK + NPCS + NPC_BANDPASS + KEY_LINES) | +| 两表一致校验 | `/tmp/verify_batch2.py`(一次性,未入库;重跑即再验) | +| voice 成品 | `web/{abyss,detective}/audio/voice/{male,female}/`(+ key/ 子目录)与平铺 `voice/.*`(abyss 21 句带滤波) | +| bgm 成品 + 脚本 | `web/{abyss,detective}/audio/bgm/`;`web/{abyss,detective}/gen/gen_bgm.sh` | +| sfx 成品 + 脚本 | `web/{abyss,detective}/audio/sfx/`;`gen_sfx.py`(SYN)+ `gen_sfx_sa3.py`(SA3) | +| 封面 | `web/{abyss,detective}/cover.png` + `gen/gen_cover.py` | +| 期望文本映射 | `web/harness/voice_text.json`(games=10) | +| 台账/总账 | `tools/mureka_log.md`(2026-09-09 批次 2 节);`docs/audio/asset-manifest.md`(abyss/detective 章节 + 批次 2 资产账) | +| staging(不入库) | `tools/gen_voice_dual_wav/{abyss,detective}/`、`web//gen/staging/`、`tools/bgm_post_wav/{abyss,detective}/`、`tools/mureka_out/{abyss,detective}-*/` | diff --git a/tools/check_all_assets.py b/tools/check_all_assets.py index 3f1146c0..0817d6e8 100644 --- a/tools/check_all_assets.py +++ b/tools/check_all_assets.py @@ -16,9 +16,10 @@ import subprocess import sys ROOT = "/home/xsl/SoundGame/web" -GAMES = ["storyline", "escape", "simon", "rhythm", "space", "rpg", "radio", "dungeon"] +GAMES = ["storyline", "escape", "simon", "rhythm", "space", "rpg", "radio", "dungeon", + "abyss", "detective"] # 平铺 voice 文件(角色/NPC,单音色,不做性别集合一致性要求) -FLAT_OK = {"storyline", "rpg", "radio"} +FLAT_OK = {"storyline", "rpg", "radio", "abyss", "detective"} def ids(d, ext): @@ -29,7 +30,7 @@ def ids(d, ext): def main(): problems = [] - print("# 全项目音频资产终检(R5 建立,批次 1 起含 radio/dungeon,2026-09-01)\n") + print("# 全项目音频资产终检(R5 建立,批次 1 起含 radio/dungeon,批次 2 起含 abyss/detective,2026-09-09)\n") print("| 游戏 | ogg | m4a | 0字节 | 解码失败 | voice male/female id 一致 | key male/female 一致且 ⊆ 主集 |") print("|---|---|---|---|---|---|---|") for g in GAMES: @@ -98,7 +99,7 @@ def main(): for p in problems: print(f"- {p}") return 1 - print("## 结论\n\n全部通过:双格式配对完整、无 0 字节、全部可解码、voice 性别目录 id 集合一致(storyline/rpg/radio 平铺角色句按约定豁免)、键盘变体目录两性别一致且 ⊆ 主集合。") + print("## 结论\n\n全部通过:双格式配对完整、无 0 字节、全部可解码、voice 性别目录 id 集合一致(storyline/rpg/radio/abyss/detective 平铺角色句按约定豁免)、键盘变体目录两性别一致且 ⊆ 主集合。") return 0 diff --git a/tools/gen_voice_dual.py b/tools/gen_voice_dual.py index f2b9af06..6ddaff25 100644 --- a/tools/gen_voice_dual.py +++ b/tools/gen_voice_dual.py @@ -59,6 +59,9 @@ SPEAKERS = { # radio 默认 speaker 仅作兜底;双性别行按 VOICE_SPK["radio"] 逐句覆盖(HOST/PROD 双配对) "radio": {"male": "Uncle_Fu", "female": "Serena"}, "dungeon": {"male": "Uncle_Fu", "female": "Serena"}, + # abyss 默认=DECK 调度(多数行);DIVER 行按 VOICE_SPK["abyss"] 逐句覆盖(radio 同型) + "abyss": {"male": "Dylan", "female": "Vivian"}, + "detective": {"male": "Uncle_Fu", "female": "Serena"}, } # instruct 对:(male, female)。None 表示两性别共用同一 instruct。 @@ -901,7 +904,365 @@ DUNGEON_LINES = [ ("unlock_none", _DUN, "金币还不够,继续攒。"), ] -NPCS = {"rpg": RPG_NPC, "radio": RADIO_NPC} +# ---------------------------------------------------------------- abyss +# 《深海回音》批次 2(docs/design/17-abyss-voice-ids.md,148 句:双性别 127 + 平铺 NPC 21)。 +# 台词全文逐字照抄 voice-ids 文档;双性别组分 DIVER(潜航员=玩家)/ DECK(母船调度/系统/ +# 向导 gd_*)两个 speaker 配对(同性别双音色,VOICE_SPK["abyss"] 覆盖,radio HOST/PROD 同型: +# 同场景高频对话的两者永不同 speaker);_k 键盘变体 11 对在 KEY_LINES["abyss"]。 +# NPC 平铺 21 句(小舟/老白/陈医官/高船长),转码时按 17-abyss.md §5.3 水声电台带通 +# (NPC_BANDPASS["abyss"]),DIVER/DECK 不滤波(母船级音质)。 +_ABY_DIVER = _pair("低沉冷静的男声,深潜救援员,干练沉着,水下通讯的轻微气声", + "低沉冷静的年轻女声,深潜救援员,干练沉着,水下通讯的轻微气声") +_ABY_DECK = _pair("清晰稳定的男声,母船潜水调度员,专业干脆,隔着耳机下达指令", + "清晰稳定的女声,母船潜水调度员,专业干脆,隔着耳机下达指令") + +ABYSS_DIVER_LINES = [ + # ---- G1 开场 ---- + ("b_open", _ABY_DIVER, "风停了。我站在问海号甲板上,下面三千米,是海燕号。"), + # ---- D1 第一潜·找船·小舟 ---- + ("d1_listen", _ABY_DIVER, "都别出声。管子那头,敲得很稳。"), + ("d1_up", _ABY_DIVER, "小舟,抓住我的肩锁。我们上。"), + ("d1_nolog", _ABY_DIVER, "日志可以补,人不能等。走了。"), + # ---- D2 第二潜·机舱·老白 ---- + ("d2_pry", _ABY_DIVER, "老白,忍住。先泄压,再撬。"), + ("d2_cut", _ABY_DIVER, "切割枪。老白,闭眼,憋气。"), + ("d2_valve", _ABY_DIVER, "电池舱我陪你去。这片海不能脏。"), + ("d2_leave", _ABY_DIVER, "阀门交给母船。你先上,腿要紧。"), + # ---- D3 第三潜·病舱·陈医官/小郑 ---- + ("d3_chen", _ABY_DIVER, "陈医官,你的手在抖。这次,听我的。"), + ("d3_zheng", _ABY_DIVER, "小郑先走。陈医官,下一潜,就是你的。"), + ("d3_spare", _ABY_DIVER, "备用瓶留下。灯,给我亮着。"), + ("d3_promise", _ABY_DIVER, "四小时内,我下来。等我。"), + # ---- D4 第四潜·指挥舱·高船长 ---- + ("d4_slow", _ABY_DIVER, "逐站停,慢慢浮。规矩不能破。"), + ("d4_fast", _ABY_DIVER, "来不及了。直浮!"), + ("d4_pick_gao", _ABY_DIVER, "船长,抓住我。海燕号的事,上面再说。"), + ("d4_pick_chen", _ABY_DIVER, "陈医官,轮到你了。抓住,上浮。"), + ("d4_pick_zheng", _ABY_DIVER, "小郑,吊床连人一起。走了。"), + ("d4_talk", _ABY_DIVER, "日志我听完了。海燕号为什么沉,我会带上去。走吧。"), + ("d4_silent", _ABY_DIVER, "纪律就是纪律。抓住,上浮。"), + # ---- F9 签收抉择(心结) ---- + ("g9_knot", _ABY_DIVER, "三年前,也是这条沟。我按时上浮了,搭档没有。"), + ("d9_bell", _ABY_DIVER, "最后一潜。不为救人,为个交代。"), + ("d9_rest", _ABY_DIVER, "交代,留给明天的人。我去医疗舱。"), + # ---- EP 结局与尾声回响 ---- + ("e1_a", _ABY_DIVER, "五个人都在甲板上。船钟的第一声,是替这条沟敲的。"), + ("e1_b", _ABY_DIVER, "三年了,我终于敢往下看。明天,想再潜一次。"), + ("e2_a", _ABY_DIVER, "五个人都救上来了。船钟,留在了沟底。"), + ("e2_b", _ABY_DIVER, "没人怪我。可半夜醒来,总听见钟在响。"), + ("e3_a", _ABY_DIVER, "没能都带回来。可我到底下去了,敲响了那口钟。"), + ("e3_b", _ABY_DIVER, "钟声替他们,在沟底响了一声。这样,也好。"), + ("e4_a", _ABY_DIVER, "我按规矩上浮了。规矩里,没写怎么难过。"), + ("e4_b", _ABY_DIVER, "沟底很安静。安静得,像三年前那天。"), +] + +ABYSS_DECK_LINES = [ + # ---- G0.a 操作提示句(触屏版;_k 变体在 KEY_LINES) ---- + ("gd_brief", _ABY_DECK, "单击继续简报。要选择时,向左或向右滑。"), + ("gd_continue", _ABY_DECK, "单击屏幕,继续。"), + ("gd_again", _ABY_DECK, "还在吗?单击继续。"), + ("gd_ping1", _ABY_DECK, "声呐待命。单击,发脉冲。"), + ("gd_ping2", _ABY_DECK, "回波不会自己来。单击。"), + ("sys_help", _ABY_DECK, "单击继续,双击重新听。上滑听状态,下滑开菜单。选择时,左右滑。"), + ("sys_menu", _ABY_DECK, "菜单。单击切换选项,双击确认。"), + ("sys_skip", _ABY_DECK, "想跳过这句,单击就行。"), + ("sys_continue_ask", _ABY_DECK, "检测到你的下潜记录。单击继续上次,右滑开始新救援。"), + # ---- G0.b 状态/系统句 ---- + ("gd_invalid", _ABY_DECK, "这个手势,水里用不上。"), + ("gd_prog1", _ABY_DECK, "滑动在这里用不上。想想调度刚说的话。"), + ("gd_prog2", _ABY_DECK, "别乱。停一下,想想下一步。"), + ("sys_menu_resume", _ABY_DECK, "回到水下。"), + ("sys_menu_restart", _ABY_DECK, "重新开始这次救援。"), + ("sys_menu_lobby", _ABY_DECK, "保存并回大厅。"), + ("sys_settings", _ABY_DECK, "音色、语速、背景音乐,请回大厅修改。"), + ("sys_new_dive", _ABY_DECK, "新的一次下潜,开始了。"), + ("prog_d1", _ABY_DECK, "现在是第一次下潜。"), + ("prog_d2", _ABY_DECK, "现在是第二次下潜。"), + ("prog_d3", _ABY_DECK, "现在是第三次下潜。"), + ("prog_d4", _ABY_DECK, "现在是最后一次下潜。"), + ("oxy_full", _ABY_DECK, "气瓶还足,救得动。"), + ("oxy_ok", _ABY_DECK, "氧气还够两潜。"), + ("oxy_low", _ABY_DECK, "氧气告急,只够最后一潜。"), + ("oxy_crit", _ABY_DECK, "氧气见底,必须上浮。"), + ("oxy_gain", _ABY_DECK, "应急瓶接上了,多三格。"), + ("dry_a", _ABY_DECK, "气瓶用干了。立刻上浮。"), + ("dry_b", _ABY_DECK, "气花在了人身上。人,都上来了。"), + # ---- G1 开场 ---- + ("b_rule", _ABY_DECK, "规矩就一条:气不够一潜,人就必须上浮。"), + ("b_goal", _ABY_DECK, "海燕号,五个人。你的双瓶,够下四潜。"), + ("b_go", _ABY_DECK, "第一潜,找船。耳朵交给声呐。"), + # ---- D1 第一潜 ---- + ("dv1_down", _ABY_DECK, "下潜开始。水会越来越黑,声音会越来越多。"), + ("dv1_teach", _ABY_DECK, "到深度了。声呐静了。单击,发一枚脉冲。"), + ("dv1_metal", _ABY_DECK, "金属回波,钢板。听见这个,就是海燕号的船身。"), + ("dv1_link", _ABY_DECK, "回波后面有杂音,是电台!接通了。"), + ("p1", _ABY_DECK, "管路在响。向左滑,停下来听敲管的暗号。向右滑,先带小舟上浮。"), + ("d1_code", _ABY_DECK, "是老白的暗号:腿被压住,先泄压,再撬。记住这个敲法。"), + ("p2", _ABY_DECK, "小舟想回去拿声呐日志。向左滑,等她。向右滑,直接上浮。"), + ("d1_surf", _ABY_DECK, "小舟出水,第一潜收工。往返,扣三格气。"), + # ---- D2 第二潜 ---- + ("dv2_down", _ABY_DECK, "第二潜,机舱。顺着敲管的声音走。"), + ("dv2_found", _ABY_DECK, "碎石回波后面,敲管声密了。机舱到了。"), + ("p3", _ABY_DECK, "钢梁压着腿。向左滑,泄压后慢慢撬。向右滑,切割枪,快切。"), + ("d2_pry_ok", _ABY_DECK, "暗号没白听。管子一松,钢梁抬起来了。"), + ("d2_pry_slow", _ABY_DECK, "摸着找泄压阀,多费了一格气。腿,出来了。"), + ("d2_flash", _ABY_DECK, "火花炸了一下,虚惊。老白咳着,腿抽出来了。"), + ("p4", _ABY_DECK, "老白要去关电池舱阀门。向左滑,陪他去。向右滑,直接上浮。"), + ("d2_valve_e", _ABY_DECK, "阀门关死了。两格气,换一片干净海。"), + ("d2_surf", _ABY_DECK, "老白出水。他一直扒着舱门,往水里看。"), + ("d2_supply", _ABY_DECK, "潜友阿凯捎来应急瓶。甲板上,粥给你温着。"), + # ---- D3 第三潜 ---- + ("dv3_down", _ABY_DECK, "第三潜,病舱。海燕号在倾斜,别恋战。"), + ("dv3_groan", _ABY_DECK, "回波发闷。船体在呻吟,它撑不久了。"), + ("p5", _ABY_DECK, "病舱一次只能走一个。向左滑,先带陈医官。向右滑,先带小郑。"), + ("d3_chen_e", _ABY_DECK, "陈医官出水。小郑固定在吊床上,呼吸平稳。"), + ("d3_zheng_e", _ABY_DECK, "小郑吊上去了。他一路,都没敢出声。"), + ("p6", _ABY_DECK, "留守的人要空气。向左滑,留下你的备用瓶。向右滑,约定下一潜来接。"), + ("d3_spare_e", _ABY_DECK, "备用瓶挂上了,能多撑十二小时。"), + ("d3_surf", _ABY_DECK, "病舱的人出水。海燕号的呻吟,隔着水都听得见。"), + # ---- D4 第四潜 ---- + ("dv4_none", _ABY_DECK, "气不够一潜了。第四潜,取消。"), + ("dv4_down", _ABY_DECK, "最后一潜,指挥舱。海燕号,撑不住了。"), + ("dv4_slide", _ABY_DECK, "它滑了!抓稳索缆,指挥舱里还有人!"), + ("p7", _ABY_DECK, "双人上浮。向左滑,逐站减压慢慢浮。向右滑,违规直浮,搏一把。"), + ("p7_chen", _ABY_DECK, "气只够带一个。向左滑,带船长。向右滑,带陈医官。"), + ("p7_zheng", _ABY_DECK, "气只够带一个。向左滑,带船长。向右滑,带小郑。"), + ("d4_slow_e", _ABY_DECK, "减压站一个个过。气瓶在跳,人稳着。"), + ("d4_fast_e", _ABY_DECK, "违规快浮。医疗舱的灯,先给你们开好。"), + ("d4_stay_lost", _ABY_DECK, "另一位,留在了倾斜的船舱。航标灯,替所有人亮着。"), + ("p8", _ABY_DECK, "船长的话没说完。向左滑,把你听到的说出来。向右滑,什么都不问,拉他上浮。"), + ("d4_surf", _ABY_DECK, "最后一潜收工。问海号的甲板,从没这么亮过。"), + # ---- F9 签收抉择 ---- + ("g9_in", _ABY_DECK, "医疗舱躺满了。你的气瓶剩多少,你比我清楚。"), + ("p9", _ABY_DECK, "海燕号的船钟,还挂在沉船上。向左滑,用最后的气,下去取。向右滑,进医疗舱。"), + ("g9_dry", _ABY_DECK, "一格气都不剩了。能带的,你都带回来了。"), + ("d9_bello", _ABY_DECK, "船钟出水。海燕号的声音,回家了。"), + # ---- EP 尾声回响 ---- + ("ep_lead", _ABY_DECK, "收队了。调度台给你记一笔——他们,后来怎样了。"), + ("ep_zhou_ok", _ABY_DECK, "小舟:归队报告打了三页。她说,声音都记得。"), + ("ep_zhou_no", _ABY_DECK, "小舟:声呐日志没了。她凭记忆,重新描了一遍海图。"), + ("ep_bai_ok", _ABY_DECK, "老白:腿保住了。阀门那夜之后,见人就夸你。"), + ("ep_bai_no", _ABY_DECK, "老白:腿打了钢钉。他说,瘸着也值。"), + ("ep_chen_ok", _ABY_DECK, "陈医官:回了医院。诊室里,挂着一盏海底的灯。"), + ("ep_chen_lost", _ABY_DECK, "陈医官:她守着灯,到最后一小时。灯,还亮着。"), + ("ep_zheng_ok", _ABY_DECK, "小郑:拆了石膏,第一件事,去看海。"), + ("ep_zheng_lost", _ABY_DECK, "小郑:吊床随船,滑进了沟底。他没来得及看日出。"), + ("ep_gao_peace", _ABY_DECK, "高船长:退役申请自己写的。第一行写着:数据不如人。"), + ("ep_gao_up", _ABY_DECK, "高船长:上了岸,一直没说话。听说,他在学敲管语。"), + ("ep_gao_lost", _ABY_DECK, "高船长:随海燕号留在沟底。航标灯,还亮着。"), + ("ep_trench_bell", _ABY_DECK, "沟底:船钟挂在问海号桅杆上。风一过,就响一声。"), + ("ep_trench_deep", _ABY_DECK, "沟底:很深,很静。海燕号睡在里面,像睡着了。"), +] + +ABYSS_LINES = ABYSS_DIVER_LINES + ABYSS_DECK_LINES + +# NPC 单音色平铺(id, speaker, instruct, 台词)→ web/abyss/audio/voice/.* +#(转码时按 17-abyss.md §5.3 逐船员水声电台带通,见 NPC_BANDPASS;staging 保留未滤波原声)。 +ABYSS_NPC = [ + # 小舟(23 岁声呐兵,女,Serena) + ("z1_a", "Serena", "年轻女声,二十三岁声呐兵,缺氧后发虚,强撑着干脆", "……有人吗?声呐兵,小舟。我们还活着,全艇五个人。"), + ("z1_b", "Serena", "年轻女声,二十三岁声呐兵,缺氧后发虚,强撑着干脆", "轮机长老白困在机舱,陈医官和小郑在病舱,船长在指挥舱。"), + ("z1_knock", "Serena", "年轻女声,二十三岁声呐兵,缺氧后发虚,强撑着干脆", "等等,你听。管子里,有人在敲。"), + ("z1_bye", "Serena", "年轻女声,二十三岁声呐兵,缺氧后发虚,强撑着干脆", "好。上面……是不是天亮了?"), + ("d1_log", "Serena", "年轻女声,二十三岁声呐兵,缺氧后发虚,强撑着干脆", "日志里有失事前,最后一段录音。给你,替我们收好。"), + # 老白(50 岁轮机长,男,Uncle_Fu) + ("bai_a", "Uncle_Fu", "沙哑爽朗的中年男声,五十岁轮机长,忍着伤痛开玩笑", "哈哈,救援的?我这腿,跟冷却管缠上了。"), + ("bai_b", "Uncle_Fu", "沙哑爽朗的中年男声,五十岁轮机长,忍着伤痛开玩笑", "艇上的药板烧了。病舱那两个,比我悬。"), + ("bai_out", "Uncle_Fu", "沙哑爽朗的中年男声,五十岁轮机长,忍着伤痛开玩笑", "腿还在!我的腿还在!走,我给你带路。"), + ("bai_thanks", "Uncle_Fu", "沙哑爽朗的中年男声,五十岁轮机长,忍着伤痛开玩笑", "这片海养着一村人。这份情,我记一辈子。"), + # 陈医官(45 岁船医,女,Vivian) + ("chen_a", "Vivian", "平静沙哑的中年女声,四十五岁船医,专业镇定,语气温和", "你来了。小郑股骨断了,我给他固定好了。"), + ("chen_b", "Vivian", "平静沙哑的中年女声,四十五岁船医,专业镇定,语气温和", "救援钟一次带一个。我是医生,我最后走。"), + ("chen_up", "Vivian", "平静沙哑的中年女声,四十五岁船医,专业镇定,语气温和", "……好。小郑,等我,上面给你热粥。"), + ("chen_stay", "Vivian", "平静沙哑的中年女声,四十五岁船医,专业镇定,语气温和", "去吧。我守着灯,灯不灭,人就不散。"), + ("chen_last", "Vivian", "平静沙哑的中年女声,四十五岁船医,专业镇定,语气温和", "去吧。灯我留着,人齐了就熄。"), + # 高船长(58 岁船长,男,Eric) + ("gao_final", "Eric", "低沉苍老的男声,五十八岁船长,疲惫克制,有老舵手的威严", "别下来。带他们回家,就算救完了。海燕号,通话结束。"), + ("gao_a", "Eric", "低沉苍老的男声,五十八岁船长,疲惫克制,有老舵手的威严", "救援员。我船上,还剩两个活人。"), + ("gao_b", "Eric", "低沉苍老的男声,五十八岁船长,疲惫克制,有老舵手的威严", "都带走,我最后上。这是船长的规矩。"), + ("gao_last", "Eric", "低沉苍老的男声,五十八岁船长,疲惫克制,有老舵手的威严", "航标灯替我留着。就说,船长睡了。"), + ("gao_c", "Eric", "低沉苍老的男声,五十八岁船长,疲惫克制,有老舵手的威严", "三年前……没什么。走吧,别耽误。"), + ("gao_confess", "Eric", "低沉苍老的男声,五十八岁船长,疲惫克制,有老舵手的威严", "日志你拿了?里面写着。是我下令,多采了一小时。"), + ("gao_peace", "Eric", "低沉苍老的男声,五十八岁船长,疲惫克制,有老舵手的威严", "……好。这一次,听救援员的。"), +] + +# ---------------------------------------------------------------- detective +# 《雨夜专案》批次 2(docs/design/18-detective-voice-ids.md,158 句:旁白 106 双性别 + 平铺 NPC 52)。 +# 台词全文逐字照抄 voice-ids 文档;旁白只做系统/指引/教学/状态/现场自勘(探长自勘)/裁决宣读, +# 不念证词(证词=证人 NPC 平铺,DETECTIVE_NPC);键盘变体 V 组 10 对在 KEY_LINES["detective"]。 +# NPC 不加电话滤波(区别于 radio/abyss——区分度靠 speaker+instruct+游戏侧签名 earcon 前缀)。 +_DET = _pair("低沉沉稳的男声,专案组探长,冷静克制,像在雨夜读卷宗", + "沉稳清晰的女声,专案组探长,冷静克制,像在雨夜读卷宗") +DETECTIVE_LINES = [ + # ---- G0 系统/菜单/帮助 ---- + ("sys_menu", _DET, "菜单。单击切换,双击确认。"), + ("sys_menu_resume", _DET, "回到证词板。"), + ("sys_menu_cf", _DET, "去指证。"), + ("sys_menu_save", _DET, "保存并回大厅。"), + ("sys_menu_help", _DET, "重新听帮助。"), + ("sys_menu_restart", _DET, "重新查这桩案子。"), + ("sys_saved", _DET, "进度已保存。"), + ("settings_hint", _DET, "音色、语速、背景音乐,请回大厅修改。"), + ("sys_help", _DET, "左右滑换卷宗,单击听取,双击重新听。长按指证,上滑状态,下滑菜单。"), + ("sys_continue_ask", _DET, "检测到未结的案子。单击继续,右滑换一桩新案。"), + ("sys_new_case", _DET, "新案开卷。"), + # ---- G1 开场/教学 ---- + ("intro_1", _DET, "雨下了一整夜。清晨,聚宝斋后院,古董商沈万川死了。"), + ("intro_2", _DET, "你是专案组探长。这桩案子,归你。"), + ("intro_3", _DET, "卷宗六份:四位证人的证词,现场勘验,法医报告。"), + ("tut_d0", _DET, "先熟悉证词板。左右滑,换卷宗。"), + ("tut_listen", _DET, "单击,听取当前卷宗。"), + ("tut_replay", _DET, "双击,重新听这一句。"), + ("tut_status", _DET, "上滑,随时听进度:问了几份,疑点几处。"), + ("tut_confront", _DET, "按住屏幕,进入指证。去找两句对不上的证词。"), + ("tut_cf_try", _DET, "练习一次。选一份证词,单击确认;再选一份,任意一句。"), + ("tut_cf_ok", _DET, "指证就是这样。练习不算数,真矛盾要靠耳朵找。"), + ("tut_done", _DET, "教学完了。开始查案。"), + ("tut_skip", _DET, "已跳过教学。左右滑换卷宗,单击听取。"), + # ---- G2 状态/条目 ---- + ("st_asked_0", _DET, "六份卷宗,一份都没听。"), + ("st_asked_1", _DET, "已听一份,还有五份。"), + ("st_asked_2", _DET, "已听两份,还有四份。"), + ("st_asked_3", _DET, "已听三份,还有三份。"), + ("st_asked_4", _DET, "已听四份,还有两份。"), + ("st_asked_5", _DET, "已听五份,还有一份。"), + ("st_asked_6", _DET, "六份都听完了。"), + ("st_doubt_0", _DET, "疑点,还没有。"), + ("st_doubt_1", _DET, "疑点,一处。"), + ("st_doubt_2", _DET, "疑点,两处。"), + ("st_doubt_3", _DET, "疑点,三处。"), + ("st_doubt_4", _DET, "疑点,四处,齐了。"), + ("item_w1", _DET, "门卫老葛的证词。"), + ("item_w2", _DET, "学徒小顺的证词。"), + ("item_w3", _DET, "账房周先生的证词。"), + ("item_w4", _DET, "房东温太太的证词。"), + ("item_scene", _DET, "现场勘验。"), + ("item_fore", _DET, "法医报告。"), + ("item_case", _DET, "结案陈词。"), + ("intro_w3", _DET, "周先生,管了十年的账,见人三分笑。"), + # ---- G3 现场勘验(探长自勘) ---- + ("sc_0", _DET, "你打伞进了后院。雨水顺着瓦檐,淌成一道帘子。"), + ("sc_1", _DET, "柴房门口,沈万川面朝下倒着。后脑一处伤,现场没有凶器。"), + ("sc_2", _DET, "后门门闩完好,是死者自己开的门。生人叫不开这扇门。"), + ("sc_3", _DET, "泥地上只有进来的脚印,没有出去的。墙根的花盆,翻了一个。"), + ("sc_4", _DET, "库房铜秤立在架上,秤砣不见了。"), + ("sc_5", _DET, "柴房墙上,挂钥匙的铜钩,空着。"), + # ---- G4 指证 ---- + ("cf_start", _DET, "指证矛盾。先选第一份证词。左右滑换,单击确认。"), + ("cf_pick_line", _DET, "左右滑,一句一句听。"), + ("cf_confirm", _DET, "就这句?单击,定下来。"), + ("cf_second", _DET, "再选与它矛盾的那份证词。"), + ("cf_self", _DET, "同一句证词,和自己比不出矛盾。"), + ("cf_same", _DET, "这两句说得一致,不矛盾。"), + ("cf_diff", _DET, "这两句,说的不是一件事。"), + ("cf_hit", _DET, "对不上!矛盾成立,记入疑点。"), + # ---- G5 结案三问 ---- + ("fin_start", _DET, "结案陈词。先指认真凶:四位证人,选一个。"), + ("fin_who_confirm", _DET, "就指认他?单击,定下来。"), + ("fin_clear", _DET, "不在场证明成立。证据放人,回到证词板。"), + ("fin_right", _DET, "证据链闭合。周先生,你还有什么话说?"), + ("fin_right_b", _DET, "证据链闭合。小顺,你还有什么话说?"), + ("fin_ask_weapon", _DET, "第二问:凶器是什么?"), + ("wp_1", _DET, "松木劈柴。"), + ("wp_2", _DET, "铜秤砣。"), + ("wp_3", _DET, "铁门闩。"), + ("wp_4", _DET, "青瓷花盆。"), + ("fin_wp_right", _DET, "对。创口的铜屑,不见了的秤砣,都指向它。"), + ("fin_wp_wrong", _DET, "不对。铜屑不答应。记下了。"), + ("fin_ask_motive", _DET, "第三问:动机是什么?"), + ("mv_1", _DET, "监守自盗。"), + ("mv_2", _DET, "寻仇泄愤。"), + ("mv_3", _DET, "劫财入户。"), + ("fin_mv_right", _DET, "对。库里短的东西,就是引线。"), + ("fin_mv_wrong", _DET, "不对。账上短的东西,才是引线。记下了。"), + # ---- G6 结算 ---- + ("settle_open", _DET, "案件归档。雨,停了。"), + ("rate_s", _DET, "教科书结案。四处疑点,一击即中。"), + ("rate_a", _DET, "干净利落的结案。"), + ("rate_b", _DET, "案子结了,卷宗上有疏漏。"), + ("rate_c", _DET, "案子是结了,卷宗上补丁不少。"), + ("st_miss_0", _DET, "指证,一次没错。"), + ("st_miss_1", _DET, "指证,错过一两回。"), + ("st_miss_2", _DET, "指证,错了不少。"), + ("settle_hook", _DET, "同一场雨里,还有别的案子。单击,接新案。"), + # ---- G7 看门狗/卡关/纠错 ---- + ("gd_board", _DET, "还在证词板。左右滑,换卷宗。"), + ("gd_cf", _DET, "还在选证词。左右滑,单击确认。"), + ("gd_fin", _DET, "在想?左右滑选,单击定。"), + ("gd_invalid", _DET, "这个操作,这里用不上。"), + ("hint_a", _DET, "探长,还没听的卷宗,先过一遍耳朵。"), + ("hint_b", _DET, "探长,证词里有对不上的地方,逐句比对。"), + ("hint_c1", _DET, "试试指证:周先生说熄了灯,温太太说灯还亮着。"), + ("hint_c2", _DET, "试试指证:周先生说铜秤卖了,现场铜秤还在。"), + ("b_hint_c1", _DET, "试试指证:小顺说熄了灯,温太太说灯还亮着。"), + ("b_hint_c2", _DET, "试试指证:小顺说铜秤卖了,现场铜秤还在。"), + ("hint_fin", _DET, "疑点齐了。去结案陈词。"), + ("recap_go", _DET, "接着查。"), +] + +# NPC 单音色平铺(id, speaker, instruct, 台词)→ web/detective/audio/voice/.* +# 证人 4 + 秦法医(≤5 约束);同一角色 speaker+instruct 恒定;不加滤波(18b 头注)。 +DETECTIVE_NPC = [ + # 老葛(62 岁门卫,Eric) + ("w1_1", "Eric", "苍老沙哑的男声,守夜人,慢悠悠,带倦意", "昨儿夜里雨大,我守着街门,一步没敢挪窝。"), + ("w1_2", "Eric", "苍老沙哑的男声,守夜人,慢悠悠,带倦意", "九点四十,雨点子刚砸下来,小顺打着伞出的街门。"), + ("w1_3", "Eric", "苍老沙哑的男声,守夜人,慢悠悠,带倦意", "十点一刻,一位先生收着黑伞进了后巷。个儿高,微驼背。"), + ("w1_4", "Eric", "苍老沙哑的男声,守夜人,慢悠悠,带倦意", "九点五十,周账房出的街门,跟我说了声,回家。"), + ("w1_5", "Eric", "苍老沙哑的男声,守夜人,慢悠悠,带倦意", "后半夜,温太太还来门房借过一回热水。"), + ("w1_6", "Eric", "苍老沙哑的男声,守夜人,慢悠悠,带倦意", "听说店里账上短了银子,沈老板撂了话,月底清账。"), + ("acc_ge", "Eric", "苍老沙哑的男声,守夜人,慢悠悠,带倦意", "我守了一夜门!温太太来借热水,她能作证!"), + # 小顺(19 岁学徒,Dylan) + ("w2_1", "Dylan", "年轻清亮的男声,学徒,怯生生的,慌张诚实", "夜里,师父打发我去城南,给刘掌柜送匣子。"), + ("w2_2", "Dylan", "年轻清亮的男声,学徒,怯生生的,慌张诚实", "雨太大,刘掌柜留我住的,今早才回店。"), + ("w2_3", "Dylan", "年轻清亮的男声,学徒,怯生生的,慌张诚实", "库房钥匙?师父嫌沉,平日就挂在柴房墙上的铜钩。"), + ("w2_4", "Dylan", "年轻清亮的男声,学徒,怯生生的,慌张诚实", "师父的账,向来只信周先生一个人。"), + ("w2_5", "Dylan", "年轻清亮的男声,学徒,怯生生的,慌张诚实", "后门那闩,生人叫不开。熟人来了,师父才亲自去开。"), + ("w2_6", "Dylan", "年轻清亮的男声,学徒,怯生生的,慌张诚实", "周先生常念叨,账房的活,越到夜里越清静。"), + ("acc_shun", "Dylan", "年轻清亮的男声,学徒,怯生生的,慌张诚实", "我宿在城南刘掌柜家,他全家都能作证!"), + ("b_shun_1", "Dylan", "年轻清亮的男声,学徒,怯生生的,慌张诚实", "我走的时候,顺手熄了店堂的灯。"), + ("b_shun_2", "Dylan", "年轻清亮的男声,学徒,怯生生的,慌张诚实", "钥匙,师父贴身带着,从不离身。"), + ("b_shun_3", "Dylan", "年轻清亮的男声,学徒,怯生生的,慌张诚实", "那伤,八成是柴房劈柴砸的。铜秤早卖给收废品的了。"), + ("b_shun_4", "Dylan", "年轻清亮的男声,学徒,怯生生的,慌张诚实", "库房的货,我一件都没碰过。"), + ("b_shun_react_1", "Dylan", "年轻清亮的男声,学徒,怯生生的,慌张诚实", "灯?我、我走的时候明明熄了……"), + ("b_shun_react_2", "Dylan", "年轻清亮的男声,学徒,怯生生的,慌张诚实", "别问了……别问了……"), + ("b_shun_break_1", "Dylan", "年轻清亮的男声,学徒,怯生生的,慌张诚实", "匣子是我送的,可我半路折回来了……"), + ("b_shun_break_2", "Dylan", "年轻清亮的男声,学徒,怯生生的,慌张诚实", "我把棉袄反穿,弓着背,葛大爷没认出我。"), + ("b_shun_break_3", "Dylan", "年轻清亮的男声,学徒,怯生生的,慌张诚实", "库房的货,我拿过三件换了钱。他说要对账……秤砣下去的时候,他看着我。"), + # 周先生(45 岁账房,pack A 真凶,Uncle_Fu——与 male 旁白同 speaker,靠签名+instruct 区分) + ("z_1", "Uncle_Fu", "平板谨慎的男声,账房,字斟句酌,被追问时渐慌", "我七点进店上夜账。九点五十对完,锁了前门回家。"), + ("z_2", "Uncle_Fu", "平板谨慎的男声,账房,字斟句酌,被追问时渐慌", "临走,我顺手熄了店堂的灯。那会儿,店里就剩老板。"), + ("z_3", "Uncle_Fu", "平板谨慎的男声,账房,字斟句酌,被追问时渐慌", "库房钥匙?沈老板贴身带着,从不离身。"), + ("z_4", "Uncle_Fu", "平板谨慎的男声,账房,字斟句酌,被追问时渐慌", "那伤,是松木劈柴砸的。铜秤?上个月卖给收废品的了。"), + ("z_5", "Uncle_Fu", "平板谨慎的男声,账房,字斟句酌,被追问时渐慌", "我到家倒头就睡,夜里的事,一概不知。"), + ("z_6", "Uncle_Fu", "平板谨慎的男声,账房,字斟句酌,被追问时渐慌", "你们要是不信,去问我家婆娘。"), + ("z_react_1", "Uncle_Fu", "平板谨慎的男声,账房,字斟句酌,被追问时渐慌", "灯?……许是我记岔了。人上了岁数,记性不济。"), + ("z_react_2", "Uncle_Fu", "平板谨慎的男声,账房,字斟句酌,被追问时渐慌", "钥匙的事……他、他有时候,也贴身带着的……"), + ("z_react_3", "Uncle_Fu", "平板谨慎的男声,账房,字斟句酌,被追问时渐慌", "秤!秤的事,你们别再问了……"), + ("z_react_4", "Uncle_Fu", "平板谨慎的男声,账房,字斟句酌,被追问时渐慌", "……随你们怎么问。我要回去了。"), + ("acc_zhou", "Uncle_Fu", "平板谨慎的男声,账房,字斟句酌,被追问时渐慌", "我十点就到家了!我家婆娘能作证!"), + ("z_break_1", "Uncle_Fu", "平板谨慎的男声,账房,字斟句酌,被追问时渐慌", "别念了……我说。库房短的三件货,是我抵了赌债。"), + ("z_break_2", "Uncle_Fu", "平板谨慎的男声,账房,字斟句酌,被追问时渐慌", "他说月底清账,要报官。我求他,他不肯。"), + ("z_break_3", "Uncle_Fu", "平板谨慎的男声,账房,字斟句酌,被追问时渐慌", "我只想把钥匙放回去,把被盗做真。抡起秤砣的时候,他认出了我。"), + ("b_zhou_1", "Uncle_Fu", "平板谨慎的男声,账房,字斟句酌,被追问时渐慌", "我对完账就回家了,夜里雨大,路上没停。"), + ("b_zhou_2", "Uncle_Fu", "平板谨慎的男声,账房,字斟句酌,被追问时渐慌", "钥匙?平时就挂在柴房铜钩上,十年了。"), + ("b_zhou_3", "Uncle_Fu", "平板谨慎的男声,账房,字斟句酌,被追问时渐慌", "柴房我常去,没什么不对劲的。"), + # 温太太(50 岁房东,Vivian) + ("wen_1", "Vivian", "快嘴泼辣的中年女声,房东太太,嗓门亮,热心肠", "雨是九点四十下的。我正收晾的衣裳,记得真真的。"), + ("wen_2", "Vivian", "快嘴泼辣的中年女声,房东太太,嗓门亮,热心肠", "十点二十,我出门倒垃圾。聚宝斋店堂的灯,亮着呢。"), + ("wen_3", "Vivian", "快嘴泼辣的中年女声,房东太太,嗓门亮,热心肠", "刚回屋,柴房后墙咯噔一响,有人爬墙!花盆翻了一个。"), + ("wen_4", "Vivian", "快嘴泼辣的中年女声,房东太太,嗓门亮,热心肠", "吓得我蒙头半宿没睡着,天亮才敢吱声。"), + ("wen_5", "Vivian", "快嘴泼辣的中年女声,房东太太,嗓门亮,热心肠", "店里短银子的事,街坊都有耳闻。上礼拜他还摔了账本。"), + ("wen_6", "Vivian", "快嘴泼辣的中年女声,房东太太,嗓门亮,热心肠", "周账房那人,见人三分笑,账算得比谁都精。"), + ("acc_wen", "Vivian", "快嘴泼辣的中年女声,房东太太,嗓门亮,热心肠", "我这把年纪翻墙?先问问那墙答不答应!"), + # 秦法医(Serena——与 female 旁白同 speaker,靠签名+报告语境区分) + ("fo_1", "Serena", "冷静干练的女声,验尸官,临床腔,短句平直", "验尸报告,念给你听。"), + ("fo_2", "Serena", "冷静干练的女声,验尸官,临床腔,短句平直", "死亡时间,昨夜十点到十点半。"), + ("fo_3", "Serena", "冷静干练的女声,验尸官,临床腔,短句平直", "后脑单次钝器伤,一击毙命。创口嵌有铜屑,凶器是铜器。"), + ("fo_4", "Serena", "冷静干练的女声,验尸官,临床腔,短句平直", "死者双手干净,没有搏斗痕迹。"), + ("fo_5", "Serena", "冷静干练的女声,验尸官,临床腔,短句平直", "鞋底是干的。当夜,死者没出过后门。"), +] + +NPCS = {"rpg": RPG_NPC, "radio": RADIO_NPC, "abyss": ABYSS_NPC, "detective": DETECTIVE_NPC} # ---------------------------------------------------------------- 键盘变体(P1 路线 a) # 「生效输入模式=键盘」的玩家听按键措辞:向左滑→按左方向键(四向同理,WASD 不进 @@ -1092,6 +1453,34 @@ KEY_LINES = { ("gd_turn", "轮到你了。按空格挥击。"), ("settle_again", "按空格,再探一次。"), ], + # abyss:11 对(17-abyss-voice-ids.md §G0.a 9 对 + §D1 dv1_teach/p1;措辞「左箭头/右箭头」 + # 「上箭头/下箭头」为文档原文,与 dungeon 的「左方向键」系两文档各自定稿措辞,不改写)。 + "abyss": [ + ("gd_brief", "按空格继续简报。要选择时,用左右方向键。"), + ("gd_continue", "按空格,继续。"), + ("gd_again", "还在吗?按空格继续。"), + ("gd_ping1", "声呐待命。按空格,发脉冲。"), + ("gd_ping2", "回波不会自己来。按空格。"), + ("sys_help", "空格继续,回车重新听。上箭头听状态,下箭头开菜单。选择时,左右箭头。"), + ("sys_menu", "菜单。空格切换选项,回车确认。"), + ("sys_skip", "想跳过这句,按空格。"), + ("sys_continue_ask", "检测到你的下潜记录。空格继续上次,右箭头开始新救援。"), + ("dv1_teach", "到深度了。声呐静了。按空格,发一枚脉冲。"), + ("p1", "管路在响。左箭头,停下来听敲管的暗号。右箭头,先带小舟上浮。"), + ], + # detective:V 组 10 对(18-detective-voice-ids.md 文末;_k 逻辑 id 落 key/ 子目录)。 + "detective": [ + ("sys_help", "方向键换卷宗,空格听取,回车重新听。按住空格指证,上键状态,下键菜单。"), + ("tut_d0", "先熟悉证词板。左右方向键,换卷宗。"), + ("tut_listen", "空格,听取当前卷宗。"), + ("tut_replay", "回车,重新听这一句。"), + ("tut_confront", "按住空格,进入指证。"), + ("gd_board", "还在证词板。左右方向键,换卷宗。"), + ("gd_cf", "还在选证词。左右方向键,空格确认。"), + ("cf_pick_line", "左右方向键,一句一句听。"), + ("cf_confirm", "就这句?空格,定下来。"), + ("settle_hook", "同一场雨里,还有别的案子。按空格,接新案。"), + ], } @@ -1121,6 +1510,12 @@ VOICE_SPK = {"radio": {}} VOICE_SPK["radio"].update({fid: ("Uncle_Fu", "Serena") for fid, _i, _t in RADIO_HOST_LINES}) VOICE_SPK["radio"].update({fid: ("Dylan", "Vivian") for fid, _i, _t in RADIO_PROD_LINES}) +# abyss 双性别行的逐句 speaker 覆盖(DIVER=潜航员玩家 / DECK=母船调度,17b 录制说明: +# female 侧玩家 Serena/调度 Vivian、male 侧玩家 Uncle_Fu/调度 Dylan,同场景永不同 speaker)。 +VOICE_SPK["abyss"] = {} +VOICE_SPK["abyss"].update({fid: ("Uncle_Fu", "Serena") for fid, _i, _t in ABYSS_DIVER_LINES}) +VOICE_SPK["abyss"].update({fid: ("Dylan", "Vivian") for fid, _i, _t in ABYSS_DECK_LINES}) + # radio 来电者电话滤波(15-radio.md §6.2,全部 + 轻压缩):按 NPC id 前缀分档, # convert 转码时应用;staging wav 保留未滤波原声以便重滤/回退(宁窄勿脏)。 NPC_BANDPASS = { @@ -1130,7 +1525,16 @@ NPC_BANDPASS = { ("k1_", "highpass=f=450,lowpass=f=3000"), # 糖糖:家用座机,小而近 ("w1_", "highpass=f=420,lowpass=f=3100"), # 陈姐:户外手机,风噪 ("h1_", "highpass=f=300,lowpass=f=3400"), # 老韩:车载蓝牙,宽而轰 - ] + ], + # abyss 船员水声电台滤波(17-abyss.md §5.3/17b 头注,全部 + 轻压缩): + # 「电台里的人」与「耳机里的人」靠音质差立住;d1_log 是小舟(z1_ 前缀外单独登记)。 + "abyss": [ + ("z1_", "highpass=f=420,lowpass=f=3200"), # 小舟:年轻女声,窄带 + ("d1_log", "highpass=f=420,lowpass=f=3200"), + ("bai_", "highpass=f=350,lowpass=f=3000"), # 老白:沙哑中年男,中窄带 + ("chen_", "highpass=f=400,lowpass=f=3100"), # 陈医官:平静女声 + ("gao_", "highpass=f=300,lowpass=f=2800"), # 高船长:苍老男声,最低最窄 + ], } _NPC_ACOMP = "acompressor=threshold=-20dB:ratio=3:attack=10:release=150" # 轻压缩(全档) @@ -1152,6 +1556,8 @@ GAMES = { "rpg": RPG_LINES, "radio": RADIO_LINES, "dungeon": DUNGEON_LINES, + "abyss": ABYSS_LINES, + "detective": DETECTIVE_LINES, } diff --git a/tools/mureka_log.md b/tools/mureka_log.md index 1749e3a0..72e8c44b 100644 --- a/tools/mureka_log.md +++ b/tools/mureka_log.md @@ -144,3 +144,32 @@ boss=候选 0(149.5s,choice 158552331321345)。 radio onair/bridge/ending 与 dungeon 4 首按各设计文档用 I=-20(成品 mean -20.4 ~ -21.3dB)。 本作无节奏关键曲目(dungeon battle/boss 的 bpm 仅为特征词,不做节拍网格同步),未跑 bpm_precise。 flac 原件存 tools/mureka_out/{radio,dungeon}-*/。 + +## 2026-09-09 批次 2:abyss《深海回音》+ detective《雨夜专案》BGM(7 首) + +预算线:本轮 $0.30(abyss 4 + detective 3 × $0.03);总预算 $2.00。后处理脚本入库: +`web/abyss/gen/gen_bgm.sh` / `web/detective/gen/gen_bgm.sh`(首个强 onset 起点 → 93s 裁剪 + +首尾 3s acrossfade(tri) 无缝循环 → loudnorm → 44.1kHz → ogg q:a 3 + m4a 128k,成品均 90.00s)。 + +| # | task id | 用途 | 状态 | 花费 | +|---|---|---|---|---| +| 30 | 160001236664321 | abyss bgm/deck(2 候选) | succeeded | $0.03 | +| 31 | 160001429798913 | abyss bgm/dive(2 候选) | succeeded | $0.03 | +| 32 | 160001576402945 | abyss bgm/comms(2 候选) | succeeded | $0.03 | +| 33 | 160001717043203 | abyss bgm/ending(2 候选) | succeeded | $0.03 | +| 34 | 160001918435329 | detective bgm/menu(2 候选) | succeeded | $0.03 | +| 35 | 160002065039361 | detective bgm/board(2 候选) | succeeded | $0.03 | +| 36 | 160002209873922 | detective bgm/verdict(2 候选) | succeeded | $0.03 | + +**本轮花费:$0.21 / $0.30。累计:$1.08 / $2.00。** 7 首全部一次成功无重试。 + +选用(拿不准选时长更接近 90s 的候选):abyss deck=候选 0(192.1s,choice 160001381367809); +dive=候选 0(179.8s,choice 160001536622595);comms=候选 1(168.7s,choice 160001689714693); +ending=候选 0(191.4s,choice 160001815478273);detective menu=候选 1(219.8s); +board=候选 0(216.8s);verdict=候选 1(223.7s)。 + +响度:abyss 4 首与 detective 3 首全部 I=-20(17-abyss.md §5.1 未设独立 menu 曲——MENU 复用 +deck 低响度淡入由游戏层控制;18-detective.md §8.1 明确 3 首 -20 LUFS)。成品 mean: +abyss -20.5 ~ -21.9dB;detective -20.2 ~ -21.7dB,与在库 BGM 同量级。 +雨声底按 18 号文档烤进 detective 曲目(省第三声流)。无节奏关键曲目,未跑 bpm_precise。 +flac 原件存 tools/mureka_out/{abyss,detective}-*/。 diff --git a/web/abyss/audio/bgm/comms.m4a b/web/abyss/audio/bgm/comms.m4a new file mode 100644 index 00000000..2dc81ec7 Binary files /dev/null and b/web/abyss/audio/bgm/comms.m4a differ diff --git a/web/abyss/audio/bgm/comms.ogg b/web/abyss/audio/bgm/comms.ogg new file mode 100644 index 00000000..a3a2bad9 Binary files /dev/null and b/web/abyss/audio/bgm/comms.ogg differ diff --git a/web/abyss/audio/bgm/deck.m4a b/web/abyss/audio/bgm/deck.m4a new file mode 100644 index 00000000..43992d1a Binary files /dev/null and b/web/abyss/audio/bgm/deck.m4a differ diff --git a/web/abyss/audio/bgm/deck.ogg b/web/abyss/audio/bgm/deck.ogg new file mode 100644 index 00000000..b9e728c7 Binary files /dev/null and b/web/abyss/audio/bgm/deck.ogg differ diff --git a/web/abyss/audio/bgm/dive.m4a b/web/abyss/audio/bgm/dive.m4a new file mode 100644 index 00000000..dd4f9a43 Binary files /dev/null and b/web/abyss/audio/bgm/dive.m4a differ diff --git a/web/abyss/audio/bgm/dive.ogg b/web/abyss/audio/bgm/dive.ogg new file mode 100644 index 00000000..73df9684 Binary files /dev/null and b/web/abyss/audio/bgm/dive.ogg differ diff --git a/web/abyss/audio/bgm/ending.m4a b/web/abyss/audio/bgm/ending.m4a new file mode 100644 index 00000000..a2ae4d8f Binary files /dev/null and b/web/abyss/audio/bgm/ending.m4a differ diff --git a/web/abyss/audio/bgm/ending.ogg b/web/abyss/audio/bgm/ending.ogg new file mode 100644 index 00000000..ccaa008b Binary files /dev/null and b/web/abyss/audio/bgm/ending.ogg differ diff --git a/web/abyss/audio/sfx/amb_deck.m4a b/web/abyss/audio/sfx/amb_deck.m4a new file mode 100644 index 00000000..d156a20a Binary files /dev/null and b/web/abyss/audio/sfx/amb_deck.m4a differ diff --git a/web/abyss/audio/sfx/amb_deck.ogg b/web/abyss/audio/sfx/amb_deck.ogg new file mode 100644 index 00000000..df347922 Binary files /dev/null and b/web/abyss/audio/sfx/amb_deck.ogg differ diff --git a/web/abyss/audio/sfx/amb_deep.m4a b/web/abyss/audio/sfx/amb_deep.m4a new file mode 100644 index 00000000..df76a13b Binary files /dev/null and b/web/abyss/audio/sfx/amb_deep.m4a differ diff --git a/web/abyss/audio/sfx/amb_deep.ogg b/web/abyss/audio/sfx/amb_deep.ogg new file mode 100644 index 00000000..b498a940 Binary files /dev/null and b/web/abyss/audio/sfx/amb_deep.ogg differ diff --git a/web/abyss/audio/sfx/ascent_chime.m4a b/web/abyss/audio/sfx/ascent_chime.m4a new file mode 100644 index 00000000..01047d46 Binary files /dev/null and b/web/abyss/audio/sfx/ascent_chime.m4a differ diff --git a/web/abyss/audio/sfx/ascent_chime.ogg b/web/abyss/audio/sfx/ascent_chime.ogg new file mode 100644 index 00000000..c2bc4c7d Binary files /dev/null and b/web/abyss/audio/sfx/ascent_chime.ogg differ diff --git a/web/abyss/audio/sfx/bell_toll.m4a b/web/abyss/audio/sfx/bell_toll.m4a new file mode 100644 index 00000000..9afb92e9 Binary files /dev/null and b/web/abyss/audio/sfx/bell_toll.m4a differ diff --git a/web/abyss/audio/sfx/bell_toll.ogg b/web/abyss/audio/sfx/bell_toll.ogg new file mode 100644 index 00000000..bb5182ee Binary files /dev/null and b/web/abyss/audio/sfx/bell_toll.ogg differ diff --git a/web/abyss/audio/sfx/breath_loop.m4a b/web/abyss/audio/sfx/breath_loop.m4a new file mode 100644 index 00000000..3445df8a Binary files /dev/null and b/web/abyss/audio/sfx/breath_loop.m4a differ diff --git a/web/abyss/audio/sfx/breath_loop.ogg b/web/abyss/audio/sfx/breath_loop.ogg new file mode 100644 index 00000000..5c3f60c1 Binary files /dev/null and b/web/abyss/audio/sfx/breath_loop.ogg differ diff --git a/web/abyss/audio/sfx/echo_debris.m4a b/web/abyss/audio/sfx/echo_debris.m4a new file mode 100644 index 00000000..1b9754e2 Binary files /dev/null and b/web/abyss/audio/sfx/echo_debris.m4a differ diff --git a/web/abyss/audio/sfx/echo_debris.ogg b/web/abyss/audio/sfx/echo_debris.ogg new file mode 100644 index 00000000..6ee88e5a Binary files /dev/null and b/web/abyss/audio/sfx/echo_debris.ogg differ diff --git a/web/abyss/audio/sfx/echo_groan.m4a b/web/abyss/audio/sfx/echo_groan.m4a new file mode 100644 index 00000000..79fe0d2a Binary files /dev/null and b/web/abyss/audio/sfx/echo_groan.m4a differ diff --git a/web/abyss/audio/sfx/echo_groan.ogg b/web/abyss/audio/sfx/echo_groan.ogg new file mode 100644 index 00000000..4b2aa2ac Binary files /dev/null and b/web/abyss/audio/sfx/echo_groan.ogg differ diff --git a/web/abyss/audio/sfx/echo_metal_l.m4a b/web/abyss/audio/sfx/echo_metal_l.m4a new file mode 100644 index 00000000..406eb6b4 Binary files /dev/null and b/web/abyss/audio/sfx/echo_metal_l.m4a differ diff --git a/web/abyss/audio/sfx/echo_metal_l.ogg b/web/abyss/audio/sfx/echo_metal_l.ogg new file mode 100644 index 00000000..a80d823e Binary files /dev/null and b/web/abyss/audio/sfx/echo_metal_l.ogg differ diff --git a/web/abyss/audio/sfx/echo_metal_r.m4a b/web/abyss/audio/sfx/echo_metal_r.m4a new file mode 100644 index 00000000..fec9d909 Binary files /dev/null and b/web/abyss/audio/sfx/echo_metal_r.m4a differ diff --git a/web/abyss/audio/sfx/echo_metal_r.ogg b/web/abyss/audio/sfx/echo_metal_r.ogg new file mode 100644 index 00000000..8f0a2a68 Binary files /dev/null and b/web/abyss/audio/sfx/echo_metal_r.ogg differ diff --git a/web/abyss/audio/sfx/landslide.m4a b/web/abyss/audio/sfx/landslide.m4a new file mode 100644 index 00000000..1e79fc8c Binary files /dev/null and b/web/abyss/audio/sfx/landslide.m4a differ diff --git a/web/abyss/audio/sfx/landslide.ogg b/web/abyss/audio/sfx/landslide.ogg new file mode 100644 index 00000000..18bb956b Binary files /dev/null and b/web/abyss/audio/sfx/landslide.ogg differ diff --git a/web/abyss/audio/sfx/link_beep.m4a b/web/abyss/audio/sfx/link_beep.m4a new file mode 100644 index 00000000..2ab2f3c0 Binary files /dev/null and b/web/abyss/audio/sfx/link_beep.m4a differ diff --git a/web/abyss/audio/sfx/link_beep.ogg b/web/abyss/audio/sfx/link_beep.ogg new file mode 100644 index 00000000..f28e2685 Binary files /dev/null and b/web/abyss/audio/sfx/link_beep.ogg differ diff --git a/web/abyss/audio/sfx/pipe_knock.m4a b/web/abyss/audio/sfx/pipe_knock.m4a new file mode 100644 index 00000000..7c92de75 Binary files /dev/null and b/web/abyss/audio/sfx/pipe_knock.m4a differ diff --git a/web/abyss/audio/sfx/pipe_knock.ogg b/web/abyss/audio/sfx/pipe_knock.ogg new file mode 100644 index 00000000..5a1208ed Binary files /dev/null and b/web/abyss/audio/sfx/pipe_knock.ogg differ diff --git a/web/abyss/audio/sfx/sonar_ping.m4a b/web/abyss/audio/sfx/sonar_ping.m4a new file mode 100644 index 00000000..7336dc55 Binary files /dev/null and b/web/abyss/audio/sfx/sonar_ping.m4a differ diff --git a/web/abyss/audio/sfx/sonar_ping.ogg b/web/abyss/audio/sfx/sonar_ping.ogg new file mode 100644 index 00000000..ba639cd5 Binary files /dev/null and b/web/abyss/audio/sfx/sonar_ping.ogg differ diff --git a/web/abyss/audio/sfx/spark_flash.m4a b/web/abyss/audio/sfx/spark_flash.m4a new file mode 100644 index 00000000..19f19741 Binary files /dev/null and b/web/abyss/audio/sfx/spark_flash.m4a differ diff --git a/web/abyss/audio/sfx/spark_flash.ogg b/web/abyss/audio/sfx/spark_flash.ogg new file mode 100644 index 00000000..94aff97d Binary files /dev/null and b/web/abyss/audio/sfx/spark_flash.ogg differ diff --git a/web/abyss/audio/sfx/static_burst.m4a b/web/abyss/audio/sfx/static_burst.m4a new file mode 100644 index 00000000..45a7e6b2 Binary files /dev/null and b/web/abyss/audio/sfx/static_burst.m4a differ diff --git a/web/abyss/audio/sfx/static_burst.ogg b/web/abyss/audio/sfx/static_burst.ogg new file mode 100644 index 00000000..2688a54e Binary files /dev/null and b/web/abyss/audio/sfx/static_burst.ogg differ diff --git a/web/abyss/audio/sfx/surf_splash.m4a b/web/abyss/audio/sfx/surf_splash.m4a new file mode 100644 index 00000000..f461d134 Binary files /dev/null and b/web/abyss/audio/sfx/surf_splash.m4a differ diff --git a/web/abyss/audio/sfx/surf_splash.ogg b/web/abyss/audio/sfx/surf_splash.ogg new file mode 100644 index 00000000..9036a2d0 Binary files /dev/null and b/web/abyss/audio/sfx/surf_splash.ogg differ diff --git a/web/abyss/audio/sfx/ui_confirm.m4a b/web/abyss/audio/sfx/ui_confirm.m4a new file mode 100644 index 00000000..ee8e839a Binary files /dev/null and b/web/abyss/audio/sfx/ui_confirm.m4a differ diff --git a/web/abyss/audio/sfx/ui_confirm.ogg b/web/abyss/audio/sfx/ui_confirm.ogg new file mode 100644 index 00000000..a242eaf4 Binary files /dev/null and b/web/abyss/audio/sfx/ui_confirm.ogg differ diff --git a/web/abyss/audio/sfx/ui_menu.m4a b/web/abyss/audio/sfx/ui_menu.m4a new file mode 100644 index 00000000..57fc83b1 Binary files /dev/null and b/web/abyss/audio/sfx/ui_menu.m4a differ diff --git a/web/abyss/audio/sfx/ui_menu.ogg b/web/abyss/audio/sfx/ui_menu.ogg new file mode 100644 index 00000000..90ba1ad9 Binary files /dev/null and b/web/abyss/audio/sfx/ui_menu.ogg differ diff --git a/web/abyss/audio/sfx/valve_close.m4a b/web/abyss/audio/sfx/valve_close.m4a new file mode 100644 index 00000000..0acbf871 Binary files /dev/null and b/web/abyss/audio/sfx/valve_close.m4a differ diff --git a/web/abyss/audio/sfx/valve_close.ogg b/web/abyss/audio/sfx/valve_close.ogg new file mode 100644 index 00000000..a9b2a6b9 Binary files /dev/null and b/web/abyss/audio/sfx/valve_close.ogg differ diff --git a/web/abyss/audio/voice/bai_a.m4a b/web/abyss/audio/voice/bai_a.m4a new file mode 100644 index 00000000..4183ca7f Binary files /dev/null and b/web/abyss/audio/voice/bai_a.m4a differ diff --git a/web/abyss/audio/voice/bai_a.ogg b/web/abyss/audio/voice/bai_a.ogg new file mode 100644 index 00000000..bc7dbc02 Binary files /dev/null and b/web/abyss/audio/voice/bai_a.ogg differ diff --git a/web/abyss/audio/voice/bai_b.m4a b/web/abyss/audio/voice/bai_b.m4a new file mode 100644 index 00000000..220f684c Binary files /dev/null and b/web/abyss/audio/voice/bai_b.m4a differ diff --git a/web/abyss/audio/voice/bai_b.ogg b/web/abyss/audio/voice/bai_b.ogg new file mode 100644 index 00000000..01332a83 Binary files /dev/null and b/web/abyss/audio/voice/bai_b.ogg differ diff --git a/web/abyss/audio/voice/bai_out.m4a b/web/abyss/audio/voice/bai_out.m4a new file mode 100644 index 00000000..2aaed6bb Binary files /dev/null and b/web/abyss/audio/voice/bai_out.m4a differ diff --git a/web/abyss/audio/voice/bai_out.ogg b/web/abyss/audio/voice/bai_out.ogg new file mode 100644 index 00000000..5f6b2b59 Binary files /dev/null and b/web/abyss/audio/voice/bai_out.ogg differ diff --git a/web/abyss/audio/voice/bai_thanks.m4a b/web/abyss/audio/voice/bai_thanks.m4a new file mode 100644 index 00000000..eff3db7f Binary files /dev/null and b/web/abyss/audio/voice/bai_thanks.m4a differ diff --git a/web/abyss/audio/voice/bai_thanks.ogg b/web/abyss/audio/voice/bai_thanks.ogg new file mode 100644 index 00000000..af361eea Binary files /dev/null and b/web/abyss/audio/voice/bai_thanks.ogg differ diff --git a/web/abyss/audio/voice/chen_a.m4a b/web/abyss/audio/voice/chen_a.m4a new file mode 100644 index 00000000..370f4e3b Binary files /dev/null and b/web/abyss/audio/voice/chen_a.m4a differ diff --git a/web/abyss/audio/voice/chen_a.ogg b/web/abyss/audio/voice/chen_a.ogg new file mode 100644 index 00000000..f878bf1d Binary files /dev/null and b/web/abyss/audio/voice/chen_a.ogg differ diff --git a/web/abyss/audio/voice/chen_b.m4a b/web/abyss/audio/voice/chen_b.m4a new file mode 100644 index 00000000..f3f89e92 Binary files /dev/null and b/web/abyss/audio/voice/chen_b.m4a differ diff --git a/web/abyss/audio/voice/chen_b.ogg b/web/abyss/audio/voice/chen_b.ogg new file mode 100644 index 00000000..dfa721c0 Binary files /dev/null and b/web/abyss/audio/voice/chen_b.ogg differ diff --git a/web/abyss/audio/voice/chen_last.m4a b/web/abyss/audio/voice/chen_last.m4a new file mode 100644 index 00000000..f53d4742 Binary files /dev/null and b/web/abyss/audio/voice/chen_last.m4a differ diff --git a/web/abyss/audio/voice/chen_last.ogg b/web/abyss/audio/voice/chen_last.ogg new file mode 100644 index 00000000..1db301d2 Binary files /dev/null and b/web/abyss/audio/voice/chen_last.ogg differ diff --git a/web/abyss/audio/voice/chen_stay.m4a b/web/abyss/audio/voice/chen_stay.m4a new file mode 100644 index 00000000..e12c8dc0 Binary files /dev/null and b/web/abyss/audio/voice/chen_stay.m4a differ diff --git a/web/abyss/audio/voice/chen_stay.ogg b/web/abyss/audio/voice/chen_stay.ogg new file mode 100644 index 00000000..8d65d2d7 Binary files /dev/null and b/web/abyss/audio/voice/chen_stay.ogg differ diff --git a/web/abyss/audio/voice/chen_up.m4a b/web/abyss/audio/voice/chen_up.m4a new file mode 100644 index 00000000..5a2a6c79 Binary files /dev/null and b/web/abyss/audio/voice/chen_up.m4a differ diff --git a/web/abyss/audio/voice/chen_up.ogg b/web/abyss/audio/voice/chen_up.ogg new file mode 100644 index 00000000..4610c755 Binary files /dev/null and b/web/abyss/audio/voice/chen_up.ogg differ diff --git a/web/abyss/audio/voice/d1_log.m4a b/web/abyss/audio/voice/d1_log.m4a new file mode 100644 index 00000000..41cc9487 Binary files /dev/null and b/web/abyss/audio/voice/d1_log.m4a differ diff --git a/web/abyss/audio/voice/d1_log.ogg b/web/abyss/audio/voice/d1_log.ogg new file mode 100644 index 00000000..079fcead Binary files /dev/null and b/web/abyss/audio/voice/d1_log.ogg differ diff --git a/web/abyss/audio/voice/female/b_go.m4a b/web/abyss/audio/voice/female/b_go.m4a new file mode 100644 index 00000000..cce4bdf2 Binary files /dev/null and b/web/abyss/audio/voice/female/b_go.m4a differ diff --git a/web/abyss/audio/voice/female/b_go.ogg b/web/abyss/audio/voice/female/b_go.ogg new file mode 100644 index 00000000..68aeb6fc Binary files /dev/null and b/web/abyss/audio/voice/female/b_go.ogg differ diff --git a/web/abyss/audio/voice/female/b_goal.m4a b/web/abyss/audio/voice/female/b_goal.m4a new file mode 100644 index 00000000..030321f6 Binary files /dev/null and b/web/abyss/audio/voice/female/b_goal.m4a differ diff --git a/web/abyss/audio/voice/female/b_goal.ogg b/web/abyss/audio/voice/female/b_goal.ogg new file mode 100644 index 00000000..5f7a2664 Binary files /dev/null and b/web/abyss/audio/voice/female/b_goal.ogg differ diff --git a/web/abyss/audio/voice/female/b_open.m4a b/web/abyss/audio/voice/female/b_open.m4a new file mode 100644 index 00000000..33234881 Binary files /dev/null and b/web/abyss/audio/voice/female/b_open.m4a differ diff --git a/web/abyss/audio/voice/female/b_open.ogg b/web/abyss/audio/voice/female/b_open.ogg new file mode 100644 index 00000000..5f504c2f Binary files /dev/null and b/web/abyss/audio/voice/female/b_open.ogg differ diff --git a/web/abyss/audio/voice/female/b_rule.m4a b/web/abyss/audio/voice/female/b_rule.m4a new file mode 100644 index 00000000..0ee2b76a Binary files /dev/null and b/web/abyss/audio/voice/female/b_rule.m4a differ diff --git a/web/abyss/audio/voice/female/b_rule.ogg b/web/abyss/audio/voice/female/b_rule.ogg new file mode 100644 index 00000000..042db90c Binary files /dev/null and b/web/abyss/audio/voice/female/b_rule.ogg differ diff --git a/web/abyss/audio/voice/female/d1_code.m4a b/web/abyss/audio/voice/female/d1_code.m4a new file mode 100644 index 00000000..4fe09763 Binary files /dev/null and b/web/abyss/audio/voice/female/d1_code.m4a differ diff --git a/web/abyss/audio/voice/female/d1_code.ogg b/web/abyss/audio/voice/female/d1_code.ogg new file mode 100644 index 00000000..721a0145 Binary files /dev/null and b/web/abyss/audio/voice/female/d1_code.ogg differ diff --git a/web/abyss/audio/voice/female/d1_listen.m4a b/web/abyss/audio/voice/female/d1_listen.m4a new file mode 100644 index 00000000..69fe97c9 Binary files /dev/null and b/web/abyss/audio/voice/female/d1_listen.m4a differ diff --git a/web/abyss/audio/voice/female/d1_listen.ogg b/web/abyss/audio/voice/female/d1_listen.ogg new file mode 100644 index 00000000..9d6b3736 Binary files /dev/null and b/web/abyss/audio/voice/female/d1_listen.ogg differ diff --git a/web/abyss/audio/voice/female/d1_nolog.m4a b/web/abyss/audio/voice/female/d1_nolog.m4a new file mode 100644 index 00000000..4a3306c4 Binary files /dev/null and b/web/abyss/audio/voice/female/d1_nolog.m4a differ diff --git a/web/abyss/audio/voice/female/d1_nolog.ogg b/web/abyss/audio/voice/female/d1_nolog.ogg new file mode 100644 index 00000000..095be4a1 Binary files /dev/null and b/web/abyss/audio/voice/female/d1_nolog.ogg differ diff --git a/web/abyss/audio/voice/female/d1_surf.m4a b/web/abyss/audio/voice/female/d1_surf.m4a new file mode 100644 index 00000000..e5241096 Binary files /dev/null and b/web/abyss/audio/voice/female/d1_surf.m4a differ diff --git a/web/abyss/audio/voice/female/d1_surf.ogg b/web/abyss/audio/voice/female/d1_surf.ogg new file mode 100644 index 00000000..69c3df8d Binary files /dev/null and b/web/abyss/audio/voice/female/d1_surf.ogg differ diff --git a/web/abyss/audio/voice/female/d1_up.m4a b/web/abyss/audio/voice/female/d1_up.m4a new file mode 100644 index 00000000..780af901 Binary files /dev/null and b/web/abyss/audio/voice/female/d1_up.m4a differ diff --git a/web/abyss/audio/voice/female/d1_up.ogg b/web/abyss/audio/voice/female/d1_up.ogg new file mode 100644 index 00000000..79c0155e Binary files /dev/null and b/web/abyss/audio/voice/female/d1_up.ogg differ diff --git a/web/abyss/audio/voice/female/d2_cut.m4a b/web/abyss/audio/voice/female/d2_cut.m4a new file mode 100644 index 00000000..36d18a76 Binary files /dev/null and b/web/abyss/audio/voice/female/d2_cut.m4a differ diff --git a/web/abyss/audio/voice/female/d2_cut.ogg b/web/abyss/audio/voice/female/d2_cut.ogg new file mode 100644 index 00000000..876103a9 Binary files /dev/null and b/web/abyss/audio/voice/female/d2_cut.ogg differ diff --git a/web/abyss/audio/voice/female/d2_flash.m4a b/web/abyss/audio/voice/female/d2_flash.m4a new file mode 100644 index 00000000..292c5324 Binary files /dev/null and b/web/abyss/audio/voice/female/d2_flash.m4a differ diff --git a/web/abyss/audio/voice/female/d2_flash.ogg b/web/abyss/audio/voice/female/d2_flash.ogg new file mode 100644 index 00000000..25e9c9ff Binary files /dev/null and b/web/abyss/audio/voice/female/d2_flash.ogg differ diff --git a/web/abyss/audio/voice/female/d2_leave.m4a b/web/abyss/audio/voice/female/d2_leave.m4a new file mode 100644 index 00000000..05673f95 Binary files /dev/null and b/web/abyss/audio/voice/female/d2_leave.m4a differ diff --git a/web/abyss/audio/voice/female/d2_leave.ogg b/web/abyss/audio/voice/female/d2_leave.ogg new file mode 100644 index 00000000..72d017d9 Binary files /dev/null and b/web/abyss/audio/voice/female/d2_leave.ogg differ diff --git a/web/abyss/audio/voice/female/d2_pry.m4a b/web/abyss/audio/voice/female/d2_pry.m4a new file mode 100644 index 00000000..cb61a213 Binary files /dev/null and b/web/abyss/audio/voice/female/d2_pry.m4a differ diff --git a/web/abyss/audio/voice/female/d2_pry.ogg b/web/abyss/audio/voice/female/d2_pry.ogg new file mode 100644 index 00000000..735406a8 Binary files /dev/null and b/web/abyss/audio/voice/female/d2_pry.ogg differ diff --git a/web/abyss/audio/voice/female/d2_pry_ok.m4a b/web/abyss/audio/voice/female/d2_pry_ok.m4a new file mode 100644 index 00000000..a78cb068 Binary files /dev/null and b/web/abyss/audio/voice/female/d2_pry_ok.m4a differ diff --git a/web/abyss/audio/voice/female/d2_pry_ok.ogg b/web/abyss/audio/voice/female/d2_pry_ok.ogg new file mode 100644 index 00000000..a09e0888 Binary files /dev/null and b/web/abyss/audio/voice/female/d2_pry_ok.ogg differ diff --git a/web/abyss/audio/voice/female/d2_pry_slow.m4a b/web/abyss/audio/voice/female/d2_pry_slow.m4a new file mode 100644 index 00000000..4318954d Binary files /dev/null and b/web/abyss/audio/voice/female/d2_pry_slow.m4a differ diff --git a/web/abyss/audio/voice/female/d2_pry_slow.ogg b/web/abyss/audio/voice/female/d2_pry_slow.ogg new file mode 100644 index 00000000..1596f4dc Binary files /dev/null and b/web/abyss/audio/voice/female/d2_pry_slow.ogg differ diff --git a/web/abyss/audio/voice/female/d2_supply.m4a b/web/abyss/audio/voice/female/d2_supply.m4a new file mode 100644 index 00000000..98c857e0 Binary files /dev/null and b/web/abyss/audio/voice/female/d2_supply.m4a differ diff --git a/web/abyss/audio/voice/female/d2_supply.ogg b/web/abyss/audio/voice/female/d2_supply.ogg new file mode 100644 index 00000000..18fa2355 Binary files /dev/null and b/web/abyss/audio/voice/female/d2_supply.ogg differ diff --git a/web/abyss/audio/voice/female/d2_surf.m4a b/web/abyss/audio/voice/female/d2_surf.m4a new file mode 100644 index 00000000..acca7f18 Binary files /dev/null and b/web/abyss/audio/voice/female/d2_surf.m4a differ diff --git a/web/abyss/audio/voice/female/d2_surf.ogg b/web/abyss/audio/voice/female/d2_surf.ogg new file mode 100644 index 00000000..51edcc5d Binary files /dev/null and b/web/abyss/audio/voice/female/d2_surf.ogg differ diff --git a/web/abyss/audio/voice/female/d2_valve.m4a b/web/abyss/audio/voice/female/d2_valve.m4a new file mode 100644 index 00000000..704ccda4 Binary files /dev/null and b/web/abyss/audio/voice/female/d2_valve.m4a differ diff --git a/web/abyss/audio/voice/female/d2_valve.ogg b/web/abyss/audio/voice/female/d2_valve.ogg new file mode 100644 index 00000000..d13ac4fa Binary files /dev/null and b/web/abyss/audio/voice/female/d2_valve.ogg differ diff --git a/web/abyss/audio/voice/female/d2_valve_e.m4a b/web/abyss/audio/voice/female/d2_valve_e.m4a new file mode 100644 index 00000000..b3ed794a Binary files /dev/null and b/web/abyss/audio/voice/female/d2_valve_e.m4a differ diff --git a/web/abyss/audio/voice/female/d2_valve_e.ogg b/web/abyss/audio/voice/female/d2_valve_e.ogg new file mode 100644 index 00000000..ef630dc3 Binary files /dev/null and b/web/abyss/audio/voice/female/d2_valve_e.ogg differ diff --git a/web/abyss/audio/voice/female/d3_chen.m4a b/web/abyss/audio/voice/female/d3_chen.m4a new file mode 100644 index 00000000..023f89d2 Binary files /dev/null and b/web/abyss/audio/voice/female/d3_chen.m4a differ diff --git a/web/abyss/audio/voice/female/d3_chen.ogg b/web/abyss/audio/voice/female/d3_chen.ogg new file mode 100644 index 00000000..7ba2948d Binary files /dev/null and b/web/abyss/audio/voice/female/d3_chen.ogg differ diff --git a/web/abyss/audio/voice/female/d3_chen_e.m4a b/web/abyss/audio/voice/female/d3_chen_e.m4a new file mode 100644 index 00000000..1fc934ed Binary files /dev/null and b/web/abyss/audio/voice/female/d3_chen_e.m4a differ diff --git a/web/abyss/audio/voice/female/d3_chen_e.ogg b/web/abyss/audio/voice/female/d3_chen_e.ogg new file mode 100644 index 00000000..c400801a Binary files /dev/null and b/web/abyss/audio/voice/female/d3_chen_e.ogg differ diff --git a/web/abyss/audio/voice/female/d3_promise.m4a b/web/abyss/audio/voice/female/d3_promise.m4a new file mode 100644 index 00000000..45df5bd1 Binary files /dev/null and b/web/abyss/audio/voice/female/d3_promise.m4a differ diff --git a/web/abyss/audio/voice/female/d3_promise.ogg b/web/abyss/audio/voice/female/d3_promise.ogg new file mode 100644 index 00000000..c4bf27dc Binary files /dev/null and b/web/abyss/audio/voice/female/d3_promise.ogg differ diff --git a/web/abyss/audio/voice/female/d3_spare.m4a b/web/abyss/audio/voice/female/d3_spare.m4a new file mode 100644 index 00000000..ef4d8bd8 Binary files /dev/null and b/web/abyss/audio/voice/female/d3_spare.m4a differ diff --git a/web/abyss/audio/voice/female/d3_spare.ogg b/web/abyss/audio/voice/female/d3_spare.ogg new file mode 100644 index 00000000..7b045e66 Binary files /dev/null and b/web/abyss/audio/voice/female/d3_spare.ogg differ diff --git a/web/abyss/audio/voice/female/d3_spare_e.m4a b/web/abyss/audio/voice/female/d3_spare_e.m4a new file mode 100644 index 00000000..a3380c8b Binary files /dev/null and b/web/abyss/audio/voice/female/d3_spare_e.m4a differ diff --git a/web/abyss/audio/voice/female/d3_spare_e.ogg b/web/abyss/audio/voice/female/d3_spare_e.ogg new file mode 100644 index 00000000..1a98be93 Binary files /dev/null and b/web/abyss/audio/voice/female/d3_spare_e.ogg differ diff --git a/web/abyss/audio/voice/female/d3_surf.m4a b/web/abyss/audio/voice/female/d3_surf.m4a new file mode 100644 index 00000000..c891db1d Binary files /dev/null and b/web/abyss/audio/voice/female/d3_surf.m4a differ diff --git a/web/abyss/audio/voice/female/d3_surf.ogg b/web/abyss/audio/voice/female/d3_surf.ogg new file mode 100644 index 00000000..f640e84b Binary files /dev/null and b/web/abyss/audio/voice/female/d3_surf.ogg differ diff --git a/web/abyss/audio/voice/female/d3_zheng.m4a b/web/abyss/audio/voice/female/d3_zheng.m4a new file mode 100644 index 00000000..a6379eb8 Binary files /dev/null and b/web/abyss/audio/voice/female/d3_zheng.m4a differ diff --git a/web/abyss/audio/voice/female/d3_zheng.ogg b/web/abyss/audio/voice/female/d3_zheng.ogg new file mode 100644 index 00000000..7e001026 Binary files /dev/null and b/web/abyss/audio/voice/female/d3_zheng.ogg differ diff --git a/web/abyss/audio/voice/female/d3_zheng_e.m4a b/web/abyss/audio/voice/female/d3_zheng_e.m4a new file mode 100644 index 00000000..d7376072 Binary files /dev/null and b/web/abyss/audio/voice/female/d3_zheng_e.m4a differ diff --git a/web/abyss/audio/voice/female/d3_zheng_e.ogg b/web/abyss/audio/voice/female/d3_zheng_e.ogg new file mode 100644 index 00000000..02997d6e Binary files /dev/null and b/web/abyss/audio/voice/female/d3_zheng_e.ogg differ diff --git a/web/abyss/audio/voice/female/d4_fast.m4a b/web/abyss/audio/voice/female/d4_fast.m4a new file mode 100644 index 00000000..32740c0c Binary files /dev/null and b/web/abyss/audio/voice/female/d4_fast.m4a differ diff --git a/web/abyss/audio/voice/female/d4_fast.ogg b/web/abyss/audio/voice/female/d4_fast.ogg new file mode 100644 index 00000000..cc622a72 Binary files /dev/null and b/web/abyss/audio/voice/female/d4_fast.ogg differ diff --git a/web/abyss/audio/voice/female/d4_fast_e.m4a b/web/abyss/audio/voice/female/d4_fast_e.m4a new file mode 100644 index 00000000..592b6fbe Binary files /dev/null and b/web/abyss/audio/voice/female/d4_fast_e.m4a differ diff --git a/web/abyss/audio/voice/female/d4_fast_e.ogg b/web/abyss/audio/voice/female/d4_fast_e.ogg new file mode 100644 index 00000000..d8de7604 Binary files /dev/null and b/web/abyss/audio/voice/female/d4_fast_e.ogg differ diff --git a/web/abyss/audio/voice/female/d4_pick_chen.m4a b/web/abyss/audio/voice/female/d4_pick_chen.m4a new file mode 100644 index 00000000..dea0e8c8 Binary files /dev/null and b/web/abyss/audio/voice/female/d4_pick_chen.m4a differ diff --git a/web/abyss/audio/voice/female/d4_pick_chen.ogg b/web/abyss/audio/voice/female/d4_pick_chen.ogg new file mode 100644 index 00000000..d435c2ad Binary files /dev/null and b/web/abyss/audio/voice/female/d4_pick_chen.ogg differ diff --git a/web/abyss/audio/voice/female/d4_pick_gao.m4a b/web/abyss/audio/voice/female/d4_pick_gao.m4a new file mode 100644 index 00000000..c1e61152 Binary files /dev/null and b/web/abyss/audio/voice/female/d4_pick_gao.m4a differ diff --git a/web/abyss/audio/voice/female/d4_pick_gao.ogg b/web/abyss/audio/voice/female/d4_pick_gao.ogg new file mode 100644 index 00000000..aefeff86 Binary files /dev/null and b/web/abyss/audio/voice/female/d4_pick_gao.ogg differ diff --git a/web/abyss/audio/voice/female/d4_pick_zheng.m4a b/web/abyss/audio/voice/female/d4_pick_zheng.m4a new file mode 100644 index 00000000..ca4174a4 Binary files /dev/null and b/web/abyss/audio/voice/female/d4_pick_zheng.m4a differ diff --git a/web/abyss/audio/voice/female/d4_pick_zheng.ogg b/web/abyss/audio/voice/female/d4_pick_zheng.ogg new file mode 100644 index 00000000..03f57284 Binary files /dev/null and b/web/abyss/audio/voice/female/d4_pick_zheng.ogg differ diff --git a/web/abyss/audio/voice/female/d4_silent.m4a b/web/abyss/audio/voice/female/d4_silent.m4a new file mode 100644 index 00000000..6f8befb1 Binary files /dev/null and b/web/abyss/audio/voice/female/d4_silent.m4a differ diff --git a/web/abyss/audio/voice/female/d4_silent.ogg b/web/abyss/audio/voice/female/d4_silent.ogg new file mode 100644 index 00000000..257795bb Binary files /dev/null and b/web/abyss/audio/voice/female/d4_silent.ogg differ diff --git a/web/abyss/audio/voice/female/d4_slow.m4a b/web/abyss/audio/voice/female/d4_slow.m4a new file mode 100644 index 00000000..630ffe88 Binary files /dev/null and b/web/abyss/audio/voice/female/d4_slow.m4a differ diff --git a/web/abyss/audio/voice/female/d4_slow.ogg b/web/abyss/audio/voice/female/d4_slow.ogg new file mode 100644 index 00000000..fd5ac6af Binary files /dev/null and b/web/abyss/audio/voice/female/d4_slow.ogg differ diff --git a/web/abyss/audio/voice/female/d4_slow_e.m4a b/web/abyss/audio/voice/female/d4_slow_e.m4a new file mode 100644 index 00000000..c6017131 Binary files /dev/null and b/web/abyss/audio/voice/female/d4_slow_e.m4a differ diff --git a/web/abyss/audio/voice/female/d4_slow_e.ogg b/web/abyss/audio/voice/female/d4_slow_e.ogg new file mode 100644 index 00000000..4dadf2cb Binary files /dev/null and b/web/abyss/audio/voice/female/d4_slow_e.ogg differ diff --git a/web/abyss/audio/voice/female/d4_stay_lost.m4a b/web/abyss/audio/voice/female/d4_stay_lost.m4a new file mode 100644 index 00000000..7030e45c Binary files /dev/null and b/web/abyss/audio/voice/female/d4_stay_lost.m4a differ diff --git a/web/abyss/audio/voice/female/d4_stay_lost.ogg b/web/abyss/audio/voice/female/d4_stay_lost.ogg new file mode 100644 index 00000000..0f8288af Binary files /dev/null and b/web/abyss/audio/voice/female/d4_stay_lost.ogg differ diff --git a/web/abyss/audio/voice/female/d4_surf.m4a b/web/abyss/audio/voice/female/d4_surf.m4a new file mode 100644 index 00000000..1de44ae3 Binary files /dev/null and b/web/abyss/audio/voice/female/d4_surf.m4a differ diff --git a/web/abyss/audio/voice/female/d4_surf.ogg b/web/abyss/audio/voice/female/d4_surf.ogg new file mode 100644 index 00000000..a4a115b5 Binary files /dev/null and b/web/abyss/audio/voice/female/d4_surf.ogg differ diff --git a/web/abyss/audio/voice/female/d4_talk.m4a b/web/abyss/audio/voice/female/d4_talk.m4a new file mode 100644 index 00000000..d6a26c01 Binary files /dev/null and b/web/abyss/audio/voice/female/d4_talk.m4a differ diff --git a/web/abyss/audio/voice/female/d4_talk.ogg b/web/abyss/audio/voice/female/d4_talk.ogg new file mode 100644 index 00000000..ffcf769f Binary files /dev/null and b/web/abyss/audio/voice/female/d4_talk.ogg differ diff --git a/web/abyss/audio/voice/female/d9_bell.m4a b/web/abyss/audio/voice/female/d9_bell.m4a new file mode 100644 index 00000000..328d7e1c Binary files /dev/null and b/web/abyss/audio/voice/female/d9_bell.m4a differ diff --git a/web/abyss/audio/voice/female/d9_bell.ogg b/web/abyss/audio/voice/female/d9_bell.ogg new file mode 100644 index 00000000..63a950bd Binary files /dev/null and b/web/abyss/audio/voice/female/d9_bell.ogg differ diff --git a/web/abyss/audio/voice/female/d9_bello.m4a b/web/abyss/audio/voice/female/d9_bello.m4a new file mode 100644 index 00000000..cb90b602 Binary files /dev/null and b/web/abyss/audio/voice/female/d9_bello.m4a differ diff --git a/web/abyss/audio/voice/female/d9_bello.ogg b/web/abyss/audio/voice/female/d9_bello.ogg new file mode 100644 index 00000000..84d71ed1 Binary files /dev/null and b/web/abyss/audio/voice/female/d9_bello.ogg differ diff --git a/web/abyss/audio/voice/female/d9_rest.m4a b/web/abyss/audio/voice/female/d9_rest.m4a new file mode 100644 index 00000000..241519ee Binary files /dev/null and b/web/abyss/audio/voice/female/d9_rest.m4a differ diff --git a/web/abyss/audio/voice/female/d9_rest.ogg b/web/abyss/audio/voice/female/d9_rest.ogg new file mode 100644 index 00000000..a890842d Binary files /dev/null and b/web/abyss/audio/voice/female/d9_rest.ogg differ diff --git a/web/abyss/audio/voice/female/dry_a.m4a b/web/abyss/audio/voice/female/dry_a.m4a new file mode 100644 index 00000000..c1ba7cb5 Binary files /dev/null and b/web/abyss/audio/voice/female/dry_a.m4a differ diff --git a/web/abyss/audio/voice/female/dry_a.ogg b/web/abyss/audio/voice/female/dry_a.ogg new file mode 100644 index 00000000..b643f9a5 Binary files /dev/null and b/web/abyss/audio/voice/female/dry_a.ogg differ diff --git a/web/abyss/audio/voice/female/dry_b.m4a b/web/abyss/audio/voice/female/dry_b.m4a new file mode 100644 index 00000000..bf64f44e Binary files /dev/null and b/web/abyss/audio/voice/female/dry_b.m4a differ diff --git a/web/abyss/audio/voice/female/dry_b.ogg b/web/abyss/audio/voice/female/dry_b.ogg new file mode 100644 index 00000000..1e28729b Binary files /dev/null and b/web/abyss/audio/voice/female/dry_b.ogg differ diff --git a/web/abyss/audio/voice/female/dv1_down.m4a b/web/abyss/audio/voice/female/dv1_down.m4a new file mode 100644 index 00000000..3b216ed1 Binary files /dev/null and b/web/abyss/audio/voice/female/dv1_down.m4a differ diff --git a/web/abyss/audio/voice/female/dv1_down.ogg b/web/abyss/audio/voice/female/dv1_down.ogg new file mode 100644 index 00000000..735c5f40 Binary files /dev/null and b/web/abyss/audio/voice/female/dv1_down.ogg differ diff --git a/web/abyss/audio/voice/female/dv1_link.m4a b/web/abyss/audio/voice/female/dv1_link.m4a new file mode 100644 index 00000000..476c831a Binary files /dev/null and b/web/abyss/audio/voice/female/dv1_link.m4a differ diff --git a/web/abyss/audio/voice/female/dv1_link.ogg b/web/abyss/audio/voice/female/dv1_link.ogg new file mode 100644 index 00000000..76e18ebe Binary files /dev/null and b/web/abyss/audio/voice/female/dv1_link.ogg differ diff --git a/web/abyss/audio/voice/female/dv1_metal.m4a b/web/abyss/audio/voice/female/dv1_metal.m4a new file mode 100644 index 00000000..61cd1016 Binary files /dev/null and b/web/abyss/audio/voice/female/dv1_metal.m4a differ diff --git a/web/abyss/audio/voice/female/dv1_metal.ogg b/web/abyss/audio/voice/female/dv1_metal.ogg new file mode 100644 index 00000000..411f6f50 Binary files /dev/null and b/web/abyss/audio/voice/female/dv1_metal.ogg differ diff --git a/web/abyss/audio/voice/female/dv1_teach.m4a b/web/abyss/audio/voice/female/dv1_teach.m4a new file mode 100644 index 00000000..8faf7ce9 Binary files /dev/null and b/web/abyss/audio/voice/female/dv1_teach.m4a differ diff --git a/web/abyss/audio/voice/female/dv1_teach.ogg b/web/abyss/audio/voice/female/dv1_teach.ogg new file mode 100644 index 00000000..13f51d38 Binary files /dev/null and b/web/abyss/audio/voice/female/dv1_teach.ogg differ diff --git a/web/abyss/audio/voice/female/dv2_down.m4a b/web/abyss/audio/voice/female/dv2_down.m4a new file mode 100644 index 00000000..4de69144 Binary files /dev/null and b/web/abyss/audio/voice/female/dv2_down.m4a differ diff --git a/web/abyss/audio/voice/female/dv2_down.ogg b/web/abyss/audio/voice/female/dv2_down.ogg new file mode 100644 index 00000000..b4021e32 Binary files /dev/null and b/web/abyss/audio/voice/female/dv2_down.ogg differ diff --git a/web/abyss/audio/voice/female/dv2_found.m4a b/web/abyss/audio/voice/female/dv2_found.m4a new file mode 100644 index 00000000..41b40b10 Binary files /dev/null and b/web/abyss/audio/voice/female/dv2_found.m4a differ diff --git a/web/abyss/audio/voice/female/dv2_found.ogg b/web/abyss/audio/voice/female/dv2_found.ogg new file mode 100644 index 00000000..ff4fd540 Binary files /dev/null and b/web/abyss/audio/voice/female/dv2_found.ogg differ diff --git a/web/abyss/audio/voice/female/dv3_down.m4a b/web/abyss/audio/voice/female/dv3_down.m4a new file mode 100644 index 00000000..dee4dce6 Binary files /dev/null and b/web/abyss/audio/voice/female/dv3_down.m4a differ diff --git a/web/abyss/audio/voice/female/dv3_down.ogg b/web/abyss/audio/voice/female/dv3_down.ogg new file mode 100644 index 00000000..cc2abbce Binary files /dev/null and b/web/abyss/audio/voice/female/dv3_down.ogg differ diff --git a/web/abyss/audio/voice/female/dv3_groan.m4a b/web/abyss/audio/voice/female/dv3_groan.m4a new file mode 100644 index 00000000..fe9e5634 Binary files /dev/null and b/web/abyss/audio/voice/female/dv3_groan.m4a differ diff --git a/web/abyss/audio/voice/female/dv3_groan.ogg b/web/abyss/audio/voice/female/dv3_groan.ogg new file mode 100644 index 00000000..a4425e1c Binary files /dev/null and b/web/abyss/audio/voice/female/dv3_groan.ogg differ diff --git a/web/abyss/audio/voice/female/dv4_down.m4a b/web/abyss/audio/voice/female/dv4_down.m4a new file mode 100644 index 00000000..595ebbb6 Binary files /dev/null and b/web/abyss/audio/voice/female/dv4_down.m4a differ diff --git a/web/abyss/audio/voice/female/dv4_down.ogg b/web/abyss/audio/voice/female/dv4_down.ogg new file mode 100644 index 00000000..22a919b5 Binary files /dev/null and b/web/abyss/audio/voice/female/dv4_down.ogg differ diff --git a/web/abyss/audio/voice/female/dv4_none.m4a b/web/abyss/audio/voice/female/dv4_none.m4a new file mode 100644 index 00000000..cf3b3cd5 Binary files /dev/null and b/web/abyss/audio/voice/female/dv4_none.m4a differ diff --git a/web/abyss/audio/voice/female/dv4_none.ogg b/web/abyss/audio/voice/female/dv4_none.ogg new file mode 100644 index 00000000..db6766bd Binary files /dev/null and b/web/abyss/audio/voice/female/dv4_none.ogg differ diff --git a/web/abyss/audio/voice/female/dv4_slide.m4a b/web/abyss/audio/voice/female/dv4_slide.m4a new file mode 100644 index 00000000..69a20a05 Binary files /dev/null and b/web/abyss/audio/voice/female/dv4_slide.m4a differ diff --git a/web/abyss/audio/voice/female/dv4_slide.ogg b/web/abyss/audio/voice/female/dv4_slide.ogg new file mode 100644 index 00000000..b634b1f1 Binary files /dev/null and b/web/abyss/audio/voice/female/dv4_slide.ogg differ diff --git a/web/abyss/audio/voice/female/e1_a.m4a b/web/abyss/audio/voice/female/e1_a.m4a new file mode 100644 index 00000000..43f12196 Binary files /dev/null and b/web/abyss/audio/voice/female/e1_a.m4a differ diff --git a/web/abyss/audio/voice/female/e1_a.ogg b/web/abyss/audio/voice/female/e1_a.ogg new file mode 100644 index 00000000..6553bab5 Binary files /dev/null and b/web/abyss/audio/voice/female/e1_a.ogg differ diff --git a/web/abyss/audio/voice/female/e1_b.m4a b/web/abyss/audio/voice/female/e1_b.m4a new file mode 100644 index 00000000..cad7b470 Binary files /dev/null and b/web/abyss/audio/voice/female/e1_b.m4a differ diff --git a/web/abyss/audio/voice/female/e1_b.ogg b/web/abyss/audio/voice/female/e1_b.ogg new file mode 100644 index 00000000..c12bbab8 Binary files /dev/null and b/web/abyss/audio/voice/female/e1_b.ogg differ diff --git a/web/abyss/audio/voice/female/e2_a.m4a b/web/abyss/audio/voice/female/e2_a.m4a new file mode 100644 index 00000000..8d976db4 Binary files /dev/null and b/web/abyss/audio/voice/female/e2_a.m4a differ diff --git a/web/abyss/audio/voice/female/e2_a.ogg b/web/abyss/audio/voice/female/e2_a.ogg new file mode 100644 index 00000000..e3409c55 Binary files /dev/null and b/web/abyss/audio/voice/female/e2_a.ogg differ diff --git a/web/abyss/audio/voice/female/e2_b.m4a b/web/abyss/audio/voice/female/e2_b.m4a new file mode 100644 index 00000000..869b4aa5 Binary files /dev/null and b/web/abyss/audio/voice/female/e2_b.m4a differ diff --git a/web/abyss/audio/voice/female/e2_b.ogg b/web/abyss/audio/voice/female/e2_b.ogg new file mode 100644 index 00000000..f9f97d89 Binary files /dev/null and b/web/abyss/audio/voice/female/e2_b.ogg differ diff --git a/web/abyss/audio/voice/female/e3_a.m4a b/web/abyss/audio/voice/female/e3_a.m4a new file mode 100644 index 00000000..1954137e Binary files /dev/null and b/web/abyss/audio/voice/female/e3_a.m4a differ diff --git a/web/abyss/audio/voice/female/e3_a.ogg b/web/abyss/audio/voice/female/e3_a.ogg new file mode 100644 index 00000000..b1bac152 Binary files /dev/null and b/web/abyss/audio/voice/female/e3_a.ogg differ diff --git a/web/abyss/audio/voice/female/e3_b.m4a b/web/abyss/audio/voice/female/e3_b.m4a new file mode 100644 index 00000000..3ab2cc3c Binary files /dev/null and b/web/abyss/audio/voice/female/e3_b.m4a differ diff --git a/web/abyss/audio/voice/female/e3_b.ogg b/web/abyss/audio/voice/female/e3_b.ogg new file mode 100644 index 00000000..539c8137 Binary files /dev/null and b/web/abyss/audio/voice/female/e3_b.ogg differ diff --git a/web/abyss/audio/voice/female/e4_a.m4a b/web/abyss/audio/voice/female/e4_a.m4a new file mode 100644 index 00000000..ce7ed7a3 Binary files /dev/null and b/web/abyss/audio/voice/female/e4_a.m4a differ diff --git a/web/abyss/audio/voice/female/e4_a.ogg b/web/abyss/audio/voice/female/e4_a.ogg new file mode 100644 index 00000000..bf874760 Binary files /dev/null and b/web/abyss/audio/voice/female/e4_a.ogg differ diff --git a/web/abyss/audio/voice/female/e4_b.m4a b/web/abyss/audio/voice/female/e4_b.m4a new file mode 100644 index 00000000..22334028 Binary files /dev/null and b/web/abyss/audio/voice/female/e4_b.m4a differ diff --git a/web/abyss/audio/voice/female/e4_b.ogg b/web/abyss/audio/voice/female/e4_b.ogg new file mode 100644 index 00000000..86452157 Binary files /dev/null and b/web/abyss/audio/voice/female/e4_b.ogg differ diff --git a/web/abyss/audio/voice/female/ep_bai_no.m4a b/web/abyss/audio/voice/female/ep_bai_no.m4a new file mode 100644 index 00000000..90a193a3 Binary files /dev/null and b/web/abyss/audio/voice/female/ep_bai_no.m4a differ diff --git a/web/abyss/audio/voice/female/ep_bai_no.ogg b/web/abyss/audio/voice/female/ep_bai_no.ogg new file mode 100644 index 00000000..5cc6c4b9 Binary files /dev/null and b/web/abyss/audio/voice/female/ep_bai_no.ogg differ diff --git a/web/abyss/audio/voice/female/ep_bai_ok.m4a b/web/abyss/audio/voice/female/ep_bai_ok.m4a new file mode 100644 index 00000000..af7276ab Binary files /dev/null and b/web/abyss/audio/voice/female/ep_bai_ok.m4a differ diff --git a/web/abyss/audio/voice/female/ep_bai_ok.ogg b/web/abyss/audio/voice/female/ep_bai_ok.ogg new file mode 100644 index 00000000..26502d9e Binary files /dev/null and b/web/abyss/audio/voice/female/ep_bai_ok.ogg differ diff --git a/web/abyss/audio/voice/female/ep_chen_lost.m4a b/web/abyss/audio/voice/female/ep_chen_lost.m4a new file mode 100644 index 00000000..2123cad2 Binary files /dev/null and b/web/abyss/audio/voice/female/ep_chen_lost.m4a differ diff --git a/web/abyss/audio/voice/female/ep_chen_lost.ogg b/web/abyss/audio/voice/female/ep_chen_lost.ogg new file mode 100644 index 00000000..42d59509 Binary files /dev/null and b/web/abyss/audio/voice/female/ep_chen_lost.ogg differ diff --git a/web/abyss/audio/voice/female/ep_chen_ok.m4a b/web/abyss/audio/voice/female/ep_chen_ok.m4a new file mode 100644 index 00000000..6791227e Binary files /dev/null and b/web/abyss/audio/voice/female/ep_chen_ok.m4a differ diff --git a/web/abyss/audio/voice/female/ep_chen_ok.ogg b/web/abyss/audio/voice/female/ep_chen_ok.ogg new file mode 100644 index 00000000..ea7dca2c Binary files /dev/null and b/web/abyss/audio/voice/female/ep_chen_ok.ogg differ diff --git a/web/abyss/audio/voice/female/ep_gao_lost.m4a b/web/abyss/audio/voice/female/ep_gao_lost.m4a new file mode 100644 index 00000000..1076eede Binary files /dev/null and b/web/abyss/audio/voice/female/ep_gao_lost.m4a differ diff --git a/web/abyss/audio/voice/female/ep_gao_lost.ogg b/web/abyss/audio/voice/female/ep_gao_lost.ogg new file mode 100644 index 00000000..5487335b Binary files /dev/null and b/web/abyss/audio/voice/female/ep_gao_lost.ogg differ diff --git a/web/abyss/audio/voice/female/ep_gao_peace.m4a b/web/abyss/audio/voice/female/ep_gao_peace.m4a new file mode 100644 index 00000000..d1f68c58 Binary files /dev/null and b/web/abyss/audio/voice/female/ep_gao_peace.m4a differ diff --git a/web/abyss/audio/voice/female/ep_gao_peace.ogg b/web/abyss/audio/voice/female/ep_gao_peace.ogg new file mode 100644 index 00000000..f6b0c2f3 Binary files /dev/null and b/web/abyss/audio/voice/female/ep_gao_peace.ogg differ diff --git a/web/abyss/audio/voice/female/ep_gao_up.m4a b/web/abyss/audio/voice/female/ep_gao_up.m4a new file mode 100644 index 00000000..f72fa0f3 Binary files /dev/null and b/web/abyss/audio/voice/female/ep_gao_up.m4a differ diff --git a/web/abyss/audio/voice/female/ep_gao_up.ogg b/web/abyss/audio/voice/female/ep_gao_up.ogg new file mode 100644 index 00000000..3e33a59e Binary files /dev/null and b/web/abyss/audio/voice/female/ep_gao_up.ogg differ diff --git a/web/abyss/audio/voice/female/ep_lead.m4a b/web/abyss/audio/voice/female/ep_lead.m4a new file mode 100644 index 00000000..a64d5f27 Binary files /dev/null and b/web/abyss/audio/voice/female/ep_lead.m4a differ diff --git a/web/abyss/audio/voice/female/ep_lead.ogg b/web/abyss/audio/voice/female/ep_lead.ogg new file mode 100644 index 00000000..ed7feccc Binary files /dev/null and b/web/abyss/audio/voice/female/ep_lead.ogg differ diff --git a/web/abyss/audio/voice/female/ep_trench_bell.m4a b/web/abyss/audio/voice/female/ep_trench_bell.m4a new file mode 100644 index 00000000..9998abfc Binary files /dev/null and b/web/abyss/audio/voice/female/ep_trench_bell.m4a differ diff --git a/web/abyss/audio/voice/female/ep_trench_bell.ogg b/web/abyss/audio/voice/female/ep_trench_bell.ogg new file mode 100644 index 00000000..d04e1581 Binary files /dev/null and b/web/abyss/audio/voice/female/ep_trench_bell.ogg differ diff --git a/web/abyss/audio/voice/female/ep_trench_deep.m4a b/web/abyss/audio/voice/female/ep_trench_deep.m4a new file mode 100644 index 00000000..8391db95 Binary files /dev/null and b/web/abyss/audio/voice/female/ep_trench_deep.m4a differ diff --git a/web/abyss/audio/voice/female/ep_trench_deep.ogg b/web/abyss/audio/voice/female/ep_trench_deep.ogg new file mode 100644 index 00000000..8698001e Binary files /dev/null and b/web/abyss/audio/voice/female/ep_trench_deep.ogg differ diff --git a/web/abyss/audio/voice/female/ep_zheng_lost.m4a b/web/abyss/audio/voice/female/ep_zheng_lost.m4a new file mode 100644 index 00000000..313b2319 Binary files /dev/null and b/web/abyss/audio/voice/female/ep_zheng_lost.m4a differ diff --git a/web/abyss/audio/voice/female/ep_zheng_lost.ogg b/web/abyss/audio/voice/female/ep_zheng_lost.ogg new file mode 100644 index 00000000..c7c9250d Binary files /dev/null and b/web/abyss/audio/voice/female/ep_zheng_lost.ogg differ diff --git a/web/abyss/audio/voice/female/ep_zheng_ok.m4a b/web/abyss/audio/voice/female/ep_zheng_ok.m4a new file mode 100644 index 00000000..bfa9239e Binary files /dev/null and b/web/abyss/audio/voice/female/ep_zheng_ok.m4a differ diff --git a/web/abyss/audio/voice/female/ep_zheng_ok.ogg b/web/abyss/audio/voice/female/ep_zheng_ok.ogg new file mode 100644 index 00000000..7e4a521f Binary files /dev/null and b/web/abyss/audio/voice/female/ep_zheng_ok.ogg differ diff --git a/web/abyss/audio/voice/female/ep_zhou_no.m4a b/web/abyss/audio/voice/female/ep_zhou_no.m4a new file mode 100644 index 00000000..3d16da54 Binary files /dev/null and b/web/abyss/audio/voice/female/ep_zhou_no.m4a differ diff --git a/web/abyss/audio/voice/female/ep_zhou_no.ogg b/web/abyss/audio/voice/female/ep_zhou_no.ogg new file mode 100644 index 00000000..70d88007 Binary files /dev/null and b/web/abyss/audio/voice/female/ep_zhou_no.ogg differ diff --git a/web/abyss/audio/voice/female/ep_zhou_ok.m4a b/web/abyss/audio/voice/female/ep_zhou_ok.m4a new file mode 100644 index 00000000..23a921d1 Binary files /dev/null and b/web/abyss/audio/voice/female/ep_zhou_ok.m4a differ diff --git a/web/abyss/audio/voice/female/ep_zhou_ok.ogg b/web/abyss/audio/voice/female/ep_zhou_ok.ogg new file mode 100644 index 00000000..149f5db3 Binary files /dev/null and b/web/abyss/audio/voice/female/ep_zhou_ok.ogg differ diff --git a/web/abyss/audio/voice/female/g9_dry.m4a b/web/abyss/audio/voice/female/g9_dry.m4a new file mode 100644 index 00000000..01d47deb Binary files /dev/null and b/web/abyss/audio/voice/female/g9_dry.m4a differ diff --git a/web/abyss/audio/voice/female/g9_dry.ogg b/web/abyss/audio/voice/female/g9_dry.ogg new file mode 100644 index 00000000..100f6a13 Binary files /dev/null and b/web/abyss/audio/voice/female/g9_dry.ogg differ diff --git a/web/abyss/audio/voice/female/g9_in.m4a b/web/abyss/audio/voice/female/g9_in.m4a new file mode 100644 index 00000000..bdbc38bb Binary files /dev/null and b/web/abyss/audio/voice/female/g9_in.m4a differ diff --git a/web/abyss/audio/voice/female/g9_in.ogg b/web/abyss/audio/voice/female/g9_in.ogg new file mode 100644 index 00000000..8904b621 Binary files /dev/null and b/web/abyss/audio/voice/female/g9_in.ogg differ diff --git a/web/abyss/audio/voice/female/g9_knot.m4a b/web/abyss/audio/voice/female/g9_knot.m4a new file mode 100644 index 00000000..59512c1d Binary files /dev/null and b/web/abyss/audio/voice/female/g9_knot.m4a differ diff --git a/web/abyss/audio/voice/female/g9_knot.ogg b/web/abyss/audio/voice/female/g9_knot.ogg new file mode 100644 index 00000000..b3c28fe5 Binary files /dev/null and b/web/abyss/audio/voice/female/g9_knot.ogg differ diff --git a/web/abyss/audio/voice/female/gd_again.m4a b/web/abyss/audio/voice/female/gd_again.m4a new file mode 100644 index 00000000..c2d16c0f Binary files /dev/null and b/web/abyss/audio/voice/female/gd_again.m4a differ diff --git a/web/abyss/audio/voice/female/gd_again.ogg b/web/abyss/audio/voice/female/gd_again.ogg new file mode 100644 index 00000000..22f9adb8 Binary files /dev/null and b/web/abyss/audio/voice/female/gd_again.ogg differ diff --git a/web/abyss/audio/voice/female/gd_brief.m4a b/web/abyss/audio/voice/female/gd_brief.m4a new file mode 100644 index 00000000..2618e776 Binary files /dev/null and b/web/abyss/audio/voice/female/gd_brief.m4a differ diff --git a/web/abyss/audio/voice/female/gd_brief.ogg b/web/abyss/audio/voice/female/gd_brief.ogg new file mode 100644 index 00000000..399d2a03 Binary files /dev/null and b/web/abyss/audio/voice/female/gd_brief.ogg differ diff --git a/web/abyss/audio/voice/female/gd_continue.m4a b/web/abyss/audio/voice/female/gd_continue.m4a new file mode 100644 index 00000000..7cb94048 Binary files /dev/null and b/web/abyss/audio/voice/female/gd_continue.m4a differ diff --git a/web/abyss/audio/voice/female/gd_continue.ogg b/web/abyss/audio/voice/female/gd_continue.ogg new file mode 100644 index 00000000..db776204 Binary files /dev/null and b/web/abyss/audio/voice/female/gd_continue.ogg differ diff --git a/web/abyss/audio/voice/female/gd_invalid.m4a b/web/abyss/audio/voice/female/gd_invalid.m4a new file mode 100644 index 00000000..ee6734cc Binary files /dev/null and b/web/abyss/audio/voice/female/gd_invalid.m4a differ diff --git a/web/abyss/audio/voice/female/gd_invalid.ogg b/web/abyss/audio/voice/female/gd_invalid.ogg new file mode 100644 index 00000000..eaea2602 Binary files /dev/null and b/web/abyss/audio/voice/female/gd_invalid.ogg differ diff --git a/web/abyss/audio/voice/female/gd_ping1.m4a b/web/abyss/audio/voice/female/gd_ping1.m4a new file mode 100644 index 00000000..942489af Binary files /dev/null and b/web/abyss/audio/voice/female/gd_ping1.m4a differ diff --git a/web/abyss/audio/voice/female/gd_ping1.ogg b/web/abyss/audio/voice/female/gd_ping1.ogg new file mode 100644 index 00000000..49ee8af6 Binary files /dev/null and b/web/abyss/audio/voice/female/gd_ping1.ogg differ diff --git a/web/abyss/audio/voice/female/gd_ping2.m4a b/web/abyss/audio/voice/female/gd_ping2.m4a new file mode 100644 index 00000000..68a68495 Binary files /dev/null and b/web/abyss/audio/voice/female/gd_ping2.m4a differ diff --git a/web/abyss/audio/voice/female/gd_ping2.ogg b/web/abyss/audio/voice/female/gd_ping2.ogg new file mode 100644 index 00000000..bb8acc6c Binary files /dev/null and b/web/abyss/audio/voice/female/gd_ping2.ogg differ diff --git a/web/abyss/audio/voice/female/gd_prog1.m4a b/web/abyss/audio/voice/female/gd_prog1.m4a new file mode 100644 index 00000000..766877a1 Binary files /dev/null and b/web/abyss/audio/voice/female/gd_prog1.m4a differ diff --git a/web/abyss/audio/voice/female/gd_prog1.ogg b/web/abyss/audio/voice/female/gd_prog1.ogg new file mode 100644 index 00000000..9f292b54 Binary files /dev/null and b/web/abyss/audio/voice/female/gd_prog1.ogg differ diff --git a/web/abyss/audio/voice/female/gd_prog2.m4a b/web/abyss/audio/voice/female/gd_prog2.m4a new file mode 100644 index 00000000..be743784 Binary files /dev/null and b/web/abyss/audio/voice/female/gd_prog2.m4a differ diff --git a/web/abyss/audio/voice/female/gd_prog2.ogg b/web/abyss/audio/voice/female/gd_prog2.ogg new file mode 100644 index 00000000..d1620716 Binary files /dev/null and b/web/abyss/audio/voice/female/gd_prog2.ogg differ diff --git a/web/abyss/audio/voice/female/key/dv1_teach.m4a b/web/abyss/audio/voice/female/key/dv1_teach.m4a new file mode 100644 index 00000000..af3e1ff1 Binary files /dev/null and b/web/abyss/audio/voice/female/key/dv1_teach.m4a differ diff --git a/web/abyss/audio/voice/female/key/dv1_teach.ogg b/web/abyss/audio/voice/female/key/dv1_teach.ogg new file mode 100644 index 00000000..016f754b Binary files /dev/null and b/web/abyss/audio/voice/female/key/dv1_teach.ogg differ diff --git a/web/abyss/audio/voice/female/key/gd_again.m4a b/web/abyss/audio/voice/female/key/gd_again.m4a new file mode 100644 index 00000000..4b339c0d Binary files /dev/null and b/web/abyss/audio/voice/female/key/gd_again.m4a differ diff --git a/web/abyss/audio/voice/female/key/gd_again.ogg b/web/abyss/audio/voice/female/key/gd_again.ogg new file mode 100644 index 00000000..eb93d5de Binary files /dev/null and b/web/abyss/audio/voice/female/key/gd_again.ogg differ diff --git a/web/abyss/audio/voice/female/key/gd_brief.m4a b/web/abyss/audio/voice/female/key/gd_brief.m4a new file mode 100644 index 00000000..75eaafc8 Binary files /dev/null and b/web/abyss/audio/voice/female/key/gd_brief.m4a differ diff --git a/web/abyss/audio/voice/female/key/gd_brief.ogg b/web/abyss/audio/voice/female/key/gd_brief.ogg new file mode 100644 index 00000000..601be62f Binary files /dev/null and b/web/abyss/audio/voice/female/key/gd_brief.ogg differ diff --git a/web/abyss/audio/voice/female/key/gd_continue.m4a b/web/abyss/audio/voice/female/key/gd_continue.m4a new file mode 100644 index 00000000..037d7fca Binary files /dev/null and b/web/abyss/audio/voice/female/key/gd_continue.m4a differ diff --git a/web/abyss/audio/voice/female/key/gd_continue.ogg b/web/abyss/audio/voice/female/key/gd_continue.ogg new file mode 100644 index 00000000..b18c377c Binary files /dev/null and b/web/abyss/audio/voice/female/key/gd_continue.ogg differ diff --git a/web/abyss/audio/voice/female/key/gd_ping1.m4a b/web/abyss/audio/voice/female/key/gd_ping1.m4a new file mode 100644 index 00000000..5ba98659 Binary files /dev/null and b/web/abyss/audio/voice/female/key/gd_ping1.m4a differ diff --git a/web/abyss/audio/voice/female/key/gd_ping1.ogg b/web/abyss/audio/voice/female/key/gd_ping1.ogg new file mode 100644 index 00000000..d77cab56 Binary files /dev/null and b/web/abyss/audio/voice/female/key/gd_ping1.ogg differ diff --git a/web/abyss/audio/voice/female/key/gd_ping2.m4a b/web/abyss/audio/voice/female/key/gd_ping2.m4a new file mode 100644 index 00000000..d0f7cb12 Binary files /dev/null and b/web/abyss/audio/voice/female/key/gd_ping2.m4a differ diff --git a/web/abyss/audio/voice/female/key/gd_ping2.ogg b/web/abyss/audio/voice/female/key/gd_ping2.ogg new file mode 100644 index 00000000..cb145186 Binary files /dev/null and b/web/abyss/audio/voice/female/key/gd_ping2.ogg differ diff --git a/web/abyss/audio/voice/female/key/p1.m4a b/web/abyss/audio/voice/female/key/p1.m4a new file mode 100644 index 00000000..80f07623 Binary files /dev/null and b/web/abyss/audio/voice/female/key/p1.m4a differ diff --git a/web/abyss/audio/voice/female/key/p1.ogg b/web/abyss/audio/voice/female/key/p1.ogg new file mode 100644 index 00000000..e22c6658 Binary files /dev/null and b/web/abyss/audio/voice/female/key/p1.ogg differ diff --git a/web/abyss/audio/voice/female/key/sys_continue_ask.m4a b/web/abyss/audio/voice/female/key/sys_continue_ask.m4a new file mode 100644 index 00000000..6c932cfa Binary files /dev/null and b/web/abyss/audio/voice/female/key/sys_continue_ask.m4a differ diff --git a/web/abyss/audio/voice/female/key/sys_continue_ask.ogg b/web/abyss/audio/voice/female/key/sys_continue_ask.ogg new file mode 100644 index 00000000..20d7f8eb Binary files /dev/null and b/web/abyss/audio/voice/female/key/sys_continue_ask.ogg differ diff --git a/web/abyss/audio/voice/female/key/sys_help.m4a b/web/abyss/audio/voice/female/key/sys_help.m4a new file mode 100644 index 00000000..c11cda87 Binary files /dev/null and b/web/abyss/audio/voice/female/key/sys_help.m4a differ diff --git a/web/abyss/audio/voice/female/key/sys_help.ogg b/web/abyss/audio/voice/female/key/sys_help.ogg new file mode 100644 index 00000000..fd661523 Binary files /dev/null and b/web/abyss/audio/voice/female/key/sys_help.ogg differ diff --git a/web/abyss/audio/voice/female/key/sys_menu.m4a b/web/abyss/audio/voice/female/key/sys_menu.m4a new file mode 100644 index 00000000..d5277e5a Binary files /dev/null and b/web/abyss/audio/voice/female/key/sys_menu.m4a differ diff --git a/web/abyss/audio/voice/female/key/sys_menu.ogg b/web/abyss/audio/voice/female/key/sys_menu.ogg new file mode 100644 index 00000000..33d79549 Binary files /dev/null and b/web/abyss/audio/voice/female/key/sys_menu.ogg differ diff --git a/web/abyss/audio/voice/female/key/sys_skip.m4a b/web/abyss/audio/voice/female/key/sys_skip.m4a new file mode 100644 index 00000000..469b0a4d Binary files /dev/null and b/web/abyss/audio/voice/female/key/sys_skip.m4a differ diff --git a/web/abyss/audio/voice/female/key/sys_skip.ogg b/web/abyss/audio/voice/female/key/sys_skip.ogg new file mode 100644 index 00000000..a227c18d Binary files /dev/null and b/web/abyss/audio/voice/female/key/sys_skip.ogg differ diff --git a/web/abyss/audio/voice/female/oxy_crit.m4a b/web/abyss/audio/voice/female/oxy_crit.m4a new file mode 100644 index 00000000..87c2aaf2 Binary files /dev/null and b/web/abyss/audio/voice/female/oxy_crit.m4a differ diff --git a/web/abyss/audio/voice/female/oxy_crit.ogg b/web/abyss/audio/voice/female/oxy_crit.ogg new file mode 100644 index 00000000..7979c184 Binary files /dev/null and b/web/abyss/audio/voice/female/oxy_crit.ogg differ diff --git a/web/abyss/audio/voice/female/oxy_full.m4a b/web/abyss/audio/voice/female/oxy_full.m4a new file mode 100644 index 00000000..c1566d6f Binary files /dev/null and b/web/abyss/audio/voice/female/oxy_full.m4a differ diff --git a/web/abyss/audio/voice/female/oxy_full.ogg b/web/abyss/audio/voice/female/oxy_full.ogg new file mode 100644 index 00000000..61dab094 Binary files /dev/null and b/web/abyss/audio/voice/female/oxy_full.ogg differ diff --git a/web/abyss/audio/voice/female/oxy_gain.m4a b/web/abyss/audio/voice/female/oxy_gain.m4a new file mode 100644 index 00000000..25a2802e Binary files /dev/null and b/web/abyss/audio/voice/female/oxy_gain.m4a differ diff --git a/web/abyss/audio/voice/female/oxy_gain.ogg b/web/abyss/audio/voice/female/oxy_gain.ogg new file mode 100644 index 00000000..0585d6ab Binary files /dev/null and b/web/abyss/audio/voice/female/oxy_gain.ogg differ diff --git a/web/abyss/audio/voice/female/oxy_low.m4a b/web/abyss/audio/voice/female/oxy_low.m4a new file mode 100644 index 00000000..67695b39 Binary files /dev/null and b/web/abyss/audio/voice/female/oxy_low.m4a differ diff --git a/web/abyss/audio/voice/female/oxy_low.ogg b/web/abyss/audio/voice/female/oxy_low.ogg new file mode 100644 index 00000000..7040c97e Binary files /dev/null and b/web/abyss/audio/voice/female/oxy_low.ogg differ diff --git a/web/abyss/audio/voice/female/oxy_ok.m4a b/web/abyss/audio/voice/female/oxy_ok.m4a new file mode 100644 index 00000000..4261ca18 Binary files /dev/null and b/web/abyss/audio/voice/female/oxy_ok.m4a differ diff --git a/web/abyss/audio/voice/female/oxy_ok.ogg b/web/abyss/audio/voice/female/oxy_ok.ogg new file mode 100644 index 00000000..631e662d Binary files /dev/null and b/web/abyss/audio/voice/female/oxy_ok.ogg differ diff --git a/web/abyss/audio/voice/female/p1.m4a b/web/abyss/audio/voice/female/p1.m4a new file mode 100644 index 00000000..38ff7463 Binary files /dev/null and b/web/abyss/audio/voice/female/p1.m4a differ diff --git a/web/abyss/audio/voice/female/p1.ogg b/web/abyss/audio/voice/female/p1.ogg new file mode 100644 index 00000000..2753befd Binary files /dev/null and b/web/abyss/audio/voice/female/p1.ogg differ diff --git a/web/abyss/audio/voice/female/p2.m4a b/web/abyss/audio/voice/female/p2.m4a new file mode 100644 index 00000000..0574c178 Binary files /dev/null and b/web/abyss/audio/voice/female/p2.m4a differ diff --git a/web/abyss/audio/voice/female/p2.ogg b/web/abyss/audio/voice/female/p2.ogg new file mode 100644 index 00000000..3fb827cd Binary files /dev/null and b/web/abyss/audio/voice/female/p2.ogg differ diff --git a/web/abyss/audio/voice/female/p3.m4a b/web/abyss/audio/voice/female/p3.m4a new file mode 100644 index 00000000..8e551e84 Binary files /dev/null and b/web/abyss/audio/voice/female/p3.m4a differ diff --git a/web/abyss/audio/voice/female/p3.ogg b/web/abyss/audio/voice/female/p3.ogg new file mode 100644 index 00000000..c506e24f Binary files /dev/null and b/web/abyss/audio/voice/female/p3.ogg differ diff --git a/web/abyss/audio/voice/female/p4.m4a b/web/abyss/audio/voice/female/p4.m4a new file mode 100644 index 00000000..bc43a490 Binary files /dev/null and b/web/abyss/audio/voice/female/p4.m4a differ diff --git a/web/abyss/audio/voice/female/p4.ogg b/web/abyss/audio/voice/female/p4.ogg new file mode 100644 index 00000000..ca3cd563 Binary files /dev/null and b/web/abyss/audio/voice/female/p4.ogg differ diff --git a/web/abyss/audio/voice/female/p5.m4a b/web/abyss/audio/voice/female/p5.m4a new file mode 100644 index 00000000..45b5abe8 Binary files /dev/null and b/web/abyss/audio/voice/female/p5.m4a differ diff --git a/web/abyss/audio/voice/female/p5.ogg b/web/abyss/audio/voice/female/p5.ogg new file mode 100644 index 00000000..302761da Binary files /dev/null and b/web/abyss/audio/voice/female/p5.ogg differ diff --git a/web/abyss/audio/voice/female/p6.m4a b/web/abyss/audio/voice/female/p6.m4a new file mode 100644 index 00000000..dfe4376a Binary files /dev/null and b/web/abyss/audio/voice/female/p6.m4a differ diff --git a/web/abyss/audio/voice/female/p6.ogg b/web/abyss/audio/voice/female/p6.ogg new file mode 100644 index 00000000..2cedd410 Binary files /dev/null and b/web/abyss/audio/voice/female/p6.ogg differ diff --git a/web/abyss/audio/voice/female/p7.m4a b/web/abyss/audio/voice/female/p7.m4a new file mode 100644 index 00000000..26a451a7 Binary files /dev/null and b/web/abyss/audio/voice/female/p7.m4a differ diff --git a/web/abyss/audio/voice/female/p7.ogg b/web/abyss/audio/voice/female/p7.ogg new file mode 100644 index 00000000..da952b59 Binary files /dev/null and b/web/abyss/audio/voice/female/p7.ogg differ diff --git a/web/abyss/audio/voice/female/p7_chen.m4a b/web/abyss/audio/voice/female/p7_chen.m4a new file mode 100644 index 00000000..44e1e11c Binary files /dev/null and b/web/abyss/audio/voice/female/p7_chen.m4a differ diff --git a/web/abyss/audio/voice/female/p7_chen.ogg b/web/abyss/audio/voice/female/p7_chen.ogg new file mode 100644 index 00000000..a08b7bb6 Binary files /dev/null and b/web/abyss/audio/voice/female/p7_chen.ogg differ diff --git a/web/abyss/audio/voice/female/p7_zheng.m4a b/web/abyss/audio/voice/female/p7_zheng.m4a new file mode 100644 index 00000000..6ec2d668 Binary files /dev/null and b/web/abyss/audio/voice/female/p7_zheng.m4a differ diff --git a/web/abyss/audio/voice/female/p7_zheng.ogg b/web/abyss/audio/voice/female/p7_zheng.ogg new file mode 100644 index 00000000..ad7ac585 Binary files /dev/null and b/web/abyss/audio/voice/female/p7_zheng.ogg differ diff --git a/web/abyss/audio/voice/female/p8.m4a b/web/abyss/audio/voice/female/p8.m4a new file mode 100644 index 00000000..e7e7718c Binary files /dev/null and b/web/abyss/audio/voice/female/p8.m4a differ diff --git a/web/abyss/audio/voice/female/p8.ogg b/web/abyss/audio/voice/female/p8.ogg new file mode 100644 index 00000000..ee210c8f Binary files /dev/null and b/web/abyss/audio/voice/female/p8.ogg differ diff --git a/web/abyss/audio/voice/female/p9.m4a b/web/abyss/audio/voice/female/p9.m4a new file mode 100644 index 00000000..275b6a4f Binary files /dev/null and b/web/abyss/audio/voice/female/p9.m4a differ diff --git a/web/abyss/audio/voice/female/p9.ogg b/web/abyss/audio/voice/female/p9.ogg new file mode 100644 index 00000000..36eda7cc Binary files /dev/null and b/web/abyss/audio/voice/female/p9.ogg differ diff --git a/web/abyss/audio/voice/female/prog_d1.m4a b/web/abyss/audio/voice/female/prog_d1.m4a new file mode 100644 index 00000000..72e7f688 Binary files /dev/null and b/web/abyss/audio/voice/female/prog_d1.m4a differ diff --git a/web/abyss/audio/voice/female/prog_d1.ogg b/web/abyss/audio/voice/female/prog_d1.ogg new file mode 100644 index 00000000..7eaddb89 Binary files /dev/null and b/web/abyss/audio/voice/female/prog_d1.ogg differ diff --git a/web/abyss/audio/voice/female/prog_d2.m4a b/web/abyss/audio/voice/female/prog_d2.m4a new file mode 100644 index 00000000..2e982a45 Binary files /dev/null and b/web/abyss/audio/voice/female/prog_d2.m4a differ diff --git a/web/abyss/audio/voice/female/prog_d2.ogg b/web/abyss/audio/voice/female/prog_d2.ogg new file mode 100644 index 00000000..104bb7c1 Binary files /dev/null and b/web/abyss/audio/voice/female/prog_d2.ogg differ diff --git a/web/abyss/audio/voice/female/prog_d3.m4a b/web/abyss/audio/voice/female/prog_d3.m4a new file mode 100644 index 00000000..3ce2c198 Binary files /dev/null and b/web/abyss/audio/voice/female/prog_d3.m4a differ diff --git a/web/abyss/audio/voice/female/prog_d3.ogg b/web/abyss/audio/voice/female/prog_d3.ogg new file mode 100644 index 00000000..8ee417b0 Binary files /dev/null and b/web/abyss/audio/voice/female/prog_d3.ogg differ diff --git a/web/abyss/audio/voice/female/prog_d4.m4a b/web/abyss/audio/voice/female/prog_d4.m4a new file mode 100644 index 00000000..f411f566 Binary files /dev/null and b/web/abyss/audio/voice/female/prog_d4.m4a differ diff --git a/web/abyss/audio/voice/female/prog_d4.ogg b/web/abyss/audio/voice/female/prog_d4.ogg new file mode 100644 index 00000000..65792f10 Binary files /dev/null and b/web/abyss/audio/voice/female/prog_d4.ogg differ diff --git a/web/abyss/audio/voice/female/sys_continue_ask.m4a b/web/abyss/audio/voice/female/sys_continue_ask.m4a new file mode 100644 index 00000000..ff41623c Binary files /dev/null and b/web/abyss/audio/voice/female/sys_continue_ask.m4a differ diff --git a/web/abyss/audio/voice/female/sys_continue_ask.ogg b/web/abyss/audio/voice/female/sys_continue_ask.ogg new file mode 100644 index 00000000..22b6f114 Binary files /dev/null and b/web/abyss/audio/voice/female/sys_continue_ask.ogg differ diff --git a/web/abyss/audio/voice/female/sys_help.m4a b/web/abyss/audio/voice/female/sys_help.m4a new file mode 100644 index 00000000..bdb187a5 Binary files /dev/null and b/web/abyss/audio/voice/female/sys_help.m4a differ diff --git a/web/abyss/audio/voice/female/sys_help.ogg b/web/abyss/audio/voice/female/sys_help.ogg new file mode 100644 index 00000000..f2b74cd0 Binary files /dev/null and b/web/abyss/audio/voice/female/sys_help.ogg differ diff --git a/web/abyss/audio/voice/female/sys_menu.m4a b/web/abyss/audio/voice/female/sys_menu.m4a new file mode 100644 index 00000000..715d63eb Binary files /dev/null and b/web/abyss/audio/voice/female/sys_menu.m4a differ diff --git a/web/abyss/audio/voice/female/sys_menu.ogg b/web/abyss/audio/voice/female/sys_menu.ogg new file mode 100644 index 00000000..2aabbfe6 Binary files /dev/null and b/web/abyss/audio/voice/female/sys_menu.ogg differ diff --git a/web/abyss/audio/voice/female/sys_menu_lobby.m4a b/web/abyss/audio/voice/female/sys_menu_lobby.m4a new file mode 100644 index 00000000..ea0e941e Binary files /dev/null and b/web/abyss/audio/voice/female/sys_menu_lobby.m4a differ diff --git a/web/abyss/audio/voice/female/sys_menu_lobby.ogg b/web/abyss/audio/voice/female/sys_menu_lobby.ogg new file mode 100644 index 00000000..39e1405a Binary files /dev/null and b/web/abyss/audio/voice/female/sys_menu_lobby.ogg differ diff --git a/web/abyss/audio/voice/female/sys_menu_restart.m4a b/web/abyss/audio/voice/female/sys_menu_restart.m4a new file mode 100644 index 00000000..7b81be4d Binary files /dev/null and b/web/abyss/audio/voice/female/sys_menu_restart.m4a differ diff --git a/web/abyss/audio/voice/female/sys_menu_restart.ogg b/web/abyss/audio/voice/female/sys_menu_restart.ogg new file mode 100644 index 00000000..0f237bb0 Binary files /dev/null and b/web/abyss/audio/voice/female/sys_menu_restart.ogg differ diff --git a/web/abyss/audio/voice/female/sys_menu_resume.m4a b/web/abyss/audio/voice/female/sys_menu_resume.m4a new file mode 100644 index 00000000..2c6ebcec Binary files /dev/null and b/web/abyss/audio/voice/female/sys_menu_resume.m4a differ diff --git a/web/abyss/audio/voice/female/sys_menu_resume.ogg b/web/abyss/audio/voice/female/sys_menu_resume.ogg new file mode 100644 index 00000000..ae43ce62 Binary files /dev/null and b/web/abyss/audio/voice/female/sys_menu_resume.ogg differ diff --git a/web/abyss/audio/voice/female/sys_new_dive.m4a b/web/abyss/audio/voice/female/sys_new_dive.m4a new file mode 100644 index 00000000..7251c09a Binary files /dev/null and b/web/abyss/audio/voice/female/sys_new_dive.m4a differ diff --git a/web/abyss/audio/voice/female/sys_new_dive.ogg b/web/abyss/audio/voice/female/sys_new_dive.ogg new file mode 100644 index 00000000..11d4b9c3 Binary files /dev/null and b/web/abyss/audio/voice/female/sys_new_dive.ogg differ diff --git a/web/abyss/audio/voice/female/sys_settings.m4a b/web/abyss/audio/voice/female/sys_settings.m4a new file mode 100644 index 00000000..0084c028 Binary files /dev/null and b/web/abyss/audio/voice/female/sys_settings.m4a differ diff --git a/web/abyss/audio/voice/female/sys_settings.ogg b/web/abyss/audio/voice/female/sys_settings.ogg new file mode 100644 index 00000000..f5eb6d05 Binary files /dev/null and b/web/abyss/audio/voice/female/sys_settings.ogg differ diff --git a/web/abyss/audio/voice/female/sys_skip.m4a b/web/abyss/audio/voice/female/sys_skip.m4a new file mode 100644 index 00000000..eef612b3 Binary files /dev/null and b/web/abyss/audio/voice/female/sys_skip.m4a differ diff --git a/web/abyss/audio/voice/female/sys_skip.ogg b/web/abyss/audio/voice/female/sys_skip.ogg new file mode 100644 index 00000000..81000771 Binary files /dev/null and b/web/abyss/audio/voice/female/sys_skip.ogg differ diff --git a/web/abyss/audio/voice/gao_a.m4a b/web/abyss/audio/voice/gao_a.m4a new file mode 100644 index 00000000..31308f6f Binary files /dev/null and b/web/abyss/audio/voice/gao_a.m4a differ diff --git a/web/abyss/audio/voice/gao_a.ogg b/web/abyss/audio/voice/gao_a.ogg new file mode 100644 index 00000000..f9a608ba Binary files /dev/null and b/web/abyss/audio/voice/gao_a.ogg differ diff --git a/web/abyss/audio/voice/gao_b.m4a b/web/abyss/audio/voice/gao_b.m4a new file mode 100644 index 00000000..851f2418 Binary files /dev/null and b/web/abyss/audio/voice/gao_b.m4a differ diff --git a/web/abyss/audio/voice/gao_b.ogg b/web/abyss/audio/voice/gao_b.ogg new file mode 100644 index 00000000..23387e5c Binary files /dev/null and b/web/abyss/audio/voice/gao_b.ogg differ diff --git a/web/abyss/audio/voice/gao_c.m4a b/web/abyss/audio/voice/gao_c.m4a new file mode 100644 index 00000000..26987683 Binary files /dev/null and b/web/abyss/audio/voice/gao_c.m4a differ diff --git a/web/abyss/audio/voice/gao_c.ogg b/web/abyss/audio/voice/gao_c.ogg new file mode 100644 index 00000000..deb17e8a Binary files /dev/null and b/web/abyss/audio/voice/gao_c.ogg differ diff --git a/web/abyss/audio/voice/gao_confess.m4a b/web/abyss/audio/voice/gao_confess.m4a new file mode 100644 index 00000000..5ff6a7e5 Binary files /dev/null and b/web/abyss/audio/voice/gao_confess.m4a differ diff --git a/web/abyss/audio/voice/gao_confess.ogg b/web/abyss/audio/voice/gao_confess.ogg new file mode 100644 index 00000000..71c24748 Binary files /dev/null and b/web/abyss/audio/voice/gao_confess.ogg differ diff --git a/web/abyss/audio/voice/gao_final.m4a b/web/abyss/audio/voice/gao_final.m4a new file mode 100644 index 00000000..1e16db19 Binary files /dev/null and b/web/abyss/audio/voice/gao_final.m4a differ diff --git a/web/abyss/audio/voice/gao_final.ogg b/web/abyss/audio/voice/gao_final.ogg new file mode 100644 index 00000000..6e849189 Binary files /dev/null and b/web/abyss/audio/voice/gao_final.ogg differ diff --git a/web/abyss/audio/voice/gao_last.m4a b/web/abyss/audio/voice/gao_last.m4a new file mode 100644 index 00000000..7b614026 Binary files /dev/null and b/web/abyss/audio/voice/gao_last.m4a differ diff --git a/web/abyss/audio/voice/gao_last.ogg b/web/abyss/audio/voice/gao_last.ogg new file mode 100644 index 00000000..9f30a111 Binary files /dev/null and b/web/abyss/audio/voice/gao_last.ogg differ diff --git a/web/abyss/audio/voice/gao_peace.m4a b/web/abyss/audio/voice/gao_peace.m4a new file mode 100644 index 00000000..452aa314 Binary files /dev/null and b/web/abyss/audio/voice/gao_peace.m4a differ diff --git a/web/abyss/audio/voice/gao_peace.ogg b/web/abyss/audio/voice/gao_peace.ogg new file mode 100644 index 00000000..e60935b5 Binary files /dev/null and b/web/abyss/audio/voice/gao_peace.ogg differ diff --git a/web/abyss/audio/voice/male/b_go.m4a b/web/abyss/audio/voice/male/b_go.m4a new file mode 100644 index 00000000..7469764b Binary files /dev/null and b/web/abyss/audio/voice/male/b_go.m4a differ diff --git a/web/abyss/audio/voice/male/b_go.ogg b/web/abyss/audio/voice/male/b_go.ogg new file mode 100644 index 00000000..58939917 Binary files /dev/null and b/web/abyss/audio/voice/male/b_go.ogg differ diff --git a/web/abyss/audio/voice/male/b_goal.m4a b/web/abyss/audio/voice/male/b_goal.m4a new file mode 100644 index 00000000..fd170f2a Binary files /dev/null and b/web/abyss/audio/voice/male/b_goal.m4a differ diff --git a/web/abyss/audio/voice/male/b_goal.ogg b/web/abyss/audio/voice/male/b_goal.ogg new file mode 100644 index 00000000..fe5c7b37 Binary files /dev/null and b/web/abyss/audio/voice/male/b_goal.ogg differ diff --git a/web/abyss/audio/voice/male/b_open.m4a b/web/abyss/audio/voice/male/b_open.m4a new file mode 100644 index 00000000..e26200ad Binary files /dev/null and b/web/abyss/audio/voice/male/b_open.m4a differ diff --git a/web/abyss/audio/voice/male/b_open.ogg b/web/abyss/audio/voice/male/b_open.ogg new file mode 100644 index 00000000..c934a7cf Binary files /dev/null and b/web/abyss/audio/voice/male/b_open.ogg differ diff --git a/web/abyss/audio/voice/male/b_rule.m4a b/web/abyss/audio/voice/male/b_rule.m4a new file mode 100644 index 00000000..80104c8e Binary files /dev/null and b/web/abyss/audio/voice/male/b_rule.m4a differ diff --git a/web/abyss/audio/voice/male/b_rule.ogg b/web/abyss/audio/voice/male/b_rule.ogg new file mode 100644 index 00000000..54504bfd Binary files /dev/null and b/web/abyss/audio/voice/male/b_rule.ogg differ diff --git a/web/abyss/audio/voice/male/d1_code.m4a b/web/abyss/audio/voice/male/d1_code.m4a new file mode 100644 index 00000000..865475f7 Binary files /dev/null and b/web/abyss/audio/voice/male/d1_code.m4a differ diff --git a/web/abyss/audio/voice/male/d1_code.ogg b/web/abyss/audio/voice/male/d1_code.ogg new file mode 100644 index 00000000..fa796761 Binary files /dev/null and b/web/abyss/audio/voice/male/d1_code.ogg differ diff --git a/web/abyss/audio/voice/male/d1_listen.m4a b/web/abyss/audio/voice/male/d1_listen.m4a new file mode 100644 index 00000000..68a29e92 Binary files /dev/null and b/web/abyss/audio/voice/male/d1_listen.m4a differ diff --git a/web/abyss/audio/voice/male/d1_listen.ogg b/web/abyss/audio/voice/male/d1_listen.ogg new file mode 100644 index 00000000..d7f6c819 Binary files /dev/null and b/web/abyss/audio/voice/male/d1_listen.ogg differ diff --git a/web/abyss/audio/voice/male/d1_nolog.m4a b/web/abyss/audio/voice/male/d1_nolog.m4a new file mode 100644 index 00000000..7ec45948 Binary files /dev/null and b/web/abyss/audio/voice/male/d1_nolog.m4a differ diff --git a/web/abyss/audio/voice/male/d1_nolog.ogg b/web/abyss/audio/voice/male/d1_nolog.ogg new file mode 100644 index 00000000..58ed6cf3 Binary files /dev/null and b/web/abyss/audio/voice/male/d1_nolog.ogg differ diff --git a/web/abyss/audio/voice/male/d1_surf.m4a b/web/abyss/audio/voice/male/d1_surf.m4a new file mode 100644 index 00000000..6a8f3f52 Binary files /dev/null and b/web/abyss/audio/voice/male/d1_surf.m4a differ diff --git a/web/abyss/audio/voice/male/d1_surf.ogg b/web/abyss/audio/voice/male/d1_surf.ogg new file mode 100644 index 00000000..fb2013de Binary files /dev/null and b/web/abyss/audio/voice/male/d1_surf.ogg differ diff --git a/web/abyss/audio/voice/male/d1_up.m4a b/web/abyss/audio/voice/male/d1_up.m4a new file mode 100644 index 00000000..2056d9e0 Binary files /dev/null and b/web/abyss/audio/voice/male/d1_up.m4a differ diff --git a/web/abyss/audio/voice/male/d1_up.ogg b/web/abyss/audio/voice/male/d1_up.ogg new file mode 100644 index 00000000..8ad5b869 Binary files /dev/null and b/web/abyss/audio/voice/male/d1_up.ogg differ diff --git a/web/abyss/audio/voice/male/d2_cut.m4a b/web/abyss/audio/voice/male/d2_cut.m4a new file mode 100644 index 00000000..e7aa41eb Binary files /dev/null and b/web/abyss/audio/voice/male/d2_cut.m4a differ diff --git a/web/abyss/audio/voice/male/d2_cut.ogg b/web/abyss/audio/voice/male/d2_cut.ogg new file mode 100644 index 00000000..7c5c46a1 Binary files /dev/null and b/web/abyss/audio/voice/male/d2_cut.ogg differ diff --git a/web/abyss/audio/voice/male/d2_flash.m4a b/web/abyss/audio/voice/male/d2_flash.m4a new file mode 100644 index 00000000..4db0d21d Binary files /dev/null and b/web/abyss/audio/voice/male/d2_flash.m4a differ diff --git a/web/abyss/audio/voice/male/d2_flash.ogg b/web/abyss/audio/voice/male/d2_flash.ogg new file mode 100644 index 00000000..83f6d068 Binary files /dev/null and b/web/abyss/audio/voice/male/d2_flash.ogg differ diff --git a/web/abyss/audio/voice/male/d2_leave.m4a b/web/abyss/audio/voice/male/d2_leave.m4a new file mode 100644 index 00000000..542a8c70 Binary files /dev/null and b/web/abyss/audio/voice/male/d2_leave.m4a differ diff --git a/web/abyss/audio/voice/male/d2_leave.ogg b/web/abyss/audio/voice/male/d2_leave.ogg new file mode 100644 index 00000000..d7a5b0f6 Binary files /dev/null and b/web/abyss/audio/voice/male/d2_leave.ogg differ diff --git a/web/abyss/audio/voice/male/d2_pry.m4a b/web/abyss/audio/voice/male/d2_pry.m4a new file mode 100644 index 00000000..57df25d4 Binary files /dev/null and b/web/abyss/audio/voice/male/d2_pry.m4a differ diff --git a/web/abyss/audio/voice/male/d2_pry.ogg b/web/abyss/audio/voice/male/d2_pry.ogg new file mode 100644 index 00000000..8819dbc2 Binary files /dev/null and b/web/abyss/audio/voice/male/d2_pry.ogg differ diff --git a/web/abyss/audio/voice/male/d2_pry_ok.m4a b/web/abyss/audio/voice/male/d2_pry_ok.m4a new file mode 100644 index 00000000..6d3f3d50 Binary files /dev/null and b/web/abyss/audio/voice/male/d2_pry_ok.m4a differ diff --git a/web/abyss/audio/voice/male/d2_pry_ok.ogg b/web/abyss/audio/voice/male/d2_pry_ok.ogg new file mode 100644 index 00000000..12554280 Binary files /dev/null and b/web/abyss/audio/voice/male/d2_pry_ok.ogg differ diff --git a/web/abyss/audio/voice/male/d2_pry_slow.m4a b/web/abyss/audio/voice/male/d2_pry_slow.m4a new file mode 100644 index 00000000..b7e313a0 Binary files /dev/null and b/web/abyss/audio/voice/male/d2_pry_slow.m4a differ diff --git a/web/abyss/audio/voice/male/d2_pry_slow.ogg b/web/abyss/audio/voice/male/d2_pry_slow.ogg new file mode 100644 index 00000000..e25fb067 Binary files /dev/null and b/web/abyss/audio/voice/male/d2_pry_slow.ogg differ diff --git a/web/abyss/audio/voice/male/d2_supply.m4a b/web/abyss/audio/voice/male/d2_supply.m4a new file mode 100644 index 00000000..4e1ec762 Binary files /dev/null and b/web/abyss/audio/voice/male/d2_supply.m4a differ diff --git a/web/abyss/audio/voice/male/d2_supply.ogg b/web/abyss/audio/voice/male/d2_supply.ogg new file mode 100644 index 00000000..4d443bf0 Binary files /dev/null and b/web/abyss/audio/voice/male/d2_supply.ogg differ diff --git a/web/abyss/audio/voice/male/d2_surf.m4a b/web/abyss/audio/voice/male/d2_surf.m4a new file mode 100644 index 00000000..82cfb7d8 Binary files /dev/null and b/web/abyss/audio/voice/male/d2_surf.m4a differ diff --git a/web/abyss/audio/voice/male/d2_surf.ogg b/web/abyss/audio/voice/male/d2_surf.ogg new file mode 100644 index 00000000..4174bcdf Binary files /dev/null and b/web/abyss/audio/voice/male/d2_surf.ogg differ diff --git a/web/abyss/audio/voice/male/d2_valve.m4a b/web/abyss/audio/voice/male/d2_valve.m4a new file mode 100644 index 00000000..b2b4741b Binary files /dev/null and b/web/abyss/audio/voice/male/d2_valve.m4a differ diff --git a/web/abyss/audio/voice/male/d2_valve.ogg b/web/abyss/audio/voice/male/d2_valve.ogg new file mode 100644 index 00000000..a59b6d5b Binary files /dev/null and b/web/abyss/audio/voice/male/d2_valve.ogg differ diff --git a/web/abyss/audio/voice/male/d2_valve_e.m4a b/web/abyss/audio/voice/male/d2_valve_e.m4a new file mode 100644 index 00000000..f4229664 Binary files /dev/null and b/web/abyss/audio/voice/male/d2_valve_e.m4a differ diff --git a/web/abyss/audio/voice/male/d2_valve_e.ogg b/web/abyss/audio/voice/male/d2_valve_e.ogg new file mode 100644 index 00000000..c137c403 Binary files /dev/null and b/web/abyss/audio/voice/male/d2_valve_e.ogg differ diff --git a/web/abyss/audio/voice/male/d3_chen.m4a b/web/abyss/audio/voice/male/d3_chen.m4a new file mode 100644 index 00000000..4f564da2 Binary files /dev/null and b/web/abyss/audio/voice/male/d3_chen.m4a differ diff --git a/web/abyss/audio/voice/male/d3_chen.ogg b/web/abyss/audio/voice/male/d3_chen.ogg new file mode 100644 index 00000000..c70cdd80 Binary files /dev/null and b/web/abyss/audio/voice/male/d3_chen.ogg differ diff --git a/web/abyss/audio/voice/male/d3_chen_e.m4a b/web/abyss/audio/voice/male/d3_chen_e.m4a new file mode 100644 index 00000000..f7043ef1 Binary files /dev/null and b/web/abyss/audio/voice/male/d3_chen_e.m4a differ diff --git a/web/abyss/audio/voice/male/d3_chen_e.ogg b/web/abyss/audio/voice/male/d3_chen_e.ogg new file mode 100644 index 00000000..48d1a7d9 Binary files /dev/null and b/web/abyss/audio/voice/male/d3_chen_e.ogg differ diff --git a/web/abyss/audio/voice/male/d3_promise.m4a b/web/abyss/audio/voice/male/d3_promise.m4a new file mode 100644 index 00000000..4d5567b5 Binary files /dev/null and b/web/abyss/audio/voice/male/d3_promise.m4a differ diff --git a/web/abyss/audio/voice/male/d3_promise.ogg b/web/abyss/audio/voice/male/d3_promise.ogg new file mode 100644 index 00000000..b983c4aa Binary files /dev/null and b/web/abyss/audio/voice/male/d3_promise.ogg differ diff --git a/web/abyss/audio/voice/male/d3_spare.m4a b/web/abyss/audio/voice/male/d3_spare.m4a new file mode 100644 index 00000000..f97904ab Binary files /dev/null and b/web/abyss/audio/voice/male/d3_spare.m4a differ diff --git a/web/abyss/audio/voice/male/d3_spare.ogg b/web/abyss/audio/voice/male/d3_spare.ogg new file mode 100644 index 00000000..1fab6e38 Binary files /dev/null and b/web/abyss/audio/voice/male/d3_spare.ogg differ diff --git a/web/abyss/audio/voice/male/d3_spare_e.m4a b/web/abyss/audio/voice/male/d3_spare_e.m4a new file mode 100644 index 00000000..40d878b5 Binary files /dev/null and b/web/abyss/audio/voice/male/d3_spare_e.m4a differ diff --git a/web/abyss/audio/voice/male/d3_spare_e.ogg b/web/abyss/audio/voice/male/d3_spare_e.ogg new file mode 100644 index 00000000..eeee9ed1 Binary files /dev/null and b/web/abyss/audio/voice/male/d3_spare_e.ogg differ diff --git a/web/abyss/audio/voice/male/d3_surf.m4a b/web/abyss/audio/voice/male/d3_surf.m4a new file mode 100644 index 00000000..ea6e47bf Binary files /dev/null and b/web/abyss/audio/voice/male/d3_surf.m4a differ diff --git a/web/abyss/audio/voice/male/d3_surf.ogg b/web/abyss/audio/voice/male/d3_surf.ogg new file mode 100644 index 00000000..51724036 Binary files /dev/null and b/web/abyss/audio/voice/male/d3_surf.ogg differ diff --git a/web/abyss/audio/voice/male/d3_zheng.m4a b/web/abyss/audio/voice/male/d3_zheng.m4a new file mode 100644 index 00000000..ff75185d Binary files /dev/null and b/web/abyss/audio/voice/male/d3_zheng.m4a differ diff --git a/web/abyss/audio/voice/male/d3_zheng.ogg b/web/abyss/audio/voice/male/d3_zheng.ogg new file mode 100644 index 00000000..fa395050 Binary files /dev/null and b/web/abyss/audio/voice/male/d3_zheng.ogg differ diff --git a/web/abyss/audio/voice/male/d3_zheng_e.m4a b/web/abyss/audio/voice/male/d3_zheng_e.m4a new file mode 100644 index 00000000..8ff378b2 Binary files /dev/null and b/web/abyss/audio/voice/male/d3_zheng_e.m4a differ diff --git a/web/abyss/audio/voice/male/d3_zheng_e.ogg b/web/abyss/audio/voice/male/d3_zheng_e.ogg new file mode 100644 index 00000000..5277fc6a Binary files /dev/null and b/web/abyss/audio/voice/male/d3_zheng_e.ogg differ diff --git a/web/abyss/audio/voice/male/d4_fast.m4a b/web/abyss/audio/voice/male/d4_fast.m4a new file mode 100644 index 00000000..bfa8de40 Binary files /dev/null and b/web/abyss/audio/voice/male/d4_fast.m4a differ diff --git a/web/abyss/audio/voice/male/d4_fast.ogg b/web/abyss/audio/voice/male/d4_fast.ogg new file mode 100644 index 00000000..bc32dd07 Binary files /dev/null and b/web/abyss/audio/voice/male/d4_fast.ogg differ diff --git a/web/abyss/audio/voice/male/d4_fast_e.m4a b/web/abyss/audio/voice/male/d4_fast_e.m4a new file mode 100644 index 00000000..07763eb6 Binary files /dev/null and b/web/abyss/audio/voice/male/d4_fast_e.m4a differ diff --git a/web/abyss/audio/voice/male/d4_fast_e.ogg b/web/abyss/audio/voice/male/d4_fast_e.ogg new file mode 100644 index 00000000..746dd917 Binary files /dev/null and b/web/abyss/audio/voice/male/d4_fast_e.ogg differ diff --git a/web/abyss/audio/voice/male/d4_pick_chen.m4a b/web/abyss/audio/voice/male/d4_pick_chen.m4a new file mode 100644 index 00000000..bf49bfb6 Binary files /dev/null and b/web/abyss/audio/voice/male/d4_pick_chen.m4a differ diff --git a/web/abyss/audio/voice/male/d4_pick_chen.ogg b/web/abyss/audio/voice/male/d4_pick_chen.ogg new file mode 100644 index 00000000..6b3c5459 Binary files /dev/null and b/web/abyss/audio/voice/male/d4_pick_chen.ogg differ diff --git a/web/abyss/audio/voice/male/d4_pick_gao.m4a b/web/abyss/audio/voice/male/d4_pick_gao.m4a new file mode 100644 index 00000000..015f88bd Binary files /dev/null and b/web/abyss/audio/voice/male/d4_pick_gao.m4a differ diff --git a/web/abyss/audio/voice/male/d4_pick_gao.ogg b/web/abyss/audio/voice/male/d4_pick_gao.ogg new file mode 100644 index 00000000..ac93df4c Binary files /dev/null and b/web/abyss/audio/voice/male/d4_pick_gao.ogg differ diff --git a/web/abyss/audio/voice/male/d4_pick_zheng.m4a b/web/abyss/audio/voice/male/d4_pick_zheng.m4a new file mode 100644 index 00000000..29a2aeb1 Binary files /dev/null and b/web/abyss/audio/voice/male/d4_pick_zheng.m4a differ diff --git a/web/abyss/audio/voice/male/d4_pick_zheng.ogg b/web/abyss/audio/voice/male/d4_pick_zheng.ogg new file mode 100644 index 00000000..cab46981 Binary files /dev/null and b/web/abyss/audio/voice/male/d4_pick_zheng.ogg differ diff --git a/web/abyss/audio/voice/male/d4_silent.m4a b/web/abyss/audio/voice/male/d4_silent.m4a new file mode 100644 index 00000000..5c6fe896 Binary files /dev/null and b/web/abyss/audio/voice/male/d4_silent.m4a differ diff --git a/web/abyss/audio/voice/male/d4_silent.ogg b/web/abyss/audio/voice/male/d4_silent.ogg new file mode 100644 index 00000000..ce8ef572 Binary files /dev/null and b/web/abyss/audio/voice/male/d4_silent.ogg differ diff --git a/web/abyss/audio/voice/male/d4_slow.m4a b/web/abyss/audio/voice/male/d4_slow.m4a new file mode 100644 index 00000000..afb62972 Binary files /dev/null and b/web/abyss/audio/voice/male/d4_slow.m4a differ diff --git a/web/abyss/audio/voice/male/d4_slow.ogg b/web/abyss/audio/voice/male/d4_slow.ogg new file mode 100644 index 00000000..880debdc Binary files /dev/null and b/web/abyss/audio/voice/male/d4_slow.ogg differ diff --git a/web/abyss/audio/voice/male/d4_slow_e.m4a b/web/abyss/audio/voice/male/d4_slow_e.m4a new file mode 100644 index 00000000..6d7d13ac Binary files /dev/null and b/web/abyss/audio/voice/male/d4_slow_e.m4a differ diff --git a/web/abyss/audio/voice/male/d4_slow_e.ogg b/web/abyss/audio/voice/male/d4_slow_e.ogg new file mode 100644 index 00000000..18657e80 Binary files /dev/null and b/web/abyss/audio/voice/male/d4_slow_e.ogg differ diff --git a/web/abyss/audio/voice/male/d4_stay_lost.m4a b/web/abyss/audio/voice/male/d4_stay_lost.m4a new file mode 100644 index 00000000..7f35329c Binary files /dev/null and b/web/abyss/audio/voice/male/d4_stay_lost.m4a differ diff --git a/web/abyss/audio/voice/male/d4_stay_lost.ogg b/web/abyss/audio/voice/male/d4_stay_lost.ogg new file mode 100644 index 00000000..8d87097e Binary files /dev/null and b/web/abyss/audio/voice/male/d4_stay_lost.ogg differ diff --git a/web/abyss/audio/voice/male/d4_surf.m4a b/web/abyss/audio/voice/male/d4_surf.m4a new file mode 100644 index 00000000..61495123 Binary files /dev/null and b/web/abyss/audio/voice/male/d4_surf.m4a differ diff --git a/web/abyss/audio/voice/male/d4_surf.ogg b/web/abyss/audio/voice/male/d4_surf.ogg new file mode 100644 index 00000000..5671feb0 Binary files /dev/null and b/web/abyss/audio/voice/male/d4_surf.ogg differ diff --git a/web/abyss/audio/voice/male/d4_talk.m4a b/web/abyss/audio/voice/male/d4_talk.m4a new file mode 100644 index 00000000..ef25485c Binary files /dev/null and b/web/abyss/audio/voice/male/d4_talk.m4a differ diff --git a/web/abyss/audio/voice/male/d4_talk.ogg b/web/abyss/audio/voice/male/d4_talk.ogg new file mode 100644 index 00000000..70bf0b60 Binary files /dev/null and b/web/abyss/audio/voice/male/d4_talk.ogg differ diff --git a/web/abyss/audio/voice/male/d9_bell.m4a b/web/abyss/audio/voice/male/d9_bell.m4a new file mode 100644 index 00000000..15991573 Binary files /dev/null and b/web/abyss/audio/voice/male/d9_bell.m4a differ diff --git a/web/abyss/audio/voice/male/d9_bell.ogg b/web/abyss/audio/voice/male/d9_bell.ogg new file mode 100644 index 00000000..197c6f09 Binary files /dev/null and b/web/abyss/audio/voice/male/d9_bell.ogg differ diff --git a/web/abyss/audio/voice/male/d9_bello.m4a b/web/abyss/audio/voice/male/d9_bello.m4a new file mode 100644 index 00000000..bd8dfe3a Binary files /dev/null and b/web/abyss/audio/voice/male/d9_bello.m4a differ diff --git a/web/abyss/audio/voice/male/d9_bello.ogg b/web/abyss/audio/voice/male/d9_bello.ogg new file mode 100644 index 00000000..b49064b4 Binary files /dev/null and b/web/abyss/audio/voice/male/d9_bello.ogg differ diff --git a/web/abyss/audio/voice/male/d9_rest.m4a b/web/abyss/audio/voice/male/d9_rest.m4a new file mode 100644 index 00000000..2bdf4d7c Binary files /dev/null and b/web/abyss/audio/voice/male/d9_rest.m4a differ diff --git a/web/abyss/audio/voice/male/d9_rest.ogg b/web/abyss/audio/voice/male/d9_rest.ogg new file mode 100644 index 00000000..40907fb2 Binary files /dev/null and b/web/abyss/audio/voice/male/d9_rest.ogg differ diff --git a/web/abyss/audio/voice/male/dry_a.m4a b/web/abyss/audio/voice/male/dry_a.m4a new file mode 100644 index 00000000..063c1f4d Binary files /dev/null and b/web/abyss/audio/voice/male/dry_a.m4a differ diff --git a/web/abyss/audio/voice/male/dry_a.ogg b/web/abyss/audio/voice/male/dry_a.ogg new file mode 100644 index 00000000..bf594368 Binary files /dev/null and b/web/abyss/audio/voice/male/dry_a.ogg differ diff --git a/web/abyss/audio/voice/male/dry_b.m4a b/web/abyss/audio/voice/male/dry_b.m4a new file mode 100644 index 00000000..af156cc5 Binary files /dev/null and b/web/abyss/audio/voice/male/dry_b.m4a differ diff --git a/web/abyss/audio/voice/male/dry_b.ogg b/web/abyss/audio/voice/male/dry_b.ogg new file mode 100644 index 00000000..90720d10 Binary files /dev/null and b/web/abyss/audio/voice/male/dry_b.ogg differ diff --git a/web/abyss/audio/voice/male/dv1_down.m4a b/web/abyss/audio/voice/male/dv1_down.m4a new file mode 100644 index 00000000..42c9ae47 Binary files /dev/null and b/web/abyss/audio/voice/male/dv1_down.m4a differ diff --git a/web/abyss/audio/voice/male/dv1_down.ogg b/web/abyss/audio/voice/male/dv1_down.ogg new file mode 100644 index 00000000..3afaa4de Binary files /dev/null and b/web/abyss/audio/voice/male/dv1_down.ogg differ diff --git a/web/abyss/audio/voice/male/dv1_link.m4a b/web/abyss/audio/voice/male/dv1_link.m4a new file mode 100644 index 00000000..181a6463 Binary files /dev/null and b/web/abyss/audio/voice/male/dv1_link.m4a differ diff --git a/web/abyss/audio/voice/male/dv1_link.ogg b/web/abyss/audio/voice/male/dv1_link.ogg new file mode 100644 index 00000000..ad3c6cc5 Binary files /dev/null and b/web/abyss/audio/voice/male/dv1_link.ogg differ diff --git a/web/abyss/audio/voice/male/dv1_metal.m4a b/web/abyss/audio/voice/male/dv1_metal.m4a new file mode 100644 index 00000000..dc1a7aac Binary files /dev/null and b/web/abyss/audio/voice/male/dv1_metal.m4a differ diff --git a/web/abyss/audio/voice/male/dv1_metal.ogg b/web/abyss/audio/voice/male/dv1_metal.ogg new file mode 100644 index 00000000..30bb4207 Binary files /dev/null and b/web/abyss/audio/voice/male/dv1_metal.ogg differ diff --git a/web/abyss/audio/voice/male/dv1_teach.m4a b/web/abyss/audio/voice/male/dv1_teach.m4a new file mode 100644 index 00000000..8b55cdb5 Binary files /dev/null and b/web/abyss/audio/voice/male/dv1_teach.m4a differ diff --git a/web/abyss/audio/voice/male/dv1_teach.ogg b/web/abyss/audio/voice/male/dv1_teach.ogg new file mode 100644 index 00000000..8dea811a Binary files /dev/null and b/web/abyss/audio/voice/male/dv1_teach.ogg differ diff --git a/web/abyss/audio/voice/male/dv2_down.m4a b/web/abyss/audio/voice/male/dv2_down.m4a new file mode 100644 index 00000000..c2f0a4ef Binary files /dev/null and b/web/abyss/audio/voice/male/dv2_down.m4a differ diff --git a/web/abyss/audio/voice/male/dv2_down.ogg b/web/abyss/audio/voice/male/dv2_down.ogg new file mode 100644 index 00000000..577f0a89 Binary files /dev/null and b/web/abyss/audio/voice/male/dv2_down.ogg differ diff --git a/web/abyss/audio/voice/male/dv2_found.m4a b/web/abyss/audio/voice/male/dv2_found.m4a new file mode 100644 index 00000000..1c5c6c83 Binary files /dev/null and b/web/abyss/audio/voice/male/dv2_found.m4a differ diff --git a/web/abyss/audio/voice/male/dv2_found.ogg b/web/abyss/audio/voice/male/dv2_found.ogg new file mode 100644 index 00000000..126284f7 Binary files /dev/null and b/web/abyss/audio/voice/male/dv2_found.ogg differ diff --git a/web/abyss/audio/voice/male/dv3_down.m4a b/web/abyss/audio/voice/male/dv3_down.m4a new file mode 100644 index 00000000..36ff5a66 Binary files /dev/null and b/web/abyss/audio/voice/male/dv3_down.m4a differ diff --git a/web/abyss/audio/voice/male/dv3_down.ogg b/web/abyss/audio/voice/male/dv3_down.ogg new file mode 100644 index 00000000..f1c7a6e8 Binary files /dev/null and b/web/abyss/audio/voice/male/dv3_down.ogg differ diff --git a/web/abyss/audio/voice/male/dv3_groan.m4a b/web/abyss/audio/voice/male/dv3_groan.m4a new file mode 100644 index 00000000..8830a439 Binary files /dev/null and b/web/abyss/audio/voice/male/dv3_groan.m4a differ diff --git a/web/abyss/audio/voice/male/dv3_groan.ogg b/web/abyss/audio/voice/male/dv3_groan.ogg new file mode 100644 index 00000000..f7f17b91 Binary files /dev/null and b/web/abyss/audio/voice/male/dv3_groan.ogg differ diff --git a/web/abyss/audio/voice/male/dv4_down.m4a b/web/abyss/audio/voice/male/dv4_down.m4a new file mode 100644 index 00000000..2345c069 Binary files /dev/null and b/web/abyss/audio/voice/male/dv4_down.m4a differ diff --git a/web/abyss/audio/voice/male/dv4_down.ogg b/web/abyss/audio/voice/male/dv4_down.ogg new file mode 100644 index 00000000..bddd0c90 Binary files /dev/null and b/web/abyss/audio/voice/male/dv4_down.ogg differ diff --git a/web/abyss/audio/voice/male/dv4_none.m4a b/web/abyss/audio/voice/male/dv4_none.m4a new file mode 100644 index 00000000..40690873 Binary files /dev/null and b/web/abyss/audio/voice/male/dv4_none.m4a differ diff --git a/web/abyss/audio/voice/male/dv4_none.ogg b/web/abyss/audio/voice/male/dv4_none.ogg new file mode 100644 index 00000000..f032ec7e Binary files /dev/null and b/web/abyss/audio/voice/male/dv4_none.ogg differ diff --git a/web/abyss/audio/voice/male/dv4_slide.m4a b/web/abyss/audio/voice/male/dv4_slide.m4a new file mode 100644 index 00000000..ded4e536 Binary files /dev/null and b/web/abyss/audio/voice/male/dv4_slide.m4a differ diff --git a/web/abyss/audio/voice/male/dv4_slide.ogg b/web/abyss/audio/voice/male/dv4_slide.ogg new file mode 100644 index 00000000..9a346814 Binary files /dev/null and b/web/abyss/audio/voice/male/dv4_slide.ogg differ diff --git a/web/abyss/audio/voice/male/e1_a.m4a b/web/abyss/audio/voice/male/e1_a.m4a new file mode 100644 index 00000000..a1a80c70 Binary files /dev/null and b/web/abyss/audio/voice/male/e1_a.m4a differ diff --git a/web/abyss/audio/voice/male/e1_a.ogg b/web/abyss/audio/voice/male/e1_a.ogg new file mode 100644 index 00000000..e6b3152f Binary files /dev/null and b/web/abyss/audio/voice/male/e1_a.ogg differ diff --git a/web/abyss/audio/voice/male/e1_b.m4a b/web/abyss/audio/voice/male/e1_b.m4a new file mode 100644 index 00000000..c4661941 Binary files /dev/null and b/web/abyss/audio/voice/male/e1_b.m4a differ diff --git a/web/abyss/audio/voice/male/e1_b.ogg b/web/abyss/audio/voice/male/e1_b.ogg new file mode 100644 index 00000000..e103811d Binary files /dev/null and b/web/abyss/audio/voice/male/e1_b.ogg differ diff --git a/web/abyss/audio/voice/male/e2_a.m4a b/web/abyss/audio/voice/male/e2_a.m4a new file mode 100644 index 00000000..52fbea54 Binary files /dev/null and b/web/abyss/audio/voice/male/e2_a.m4a differ diff --git a/web/abyss/audio/voice/male/e2_a.ogg b/web/abyss/audio/voice/male/e2_a.ogg new file mode 100644 index 00000000..5c319e1b Binary files /dev/null and b/web/abyss/audio/voice/male/e2_a.ogg differ diff --git a/web/abyss/audio/voice/male/e2_b.m4a b/web/abyss/audio/voice/male/e2_b.m4a new file mode 100644 index 00000000..63b65449 Binary files /dev/null and b/web/abyss/audio/voice/male/e2_b.m4a differ diff --git a/web/abyss/audio/voice/male/e2_b.ogg b/web/abyss/audio/voice/male/e2_b.ogg new file mode 100644 index 00000000..e9681911 Binary files /dev/null and b/web/abyss/audio/voice/male/e2_b.ogg differ diff --git a/web/abyss/audio/voice/male/e3_a.m4a b/web/abyss/audio/voice/male/e3_a.m4a new file mode 100644 index 00000000..2cc16f84 Binary files /dev/null and b/web/abyss/audio/voice/male/e3_a.m4a differ diff --git a/web/abyss/audio/voice/male/e3_a.ogg b/web/abyss/audio/voice/male/e3_a.ogg new file mode 100644 index 00000000..5e9ccb3c Binary files /dev/null and b/web/abyss/audio/voice/male/e3_a.ogg differ diff --git a/web/abyss/audio/voice/male/e3_b.m4a b/web/abyss/audio/voice/male/e3_b.m4a new file mode 100644 index 00000000..784227e3 Binary files /dev/null and b/web/abyss/audio/voice/male/e3_b.m4a differ diff --git a/web/abyss/audio/voice/male/e3_b.ogg b/web/abyss/audio/voice/male/e3_b.ogg new file mode 100644 index 00000000..63665c8c Binary files /dev/null and b/web/abyss/audio/voice/male/e3_b.ogg differ diff --git a/web/abyss/audio/voice/male/e4_a.m4a b/web/abyss/audio/voice/male/e4_a.m4a new file mode 100644 index 00000000..8efe7657 Binary files /dev/null and b/web/abyss/audio/voice/male/e4_a.m4a differ diff --git a/web/abyss/audio/voice/male/e4_a.ogg b/web/abyss/audio/voice/male/e4_a.ogg new file mode 100644 index 00000000..83944d94 Binary files /dev/null and b/web/abyss/audio/voice/male/e4_a.ogg differ diff --git a/web/abyss/audio/voice/male/e4_b.m4a b/web/abyss/audio/voice/male/e4_b.m4a new file mode 100644 index 00000000..ba37af75 Binary files /dev/null and b/web/abyss/audio/voice/male/e4_b.m4a differ diff --git a/web/abyss/audio/voice/male/e4_b.ogg b/web/abyss/audio/voice/male/e4_b.ogg new file mode 100644 index 00000000..d1a5ecbe Binary files /dev/null and b/web/abyss/audio/voice/male/e4_b.ogg differ diff --git a/web/abyss/audio/voice/male/ep_bai_no.m4a b/web/abyss/audio/voice/male/ep_bai_no.m4a new file mode 100644 index 00000000..7ef53008 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_bai_no.m4a differ diff --git a/web/abyss/audio/voice/male/ep_bai_no.ogg b/web/abyss/audio/voice/male/ep_bai_no.ogg new file mode 100644 index 00000000..733c0182 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_bai_no.ogg differ diff --git a/web/abyss/audio/voice/male/ep_bai_ok.m4a b/web/abyss/audio/voice/male/ep_bai_ok.m4a new file mode 100644 index 00000000..2c5996e5 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_bai_ok.m4a differ diff --git a/web/abyss/audio/voice/male/ep_bai_ok.ogg b/web/abyss/audio/voice/male/ep_bai_ok.ogg new file mode 100644 index 00000000..c0d6da24 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_bai_ok.ogg differ diff --git a/web/abyss/audio/voice/male/ep_chen_lost.m4a b/web/abyss/audio/voice/male/ep_chen_lost.m4a new file mode 100644 index 00000000..cd5f6cae Binary files /dev/null and b/web/abyss/audio/voice/male/ep_chen_lost.m4a differ diff --git a/web/abyss/audio/voice/male/ep_chen_lost.ogg b/web/abyss/audio/voice/male/ep_chen_lost.ogg new file mode 100644 index 00000000..4cdbfbb3 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_chen_lost.ogg differ diff --git a/web/abyss/audio/voice/male/ep_chen_ok.m4a b/web/abyss/audio/voice/male/ep_chen_ok.m4a new file mode 100644 index 00000000..73b20106 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_chen_ok.m4a differ diff --git a/web/abyss/audio/voice/male/ep_chen_ok.ogg b/web/abyss/audio/voice/male/ep_chen_ok.ogg new file mode 100644 index 00000000..eaab5c3d Binary files /dev/null and b/web/abyss/audio/voice/male/ep_chen_ok.ogg differ diff --git a/web/abyss/audio/voice/male/ep_gao_lost.m4a b/web/abyss/audio/voice/male/ep_gao_lost.m4a new file mode 100644 index 00000000..0017da71 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_gao_lost.m4a differ diff --git a/web/abyss/audio/voice/male/ep_gao_lost.ogg b/web/abyss/audio/voice/male/ep_gao_lost.ogg new file mode 100644 index 00000000..de2850b4 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_gao_lost.ogg differ diff --git a/web/abyss/audio/voice/male/ep_gao_peace.m4a b/web/abyss/audio/voice/male/ep_gao_peace.m4a new file mode 100644 index 00000000..091ad033 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_gao_peace.m4a differ diff --git a/web/abyss/audio/voice/male/ep_gao_peace.ogg b/web/abyss/audio/voice/male/ep_gao_peace.ogg new file mode 100644 index 00000000..93f3444c Binary files /dev/null and b/web/abyss/audio/voice/male/ep_gao_peace.ogg differ diff --git a/web/abyss/audio/voice/male/ep_gao_up.m4a b/web/abyss/audio/voice/male/ep_gao_up.m4a new file mode 100644 index 00000000..25706cd5 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_gao_up.m4a differ diff --git a/web/abyss/audio/voice/male/ep_gao_up.ogg b/web/abyss/audio/voice/male/ep_gao_up.ogg new file mode 100644 index 00000000..d99eed4a Binary files /dev/null and b/web/abyss/audio/voice/male/ep_gao_up.ogg differ diff --git a/web/abyss/audio/voice/male/ep_lead.m4a b/web/abyss/audio/voice/male/ep_lead.m4a new file mode 100644 index 00000000..1b6f3267 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_lead.m4a differ diff --git a/web/abyss/audio/voice/male/ep_lead.ogg b/web/abyss/audio/voice/male/ep_lead.ogg new file mode 100644 index 00000000..40f4c0e8 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_lead.ogg differ diff --git a/web/abyss/audio/voice/male/ep_trench_bell.m4a b/web/abyss/audio/voice/male/ep_trench_bell.m4a new file mode 100644 index 00000000..e672fa52 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_trench_bell.m4a differ diff --git a/web/abyss/audio/voice/male/ep_trench_bell.ogg b/web/abyss/audio/voice/male/ep_trench_bell.ogg new file mode 100644 index 00000000..a3957cad Binary files /dev/null and b/web/abyss/audio/voice/male/ep_trench_bell.ogg differ diff --git a/web/abyss/audio/voice/male/ep_trench_deep.m4a b/web/abyss/audio/voice/male/ep_trench_deep.m4a new file mode 100644 index 00000000..d2bbc6f5 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_trench_deep.m4a differ diff --git a/web/abyss/audio/voice/male/ep_trench_deep.ogg b/web/abyss/audio/voice/male/ep_trench_deep.ogg new file mode 100644 index 00000000..180d86da Binary files /dev/null and b/web/abyss/audio/voice/male/ep_trench_deep.ogg differ diff --git a/web/abyss/audio/voice/male/ep_zheng_lost.m4a b/web/abyss/audio/voice/male/ep_zheng_lost.m4a new file mode 100644 index 00000000..a523b9d9 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_zheng_lost.m4a differ diff --git a/web/abyss/audio/voice/male/ep_zheng_lost.ogg b/web/abyss/audio/voice/male/ep_zheng_lost.ogg new file mode 100644 index 00000000..1bb0bb54 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_zheng_lost.ogg differ diff --git a/web/abyss/audio/voice/male/ep_zheng_ok.m4a b/web/abyss/audio/voice/male/ep_zheng_ok.m4a new file mode 100644 index 00000000..d9d312f2 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_zheng_ok.m4a differ diff --git a/web/abyss/audio/voice/male/ep_zheng_ok.ogg b/web/abyss/audio/voice/male/ep_zheng_ok.ogg new file mode 100644 index 00000000..427966f1 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_zheng_ok.ogg differ diff --git a/web/abyss/audio/voice/male/ep_zhou_no.m4a b/web/abyss/audio/voice/male/ep_zhou_no.m4a new file mode 100644 index 00000000..ee42df32 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_zhou_no.m4a differ diff --git a/web/abyss/audio/voice/male/ep_zhou_no.ogg b/web/abyss/audio/voice/male/ep_zhou_no.ogg new file mode 100644 index 00000000..2f80722b Binary files /dev/null and b/web/abyss/audio/voice/male/ep_zhou_no.ogg differ diff --git a/web/abyss/audio/voice/male/ep_zhou_ok.m4a b/web/abyss/audio/voice/male/ep_zhou_ok.m4a new file mode 100644 index 00000000..25d92171 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_zhou_ok.m4a differ diff --git a/web/abyss/audio/voice/male/ep_zhou_ok.ogg b/web/abyss/audio/voice/male/ep_zhou_ok.ogg new file mode 100644 index 00000000..10be03e7 Binary files /dev/null and b/web/abyss/audio/voice/male/ep_zhou_ok.ogg differ diff --git a/web/abyss/audio/voice/male/g9_dry.m4a b/web/abyss/audio/voice/male/g9_dry.m4a new file mode 100644 index 00000000..0e0caaad Binary files /dev/null and b/web/abyss/audio/voice/male/g9_dry.m4a differ diff --git a/web/abyss/audio/voice/male/g9_dry.ogg b/web/abyss/audio/voice/male/g9_dry.ogg new file mode 100644 index 00000000..6b75362a Binary files /dev/null and b/web/abyss/audio/voice/male/g9_dry.ogg differ diff --git a/web/abyss/audio/voice/male/g9_in.m4a b/web/abyss/audio/voice/male/g9_in.m4a new file mode 100644 index 00000000..163b421c Binary files /dev/null and b/web/abyss/audio/voice/male/g9_in.m4a differ diff --git a/web/abyss/audio/voice/male/g9_in.ogg b/web/abyss/audio/voice/male/g9_in.ogg new file mode 100644 index 00000000..d9a49cf8 Binary files /dev/null and b/web/abyss/audio/voice/male/g9_in.ogg differ diff --git a/web/abyss/audio/voice/male/g9_knot.m4a b/web/abyss/audio/voice/male/g9_knot.m4a new file mode 100644 index 00000000..e4db0943 Binary files /dev/null and b/web/abyss/audio/voice/male/g9_knot.m4a differ diff --git a/web/abyss/audio/voice/male/g9_knot.ogg b/web/abyss/audio/voice/male/g9_knot.ogg new file mode 100644 index 00000000..e1688629 Binary files /dev/null and b/web/abyss/audio/voice/male/g9_knot.ogg differ diff --git a/web/abyss/audio/voice/male/gd_again.m4a b/web/abyss/audio/voice/male/gd_again.m4a new file mode 100644 index 00000000..58f59de6 Binary files /dev/null and b/web/abyss/audio/voice/male/gd_again.m4a differ diff --git a/web/abyss/audio/voice/male/gd_again.ogg b/web/abyss/audio/voice/male/gd_again.ogg new file mode 100644 index 00000000..d785e33b Binary files /dev/null and b/web/abyss/audio/voice/male/gd_again.ogg differ diff --git a/web/abyss/audio/voice/male/gd_brief.m4a b/web/abyss/audio/voice/male/gd_brief.m4a new file mode 100644 index 00000000..46b80b45 Binary files /dev/null and b/web/abyss/audio/voice/male/gd_brief.m4a differ diff --git a/web/abyss/audio/voice/male/gd_brief.ogg b/web/abyss/audio/voice/male/gd_brief.ogg new file mode 100644 index 00000000..1a57619a Binary files /dev/null and b/web/abyss/audio/voice/male/gd_brief.ogg differ diff --git a/web/abyss/audio/voice/male/gd_continue.m4a b/web/abyss/audio/voice/male/gd_continue.m4a new file mode 100644 index 00000000..9791d20a Binary files /dev/null and b/web/abyss/audio/voice/male/gd_continue.m4a differ diff --git a/web/abyss/audio/voice/male/gd_continue.ogg b/web/abyss/audio/voice/male/gd_continue.ogg new file mode 100644 index 00000000..e019c28b Binary files /dev/null and b/web/abyss/audio/voice/male/gd_continue.ogg differ diff --git a/web/abyss/audio/voice/male/gd_invalid.m4a b/web/abyss/audio/voice/male/gd_invalid.m4a new file mode 100644 index 00000000..70b53177 Binary files /dev/null and b/web/abyss/audio/voice/male/gd_invalid.m4a differ diff --git a/web/abyss/audio/voice/male/gd_invalid.ogg b/web/abyss/audio/voice/male/gd_invalid.ogg new file mode 100644 index 00000000..0f201840 Binary files /dev/null and b/web/abyss/audio/voice/male/gd_invalid.ogg differ diff --git a/web/abyss/audio/voice/male/gd_ping1.m4a b/web/abyss/audio/voice/male/gd_ping1.m4a new file mode 100644 index 00000000..9bda55cb Binary files /dev/null and b/web/abyss/audio/voice/male/gd_ping1.m4a differ diff --git a/web/abyss/audio/voice/male/gd_ping1.ogg b/web/abyss/audio/voice/male/gd_ping1.ogg new file mode 100644 index 00000000..1989ec2c Binary files /dev/null and b/web/abyss/audio/voice/male/gd_ping1.ogg differ diff --git a/web/abyss/audio/voice/male/gd_ping2.m4a b/web/abyss/audio/voice/male/gd_ping2.m4a new file mode 100644 index 00000000..1b8cf87f Binary files /dev/null and b/web/abyss/audio/voice/male/gd_ping2.m4a differ diff --git a/web/abyss/audio/voice/male/gd_ping2.ogg b/web/abyss/audio/voice/male/gd_ping2.ogg new file mode 100644 index 00000000..22a6d1e5 Binary files /dev/null and b/web/abyss/audio/voice/male/gd_ping2.ogg differ diff --git a/web/abyss/audio/voice/male/gd_prog1.m4a b/web/abyss/audio/voice/male/gd_prog1.m4a new file mode 100644 index 00000000..1b9410a0 Binary files /dev/null and b/web/abyss/audio/voice/male/gd_prog1.m4a differ diff --git a/web/abyss/audio/voice/male/gd_prog1.ogg b/web/abyss/audio/voice/male/gd_prog1.ogg new file mode 100644 index 00000000..8ebe66a9 Binary files /dev/null and b/web/abyss/audio/voice/male/gd_prog1.ogg differ diff --git a/web/abyss/audio/voice/male/gd_prog2.m4a b/web/abyss/audio/voice/male/gd_prog2.m4a new file mode 100644 index 00000000..af057221 Binary files /dev/null and b/web/abyss/audio/voice/male/gd_prog2.m4a differ diff --git a/web/abyss/audio/voice/male/gd_prog2.ogg b/web/abyss/audio/voice/male/gd_prog2.ogg new file mode 100644 index 00000000..16c47f3d Binary files /dev/null and b/web/abyss/audio/voice/male/gd_prog2.ogg differ diff --git a/web/abyss/audio/voice/male/key/dv1_teach.m4a b/web/abyss/audio/voice/male/key/dv1_teach.m4a new file mode 100644 index 00000000..d0f576fc Binary files /dev/null and b/web/abyss/audio/voice/male/key/dv1_teach.m4a differ diff --git a/web/abyss/audio/voice/male/key/dv1_teach.ogg b/web/abyss/audio/voice/male/key/dv1_teach.ogg new file mode 100644 index 00000000..41658a46 Binary files /dev/null and b/web/abyss/audio/voice/male/key/dv1_teach.ogg differ diff --git a/web/abyss/audio/voice/male/key/gd_again.m4a b/web/abyss/audio/voice/male/key/gd_again.m4a new file mode 100644 index 00000000..f17a67c4 Binary files /dev/null and b/web/abyss/audio/voice/male/key/gd_again.m4a differ diff --git a/web/abyss/audio/voice/male/key/gd_again.ogg b/web/abyss/audio/voice/male/key/gd_again.ogg new file mode 100644 index 00000000..7715ddf7 Binary files /dev/null and b/web/abyss/audio/voice/male/key/gd_again.ogg differ diff --git a/web/abyss/audio/voice/male/key/gd_brief.m4a b/web/abyss/audio/voice/male/key/gd_brief.m4a new file mode 100644 index 00000000..2cd222d1 Binary files /dev/null and b/web/abyss/audio/voice/male/key/gd_brief.m4a differ diff --git a/web/abyss/audio/voice/male/key/gd_brief.ogg b/web/abyss/audio/voice/male/key/gd_brief.ogg new file mode 100644 index 00000000..530c3c94 Binary files /dev/null and b/web/abyss/audio/voice/male/key/gd_brief.ogg differ diff --git a/web/abyss/audio/voice/male/key/gd_continue.m4a b/web/abyss/audio/voice/male/key/gd_continue.m4a new file mode 100644 index 00000000..10a2b489 Binary files /dev/null and b/web/abyss/audio/voice/male/key/gd_continue.m4a differ diff --git a/web/abyss/audio/voice/male/key/gd_continue.ogg b/web/abyss/audio/voice/male/key/gd_continue.ogg new file mode 100644 index 00000000..f6c3b45c Binary files /dev/null and b/web/abyss/audio/voice/male/key/gd_continue.ogg differ diff --git a/web/abyss/audio/voice/male/key/gd_ping1.m4a b/web/abyss/audio/voice/male/key/gd_ping1.m4a new file mode 100644 index 00000000..ff7b41a2 Binary files /dev/null and b/web/abyss/audio/voice/male/key/gd_ping1.m4a differ diff --git a/web/abyss/audio/voice/male/key/gd_ping1.ogg b/web/abyss/audio/voice/male/key/gd_ping1.ogg new file mode 100644 index 00000000..46f20dab Binary files /dev/null and b/web/abyss/audio/voice/male/key/gd_ping1.ogg differ diff --git a/web/abyss/audio/voice/male/key/gd_ping2.m4a b/web/abyss/audio/voice/male/key/gd_ping2.m4a new file mode 100644 index 00000000..29c1bc03 Binary files /dev/null and b/web/abyss/audio/voice/male/key/gd_ping2.m4a differ diff --git a/web/abyss/audio/voice/male/key/gd_ping2.ogg b/web/abyss/audio/voice/male/key/gd_ping2.ogg new file mode 100644 index 00000000..43841cf6 Binary files /dev/null and b/web/abyss/audio/voice/male/key/gd_ping2.ogg differ diff --git a/web/abyss/audio/voice/male/key/p1.m4a b/web/abyss/audio/voice/male/key/p1.m4a new file mode 100644 index 00000000..a89ac538 Binary files /dev/null and b/web/abyss/audio/voice/male/key/p1.m4a differ diff --git a/web/abyss/audio/voice/male/key/p1.ogg b/web/abyss/audio/voice/male/key/p1.ogg new file mode 100644 index 00000000..232f598b Binary files /dev/null and b/web/abyss/audio/voice/male/key/p1.ogg differ diff --git a/web/abyss/audio/voice/male/key/sys_continue_ask.m4a b/web/abyss/audio/voice/male/key/sys_continue_ask.m4a new file mode 100644 index 00000000..2c87b7a8 Binary files /dev/null and b/web/abyss/audio/voice/male/key/sys_continue_ask.m4a differ diff --git a/web/abyss/audio/voice/male/key/sys_continue_ask.ogg b/web/abyss/audio/voice/male/key/sys_continue_ask.ogg new file mode 100644 index 00000000..d3d0f1a9 Binary files /dev/null and b/web/abyss/audio/voice/male/key/sys_continue_ask.ogg differ diff --git a/web/abyss/audio/voice/male/key/sys_help.m4a b/web/abyss/audio/voice/male/key/sys_help.m4a new file mode 100644 index 00000000..d2de83d3 Binary files /dev/null and b/web/abyss/audio/voice/male/key/sys_help.m4a differ diff --git a/web/abyss/audio/voice/male/key/sys_help.ogg b/web/abyss/audio/voice/male/key/sys_help.ogg new file mode 100644 index 00000000..ae58bf1e Binary files /dev/null and b/web/abyss/audio/voice/male/key/sys_help.ogg differ diff --git a/web/abyss/audio/voice/male/key/sys_menu.m4a b/web/abyss/audio/voice/male/key/sys_menu.m4a new file mode 100644 index 00000000..a17ab22d Binary files /dev/null and b/web/abyss/audio/voice/male/key/sys_menu.m4a differ diff --git a/web/abyss/audio/voice/male/key/sys_menu.ogg b/web/abyss/audio/voice/male/key/sys_menu.ogg new file mode 100644 index 00000000..82f76587 Binary files /dev/null and b/web/abyss/audio/voice/male/key/sys_menu.ogg differ diff --git a/web/abyss/audio/voice/male/key/sys_skip.m4a b/web/abyss/audio/voice/male/key/sys_skip.m4a new file mode 100644 index 00000000..98aeb6a8 Binary files /dev/null and b/web/abyss/audio/voice/male/key/sys_skip.m4a differ diff --git a/web/abyss/audio/voice/male/key/sys_skip.ogg b/web/abyss/audio/voice/male/key/sys_skip.ogg new file mode 100644 index 00000000..d4ca52b4 Binary files /dev/null and b/web/abyss/audio/voice/male/key/sys_skip.ogg differ diff --git a/web/abyss/audio/voice/male/oxy_crit.m4a b/web/abyss/audio/voice/male/oxy_crit.m4a new file mode 100644 index 00000000..b7b9c5f5 Binary files /dev/null and b/web/abyss/audio/voice/male/oxy_crit.m4a differ diff --git a/web/abyss/audio/voice/male/oxy_crit.ogg b/web/abyss/audio/voice/male/oxy_crit.ogg new file mode 100644 index 00000000..a963cfd8 Binary files /dev/null and b/web/abyss/audio/voice/male/oxy_crit.ogg differ diff --git a/web/abyss/audio/voice/male/oxy_full.m4a b/web/abyss/audio/voice/male/oxy_full.m4a new file mode 100644 index 00000000..ac19d789 Binary files /dev/null and b/web/abyss/audio/voice/male/oxy_full.m4a differ diff --git a/web/abyss/audio/voice/male/oxy_full.ogg b/web/abyss/audio/voice/male/oxy_full.ogg new file mode 100644 index 00000000..6fc1d382 Binary files /dev/null and b/web/abyss/audio/voice/male/oxy_full.ogg differ diff --git a/web/abyss/audio/voice/male/oxy_gain.m4a b/web/abyss/audio/voice/male/oxy_gain.m4a new file mode 100644 index 00000000..7a84ff1b Binary files /dev/null and b/web/abyss/audio/voice/male/oxy_gain.m4a differ diff --git a/web/abyss/audio/voice/male/oxy_gain.ogg b/web/abyss/audio/voice/male/oxy_gain.ogg new file mode 100644 index 00000000..86eb28e5 Binary files /dev/null and b/web/abyss/audio/voice/male/oxy_gain.ogg differ diff --git a/web/abyss/audio/voice/male/oxy_low.m4a b/web/abyss/audio/voice/male/oxy_low.m4a new file mode 100644 index 00000000..4360c89e Binary files /dev/null and b/web/abyss/audio/voice/male/oxy_low.m4a differ diff --git a/web/abyss/audio/voice/male/oxy_low.ogg b/web/abyss/audio/voice/male/oxy_low.ogg new file mode 100644 index 00000000..90aaba78 Binary files /dev/null and b/web/abyss/audio/voice/male/oxy_low.ogg differ diff --git a/web/abyss/audio/voice/male/oxy_ok.m4a b/web/abyss/audio/voice/male/oxy_ok.m4a new file mode 100644 index 00000000..df051007 Binary files /dev/null and b/web/abyss/audio/voice/male/oxy_ok.m4a differ diff --git a/web/abyss/audio/voice/male/oxy_ok.ogg b/web/abyss/audio/voice/male/oxy_ok.ogg new file mode 100644 index 00000000..07ee4aee Binary files /dev/null and b/web/abyss/audio/voice/male/oxy_ok.ogg differ diff --git a/web/abyss/audio/voice/male/p1.m4a b/web/abyss/audio/voice/male/p1.m4a new file mode 100644 index 00000000..f2e63602 Binary files /dev/null and b/web/abyss/audio/voice/male/p1.m4a differ diff --git a/web/abyss/audio/voice/male/p1.ogg b/web/abyss/audio/voice/male/p1.ogg new file mode 100644 index 00000000..34b20267 Binary files /dev/null and b/web/abyss/audio/voice/male/p1.ogg differ diff --git a/web/abyss/audio/voice/male/p2.m4a b/web/abyss/audio/voice/male/p2.m4a new file mode 100644 index 00000000..5f1ceafe Binary files /dev/null and b/web/abyss/audio/voice/male/p2.m4a differ diff --git a/web/abyss/audio/voice/male/p2.ogg b/web/abyss/audio/voice/male/p2.ogg new file mode 100644 index 00000000..c20dc80a Binary files /dev/null and b/web/abyss/audio/voice/male/p2.ogg differ diff --git a/web/abyss/audio/voice/male/p3.m4a b/web/abyss/audio/voice/male/p3.m4a new file mode 100644 index 00000000..6d61782d Binary files /dev/null and b/web/abyss/audio/voice/male/p3.m4a differ diff --git a/web/abyss/audio/voice/male/p3.ogg b/web/abyss/audio/voice/male/p3.ogg new file mode 100644 index 00000000..cc44733c Binary files /dev/null and b/web/abyss/audio/voice/male/p3.ogg differ diff --git a/web/abyss/audio/voice/male/p4.m4a b/web/abyss/audio/voice/male/p4.m4a new file mode 100644 index 00000000..b7273e0c Binary files /dev/null and b/web/abyss/audio/voice/male/p4.m4a differ diff --git a/web/abyss/audio/voice/male/p4.ogg b/web/abyss/audio/voice/male/p4.ogg new file mode 100644 index 00000000..a29fa850 Binary files /dev/null and b/web/abyss/audio/voice/male/p4.ogg differ diff --git a/web/abyss/audio/voice/male/p5.m4a b/web/abyss/audio/voice/male/p5.m4a new file mode 100644 index 00000000..a867448f Binary files /dev/null and b/web/abyss/audio/voice/male/p5.m4a differ diff --git a/web/abyss/audio/voice/male/p5.ogg b/web/abyss/audio/voice/male/p5.ogg new file mode 100644 index 00000000..f155e564 Binary files /dev/null and b/web/abyss/audio/voice/male/p5.ogg differ diff --git a/web/abyss/audio/voice/male/p6.m4a b/web/abyss/audio/voice/male/p6.m4a new file mode 100644 index 00000000..c6fbe239 Binary files /dev/null and b/web/abyss/audio/voice/male/p6.m4a differ diff --git a/web/abyss/audio/voice/male/p6.ogg b/web/abyss/audio/voice/male/p6.ogg new file mode 100644 index 00000000..939ad4fb Binary files /dev/null and b/web/abyss/audio/voice/male/p6.ogg differ diff --git a/web/abyss/audio/voice/male/p7.m4a b/web/abyss/audio/voice/male/p7.m4a new file mode 100644 index 00000000..a5bc9fc4 Binary files /dev/null and b/web/abyss/audio/voice/male/p7.m4a differ diff --git a/web/abyss/audio/voice/male/p7.ogg b/web/abyss/audio/voice/male/p7.ogg new file mode 100644 index 00000000..86f7121f Binary files /dev/null and b/web/abyss/audio/voice/male/p7.ogg differ diff --git a/web/abyss/audio/voice/male/p7_chen.m4a b/web/abyss/audio/voice/male/p7_chen.m4a new file mode 100644 index 00000000..cd3bf0eb Binary files /dev/null and b/web/abyss/audio/voice/male/p7_chen.m4a differ diff --git a/web/abyss/audio/voice/male/p7_chen.ogg b/web/abyss/audio/voice/male/p7_chen.ogg new file mode 100644 index 00000000..d87a5a85 Binary files /dev/null and b/web/abyss/audio/voice/male/p7_chen.ogg differ diff --git a/web/abyss/audio/voice/male/p7_zheng.m4a b/web/abyss/audio/voice/male/p7_zheng.m4a new file mode 100644 index 00000000..b1532deb Binary files /dev/null and b/web/abyss/audio/voice/male/p7_zheng.m4a differ diff --git a/web/abyss/audio/voice/male/p7_zheng.ogg b/web/abyss/audio/voice/male/p7_zheng.ogg new file mode 100644 index 00000000..411d178c Binary files /dev/null and b/web/abyss/audio/voice/male/p7_zheng.ogg differ diff --git a/web/abyss/audio/voice/male/p8.m4a b/web/abyss/audio/voice/male/p8.m4a new file mode 100644 index 00000000..9bf136a7 Binary files /dev/null and b/web/abyss/audio/voice/male/p8.m4a differ diff --git a/web/abyss/audio/voice/male/p8.ogg b/web/abyss/audio/voice/male/p8.ogg new file mode 100644 index 00000000..424a8f3c Binary files /dev/null and b/web/abyss/audio/voice/male/p8.ogg differ diff --git a/web/abyss/audio/voice/male/p9.m4a b/web/abyss/audio/voice/male/p9.m4a new file mode 100644 index 00000000..5700d795 Binary files /dev/null and b/web/abyss/audio/voice/male/p9.m4a differ diff --git a/web/abyss/audio/voice/male/p9.ogg b/web/abyss/audio/voice/male/p9.ogg new file mode 100644 index 00000000..b8a4b3ea Binary files /dev/null and b/web/abyss/audio/voice/male/p9.ogg differ diff --git a/web/abyss/audio/voice/male/prog_d1.m4a b/web/abyss/audio/voice/male/prog_d1.m4a new file mode 100644 index 00000000..576f9fef Binary files /dev/null and b/web/abyss/audio/voice/male/prog_d1.m4a differ diff --git a/web/abyss/audio/voice/male/prog_d1.ogg b/web/abyss/audio/voice/male/prog_d1.ogg new file mode 100644 index 00000000..a2a82e7a Binary files /dev/null and b/web/abyss/audio/voice/male/prog_d1.ogg differ diff --git a/web/abyss/audio/voice/male/prog_d2.m4a b/web/abyss/audio/voice/male/prog_d2.m4a new file mode 100644 index 00000000..3349d68f Binary files /dev/null and b/web/abyss/audio/voice/male/prog_d2.m4a differ diff --git a/web/abyss/audio/voice/male/prog_d2.ogg b/web/abyss/audio/voice/male/prog_d2.ogg new file mode 100644 index 00000000..3faf4655 Binary files /dev/null and b/web/abyss/audio/voice/male/prog_d2.ogg differ diff --git a/web/abyss/audio/voice/male/prog_d3.m4a b/web/abyss/audio/voice/male/prog_d3.m4a new file mode 100644 index 00000000..d7505497 Binary files /dev/null and b/web/abyss/audio/voice/male/prog_d3.m4a differ diff --git a/web/abyss/audio/voice/male/prog_d3.ogg b/web/abyss/audio/voice/male/prog_d3.ogg new file mode 100644 index 00000000..f49dcf0e Binary files /dev/null and b/web/abyss/audio/voice/male/prog_d3.ogg differ diff --git a/web/abyss/audio/voice/male/prog_d4.m4a b/web/abyss/audio/voice/male/prog_d4.m4a new file mode 100644 index 00000000..15d414a7 Binary files /dev/null and b/web/abyss/audio/voice/male/prog_d4.m4a differ diff --git a/web/abyss/audio/voice/male/prog_d4.ogg b/web/abyss/audio/voice/male/prog_d4.ogg new file mode 100644 index 00000000..80df1f9f Binary files /dev/null and b/web/abyss/audio/voice/male/prog_d4.ogg differ diff --git a/web/abyss/audio/voice/male/sys_continue_ask.m4a b/web/abyss/audio/voice/male/sys_continue_ask.m4a new file mode 100644 index 00000000..71b2b26a Binary files /dev/null and b/web/abyss/audio/voice/male/sys_continue_ask.m4a differ diff --git a/web/abyss/audio/voice/male/sys_continue_ask.ogg b/web/abyss/audio/voice/male/sys_continue_ask.ogg new file mode 100644 index 00000000..9adab109 Binary files /dev/null and b/web/abyss/audio/voice/male/sys_continue_ask.ogg differ diff --git a/web/abyss/audio/voice/male/sys_help.m4a b/web/abyss/audio/voice/male/sys_help.m4a new file mode 100644 index 00000000..9acdf49c Binary files /dev/null and b/web/abyss/audio/voice/male/sys_help.m4a differ diff --git a/web/abyss/audio/voice/male/sys_help.ogg b/web/abyss/audio/voice/male/sys_help.ogg new file mode 100644 index 00000000..ab0058c3 Binary files /dev/null and b/web/abyss/audio/voice/male/sys_help.ogg differ diff --git a/web/abyss/audio/voice/male/sys_menu.m4a b/web/abyss/audio/voice/male/sys_menu.m4a new file mode 100644 index 00000000..ad531467 Binary files /dev/null and b/web/abyss/audio/voice/male/sys_menu.m4a differ diff --git a/web/abyss/audio/voice/male/sys_menu.ogg b/web/abyss/audio/voice/male/sys_menu.ogg new file mode 100644 index 00000000..15a57899 Binary files /dev/null and b/web/abyss/audio/voice/male/sys_menu.ogg differ diff --git a/web/abyss/audio/voice/male/sys_menu_lobby.m4a b/web/abyss/audio/voice/male/sys_menu_lobby.m4a new file mode 100644 index 00000000..f91cd895 Binary files /dev/null and b/web/abyss/audio/voice/male/sys_menu_lobby.m4a differ diff --git a/web/abyss/audio/voice/male/sys_menu_lobby.ogg b/web/abyss/audio/voice/male/sys_menu_lobby.ogg new file mode 100644 index 00000000..5bf69758 Binary files /dev/null and b/web/abyss/audio/voice/male/sys_menu_lobby.ogg differ diff --git a/web/abyss/audio/voice/male/sys_menu_restart.m4a b/web/abyss/audio/voice/male/sys_menu_restart.m4a new file mode 100644 index 00000000..8b25995d Binary files /dev/null and b/web/abyss/audio/voice/male/sys_menu_restart.m4a differ diff --git a/web/abyss/audio/voice/male/sys_menu_restart.ogg b/web/abyss/audio/voice/male/sys_menu_restart.ogg new file mode 100644 index 00000000..d5b32e3d Binary files /dev/null and b/web/abyss/audio/voice/male/sys_menu_restart.ogg differ diff --git a/web/abyss/audio/voice/male/sys_menu_resume.m4a b/web/abyss/audio/voice/male/sys_menu_resume.m4a new file mode 100644 index 00000000..68bf42b5 Binary files /dev/null and b/web/abyss/audio/voice/male/sys_menu_resume.m4a differ diff --git a/web/abyss/audio/voice/male/sys_menu_resume.ogg b/web/abyss/audio/voice/male/sys_menu_resume.ogg new file mode 100644 index 00000000..cde195b0 Binary files /dev/null and b/web/abyss/audio/voice/male/sys_menu_resume.ogg differ diff --git a/web/abyss/audio/voice/male/sys_new_dive.m4a b/web/abyss/audio/voice/male/sys_new_dive.m4a new file mode 100644 index 00000000..91877fd0 Binary files /dev/null and b/web/abyss/audio/voice/male/sys_new_dive.m4a differ diff --git a/web/abyss/audio/voice/male/sys_new_dive.ogg b/web/abyss/audio/voice/male/sys_new_dive.ogg new file mode 100644 index 00000000..c6af4500 Binary files /dev/null and b/web/abyss/audio/voice/male/sys_new_dive.ogg differ diff --git a/web/abyss/audio/voice/male/sys_settings.m4a b/web/abyss/audio/voice/male/sys_settings.m4a new file mode 100644 index 00000000..5eec36bc Binary files /dev/null and b/web/abyss/audio/voice/male/sys_settings.m4a differ diff --git a/web/abyss/audio/voice/male/sys_settings.ogg b/web/abyss/audio/voice/male/sys_settings.ogg new file mode 100644 index 00000000..02a1dd23 Binary files /dev/null and b/web/abyss/audio/voice/male/sys_settings.ogg differ diff --git a/web/abyss/audio/voice/male/sys_skip.m4a b/web/abyss/audio/voice/male/sys_skip.m4a new file mode 100644 index 00000000..8b3e2400 Binary files /dev/null and b/web/abyss/audio/voice/male/sys_skip.m4a differ diff --git a/web/abyss/audio/voice/male/sys_skip.ogg b/web/abyss/audio/voice/male/sys_skip.ogg new file mode 100644 index 00000000..93264c8e Binary files /dev/null and b/web/abyss/audio/voice/male/sys_skip.ogg differ diff --git a/web/abyss/audio/voice/z1_a.m4a b/web/abyss/audio/voice/z1_a.m4a new file mode 100644 index 00000000..beae16c0 Binary files /dev/null and b/web/abyss/audio/voice/z1_a.m4a differ diff --git a/web/abyss/audio/voice/z1_a.ogg b/web/abyss/audio/voice/z1_a.ogg new file mode 100644 index 00000000..370f35ab Binary files /dev/null and b/web/abyss/audio/voice/z1_a.ogg differ diff --git a/web/abyss/audio/voice/z1_b.m4a b/web/abyss/audio/voice/z1_b.m4a new file mode 100644 index 00000000..68d9d7cb Binary files /dev/null and b/web/abyss/audio/voice/z1_b.m4a differ diff --git a/web/abyss/audio/voice/z1_b.ogg b/web/abyss/audio/voice/z1_b.ogg new file mode 100644 index 00000000..8e4c2b4c Binary files /dev/null and b/web/abyss/audio/voice/z1_b.ogg differ diff --git a/web/abyss/audio/voice/z1_bye.m4a b/web/abyss/audio/voice/z1_bye.m4a new file mode 100644 index 00000000..dd1e07e5 Binary files /dev/null and b/web/abyss/audio/voice/z1_bye.m4a differ diff --git a/web/abyss/audio/voice/z1_bye.ogg b/web/abyss/audio/voice/z1_bye.ogg new file mode 100644 index 00000000..8f4d22cb Binary files /dev/null and b/web/abyss/audio/voice/z1_bye.ogg differ diff --git a/web/abyss/audio/voice/z1_knock.m4a b/web/abyss/audio/voice/z1_knock.m4a new file mode 100644 index 00000000..d7b869d7 Binary files /dev/null and b/web/abyss/audio/voice/z1_knock.m4a differ diff --git a/web/abyss/audio/voice/z1_knock.ogg b/web/abyss/audio/voice/z1_knock.ogg new file mode 100644 index 00000000..25df74cc Binary files /dev/null and b/web/abyss/audio/voice/z1_knock.ogg differ diff --git a/web/abyss/cover.png b/web/abyss/cover.png new file mode 100644 index 00000000..0769aae6 Binary files /dev/null and b/web/abyss/cover.png differ diff --git a/web/abyss/game.js b/web/abyss/game.js new file mode 100644 index 00000000..066d0615 --- /dev/null +++ b/web/abyss/game.js @@ -0,0 +1,1727 @@ +/** + * 《深海回音》——深海救援叙事 RPG(docs/design/17-abyss.md)。 + * + * 你是深潜救援员:声呐里是被困海沟的潜艇「海燕号」——氧气在倒数,每次下潜 + * 都是抉择:先救谁、花多少气、留不留、上不上去。148 句台词见 + * 17-abyss-voice-ids.md;4 潜共享主干、9 抉择点(单局实走 7-9)、4 结局、 + * 跨潜只传标量(oxy / saved{} / stayer / 6 flag + bond 好感派生账)。 + * + * 6 宏状态(§2.1):INTRO / DIVE / COMMS / SURFACE / ENDING + MENU 覆盖层 + * (另加 ASK = 有存档进游戏的「继续上次?」询问态,radio 同款前置瞬态)。 + * DIVE 内分两段:下潜链(auto)→ 搜索等待(单击=发一枚声呐脉冲,宪法 §5.1 + * 「检查」义;回波→静默一拍→解读,R2 声呐主导段)。 + * + * 手势(§2.3,6+2 全覆盖零豁免): + * - tap :播放中=跳过本句(p* 不可跳 / F5 链末句 / F4 续玩锁); + * 声呐步等待中=快进该回波余音(危机拍前置窗除外); + * 链末=继续;搜索等待=发一枚声呐脉冲;ENDING 结算态=再来一局 + * - doubletap :重听当前节点台词(含抉择提示);搜索等待=重听上一条回波 + * +其解读句(尚无回波=无效,零静默);ENDING=重听结局链; + * MENU=确认执行(§5.2 模态约定覆盖重听) + * - left / right :COMMS/SURFACE 抉择(左=走心多花气 / 右=克制省气;p* 播放中 + * 缓存 pendingChoice);其余态无效 → error 震 + gd_invalid + * - up :帮助+状态(潜次+氧档),G6 顺序 prog → oxy → sys_help + * - down :模态菜单(单击循环 / 双击确认 / 上滑帮助 / 下滑关闭) + * - longpress :引擎接管旁白 1.7×(叙事类不豁免,游戏不收此手势) + * + * 氧气经济(§4.3):步进资源非实时倒计时。每潜开始扣 3(D4 双载额外 +1); + * 抉择按 §4.2 扣;mid-dive 扣至负数一律 floor 到 0(人已挂上吊索照常带上), + * floor 触发干涸链 dry_a(链入口 800ms 手势死窗)→ 上浮结算内 dry_b(归因 + * 真话)。档位以 floor(oxy/3)(可下潜次数)划界,只在跨档时挂上浮结算链尾; + * SURFACE(3) 结算后走氧气门三形态(≥4 双载 / =3 单载 / ≤2 终局取消链)。 + * + * 声呐双通道(§6.2):sonarGate 空闲门——信息类回波(echo_* / pipe_knock / + * groan)只在 voice idle 时播出,否则挂队列表、链末补发(R1 零重叠); + * 危机拍(D2 火花 / D4 滑坡)前置 2s 起播(danger 签名 + 音效同窗,D4 叠 + * danger 震循环 ≥400ms 间隔);语音在播时 amb_deep 降 -6dB(R3 常驻退位)。 + * + * 结算死窗(H1 内化):终局取消链 / 干涸链 / e* 结局链入口盖 800ms 手势 + * 死窗,窗内手势一律吞掉,结算链完整播出。 + * + * 看门狗双轨(§7.2):无操作轨(guide watchdog,任何手势复位;搜索等待 + * [10,25] gd_ping1/gd_ping2 两级不同句;叙事链末 [25,50]/[13,30] + * gd_continue/gd_again)+ 无进展轨(游戏层计时 60/120s gd_prog1/gd_prog2, + * 仅挂搜索等待与 COMMS 链末两个可饿死点;无效手势不复位——乱按 96s 也必被 + * 抢救;两轨同级交叉登记去重;guide=off 不挂任何定时器)。 + * + * radio R2/R3 修复清单全部继承(15-radio.md 实现核对 #1-8 + G1-G6): + * 链末等待(auto 标志生效)/ longpress 不误报无效 / 菜单报名游戏侧排队 / + * 首潜前重听报无效 / ENDING 上滑含状态 / stop() 先 seqToken++ 作废在途链 / + * newRun 清抉择武装 / 循环垫句柄防双链;G1 帮助链去重(同等待点第 2 次起 + * 纯音频标记)、G2 oxy_crit 附 warning 震、G4 不可跳句空点 beat 轻震、 + * G5 首播必完整复播可即掐、G6 状态链顺序。 + * + * 资产缺失兜底:引擎 console.error 静默跳过且不回调 onComplete;单句语音 + * 看门狗 30s 且 isVoicePlaying()===false 才自动续链(storyline N1 先例照抄) + * ——屏蔽任意 voice 资产仍可一路走到结局,不卡死。环境音循环(amb_deck / + * amb_deep / breath_loop)由游戏层 buffer.loop=true 持句柄(radio _ringSrc + * 先例,无重触发定时器,无头测试可复现)。 + */ +import { Game } from '../engine/game.js'; +import { Guide } from '../engine/guide.js'; +import { vibrate } from '../engine/haptics.js'; + +// ---------------------------------------------------------------- 声呐时序常量 + +// 声呐链驻留时长(ms):脉冲→回波静默一拍 / 回波余音 / 危机前置 2s(L2) +const PING_GAP_MS = 900; +const W_Metal = 2200; +const W_Knock = 2600; +const W_Groan = 4000; +const W_Crisis = 2000; +const W_Bell = 3200; +const DANGER_LOOP_MS = 500; // danger 震循环间隔(≥400ms,§5.2) + +// ---------------------------------------------------------------- 节点表 + +/** 声呐步:信息音/危机前置音,wait = 起播后链驻留时长(ms)。 */ +function S(sfx, wait, extra = {}) { + return { sfx, wait, danger: false, dangerLoop: false, volume: undefined, ...extra }; +} + +/** 叙事节点。voices 顺序播放;pre/post 为声呐步(链内零重叠,§6.2 R1)。 */ +function N(voices, opts = {}) { + return { + voices, prompt: null, left: null, right: null, next: null, dyn: null, + pre: null, post: null, sfx: null, auto: false, enterComms: false, + ...opts, + }; +} +/** 抉择节点:本体无台词,链尾接 p* 提示句(PROMPT_RE 命中 → 不可跳)。 */ +function P(prompt, left, right, opts = {}) { + return { + voices: [], prompt, left, right, next: null, dyn: null, + pre: null, post: null, sfx: null, auto: false, enterComms: false, + ...opts, + }; +} + +/** + * 全部节点(id = 首句 voice id;台词全文见 17-abyss-voice-ids.md,148 句)。 + * next 为 '@x' 虚拟目标:@dive 进第一潜 / @search 武装搜索等待 / + * @surfaceN 上浮结算 / @surfaceNext 结算后分流(下一潜 / 氧气门 / 签收)/ + * @signoff 签收链 / @ending 进结局。p7 提示与目标按氧气门形态动态解析 + * (双载 p7 / 单载 p7_chen|p7_zheng,dyn='p7')。 + */ +const NODES = { + // ---- INTRO 开场链(甲板口播 + 一句规矩 + 今晚目标;链末等待单击,§2.2) + b_open: N(['voice/b_open'], { next: 'b_rule' }), + b_rule: N(['voice/b_rule'], { next: 'b_goal' }), + b_goal: N(['voice/b_goal'], { next: 'b_go' }), + b_go: N(['voice/b_go'], { next: '@dive' }), + + // ---- D1 找船·小舟(教学潜:单击=脉冲 / 金属回波=船身,两大教学承诺) + dv1_down: N(['voice/dv1_down'], { auto: true, next: 'dv1_teach' }), + dv1_teach: N(['voice/dv1_teach'], { auto: true, next: '@search' }), + // 搜索回波链:脉冲 → 静默一拍 → echo_metal_l → 解读 dv1_metal(承诺音) + sv1_echo: N(['voice/dv1_metal'], { + auto: true, + pre: [S('sfx/sonar_ping', PING_GAP_MS), S('sfx/echo_metal_l', W_Metal)], + next: 'dv1_link', + }), + dv1_link: N(['voice/dv1_link'], { sfx: 'sfx/link_beep', auto: true, next: 'z1_a' }), + z1_a: N(['voice/z1_a'], { sfx: 'sfx/static_burst', enterComms: true, next: 'z1_b' }), + z1_b: N(['voice/z1_b'], { next: 'z1_knock' }), + // z1_knock 垫 pipe_knock 两遍(线索音 2-3 次取确定值 2;语音播完才响,R1) + z1_knock: N(['voice/z1_knock'], { + post: [S('sfx/pipe_knock', W_Knock), S('sfx/pipe_knock', W_Knock)], + next: 'p1', + }), + p1: P('voice/p1', 'd1_listen', 'd1_up'), + d1_listen: N(['voice/d1_listen'], { post: [S('sfx/pipe_knock', W_Knock)], next: 'd1_code' }), + d1_code: N(['voice/d1_code'], { next: 'p2' }), + d1_up: N(['voice/d1_up'], { next: 'z1_bye' }), + z1_bye: N(['voice/z1_bye'], { next: 'p2' }), + p2: P('voice/p2', 'd1_log', 'd1_nolog'), + d1_log: N(['voice/d1_log'], { next: '@surface1' }), + d1_nolog: N(['voice/d1_nolog'], { next: '@surface1' }), + + // ---- SURFACE(1) 上浮结算(自动链 ≤15s:扣账句 → 跨档档位句 → 直接下一潜) + s1_surf: N(['voice/d1_surf'], { auto: true, next: 's_tier' }), + + // ---- D2 机舱·老白(危机拍#1:danger 签名 + spark_flash 前置 2s) + dv2_down: N(['voice/dv2_down'], { auto: true, next: '@search' }), + sv2_echo: N(['voice/dv2_found'], { + auto: true, + pre: [S('sfx/sonar_ping', PING_GAP_MS), S('sfx/echo_debris', W_Metal), S('sfx/pipe_knock', W_Knock)], + next: 'bai_a', + }), + bai_a: N(['voice/bai_a'], { enterComms: true, next: 'bai_b' }), + bai_b: N(['voice/bai_b'], { next: 'p3' }), + p3: P('voice/p3', 'd2_pry', 'd2_cut'), + d2_pry: N(['voice/d2_pry'], { next: 'd2_pryres' }), + // 节点内条件选播(radio w1_say 先例):knowsPipe 决定 -0 / -1 的差分收尾 + d2_pryres: N([], { dyn: 'd2pry', next: 'bai_out' }), + d2_cut: N(['voice/d2_cut'], { + auto: true, + post: [S('sfx/spark_flash', W_Crisis, { danger: true })], + next: 'd2_flash', + }), + d2_flash: N(['voice/d2_flash'], { next: 'bai_out' }), + bai_out: N(['voice/bai_out'], { next: 'p4' }), + p4: P('voice/p4', 'd2_valve', 'd2_leave'), + d2_valve: N(['voice/d2_valve'], { post: [S('sfx/valve_close', W_Metal)], next: 'd2_valve_e' }), + d2_valve_e: N(['voice/d2_valve_e'], { next: 'bai_thanks' }), + bai_thanks: N(['voice/bai_thanks'], { next: '@surface2' }), + d2_leave: N(['voice/d2_leave'], { next: '@surface2' }), + + // ---- SURFACE(2):d2_surf → d2_supply → oxy_gain(应急瓶 +3,全局唯一) + s2_surf: N(['voice/d2_surf'], { auto: true, next: 's2_supply' }), + s2_supply: N(['voice/d2_supply'], { auto: true, next: 's2_gain' }), + s2_gain: N(['voice/oxy_gain'], { auto: true, next: 's_tier' }), + + // ---- D3 病舱·陈医官/小郑(回波预警埋 D4 险;小郑无台词,吊床固定) + dv3_down: N(['voice/dv3_down'], { auto: true, next: '@search' }), + sv3_echo: N(['voice/dv3_groan'], { + auto: true, + pre: [S('sfx/sonar_ping', PING_GAP_MS), S('sfx/echo_groan', W_Groan)], + next: 'chen_a', + }), + chen_a: N(['voice/chen_a'], { enterComms: true, next: 'chen_b' }), + chen_b: N(['voice/chen_b'], { next: 'p5' }), + p5: P('voice/p5', 'd3_chen', 'd3_zheng'), + d3_chen: N(['voice/d3_chen'], { next: 'chen_up' }), + chen_up: N(['voice/chen_up'], { next: 'd3_chen_e' }), + d3_chen_e: N(['voice/d3_chen_e'], { next: 'p6' }), + d3_zheng: N(['voice/d3_zheng'], { next: 'd3_zheng_e' }), + d3_zheng_e: N(['voice/d3_zheng_e'], { next: 'chen_stay' }), + chen_stay: N(['voice/chen_stay'], { next: 'p6' }), + p6: P('voice/p6', 'd3_spare', 'd3_promise'), + d3_spare: N(['voice/d3_spare'], { next: 'd3_spare_e' }), + d3_spare_e: N(['voice/d3_spare_e'], { next: '@surface3' }), + d3_promise: N(['voice/d3_promise'], { next: '@surface3' }), + + // ---- SURFACE(3):d3_surf → 档位 → 氧气门三形态 + s3_surf: N(['voice/d3_surf'], { auto: true, next: 's_tier' }), + + // ---- D4 指挥舱·高船长(高潮:滑坡危机拍前置 2s 渐强 + danger 震循环) + dv4_down: N(['voice/dv4_down'], { auto: true, next: '@search' }), + // 回波渐强 + 金属回波(§6.3 承诺:D4 接近指挥舱必播 echo_metal*,取 _r 声像) + sv4_echo: N([], { + auto: true, + pre: [S('sfx/sonar_ping', PING_GAP_MS), S('sfx/echo_groan', W_Groan), S('sfx/echo_metal_r', W_Metal)], + next: 'sv4_crisis', + }), + sv4_crisis: N(['voice/dv4_slide'], { + auto: true, + pre: [S('sfx/landslide', W_Crisis, { danger: true, dangerLoop: true })], + next: 'gao_a', + }), + gao_a: N(['voice/gao_a'], { enterComms: true, next: 'gao_b' }), + gao_b: N(['voice/gao_b'], { next: 'p7' }), + // p7 三形态:双载(快慢浮)/ 单载(按 stayer 选 p7_chen|p7_zheng,谁上) + p7: P(null, null, null, { dyn: 'p7' }), + d4_slow: N(['voice/d4_slow'], { next: 'd4_slow_e' }), + d4_slow_e: N(['voice/d4_slow_e'], { next: 'd4_confess' }), + d4_fast: N(['voice/d4_fast'], { next: 'd4_fast_e' }), + d4_fast_e: N(['voice/d4_fast_e'], { next: 'd4_confess' }), + // 心结引入差分:hasLog ? gao_confess : gao_c + d4_confess: N([], { dyn: 'gaoconfess', next: 'p8' }), + p8: P('voice/p8', 'd4_talk', 'd4_silent'), + d4_talk: N(['voice/d4_talk'], { next: 'gao_peace' }), + gao_peace: N(['voice/gao_peace'], { next: '@surface4' }), + d4_silent: N(['voice/d4_silent'], { next: '@surface4' }), + d4_pick_gao: N(['voice/d4_pick_gao'], { next: 'd4_leave' }), + // 带走船长时留守者留别:chen_last | d4_stay_lost(克制措辞);船长在乘 → + // 照常走心结差分 → p8(voice-ids:仅带走留守者的右路才跳过 p8) + d4_leave: N([], { dyn: 'stayleave', next: 'd4_confess' }), + d4_pick_chen: N(['voice/d4_pick_chen'], { auto: true, next: 'd4_gaolast' }), + d4_pick_zheng: N(['voice/d4_pick_zheng'], { auto: true, next: 'd4_gaolast' }), + // 带走留守者时船长留别(p8 跳过,直达上浮) + d4_gaolast: N(['voice/gao_last'], { auto: true, next: '@surface4' }), + + // ---- SURFACE(4):d4_surf → 档位 → 签收 + s4_surf: N(['voice/d4_surf'], { auto: true, next: 's_tier' }), + + // ---- 档位节点(dyn='tier':只在跨档时播分档整句;空则直通) + s_tier: N([], { dyn: 'tier', auto: true, next: '@surfaceNext' }), + + // ---- 终局取消链(氧气门 ≤2 形态;入口 800ms 手势死窗) + c_none: N(['voice/dv4_none'], { auto: true, next: 'c_final' }), + c_final: N(['voice/gao_final'], { auto: true, next: '@signoff' }), + + // ---- 签收 + ◆p9 心结抉择(p9 左需 oxy≥1;不足时 g9_dry 播出后自动走右) + g9_in: N(['voice/g9_in'], { auto: true, next: 'g9_knot' }), + g9_knot: N(['voice/g9_knot'], { next: 'p9' }), + p9: P('voice/p9', 'd9_bell', 'd9_rest'), + g9_dry: N(['voice/g9_dry'], { auto: true, next: 'd9_rest' }), + d9_bell: N(['voice/d9_bell'], { auto: true, post: [S('sfx/bell_toll', W_Bell)], next: 'd9_bello' }), + d9_bello: N(['voice/d9_bello'], { auto: true, next: '@ending' }), + d9_rest: N(['voice/d9_rest'], { auto: true, next: '@ending' }), +}; + +// ---------------------------------------------------------------- 常量表 + +/** 潜次舱段(summary / 状态链用)。 */ +const SEGS = ['找船', '机舱', '病舱', '指挥舱']; + +/** 上滑状态链「第几潜」分档整句(§4.3 数字规避:禁止运行时拼接)。 */ +const PROG_VOICES = ['voice/prog_d1', 'voice/prog_d2', 'voice/prog_d3', 'voice/prog_d4']; +/** 氧气档位分档整句(floor(oxy/3)=可下潜次数划界,句义逐格为真,§4.3)。 */ +const TIER_VOICES = { full: 'voice/oxy_full', ok: 'voice/oxy_ok', low: 'voice/oxy_low', crit: 'voice/oxy_crit' }; + +const MENU_ITEMS = [ + { id: 'resume', voice: 'voice/sys_menu_resume' }, // 回到水下 + { id: 'restart', voice: 'voice/sys_menu_restart' }, // 重新开始这次救援 + { id: 'lobby', voice: 'voice/sys_menu_lobby' }, // 保存并回大厅 +]; + +/** 4 结局(§3.4:savedCount × p9 四选一)。 */ +const ENDINGS = { + e1: { name: '海燕归钟', voices: ['voice/e1_a', 'voice/e1_b'] }, + e2: { name: '满载的黎明', voices: ['voice/e2_a', 'voice/e2_b'] }, + e3: { name: '最后一潜', voices: ['voice/e3_a', 'voice/e3_b'] }, + e4: { name: '按规程上浮', voices: ['voice/e4_a', 'voice/e4_b'] }, +}; + +const ALL_SFX = [ + 'sfx/sonar_ping', 'sfx/echo_metal_l', 'sfx/echo_metal_r', 'sfx/echo_debris', + 'sfx/echo_groan', 'sfx/pipe_knock', 'sfx/link_beep', 'sfx/static_burst', + 'sfx/spark_flash', 'sfx/valve_close', 'sfx/landslide', 'sfx/surf_splash', + 'sfx/ascent_chime', 'sfx/bell_toll', 'sfx/amb_deep', 'sfx/amb_deck', + 'sfx/breath_loop', 'sfx/ui_confirm', 'sfx/ui_menu', +]; + +const SAVE_KEY = 'soundgame.progress.abyss'; +const AUTH_KEY = 'soundgame.auth'; + +/** 抉择提示音 id(voice/p1~p9,含 p7_chen/p7_zheng):播放中单击不可跳。 */ +const PROMPT_RE = /^voice\/p\d/; + +/** 单句语音看门狗:超时且引擎确认 voice 未在播 = 资产缺失才续链(N1 先例)。 */ +const VOICE_WATCHDOG_MS = 30000; +/** 结算链手势死窗(dungeon H1 同款):窗内手势一律吞掉。 */ +const SETTLE_GRACE_MS = 800; + +// 向导层看门狗(§7.2;full 档 say 由 guide 播,standard 档 onEscalate 游戏层补播) +const GUIDE_WATCHDOG_AT = [25000, 50000]; // full:叙事链末 / ENDING 结算等待 +const IDLE_HINT_AT = [13000, 30000]; // standard:同 storyline P2-3 先例 +const CHOICE_HINT_AT = [25000, 50000]; // 抉择点:两级都重播当前 p* 提示句本身 +const SEARCH_WATCHDOG_AT = [10000, 25000]; // 搜索等待两级**不同句**(gd_ping1≠gd_ping2) +const PROG_AT = [60000, 120000]; // 无进展轨(游戏层计时,乱按抢救) + +/** 氧气档位(§4.3):floor(oxy/3) = 可下潜次数,句子说的就是判定用的。 */ +function tierOf(oxy) { + const dives = Math.floor(Math.max(0, oxy) / 3); + if (dives >= 3) return 'full'; + if (dives === 2) return 'ok'; + if (dives === 1) return 'low'; + return 'crit'; +} + +/** 节点 id → 潜号(断点续玩兜底:存档缺 dive 字段时按节点前缀推断)。 */ +function diveOfNode(id) { + if (/^(dv|sv)(\d)/.test(id)) return Number(id[2]); + if (/^s(\d)_/.test(id)) return Number(id[1]); + if (/^z1_|^d1_|^p[12]$/.test(id)) return 1; + if (/^bai_|^d2_|^p[34]$/.test(id)) return 2; + if (/^chen_|^d3_|^p[56]$/.test(id)) return 3; + if (/^gao_|^d4_|^g9_|^d9_|^p[789]$|^c_|^e[1-4]$|^ep_/.test(id)) return 4; + return 0; +} + +export class AbyssGame extends Game { + constructor(audio, root) { + super(audio, root); + this.guide = new Guide(audio); // 全局向导层(guide='off' 时全部静默 no-op) + + this.phase = 'INTRO'; // INTRO|DIVE|COMMS|SURFACE|ENDING|MENU|ASK + this.returnPhase = null; // MENU 关闭后回原状态 + this.returnNode = null; + this.returnSearch = false; // MENU 前是否在搜索等待 + + this.nodeId = 'b_open'; + this.diveN = 0; // 当前潜号(0=甲板简报) + this.diveForm = null; // 氧气门形态:'double'|'single'|'cancel'(D4 前置) + this.endingId = null; + + this.voicePlaying = false; + this.currentVoiceId = null; // 链上当前句 id(null=声呐步) + this.promptPlaying = false; // 当前句是抉择提示 p* + this.choiceArmed = false; + this.pendingChoice = null; + this.awaitingRestart = false; + this.leaving = false; // 菜单「保存并回大厅」已确认 + this.searchArmed = false; // DIVE 搜索等待(单击=发脉冲) + this.seqToken = 0; + + // ---- 跨潜标量(§4.1;跨潜只传标量,分支不爆炸) + this.oxy = 12; // 氧气格(步进资源:只在事件点扣) + this.supply = false; // D2 上浮后应急瓶已发(全局唯一) + this.saved = { zhou: 0, bai: 0, chen: 0, zheng: 0, gao: 0 }; + this.stayer = null; // p5 后留守病舱者('chen'|'zheng') + this.flags = { + knowsPipe: false, hasLog: false, valve: false, + spareGift: false, gaoPeace: false, bell: false, + }; + this.bond = { zhou: 0, bai: 0 }; // 好感(只影响回响选播,不影响结局判定) + this.endings = {}; // 结局收集(跨局保留) + this.runs = 0; + + this.lastAnnouncedTier = null; // 跨档播报基准(null=首潜结算必播档位) + this.lastCloser = null; // 上一节点末句(结算双击重听备用) + this.saveStash = null; // ASK 态暂存的存档 + this._lastEcho = null; // 搜索双击重听(回波步+解读句) + this.sysSkipShown = false; // sys_skip 每实例一次 + this._briefShown = false; // gd_brief 每实例一次(提示永不逐字重复) + this._menuQueue = []; // 菜单项报名队列(游戏侧排队,零截断) + this._helpSeen = new Set(); // G1:已全量播过 sys_help 口令的等待点 + + // F4 断点续玩跳句锁:_resumeGuard 置位,恢复链首句起播时落锁,播完即清 + this._resumeGuard = false; + this._skipLockId = null; + this._graceUntil = 0; // H1:结算手势死窗截止时刻 + + // 声呐双通道(§6.2):空闲门 + 排队补发(回波与语音零重叠,R1) + this._sonarQueue = []; + this._sonarSkip = null; // 声呐步等待中的单击快进(危机前置窗除外) + this._voiceIntervals = []; // 转写/断言用:voice 占用区间 {id,start,end} + this._sonarPlays = []; // 转写/断言用:声呐音播出时刻 {id,t} + + // 干涸链(§4.3):mid-dive floor 至 0 → dry_a(当场合)→ dry_b(上浮结算内) + this._dryPending = false; + this._dryAQueued = false; + this._dryBQueued = false; + + // 环境音循环句柄(amb_deck/amb_deep/breath_loop;buffer.loop 无重触发定时器) + this._loops = {}; + this._ambDucked = false; // R3:语音在播时 amb 降 -6dB(gain ×0.5) + this._breathOn = false; // 低氧混音签名(tier=low/crit 且水下) + this._breathLog = []; // 断言用:呼吸垫起停轨迹 + + // 无进展轨(游戏层计时,§7.2 双轨之「乱按也抢救」) + this._prog = null; + this._progToken = 0; + + // danger 震循环(D4 危机拍专用,间隔 ≥400ms) + this._dangerLoop = null; + } + + // ---------------------------------------------------------------- Game + + start() { + this.audio.play('bgm/deck'); // 甲板(bgm=0 时引擎只记意图,重开自动补播) + this.audio.prewarm(ALL_SFX); // 预解码短音效 + this._startLoop('sfx/amb_deck', 0.6); + const local = this._loadLocal(); + if (local && local.phase && local.node) { + this._askContinue(local); + return; + } + // 无本地存档:登录态下发服务器存档(1.5s 内未返回/无存档 → 直接开新救援) + let auth = null; + try { auth = JSON.parse(localStorage.getItem(AUTH_KEY) || 'null'); } catch (_) { /* 静默 */ } + if (!(auth && auth.token)) { this.newRun(); return; } + let settled = false; + const goFresh = () => { if (settled) return; settled = true; this.newRun(); }; + fetch('/api/progress?game=abyss', { headers: { Authorization: `Bearer ${auth.token}` } }) + .then((r) => (r.ok ? r.json() : null)) + .then((j) => { + if (settled) return; + settled = true; + const d = j && j.data; + if (d && d.phase && d.node) this._askContinue(d); + else goFresh(); + }) + .catch(goFresh); + setTimeout(goFresh, 1500); + } + + stop() { + this.seqToken++; // 作废一切在途链回调:stop 后中断链不得经 30s 看门狗复活 + this._stopAllLoops(); + this._stopDangerLoop(); + this._clearProg(); + this._graceUntil = 0; + this._resumeGuard = false; + this._skipLockId = null; + this._sonarQueue.length = 0; + this._closeVoiceRecs(); + this.audio.stopAll(); + } + + onGesture(g) { + this.guide.disarm(); // 任何手势复位看门狗(guide=off 时 no-op) + // H1 结算死窗:终局取消链/干涸链/结局链开播 800ms 内的在途手势一律吞掉 + // (ghost 手势不算玩家输入——落进结算态会被当 tap 掐死结算链半句) + if (performance.now() < this._graceUntil) return; + switch (this.phase) { + case 'ASK': this._onAskGesture(g); break; + case 'MENU': this._onMenuGesture(g); break; + case 'DIVE': + if (this.searchArmed) this._onSearchGesture(g); + else this._onStoryGesture(g); + break; + case 'INTRO': case 'COMMS': case 'SURFACE': this._onStoryGesture(g); break; + case 'ENDING': this._onEndingGesture(g); break; + default: break; + } + } + + // ---------------------------------------------------------------- 手势:ASK + + _onAskGesture(g) { + const go = g === 'tap' || g === 'right'; // 推进手势:局面即将改变,不重挂 + if (g === 'tap') this._resume(this.saveStash); + else if (g === 'right') this.newRun(); + else if (g === 'longpress' || g === 'longpressend' || g === 'tapcancel') { /* 引擎快进接管 */ } + else this.guide.error({ voice: 'gd_invalid', onceKey: 'abyss_invalid' }); + // F1:非推进手势拆掉的 ASK 空闲看门狗处理完立即重挂(否则询问态无限静默) + if (!go && this.phase === 'ASK' && !this.voicePlaying) this._armIdleWatchdog('ASK'); + } + + // ---------------------------------------------------------------- 手势:DIVE 搜索等待 + + _onSearchGesture(g) { + switch (g) { + case 'tap': + this._firePing(); // 单击=发一枚声呐脉冲(宪法 §5.1「检查」义;恒出 ping) + break; + case 'doubletap': + // 重听上一条回波+其解读句(R5);尚无回波=无效手势零静默(首潜前) + this._replayEcho(); + break; + case 'up': { + // 帮助+状态(潜次+氧档),留在搜索等待(§2.3 DIVE 行) + this.seqToken++; + this.audio.stopVoice(); + const ids = this._statusIds(); + this._withHelp(ids, `S:search${this.diveN}`); + this.searchArmed = false; + this._playSeqSteps(ids, () => { + this.voicePlaying = false; + this._enterSearch(); + }); + break; + } + case 'down': + this.openMenu(); + break; + case 'left': + case 'right': + // 无效手势零静默(§2.3 DIVE 行:喂无进展轨——不复位 prog 计时) + this.guide.error({ voice: 'gd_invalid', onceKey: 'abyss_invalid' }); + this._rearmSearch(); + break; + default: + this._rearmSearch(); // F1:快进等引擎接管手势同款(空闲态才重挂) + break; // longpress/longpressend/tapcancel:引擎快进接管,游戏不处理 + } + } + + /** F1:搜索空闲(无对白在播)时重挂两级搜索看门狗;重听/播报在播时不抢挂。 */ + _rearmSearch() { + if (this.phase === 'DIVE' && this.searchArmed && !this.audio.isVoicePlaying()) { + this._armSearchWatchdog(); + } + } + + /** 单击=发脉冲:搜索等待 → 回波链(脉冲 → 静默一拍 → 回波 → 解读)。 */ + _firePing() { + if (!this.searchArmed) return; + this.searchArmed = false; + this._clearProg(); // 有真实进展:无进展轨撤防(当次等待点作废) + this.playNode(`sv${this.diveN}_echo`); + } + + /** 双击重听上一条回波+其解读句(无回波=无效)。 */ + _replayEcho() { + if (!this._lastEcho) { + this.guide.error({ voice: 'gd_invalid', onceKey: 'abyss_invalid' }); + this._rearmSearch(); + return; + } + this.seqToken++; + this.audio.stopVoice(); + this.searchArmed = false; + const steps = [...(this._lastEcho.pre || [])]; + if (this._lastEcho.voice) steps.push(this._lastEcho.voice); + this._playSeqSteps(steps, () => { + this.voicePlaying = false; + this._enterSearch(); + }); + } + + // ---------------------------------------------------------------- 手势:INTRO / DIVE 链 / COMMS / SURFACE + + _onStoryGesture(g) { + const node = NODES[this.nodeId]; + if (!node) return; + switch (g) { + case 'tap': + if (this.voicePlaying) { + if (this.currentVoiceId == null) { + // 声呐步等待中:快进回波余音(危机拍前置窗不快进,vibrate beat 标记) + if (this._sonarSkip) this._sonarSkip(); + else vibrate('beat'); + return; + } + // 旁白播放中单击 = 跳过本句(链继续);不可跳的三类: + // ① 抉择提示 p*(PROMPT_RE 门,盲玩公平); + // ② F5 链末句——等待/过场/抉择交接前的末句; + // ③ F4 续玩锁——断点恢复后的首句播完才武装跳句。 + if (!this.promptPlaying && !this._isTailSentence() + && this.currentVoiceId !== this._skipLockId) { + this.audio.skipVoice(); + } else { + // G4:不可跳句上的单击「点了但没选上」补 beat 轻震(可区分签名) + vibrate('beat'); + } + } else if (this._isPromptNode(node) && this.choiceArmed) { + this.armChoiceHint(); // 抉择等待态空点 = 重新挂载兜底(25s 计时重置) + } else if (node.next != null) { + this._clearProg(); // 有真实进展 + this.advance(node.next); + } + break; + case 'doubletap': + this.replayNode(); + break; + case 'left': + case 'right': { + if (!this._isPromptNode(node)) { + // 无效手势零静默(§2.3):error 震 + full 档 gd_invalid(onceKey 同局一次) + this.guide.error({ voice: 'gd_invalid', onceKey: 'abyss_invalid' }); + // F1:onGesture 入口的 disarm 已拆链末看门狗,处理完立即重挂 + if (this._inStoryWait()) this._armIdleWatchdog(this.phase); + return; + } + if (this.choiceArmed) this.choose(g); + else if (this.voicePlaying) this.pendingChoice = g; // p* 播放中缓存(盲玩公平) + break; + } + case 'up': + this._playHelpAndResume(); + break; + case 'down': + this.openMenu(); + break; + default: + // longpress/longpressend/tapcancel:引擎快进接管,游戏不处理; + // F1 同款:链末等待态下它们同样拆掉了看门狗,立即重挂 + if (this._inStoryWait()) this._armIdleWatchdog(this.phase); + break; + } + } + + /** 当前是否处于「链末等待单击」态(F1 重挂判据,与 _onNodeSeqDone 等待分支同口径)。 */ + _inStoryWait() { + const node = NODES[this.nodeId]; + return !!(node && !this.voicePlaying && !this._isPromptNode(node) && node.next != null + && !node.auto); + } + + /** + * F5 链末句保护:当前句是本节点末句、且节点把控制权交出(链末等待单击 / + * @x 过场转移 / 直入抉择提示)时,屏蔽「跳过本句」。自动链中段不受保护。 + */ + _isTailSentence() { + const node = NODES[this.nodeId]; + if (!node || !this.voicePlaying || this.promptPlaying) return false; + if (this.currentVoiceId == null) return false; // 声呐步不适用(由快进语义接管) + const voices = this._nodeSteps(this.nodeId).filter((s) => typeof s === 'string'); + const tail = voices.length ? voices[voices.length - 1] : null; + if (tail == null || this.currentVoiceId !== tail) return false; // 仅护末句 + if (!node.auto) return true; // 链末等待型节点(本作叙事句) + if (node.next == null || this._isTransition(node.next)) return true; // 自动链终点 + const nx = NODES[node.next]; + return !!(nx && (nx.prompt != null || nx.dyn === 'p7')); // 自动链直入抉择提示 + } + + // ---------------------------------------------------------------- 手势:ENDING + + _onEndingGesture(g) { + switch (g) { + case 'tap': + if (this.awaitingRestart) this.newRun(); + else if (this.voicePlaying) { + if (this.currentVoiceId != null && this.currentVoiceId !== this._skipLockId) { + this.audio.skipVoice(); // F4:续玩恢复的首句播完才武装跳句 + } else { + vibrate('beat'); // G4:续玩锁首句/声呐步上的空点单击同款轻震标记 + } + } + break; + case 'doubletap': + // 重听结局链(结算态/播放中皆可;重播期间清结算态,播完回到结算态) + this.awaitingRestart = false; + this.seqToken++; + this.audio.stopVoice(); + this._playEndingChain(); + break; + case 'up': { + // 状态(结局主句 + 氧档整句)在前 + 帮助垫后(G6),G2:crit 附 warning 震, + // G1:同一等待点第 2 次起口令换纯音频标记;播完回结算态 + this.seqToken++; + this.audio.stopVoice(); + const def = ENDINGS[this.endingId] || ENDINGS.e4; + const ids = this._withHelp(this._statusIds(), `E:${this.endingId}`); + void def; + this._playSeqSteps(ids, () => { + this.voicePlaying = false; + if (this.phase === 'ENDING') { + this.awaitingRestart = true; + this._armIdleWatchdog('ENDING'); + } + }); + break; + } + case 'down': + this.openMenu(); + break; + case 'left': + case 'right': + this.guide.error({ voice: 'gd_invalid', onceKey: 'abyss_invalid' }); // §2.3 ENDING 行 + // F1:结算等待态的看门狗被 disarm 拆掉后立即重挂 + if (this.awaitingRestart && !this.voicePlaying) this._armIdleWatchdog('ENDING'); + break; + default: + // F1 同款:快进等引擎接管手势不拆结算等待的看门狗 + if (this.awaitingRestart && !this.voicePlaying) this._armIdleWatchdog('ENDING'); + break; // longpress/longpressend/tapcancel:引擎快进接管,游戏不处理 + } + } + + // ---------------------------------------------------------------- 手势:MENU(§5.2 模态约定) + + _onMenuGesture(g) { + switch (g) { + case 'tap': { + // 单击循环切换:报名经游戏侧队列**接在当前报名之后**(不截断)。 + // 不用引擎 queue:true——引擎在 onComplete 里先起链播下一句、后 shift + // 队列,排队句会把链播下一句顶掉(radio 实现修复 #3 的根因)。 + this.menuIdx = (this.menuIdx + 1) % MENU_ITEMS.length; + this._menuQueue.push(MENU_ITEMS[this.menuIdx].voice); + if (!this.voicePlaying) this._drainMenuQueue(); + break; + } + case 'doubletap': + this._execMenuItem(MENU_ITEMS[this.menuIdx].id); // 双击确认执行 + break; + case 'up': { + // 帮助 + 菜单内设置提示 + 复述当前项(清空报名队列:帮助链顶掉一切) + this._menuQueue = []; + this.seqToken++; + this.audio.stopVoice(); + const ids = this._withHelp([], 'M'); + ids.push('voice/sys_settings', MENU_ITEMS[this.menuIdx].voice); + this._playSeqSteps(ids, () => { + this.voicePlaying = false; + }); + break; + } + case 'down': + this.closeMenu(); // 关闭回原状态(重播当前节点 = 局面复述) + break; + case 'left': + case 'right': + this.guide.error({}); // 菜单内左右滑无效:error 震(§2.3 MENU 行,无语音) + break; + default: + break; // longpress/longpressend/tapcancel:引擎快进接管(快进菜单语音) + } + } + + _execMenuItem(id) { + this._menuQueue = []; // 确认执行:待播报名作废 + if (id === 'resume') { + this.closeMenu(); + } else if (id === 'restart') { + this.seqToken++; + this.audio.stopVoice(); + this.newRun(); + } else if (id === 'lobby') { + // 保存并回大厅:确认句播出且存档落盘后跳转(§2.2「确认后跳转前播」) + this.leaving = true; + this.seqToken++; + this.audio.stopVoice(); + this._save(); + this._playSeqSteps(['voice/sys_menu_lobby'], () => { + this.voicePlaying = false; + try { + if (typeof window !== 'undefined' && window.location) window.location.href = '../'; + } catch (_) { /* 无头环境无 location:留档 leaving 标记 */ } + }); + } + } + + // ---------------------------------------------------------------- 菜单开/关 + + openMenu() { + this.returnPhase = this.phase; + this.returnNode = this.nodeId; + this.returnSearch = this.searchArmed; + this.phase = 'MENU'; + this.menuIdx = 0; + this._menuQueue = []; + this._sonarQueue.length = 0; + this._clearProg(); + this._stopDangerLoop(); + this.seqToken++; + this.audio.stopVoice(); + this._closeVoiceRecs(); + this.audio.play('sfx/ui_menu'); + this.audio.play('bgm/deck', { fadeMs: 500, volume: 0.4 }); // MENU 低响度淡入 + this.guide.state('menu'); + this._playSeqSteps(['voice/sys_menu', MENU_ITEMS[0].voice], () => { + this.voicePlaying = false; + this._drainMenuQueue(); + }); + } + + /** 菜单报名队列排水:链播完接播玩家循环切换过的项(游戏侧排队,零截断)。 */ + _drainMenuQueue() { + if (this.phase !== 'MENU' || this._menuQueue.length === 0) return; + const ids = this._menuQueue.splice(0); + this._playSeqSteps(ids, () => { + this.voicePlaying = false; + this._drainMenuQueue(); + }); + } + + closeMenu() { + this.seqToken++; + this._menuQueue = []; // 未播完的报名作废(局面复述接管) + this.audio.stopVoice(); + const back = this.returnPhase || 'INTRO'; + this.phase = back; + this.audio.play('sfx/ui_confirm'); + if (back === 'DIVE') this.audio.play('bgm/dive', { fadeMs: 800 }); + else if (back === 'COMMS') this.audio.play('bgm/comms', { fadeMs: 800 }); + else if (back === 'ENDING') this.audio.play('bgm/ending', { fadeMs: 800 }); + else this.audio.play('bgm/deck', { fadeMs: 800 }); + this.guide.state(back === 'ENDING' || back === 'SURFACE' ? 'settle' : 'explore'); + this._updateBreath(); + if (back === 'DIVE' && this.returnSearch) { + this._enterSearch(); // 搜索等待恢复:turn 签名 + 两级看门狗(局面复述) + } else if (back === 'ENDING') { + this._playEndingChain(); // 重播结局链(局面复述) + } else if (back === 'ASK') { + this._playSeqSteps(['voice/sys_continue_ask'], () => { + this.voicePlaying = false; + this._armIdleWatchdog('ASK'); + }); + } else { + this.playNode(this.returnNode); // INTRO/COMMS/SURFACE:重播当前节点 + } + } + + // ---------------------------------------------------------------- 救援流转 + + /** 新救援:重置跨潜标量(结局收集跨局保留),回 INTRO(sys_new_dive 先行)。 */ + newRun() { + this.runs += 1; + this.oxy = 12; + this.supply = false; + this.saved = { zhou: 0, bai: 0, chen: 0, zheng: 0, gao: 0 }; + this.stayer = null; + this.flags = { + knowsPipe: false, hasLog: false, valve: false, + spareGift: false, gaoPeace: false, bell: false, + }; + this.bond = { zhou: 0, bai: 0 }; + this.diveN = 0; + this.diveForm = null; + this.endingId = null; + this.lastAnnouncedTier = null; + this.lastCloser = null; + this.leaving = false; + this.saveStash = null; + this._lastEcho = null; + this._dryPending = false; + this._dryAQueued = false; + this._dryBQueued = false; + this.choiceArmed = false; // 清掉上一局的抉择武装(radio 实现修复 #7) + this.pendingChoice = null; + this.searchArmed = false; + this.awaitingRestart = false; + this._clearProg(); + this._stopDangerLoop(); + this.seqToken++; + this.audio.stopVoice(); + this._stopAllLoops(); + this._closeVoiceRecs(); + this._sonarQueue.length = 0; + this.phase = 'INTRO'; + this.audio.play('bgm/deck'); + this._startLoop('sfx/amb_deck', 0.6); + this.guide.state('explore'); + this._save(); + this._playSeqSteps(['voice/sys_new_dive'], () => this.playNode('b_open')); + } + + /** + * 进潜:扣固定消耗(每次下潜扣 3;D4 双载额外 +1,§4.3),BGM crossfade 进 + * dive 曲,amb_deep 循环垫起,播下潜链(auto)→ 链末武装搜索等待。 + * noCharge:断点续玩恢复用(存档 oxy 已是扣后值,不二次扣账)。 + */ + enterDive(n, opts = {}) { + this.diveN = n; + if (!opts.noCharge) { + const cost = 3 + (n === 4 && this.diveForm === 'double' ? 1 : 0); + this.oxy = Math.max(0, this.oxy - cost); + } + this.phase = 'DIVE'; + this.searchArmed = false; + this._stopLoop('sfx/amb_deck'); + this.audio.play('bgm/dive', { fadeMs: 800 }); + this.guide.state('explore'); + this._startLoop('sfx/amb_deep', 0.6); + this._updateBreath(); + this.nodeId = `dv${n}_down`; // 存档 node 先行(写入时机:每潜 DIVE 进入) + this._save(); + this.playNode(`dv${n}_down`); + } + + /** 搜索等待武装(下潜链末):turn 签名+confirm 震 + 两级不同句看门狗 + 无进展轨。 */ + _enterSearch() { + this.searchArmed = true; + this.phase = 'DIVE'; + this.guide.turn({ haptic: true }); // 「轮到你发脉冲」(首次前 dv1_teach 已口播) + this._armSearchWatchdog(); + this._armProg(`search:${this.diveN}`); + } + + /** 接通 COMMS(z1_a/bai_a/chen_a/gao_a 节点 enterComms 标记触发)。 */ + _enterComms() { + this.phase = 'COMMS'; + this.searchArmed = false; + this.audio.play('bgm/comms', { fadeMs: 600 }); + this.guide.state('explore'); + this._updateBreath(); + } + + /** + * 上浮结算(自动链 ≤15s):出水 sfx → d*_surf →(D2 后)d2_supply+oxy_gain + * →(跨档才播)档位句 → @surfaceNext 分流。补给/氧气门/干涸账目全部在入口 + * 一次性结清(supply 幂等守卫、resume 不重复发瓶)。 + */ + enterSurface(n) { + this.diveN = n; + this.phase = 'SURFACE'; + this.searchArmed = false; + this._stopLoop('sfx/amb_deep'); + this._startLoop('sfx/amb_deck', 0.6); + this._updateBreath(); + this.guide.state('settle'); + this.audio.play('bgm/deck', { fadeMs: 800 }); + this.audio.play('sfx/surf_splash'); + this.audio.play('sfx/ascent_chime'); + if (n === 1) this.saved.zhou = 1; // 教学潜保底救上(恒活) + if (n === 2) this.saved.bai = 1; // 机舱潜恒可成(预算数学保证,§4.4) + if (n === 2 && !this.supply) { + this.supply = true; // 应急瓶 +3:全局唯一一次 + this.oxy += 3; + } + if (this._dryPending) { + this._dryPending = false; + this._dryBQueued = true; // 干涸链归因句(上浮结算内,链入口死窗) + } + this._save(); // 写入时机:SURFACE 进入 + this.playNode(`s${n}_surf`); + } + + /** 结算链末分流:D1/D2 → 下一潜;D3 → 氧气门;D4 → 签收。 */ + _surfaceNext() { + if (this.diveN <= 2) this.enterDive(this.diveN + 1); + else if (this.diveN === 3) this._oxygenGate(); + else this.playNode('g9_in'); + } + + /** + * 第四潜氧气门(§4.3 三形态):oxy≥4 → 双载(带船长+留守者,p7 快慢浮); + * oxy=3 → 单载(p7 按 stayer 选 p7_chen|p7_zheng);oxy≤2 → 终局取消链。 + */ + _oxygenGate() { + if (this.oxy >= 4) { + this.diveForm = 'double'; + this.enterDive(4); + } else if (this.oxy === 3) { + this.diveForm = 'single'; + this.enterDive(4); + } else { + this.diveForm = 'cancel'; + this._grace(); // H1:终局取消链入口手势死窗 + this._save(); + this.playNode('c_none'); + } + } + + /** ENDING:savedCount × p9 四选一 + 尾声回响链,结算态等待单击重开。 */ + enterEnding(forced) { + const savedCount = ['zhou', 'bai', 'chen', 'zheng', 'gao'] + .filter((k) => this.saved[k]).length; + const ending = forced + || (savedCount === 5 ? (this.flags.bell ? 'e1' : 'e2') : (this.flags.bell ? 'e3' : 'e4')); + this.endingId = ending; + this.endings[ending] = true; + this.phase = 'ENDING'; + this.nodeId = ending; + this.awaitingRestart = false; + this._grace(); // H1:结局链入口手势死窗 + this._stopLoop('sfx/amb_deep'); + this._startLoop('sfx/amb_deck', 0.5); + this._stopDangerLoop(); + this.guide.state('settle'); + this.audio.play('bgm/ending', { fadeMs: 1500 }); // p9 结果链播完后淡入 + this._updateBreath(); // 收队即停呼吸垫 + this._save(); // 写入时机:ENDING 进入 + this._playEndingChain(); + } + + /** 尾声回响(P2):逐人按好感/生死双线点名 + 沟底按 bell 二选一。 */ + _epChain() { + const ids = []; + ids.push(this.bond.zhou ? 'voice/ep_zhou_ok' : 'voice/ep_zhou_no'); + ids.push(this.bond.bai ? 'voice/ep_bai_ok' : 'voice/ep_bai_no'); + ids.push(this.saved.chen ? 'voice/ep_chen_ok' : 'voice/ep_chen_lost'); + ids.push(this.saved.zheng ? 'voice/ep_zheng_ok' : 'voice/ep_zheng_lost'); + if (!this.saved.gao) ids.push('voice/ep_gao_lost'); + else ids.push(this.flags.gaoPeace ? 'voice/ep_gao_peace' : 'voice/ep_gao_up'); + ids.push(this.flags.bell ? 'voice/ep_trench_bell' : 'voice/ep_trench_deep'); + return ids; + } + + _playEndingChain() { + const def = ENDINGS[this.endingId] || ENDINGS.e4; + const chain = [...def.voices, 'voice/ep_lead', ...this._epChain()]; + this._playSeqSteps(chain, () => { + this.voicePlaying = false; + this.awaitingRestart = true; + this._armIdleWatchdog('ENDING'); + }); + } + + // ---------------------------------------------------------------- 存档续玩 + + _askContinue(save) { + this.saveStash = save; + // 预载 runs/endings:ASK 右滑=新救援走 newRun(+1),与结算重开同口径 + //(radio 遗留项在 abyss 修复:被拒绝的存档也把局数账带给下一局) + this.runs = (save && save.runs) || 0; + this.endings = { ...((save && save.endings) || {}) }; + this.phase = 'ASK'; + this._playSeqSteps(['voice/sys_continue_ask'], () => { + this.voicePlaying = false; + this._armIdleWatchdog('ASK'); + }); + } + + /** 断点续玩:恢复 phase+node+全部标量并重播当前节点链(一句话局面复述,L5)。 */ + _resume(s) { + if (!s) { this.newRun(); return; } + this.saveStash = null; + this.oxy = s.oxy | 0; + this.supply = !!s.supply; + this.saved = { zhou: 0, bai: 0, chen: 0, zheng: 0, gao: 0, ...(s.saved || {}) }; + this.stayer = s.stayer || null; + this.flags = { + knowsPipe: false, hasLog: false, valve: false, + spareGift: false, gaoPeace: false, bell: false, ...(s.flags || {}), + }; + this.bond = { zhou: 0, bai: 0, ...(s.bond || {}) }; + this.endings = { ...(s.endings || {}) }; + this.runs = s.runs || 1; + this.diveN = s.dive || diveOfNode(s.node); + this.diveForm = s.form || null; + this._dryPending = !!s.dryPending; + this.lastAnnouncedTier = tierOf(this.oxy); // 恢复不重播跨档句(账目已播过) + this._lastEcho = null; + this.leaving = false; + this.seqToken++; + this.audio.stopVoice(); + this._resumeGuard = true; // F4:恢复链首句播完才武装跳句(经 _playSeqSteps 消费) + const phase = s.phase; + if (phase === 'DIVE') { + this.enterDive(this.diveN >= 1 ? this.diveN : 1, { noCharge: true }); + } else if (phase === 'SURFACE') { + this.enterSurface(this.diveN >= 1 && this.diveN <= 4 ? this.diveN : 4); + } else if (phase === 'ENDING') { + this.enterEnding(s.node && ENDINGS[s.node] ? s.node : undefined); + } else { + this.phase = 'INTRO'; + this.audio.play('bgm/deck'); + this._startLoop('sfx/amb_deck', 0.6); + this.guide.state('explore'); + if (phase === 'COMMS') { + this.phase = 'COMMS'; + this.audio.play('bgm/comms'); + this._startLoop('sfx/amb_deep', 0.6); + } + this._updateBreath(); + this.playNode(NODES[s.node] ? s.node : 'b_open'); + } + } + + // ---------------------------------------------------------------- 节点链驱动 + + playNode(id) { + const node = NODES[id]; + if (!node) { + console.error('[AbyssGame] 未知节点:', id); + return; + } + this.nodeId = id; + this.choiceArmed = false; + this.pendingChoice = null; + this.awaitingRestart = false; + this._stopDangerLoop(); + if (node.enterComms) this._enterComms(); + if (node.sfx) this.audio.play(node.sfx); // 非信息类一次性垫音(link_beep 等) + // 搜索双击重听素材:回波步(不含脉冲)+ 解读句 + if (/^sv\d_echo$/.test(id)) { + this._lastEcho = { + pre: (node.pre || []).filter((st) => st.sfx !== 'sfx/sonar_ping'), + voice: node.voices[0] || null, + }; + } + let steps = this._nodeSteps(id); + if (node.dyn === 'tier') { + // 档位节点:只在跨档时播分档整句(账目真实;sidefx 在此一次结清) + const tierVoice = this._tierPending(); + if (tierVoice) { + this.lastAnnouncedTier = tierOf(this.oxy); + if (this.lastAnnouncedTier === 'crit') vibrate('warning'); // G2:crit 附 warning 震 + this._updateBreath(); + steps = [...steps, tierVoice]; + } + } + const prompt = this._promptOf(node); + if (prompt) steps = [...steps, prompt]; + if (id === 'b_open') { + // F8 先例:操作口令 gd_brief 前置开场链最前(仅 full 档;每实例一次) + if (this.guide.level() === 'full' && !this._briefShown) { + this._briefShown = true; + steps = ['voice/gd_brief', ...steps]; + } + // sys_skip:开场每实例一次(storyline skip_hint 先例) + if (!this.sysSkipShown) { + this.sysSkipShown = true; + steps = [...steps, 'voice/sys_skip']; + } + } + if (this._dryAQueued) { + // 干涸链首句(mid-dive floor 至 0 当场合播;当前救援照常完成) + this._dryAQueued = false; + this._grace(); + steps = ['voice/dry_a', ...steps]; + } + if (this._dryBQueued) { + // 干涸链归因句(上浮结算内) + this._dryBQueued = false; + this._grace(); + steps = ['voice/dry_b', ...steps]; + } + this._playSeqSteps(steps, () => this._onNodeSeqDone(node, steps)); + } + + /** 节点基础步序:pre 声呐步 → voices(含节点内条件选播)→ post 声呐步。 */ + _nodeSteps(id) { + const node = NODES[id]; + if (!node) return []; + return [...(node.pre || []), ...this._dynVoices(node), ...(node.post || [])]; + } + + /** 节点内条件选播(radio w1_say 先例:同一节点 id,差分选组,树形不变)。 */ + _dynVoices(node) { + switch (node.dyn) { + case 'd2pry': + return [this.flags.knowsPipe ? 'voice/d2_pry_ok' : 'voice/d2_pry_slow']; + case 'gaoconfess': + return [this.flags.hasLog ? 'voice/gao_confess' : 'voice/gao_c']; + case 'stayleave': + return [this.stayer === 'chen' ? 'voice/chen_last' : 'voice/d4_stay_lost']; + case 'p7': + case 'tier': + return []; + default: + return [...node.voices]; + } + } + + /** p7 三形态提示句(双载 p7 / 单载按 stayer)。 */ + _promptOf(node) { + if (node.dyn === 'p7') { + if (this.diveForm === 'double') return 'voice/p7'; + return this.stayer === 'chen' ? 'voice/p7_chen' : 'voice/p7_zheng'; + } + return node.prompt; + } + + /** 抉择目标解析(氧气门形态决定 p7 分叉;跨潜状态只有标量,无组合节点)。 */ + _targetsOf(node) { + if (node.dyn === 'p7') { + if (this.diveForm === 'double') return { left: 'd4_slow', right: 'd4_fast' }; + return { + left: 'd4_pick_gao', + right: this.stayer === 'chen' ? 'd4_pick_chen' : 'd4_pick_zheng', + }; + } + return { left: node.left, right: node.right }; + } + + _isPromptNode(node) { + return !!(node && (node.prompt != null || node.dyn === 'p7')); + } + + /** 双击重听当前节点台词(含抉择提示);ENDING 态重听结局链。 */ + replayNode() { + this.seqToken++; + this.audio.stopVoice(); + if (this.phase === 'ENDING') { + this.awaitingRestart = false; + this._playEndingChain(); + return; + } + if (!NODES[this.nodeId]) return; + this.playNode(this.nodeId); // G5:复播不重新武装 F4 锁(锁不随复播复活) + } + + /** '@x' 虚拟目标 = 过场转移(自动衔接)。 */ + _isTransition(target) { + return typeof target === 'string' && target.startsWith('@'); + } + + /** + * 节点链自然播完的公共收尾:抉择武装 / 自动过场 / 链末等待。 + * 自动衔接仅限节点表标注 auto 的段落(下潜链/上浮结算链/危机拍/签收链/ + * p9 结果链)。@x 过场目标本身**由链末单击触发**(§2.2「INTRO ──链完单击 + * ──> DIVE」/ 结果节点 → 上浮 = 玩家的「带人上来」动作);其余叙事节点链末 + * 等待单击(§2.3「链末=继续」)并挂 gd_continue/gd_again 看门狗;COMMS 链末 + * 另挂无进展轨(§7.2 可饿死点)。 + */ + _onNodeSeqDone(node, steps) { + this.voicePlaying = false; + this._setAmbDucked(false); + const voices = steps.filter((x) => typeof x === 'string'); + if (voices.length > 0) this.lastCloser = voices[voices.length - 1]; + if (this._isPromptNode(node)) { + this.choiceArmed = true; // W2 契约:播完当刻即可选,零插播 + if (this.pendingChoice) { + const g = this.pendingChoice; + this.pendingChoice = null; + this.choose(g); // 缓存的抉择立即生效(不丢、不重复) + } else { + this.armChoiceHint(); + } + return; + } + if (node.next == null) return; + if (node.auto) { + this.advance(node.next); + return; + } + this._armIdleWatchdog(this.phase); // 链末等待单击(tap=继续) + if (this.phase === 'COMMS') this._armProg(`n:${this.nodeId}`); // 无进展轨挂点 + } + + /** 链完转移:'@x' 虚拟目标 = 过场自动衔接;普通节点 = 播下一节点。 */ + advance(target) { + if (target == null) return; + switch (target) { + case '@dive': this.enterDive(1); return; + case '@search': this._enterSearch(); return; + case '@surface1': this.enterSurface(1); return; + case '@surface2': this.enterSurface(2); return; + case '@surface3': this.enterSurface(3); return; + case '@surface4': this.enterSurface(4); return; + case '@surfaceNext': this._surfaceNext(); return; + case '@signoff': this.playNode('g9_in'); return; + case '@ending': this.enterEnding(); return; + default: this.playNode(target); + } + } + + /** 抉择:计分(§4.2)→ 存档 → 进结果节点。p9 左在 oxy<1 时封锁走 g9_dry。 */ + choose(g) { + const node = NODES[this.nodeId]; + if (!node || !this._isPromptNode(node)) return; + this.choiceArmed = false; + this.audio.play('sfx/ui_confirm'); + const targets = this._targetsOf(node); + let target = g === 'left' ? targets.left : targets.right; + if (this.nodeId === 'p9' && g === 'left' && this.oxy < 1) { + target = 'g9_dry'; // 左路封锁:播出后自动走右,不摆假选项(§3.2) + } + this._applyChoice(this.nodeId, g); + this._clearProg(); // 抉择结算 = 有真实进展 + this._save(); // 写入时机:每个抉择结算后 + this.advance(target); + } + + /** 抉择计分表(§4.2:左=走心·多花气 / 右=克制·省气;floor 至 0 触发干涸链)。 */ + _applyChoice(nodeId, g) { + const L = g === 'left'; + switch (nodeId) { + case 'p1': + if (L) { this.flags.knowsPipe = true; this.bond.zhou = 1; this._spend(1); } + break; + case 'p2': + if (L) { this.flags.hasLog = true; this.bond.zhou = 1; this._spend(1); } + break; + case 'p3': + if (L) { this.bond.bai = 1; this._spend(this.flags.knowsPipe ? 0 : 1); } + break; + case 'p4': + if (L) { this.flags.valve = true; this.bond.bai = 1; this._spend(2); } + break; + case 'p5': + // 唯一无气差的双义抉择:stayer 决定 D4 单载差分 + if (L) { this.saved.chen = 1; this.stayer = 'zheng'; } + else { this.saved.zheng = 1; this.stayer = 'chen'; } + break; + case 'p6': + if (L) { this.flags.spareGift = true; this._spend(2); } + break; + case 'p7': + if (this.diveForm === 'double') { + this.saved.gao = 1; + if (this.stayer) this.saved[this.stayer] = 1; + if (L) this._spend(2); + } else if (L) { + this.saved.gao = 1; + } else if (this.stayer) { + this.saved[this.stayer] = 1; + } + break; + case 'p8': + if (L) this.flags.gaoPeace = true; + break; + case 'p9': + if (L && this.oxy >= 1) this.flags.bell = true; // 左路需 oxy≥1(choose 已拦) + break; + default: + break; + } + } + + /** + * 氧气支出(§4.3 宽限):mid-dive 扣至负数一律 floor 到 0,不中断当前救援 + * (人已挂上吊索照常带上);从正值 floor 到 0 时触发干涸链。 + */ + _spend(cost) { + if (cost <= 0) return; + const before = this.oxy; + this.oxy = Math.max(0, this.oxy - cost); + this._updateBreath(); + if (before > 0 && this.oxy === 0) { + this._dryAQueued = true; // dry_a 挂下一结果节点链首(含 800ms 死窗) + this._dryPending = true; // dry_b 挂上浮结算链首 + } + } + + // ---------------------------------------------------------------- 档位 / 状态链 + + /** 跨档待播档位句(无跨档返回 null;副作用由 playNode('s_tier') 结清)。 */ + _tierPending() { + const tier = tierOf(this.oxy); + if (tier === this.lastAnnouncedTier) return null; + return TIER_VOICES[tier]; + } + + /** 上滑状态链(G6 顺序:prog_dN → oxy 档 → sys_help;G2:crit 附 warning 震)。 */ + _statusIds() { + const ids = []; + if (this.phase === 'ENDING') { + const def = ENDINGS[this.endingId] || ENDINGS.e4; + ids.push(def.voices[0]); // 结局主句 = 结局名播报(storyline P3-2 先例) + } else if (this.diveN >= 1 && this.diveN <= 4) { + ids.push(PROG_VOICES[this.diveN - 1]); + } + if (tierOf(this.oxy) === 'crit') vibrate('warning'); + ids.push(TIER_VOICES[tierOf(this.oxy)]); + return ids; + } + + /** + * G1 帮助链去重:同一等待点第 1 次全量口播 sys_help;第 2 次起改纯音频标记 + * (beat 轻震 + nudge earcon,零新增词表),状态句照播。 + */ + _helpSpokenOnce(waitKey) { + if (this._helpSeen.has(waitKey)) { + vibrate('beat'); + if (this.guide.level() !== 'off') this.guide._playEarcon('nudge'); + return false; + } + this._helpSeen.add(waitKey); + return true; + } + + /** G6:状态在前、帮助垫后——把帮助句接在状态链尾。 */ + _withHelp(ids, waitKey) { + if (this._helpSpokenOnce(waitKey)) ids.push('voice/sys_help'); + return ids; + } + + /** + * 上滑(叙事态):状态(潜次+氧档)在前 + 帮助垫后(G6),随后重播当前节点 + * (局面复述)。INTRO 开播前无潜次可报,仅氧档+帮助后重播当前段。 + */ + _playHelpAndResume() { + this.seqToken++; + this.audio.stopVoice(); + const ids = this._statusIds(); + this._withHelp(ids, `S:${this.nodeId}`); + this._playSeqSteps(ids, () => { + this.voicePlaying = false; + this.playNode(this.nodeId); + }); + } + + // ---------------------------------------------------------------- 看门狗双轨(§7.2) + + /** 无操作轨·叙事等待(ASK/INTRO/COMMS/SURFACE 链末/ENDING 结算): + * full 25s/50s,standard 13s/30s;两级不同句、用尽即停、永不惩罚。 */ + _armIdleWatchdog(phaseName) { + const full = this.guide.level() === 'full'; + this.guide.watchdog({ + arm: true, + at: full ? GUIDE_WATCHDOG_AT : IDLE_HINT_AT, + say: ['gd_continue', 'gd_again'], + onEscalate: (n) => { + this._progSaid(n); // 双轨去重:无操作轨已播的同级,无进展轨跳过 + if (full) return; + this.audio.play(n === 1 ? 'voice/gd_continue' : 'voice/gd_again', { queue: true }); + }, + cond: () => this.phase === phaseName && !this.voicePlaying, + }); + } + + /** 无操作轨·搜索等待:两级不同句 gd_ping1/gd_ping2(10s/25s,绝不逐字重播)。 */ + _armSearchWatchdog() { + const full = this.guide.level() === 'full'; + this.guide.watchdog({ + arm: true, + at: SEARCH_WATCHDOG_AT, // full/standard 同参数;standard 由游戏层补播 + say: ['gd_ping1', 'gd_ping2'], + onEscalate: (n) => { + this._progSaid(n); + if (full) return; + this.audio.play(n === 1 ? 'voice/gd_ping1' : 'voice/gd_ping2', { queue: true }); + }, + cond: () => this.phase === 'DIVE' && this.searchArmed, + }); + } + + /** 抉择点静默兜底:25s/50s 重播当前 p* 提示句(复述选项=合法帮助,N3 先例)。 */ + armChoiceHint() { + this.guide.watchdog({ + arm: true, + at: CHOICE_HINT_AT, + onEscalate: () => { + const prompt = this._promptOf(NODES[this.nodeId]); + if (prompt) this.audio.play(prompt, { queue: true }); + }, + cond: () => (this.phase === 'COMMS' || this.phase === 'SURFACE') + && this.choiceArmed && !this.voicePlaying, + }); + } + + /** + * 无进展轨(游戏层计时,dungeon E0-1 同型):仅挂搜索等待与 COMMS 链末两个 + * 可饿死点;60s→gd_prog1、120s→gd_prog2,用尽即停(每等待点一轮)。 + * 无效手势(滑错/空点)不复位本轨——乱按饿不死;guide=off 不挂(零定时器)。 + */ + _armProg(site) { + this._clearProg(); + if (this.guide.level() === 'off') return; + const token = ++this._progToken; + const p = { site, token, said1: false, said2: false, t1: null, t2: null }; + p.t1 = setTimeout(() => this._fireProg(p, 1), PROG_AT[0]); + p.t2 = setTimeout(() => this._fireProg(p, 2), PROG_AT[1]); + this._prog = p; + } + + _fireProg(p, level) { + if (!this._prog || this._prog !== p || this._prog.token !== p.token) return; + if (p['said' + level]) return; // 双轨同级交叉登记:先播过则跳过 + p['said' + level] = true; + this.audio.play(level === 1 ? 'voice/gd_prog1' : 'voice/gd_prog2', { queue: true }); + } + + _clearProg() { + const p = this._prog; + if (!p) return; + if (p.t1) { clearTimeout(p.t1); p.t1 = null; } + if (p.t2) { clearTimeout(p.t2); p.t2 = null; } + this._prog = null; + } + + /** 双轨去重登记:无操作轨(guide watchdog)触发同级后,无进展轨跳过同义级。 */ + _progSaid(level) { + if (this._prog) this._prog['said' + level] = true; + } + + // ---------------------------------------------------------------- 语音链(storyline 范式 + 声呐步) + + /** + * 顺序播放一组步(string = 语音句;{sfx,wait,...} = 声呐步),全部自然播完 + * 后回调。被打断时回调作废(seqToken 机制)。声呐步经 sonarGate 空闲门 + * (§6.2 R1:信息类回波与语音零重叠);危机步前置 danger 签名+震(+D4 震 + * 循环)。单句 30s 看门狗:超时且 isVoicePlaying()===false(资产缺失)才 + * 自动续链;voice 真在播(长句)则续约等待,不得掐断(N1 先例照抄)。 + */ + _playSeqSteps(steps, onDone) { + const token = ++this.seqToken; + this._closeVoiceRecs(); + const firstVoice = steps.find((x) => typeof x === 'string') || null; + this._skipLockId = this._resumeGuard ? firstVoice : null; // F4:续玩锁 + this._resumeGuard = false; + this.voicePlaying = true; + this._sonarSkip = null; + const step = (i) => { + if (token !== this.seqToken) return; + if (i >= steps.length) { + this.currentVoiceId = null; + this.promptPlaying = false; + this._skipLockId = null; + this.voicePlaying = false; + this._setAmbDucked(false); + this._stopDangerLoop(); + this._closeVoiceRecs(); + if (onDone) onDone(); + // sonarGate 补发:链真正歇下(无新句接续)才排水,保证零重叠 + if (!this.voicePlaying && !this.audio.isVoicePlaying()) this._drainSonar(); + return; + } + if (i > 0 && typeof steps[i - 1] === 'string') this._skipLockId = null; // F4:武装跳句 + const s = steps[i]; + if (typeof s === 'string') { + this.currentVoiceId = s; + this.promptPlaying = PROMPT_RE.test(s); + if (this.promptPlaying) this.guide.state('choice'); // 选项句本身即 turn cue(W2) + this._setAmbDucked(true); // R3:语音在播时 amb 常驻声呐元素降 -6dB + const rec = { id: s, start: performance.now(), end: Infinity }; + this._voiceIntervals.push(rec); + let advanced = false; + const armWd = () => setTimeout(() => { + if (advanced || token !== this.seqToken) return; + if (this.audio.isVoicePlaying()) { wd = armWd(); return; } // 长句在播:续约不掐 + advanced = true; + console.warn('[AbyssGame] 语音看门狗超时(voice 未在播,判定资产缺失),自动续链:', s); + rec.end = performance.now(); + step(i + 1); + }, VOICE_WATCHDOG_MS); + let wd = armWd(); + this.audio.play(s, { + onComplete: () => { + if (advanced || token !== this.seqToken) return; + advanced = true; + clearTimeout(wd); + rec.end = performance.now(); + step(i + 1); + }, + }); + } else { + // 声呐步:信息回波/危机前置音(R1 空闲门;R4 危机拍前置 2s 起播) + this.currentVoiceId = null; + this.promptPlaying = false; + if (s.danger) { + this.guide.state('danger'); + vibrate('danger'); + if (s.dangerLoop) this._startDangerLoop(); // D4 另叠 danger 震循环 + } + this._sonar(s.sfx, s.volume != null ? { volume: s.volume } : undefined); + let done = false; + const timer = setTimeout(() => { + if (done || token !== this.seqToken) return; + done = true; + this._sonarSkip = null; + step(i + 1); + }, s.wait || 0); + if (!s.danger) { + // 回波余音等待中单击=快进(危机前置窗不快进,点按只 beat 标记) + this._sonarSkip = () => { + if (done || token !== this.seqToken) return; + done = true; + clearTimeout(timer); + this._sonarSkip = null; + step(i + 1); + }; + } + } + }; + step(0); + } + + /** G5 复播兼容入口(等价单语音链)。 */ + _playSeq(ids, onDone) { + this._playSeqSteps(ids, onDone); + } + + _closeVoiceRecs() { + const now = performance.now(); + for (const rec of this._voiceIntervals) { + if (rec.end === Infinity) rec.end = now; + } + } + + // ---------------------------------------------------------------- 声呐双通道(§6.2) + + /** + * sonarGate 空闲门(R1):信息类声呐音只在 voice idle 时播出,否则挂队列表、 + * 链末补发——回波与语音零重叠。玩家主动脉冲只在搜索等待(voice 恒静)受理, + * 按构造不重叠。 + */ + _sonar(id, opts) { + if (this.audio.isVoicePlaying()) { + this._sonarQueue.push({ id, opts }); + return; + } + this.audio.play(id, opts); + this._sonarPlays.push({ id, t: performance.now() }); + } + + _drainSonar() { + if (this._sonarQueue.length === 0) return; + if (this.audio.isVoicePlaying()) return; + const item = this._sonarQueue.shift(); + this.audio.play(item.id, item.opts); + this._sonarPlays.push({ id: item.id, t: performance.now() }); + } + + /** danger 震循环(D4 滑坡危机拍专用;间隔 ≥400ms,链推进/菜单/重开即停)。 */ + _startDangerLoop() { + this._stopDangerLoop(); + const token = this.seqToken; + const loop = { token, timer: null }; + this._dangerLoop = loop; + const fire = () => { + if (this._dangerLoop !== loop) return; + vibrate('danger'); + loop.timer = setTimeout(fire, DANGER_LOOP_MS); + }; + loop.timer = setTimeout(fire, DANGER_LOOP_MS); + } + + _stopDangerLoop() { + if (!this._dangerLoop) return; + if (this._dangerLoop.timer) clearTimeout(this._dangerLoop.timer); + this._dangerLoop = null; + } + + // ---------------------------------------------------------------- 环境音循环(游戏层 buffer.loop) + + /** + * 循环垫(amb_deck/amb_deep/breath_loop):引擎 sfx 层即发即弃无 loop—— + * 游戏侧经引擎 ctx 起循环 BufferSource 并持句柄(radio _ringSrc 先例)。 + * load 异步:落地前已被停掉/被替换则不播;资产缺失静默降级(玩法不受阻)。 + */ + _startLoop(id, vol = 0.6) { + this._stopLoop(id); // 防双循环(重入时旧句柄先清) + if (!this.audio.ctx) return; + const handle = { want: true, src: null, gain: null }; + this._loops[id] = handle; + this.audio._load(id).then((buf) => { + if (!buf || !handle.want || this._loops[id] !== handle) return; + const src = this.audio.ctx.createBufferSource(); + src.buffer = buf; + src.loop = true; + const gain = this.audio.ctx.createGain(); + gain.gain.value = this.audio.sfxVolume * vol * (this._ambDucked ? 0.5 : 1); + src.connect(gain); + gain.connect(this.audio._sfxBus); + src.start(); + handle.src = src; + handle.gain = gain; + }).catch(() => { /* 资产缺失降级:声场靠 BGM/语音承载 */ }); + } + + _stopLoop(id) { + const h = this._loops[id]; + if (!h) return; + h.want = false; + if (h.src) { + try { h.src.stop(); } catch (_) { /* 已停止 */ } + } + delete this._loops[id]; + } + + _stopAllLoops() { + for (const id of Object.keys(this._loops)) this._stopLoop(id); + } + + /** R3 常驻退位:语音在播时 amb 常驻声呐元素降 -6dB(gain ×0.5),链歇恢复。 */ + _setAmbDucked(on) { + if (this._ambDucked === on) return; + this._ambDucked = on; + const h = this._loops['sfx/amb_deep']; + if (h && h.gain) h.gain.gain.value = h.gain.gain.value * (on ? 0.5 : 2); + } + + /** + * 低氧混音签名(§4.3):tier=low/crit 且处于水下(DIVE/COMMS)时垫 + * breath_loop(低响度呼吸声);升档/上浮/收队即停。 + */ + _updateBreath() { + const want = (tierOf(this.oxy) === 'low' || tierOf(this.oxy) === 'crit') + && (this.phase === 'DIVE' || this.phase === 'COMMS'); + if (want === this._breathOn) return; + this._breathOn = want; + this._breathLog.push({ on: want, t: performance.now() }); + if (want) this._startLoop('sfx/breath_loop', 0.45); + else this._stopLoop('sfx/breath_loop'); + } + + /** H1:盖 800ms 手势死窗(终局取消链/干涸链/结局链入口)。 */ + _grace() { + this._graceUntil = performance.now() + SETTLE_GRACE_MS; + } + + // ---------------------------------------------------------------- 存档(§8) + + /** data 结构照 §8.1(超集:form/dryPending/bond 为断点续玩派生账)。 */ + _save() { + const k = Object.keys(this.endings).length; + let summary; + if (this.phase === 'ENDING' && this.endingId) { + summary = `救援收队·${ENDINGS[this.endingId].name} · 结局${k}/4`; + } else { + const seg = this.diveN >= 1 && this.diveN <= 4 + ? `第${this.diveN}潜·${SEGS[this.diveN - 1]}` + : '甲板·简报'; + summary = `${seg} · 氧气${this.oxy}格 · 结局${k}/4`; + } + const data = { + schema: 1, + updatedAt: new Date().toISOString(), + summary, + phase: this.phase === 'MENU' ? (this.returnPhase || 'INTRO') : this.phase, + node: this.nodeId, + dive: this.diveN, + oxy: this.oxy, + supply: this.supply, + saved: { ...this.saved }, + stayer: this.stayer, + flags: { ...this.flags }, + bond: { ...this.bond }, + form: this.diveForm, + dryPending: this._dryPending, + endings: { ...this.endings }, + runs: this.runs, + }; + try { localStorage.setItem(SAVE_KEY, JSON.stringify(data)); } catch (_) { /* 隐私模式 */ } + // 已登录时同步服务器(未登录静默跳过;大厅负责登录,游戏内只读写) + try { + const auth = JSON.parse(localStorage.getItem(AUTH_KEY) || 'null'); + if (!auth || !auth.token) return; + fetch('/api/progress', { + method: 'PUT', + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${auth.token}` }, + body: JSON.stringify({ game: 'abyss', data }), + }).catch(() => { /* 离线静默 */ }); + } catch (_) { /* 静默 */ } + } + + _loadLocal() { + try { + const data = JSON.parse(localStorage.getItem(SAVE_KEY)); + if (!data || data.schema !== 1 || !data.phase || !data.node) return null; + return data; + } catch (_) { + return null; + } + } +} + +export default AbyssGame; diff --git a/web/abyss/gen/gen_bgm.sh b/web/abyss/gen/gen_bgm.sh new file mode 100644 index 00000000..62b79cb9 --- /dev/null +++ b/web/abyss/gen/gen_bgm.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# 《深海回音》BGM 后处理(批次 2):Mureka flac 候选 -> 90s 无缝循环 ogg+m4a。 +# 用法: bash web/abyss/gen/gen_bgm.sh (flac 原件在 tools/mureka_out/,不入库) +# +# 工艺(tools/mureka_log.md 批次 1 同款): +# 首个强 onset 起点(silenceremove,-35dB 门限)→ 93s 裁剪 → 首尾 3s acrossfade(tri) +# 无缝循环 → loudnorm I=-20/TP=-2/LRA=11 → aresample 44.1kHz +# → ogg libvorbis q3 + m4a aac 128k,落 web/abyss/audio/bgm/。 +# 响度:4 首全部 I=-20(17-abyss.md §5.1 未给 menu 独立项——MENU 复用 deck 低响度 +# 淡入 500ms 由游戏层 gain 控制,§2.1;radio menu -24 是独立 menu pad 曲的先例,不适用)。 +# Mureka 任务 id / 选用 choice / 成本见 tools/mureka_log.md「批次 2」节;无节奏关键曲目, +# 不需要 bpm_precise(dive 的 sonar-like pulse 仅为 prompt 特征词)。 +set -euo pipefail +cd /home/xsl/SoundGame + +TMP=tools/bgm_post_wav/abyss # 中间 wav(不入库,可由本脚本复现) +DST=web/abyss/audio/bgm +mkdir -p "$TMP" "$DST" + +post() { # post + local src="$1" track="$2" lufs="$3" + local tmp="$TMP/$track.wav" + ffmpeg -y -v error -i "$src" -af \ +"silenceremove=start_periods=1:start_threshold=-35dB:start_silence=0.02,atrim=end=93,asetpts=PTS-STARTPTS,asplit[a][b];[b]atrim=start=90,asetpts=PTS-STARTPTS[t];[a][t]acrossfade=d=3:c1=tri:c2=tri,loudnorm=I=${lufs}:TP=-2:LRA=11,aresample=44100" \ + "$tmp" + ffmpeg -y -v error -i "$tmp" -c:a libvorbis -q:a 3 "$DST/$track.ogg" + ffmpeg -y -v error -i "$tmp" -c:a aac -b:a 128k "$DST/$track.m4a" + echo "$track <- $src (I=${lufs})" +} + +# 选用候选 = 更接近 90s 的那支(拿不准原则),详见 mureka_log.md 批次 2 节 +post tools/mureka_out/abyss-deck/160001236664321_0.flac deck -20 # 192.1s +post tools/mureka_out/abyss-dive/160001429798913_0.flac dive -20 # 179.8s +post tools/mureka_out/abyss-comms/160001576402945_1.flac comms -20 # 168.7s +post tools/mureka_out/abyss-ending/160001717043203_0.flac ending -20 # 191.4s + +echo "== abyss bgm done: $(ls "$DST" | wc -l) files ==" diff --git a/web/abyss/gen/gen_cover.py b/web/abyss/gen/gen_cover.py new file mode 100644 index 00000000..f56210c9 --- /dev/null +++ b/web/abyss/gen/gen_cover.py @@ -0,0 +1,106 @@ +"""web/abyss 封面:1080x1920,深潜救援主题——深海近黑蓝底、声呐同心圆、 +单点脉冲光与竖排标题《深海回音》。可复现脚本(固定 seed,无外部网络依赖)。 + +用法(PIL 探测顺序:系统 python3 → .venv-tts → 装 pillow 后任一): + python3 web/abyss/gen/gen_cover.py +""" +import math +import os +import random + +from PIL import Image, ImageDraw, ImageFilter, ImageFont + +W, H = 1080, 1920 +FONT_CANDIDATES = [ + "/home/xsl/SoundGame/web/rhythm/gen/NotoSansSC-Bold.ttf", + "/home/xsl/SoundGame/app/src/reflex/gen/NotoSansSC-Bold.ttf", +] +OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "cover.png") + + +def load_font(size): + for p in FONT_CANDIDATES: + if os.path.exists(p): + return ImageFont.truetype(p, size) + raise SystemExit("未找到中文字体,请放一份 NotoSansSC-Bold.ttf 到 web/rhythm/gen/") + + +def main(): + rng = random.Random(922) # 固定 seed:可复现 + img = Image.new("RGB", (W, H), (3, 8, 16)) + d = ImageDraw.Draw(img) + + # 背景竖向渐变:海面微光 -> 沟底近黑(下潜方向) + for y in range(H): + t = y / H + d.line([(0, y), (W, y)], + fill=(int(16 - 13 * t), int(34 - 26 * t), int(56 - 40 * t))) + + # 远景悬浮微粒(深海水雪 marine snow) + dot = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + dd = ImageDraw.Draw(dot) + for _ in range(150): + x, y = rng.randint(0, W - 1), rng.randint(0, H - 1) + r = rng.choice([1, 1, 2]) + dd.ellipse([x - r, y - r, x + r, y + r], + fill=(180, 210, 230, rng.randint(14, 46))) + img.paste(dot, (0, 0), dot) + d = ImageDraw.Draw(img) + + # 中央:声呐同心圆(你发出的脉冲),青色环从光点向外扩散 + cx, cy = W // 2, 600 + for i, (r, alpha, wd) in enumerate([(348, 30, 3), (272, 48, 4), (196, 70, 5), (124, 96, 6)]): + d.ellipse([cx - r, cy - r, cx + r, cy + r], + outline=(56, 189, 248, alpha) if i % 2 == 0 else (125, 211, 252, alpha), + width=wd) + glow = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + gd = ImageDraw.Draw(glow) + gd.ellipse([cx - 44, cy - 44, cx + 44, cy + 44], fill=(186, 230, 253, 200)) + gd.ellipse([cx - 88, cy - 88, cx + 88, cy + 88], fill=(56, 189, 248, 60)) + glow = glow.filter(ImageFilter.GaussianBlur(10)) + img.paste(glow, (0, 0), glow) + d = ImageDraw.Draw(img) + d.ellipse([cx - 20, cy - 20, cx + 20, cy + 20], fill=(240, 249, 255)) + + # 一记微弱回波:右下偏移的暗环(「深海回音」具象) + rx, ry = cx + 250, cy + 170 + for r, alpha in [(150, 40), (104, 60), (64, 84)]: + ring = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + rd = ImageDraw.Draw(ring) + rd.ellipse([rx - r, ry - r, rx + r, ry + r], + outline=(94, 234, 212, alpha), width=3) + ring = ring.filter(ImageFilter.GaussianBlur(1.2)) + img.paste(ring, (0, 0), ring) + d = ImageDraw.Draw(img) + + # 标题(居中)与副题 + f_title = load_font(172) + f_sub = load_font(54) + f_small = load_font(38) + + def ctext(y, s, font, fill, spacing=8): + bbox = d.textbbox((0, 0), s, font=font) + w = bbox[2] - bbox[0] + x = (W - w) / 2 + for ch in s: + d.text((x, y), ch, font=font, fill=fill) + bb = d.textbbox((0, 0), ch, font=font) + x += (bb[2] - bb[0]) + spacing + + ctext(1020, "深海回音", f_title, (224, 242, 254), spacing=16) + ctext(1272, "声呐里,是被困潜艇的五个人", f_sub, (110, 134, 166)) + + # 手势图例(极简两行) + hints = ["单击 发声呐脉冲", "左右滑 抉择", "上滑 氧气与进度", "下滑 菜单", "双击 重听"] + for i, s in enumerate(hints): + col, row = i % 3, i // 3 + d.text((110 + col * 300, 1420 + row * 76), s, font=f_small, fill=(92, 112, 142)) + + ctext(1700, "氧气在倒数。每次下潜,都是抉择。", f_small, (76, 94, 122)) + + img.save(OUT) + print(OUT, img.size) + + +if __name__ == "__main__": + main() diff --git a/web/abyss/gen/gen_sfx.py b/web/abyss/gen/gen_sfx.py new file mode 100644 index 00000000..cfed5f03 --- /dev/null +++ b/web/abyss/gen/gen_sfx.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""《深海回音》SYN 程序合成音效(批次 2,17-abyss.md §5.2 标注 SYN 的条目)。 + +6 个 id(其余 12 条为 Stable Audio 3 条目,见 gen_sfx_sa3.py): + sonar_ping 你发出的声呐脉冲:1.4->0.9kHz 正弦 chirp 220ms 指数衰减,-6dBFS + (设计指定电平;「回波家族统一音头」——搜索等待单击恒出此音) + pipe_knock 管路敲击暗号(老白 SOS):三短三长三短固定节拍金属敲击 + (节拍精确可复现、断言可数;短 0.12s / 长 0.36s / 间隔 0.12s) + link_beep 电台接通:双音上行 0.3s + ascent_chime 出水确认:上行三音琶音 0.4s + ui_confirm 菜单确认(全局先例:880->1320Hz 双音) + ui_menu 菜单呼出(全局先例:330+495Hz 软音) + +可复现:本脚本入库,一键出 web/abyss/audio/sfx/*.{ogg,m4a};中间 wav 在 +gen/staging/sfx/(不入库)。规格:ogg libvorbis q4 44.1kHz + m4a aac 128k; +one-shot 真峰值 <= -3dBFS(解码回读法,源归一留 MDCT 过冲余量);无 loop 类条目 +(amb_* 三条走 SA3「amb 床只去直流」工艺,在 gen_sfx_sa3.py)。 + +用法:.venv-tts/bin/python web/abyss/gen/gen_sfx.py (纯 CPU,无 GPU 依赖) +""" +import os +import subprocess + +import numpy as np +import soundfile as sf + +SR = 44100 +HERE = os.path.dirname(os.path.abspath(__file__)) +STAGING = os.path.join(HERE, "staging", "sfx") +DST = os.path.join(HERE, "..", "audio", "sfx") +FFMPEG = "/home/xsl/SoundGame/tools/bin/ffmpeg" +PEAK_SRC = 10 ** (-4.0 / 20) # 源归一 -4dBFS(有损编码过冲余量) +PEAK_LIMIT = 10 ** (-3.0 / 20) # 解码回读上限 -3dBFS +os.makedirs(STAGING, exist_ok=True) +os.makedirs(DST, exist_ok=True) + + +def t(d): + return np.arange(int(SR * d)) / SR + + +def env_exp(d, attack, release): + n = int(SR * d) + e = np.ones(n) + na, nr = max(1, int(SR * attack)), max(1, int(SR * release)) + e[:na] = np.linspace(0, 1, na) + e[-nr:] *= np.linspace(1, 0, nr) + return e + + +def lowpass(x, k): + return np.convolve(x, np.ones(k) / k, mode="same") + + +def bandpass(x, lo, hi): + kh, kl = max(1, int(SR / (np.pi * hi))), max(1, int(SR / (np.pi * lo))) + return lowpass(x, kh) - lowpass(x, kl) + + +def save(name, sig, peak_db=-4.0): + """mono 存。工艺同 tools/fix_clicks.py:去直流 → 首尾 8ms cos² 淡入淡出 → + 峰值归一指定 dBFS(sonar_ping 按设计 -6dBFS)。""" + sig = sig - np.mean(sig) + nf = max(1, int(SR * 0.008)) + fade = 0.5 * (1 - np.cos(np.pi * np.arange(nf) / nf)) + sig = sig.astype(np.float64).copy() + sig[:nf] *= fade + sig[-nf:] *= fade[::-1] + sig = sig / (np.max(np.abs(sig)) + 1e-9) * 10 ** (peak_db / 20) + sf.write(os.path.join(STAGING, name + ".wav"), sig.astype(np.float32), SR) + + +rng = np.random.default_rng(20260909) + +# ---- sonar_ping:声呐脉冲(1.4->0.9kHz 正弦 chirp 220ms 指数衰减,-6dBFS) ---- +d = 0.22 +x = t(d) +f = 1400 - (1400 - 900) * x / d # 1.4 -> 0.9kHz 线性下滑 +chirp = np.sin(2 * np.pi * np.cumsum(f) / SR) * np.exp(-x * 14) +click = bandpass(rng.standard_normal(len(x)), 2000, 6000) * np.exp(-x * 60) * 0.2 # 起振细瞬态 +save("sonar_ping", 0.92 * chirp + click, peak_db=-6.0) + +# ---- pipe_knock:管路敲击暗号(三短三长三短,SOS 节拍精确可复现/可数) ---- +# 单击=金属管体振(非谐波簇 640/1490/2330Hz,短促衰减),长短仅持续不同。 +def knock(dur, decay=26.0): + x = t(dur) + hit = sum(a * np.sin(2 * np.pi * fr * x) * np.exp(-x * decay * (1 + fr / 3000)) + for fr, a in [(640, 1.0), (1490, 0.55), (2330, 0.3), (410, 0.5)]) + thud = lowpass(rng.standard_normal(len(x)), 60) * np.exp(-x * 90) * 0.5 + return (hit + thud) * env_exp(dur, 0.002, 0.01) + +D_SHORT, D_LONG, GAP = 0.12, 0.36, 0.12 +pattern = ([D_SHORT] * 3 + [D_LONG] * 3 + [D_SHORT] * 3) # 三短三长三短 +_y = np.zeros(int(SR * (sum(pattern) + GAP * (len(pattern) - 1) + 0.15)), dtype=np.float64) +_pos = 0.15 / 2 * SR +for dur in pattern: + k = knock(dur) + _y[int(_pos):int(_pos) + len(k)] += k + _pos += int((dur + GAP) * SR) +save("pipe_knock", _y) + +# ---- link_beep:电台接通双音上行 0.3s(620->930Hz,通讯族音色,与 ui 族区分) ---- +d = 0.30 +a = np.sin(2 * np.pi * 620 * t(0.12)) * env_exp(0.12, 0.004, 0.02) +b = np.sin(2 * np.pi * 930 * t(0.16)) * env_exp(0.16, 0.004, 0.06) +save("link_beep", np.concatenate([a, np.zeros(int(SR * 0.02)), b])) + +# ---- ascent_chime:出水确认上行三音琶音 0.4s(C5-E5-G5,干净上行=「上浮完成」) ---- +y = np.zeros(int(SR * 0.40), dtype=np.float64) +for i, fr in enumerate([523.25, 659.25, 783.99]): + seg = t(0.22) + tone = (np.sin(2 * np.pi * fr * seg) + 0.4 * np.sin(2 * np.pi * fr * 2 * seg)) * np.exp(-seg * 10) + s = int(SR * i * 0.09) + n = min(len(tone), len(y) - s) + y[s:s + n] += tone[:n] * 0.6 +save("ascent_chime", y) + +# ---- ui_confirm / ui_menu:全局先例同款(storyline/radio gen_sfx.py) ---- +a = np.sin(2 * np.pi * 880 * t(0.09)) * env_exp(0.09, 0.005, 0.003) +b = np.sin(2 * np.pi * 1320 * t(0.14)) * env_exp(0.14, 0.005, 0.012) +save("ui_confirm", np.concatenate([a, np.zeros(int(SR * 0.02)), b, np.zeros(int(SR * 0.05))])) + +d = 0.35 +x = t(d) +save("ui_menu", np.concatenate([ + (np.sin(2 * np.pi * 330 * x) + 0.5 * np.sin(2 * np.pi * 495 * x)) * env_exp(d, 0.02, 0.08), + np.zeros(int(SR * 0.05))])) + + +def encode(name): + """staging wav -> ogg q4 + m4a 128k,解码回读校验:逐声道真峰值 <= -3dBFS、|直流| <= 0.001。 + 工艺:源尾部垫 50ms 静音(编码器软着陆)+ highpass=f=30(抑制 MDCT/AAC 解码直流再生); + 纯弦 AAC 过冲按实测迭代补 volume(link_beep.m4a 首轮实测 -2.82dBFS 超限,dungeon 补偿同款)。""" + src = os.path.join(STAGING, name + ".wav") + base, _sr0 = sf.read(src, always_2d=True) + pad_t = np.zeros((int(SR * 0.05), base.shape[1]), dtype=np.float64) + base = np.concatenate([base, pad_t]) + for ext, cmd in [("ogg", ["-c:a", "libvorbis", "-q:a", "4"]), + ("m4a", ["-c:a", "aac", "-b:a", "128k"])]: + out = os.path.join(DST, f"{name}.{ext}") + vol, dc_adj = 0.0, 0.0 + tmp = "/tmp/_sfx_enc_abyss.wav" + for _attempt in range(4): + sig = base - dc_adj + if vol < 0: + sig = sig * (10 ** (vol / 20)) + sf.write(tmp, sig.astype(np.float32), SR) + subprocess.run([FFMPEG, "-y", "-v", "error", "-i", tmp, "-af", "highpass=f=30", + "-ar", "44100", *cmd, out], check=True) + chk = "/tmp/_sfx_qc_abyss.wav" + subprocess.run([FFMPEG, "-y", "-v", "error", "-i", out, "-c:a", "pcm_f32le", chk], check=True) + y, _sr = sf.read(chk, always_2d=True) + tp = 20 * np.log10(np.max(np.abs(y)) + 1e-9) + dcv = float(np.mean(y.mean(axis=1))) + if tp <= -3.0 and abs(dcv) <= 0.001: + break + if tp > -3.0: + vol -= (tp + 3.0) + 0.25 # 超限差值 + 0.25dB 余量 + if abs(dcv) > 0.001: + dc_adj -= dcv + else: + raise AssertionError(f"{name}.{ext}: 补偿后仍超限 tp={tp:.2f}dBFS dc={dcv:.4f}") + print(f"[ok] {name}.{ext} peak={tp:.2f}dBFS" + (f" (vol {vol:+.2f}dB)" if vol else "")) + + +for _n in ["sonar_ping", "pipe_knock", "link_beep", "ascent_chime", "ui_confirm", "ui_menu"]: + encode(_n) +print("abyss SYN sfx done: 6 files x2 formats") diff --git a/web/abyss/gen/gen_sfx_sa3.py b/web/abyss/gen/gen_sfx_sa3.py new file mode 100644 index 00000000..f66a272d --- /dev/null +++ b/web/abyss/gen/gen_sfx_sa3.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""《深海回音》Stable Audio 3 small-sfx 音效(批次 2,17-abyss.md §5.2 SA3 条目)。 + +生成(GPU,flock 串行;模型 gated 走 hf-mirror,见 env.d/sfx.sh): + python3 gen_sfx_sa3.py --emit # 打印生成命令,原样执行 +生成 12 条原始 wav 到 gen/staging/sfx_sa3/abyss_.wav(顺序 = 下表 idx)。 +本脚本随后做后处理:修整/去直流/8ms cos² 淡化(amb 床只去直流不淡化——游戏层 +buffer.loop 循环,radio phone_ring/amb 先例)/ 响度定档 → ogg q4 + m4a 128k +(highpass=f=30,one-shot 前后垫 60/120ms 静音)→ 解码回读 QC:逐声道真峰值 +<= -3dBFS、|直流| <= 0.001;超限降增益 + 首轮重编加 18kHz 低通,最多 3 轮补偿。 + +echo_metal_l/_r:文档 prompt 带 panned slightly left/right,但引擎无运行时 pan、 +SA3 立体声分离度不可靠(dungeon 先例)——取「left」版生成源,mono 下混后 ffmpeg +pan 派生 _l/_r 预 pan 立体声(能量只在目标声道,脚本断言极性;dungeon side_* 工艺)。 + +prompt 均出自 17-abyss.md §5.2(英文原稿);定档理由: + spark_flash/landslide -4(D2/D4 危机拍,前置 2s 渐强同窗) + echo_metal -5 / echo_groan -6 / surf_splash -6(信息音/叙事音,主层次) + valve_close -5 / bell_toll -5(道具 payoff 音) + echo_debris -7 / static_burst -8(垫在通话底下的纹理层) + amb_* RMS -26 / 峰值帽 -8(环境床,escape/adio amb 先例) + +用法:.venv-tts/bin/python web/abyss/gen/gen_sfx_sa3.py [--emit] +""" +import os +import subprocess +import sys + +import numpy as np +import soundfile as sf + +SR = 44100 +HERE = os.path.dirname(os.path.abspath(__file__)) +STAGING = os.path.join(HERE, "staging", "sfx_sa3") +DST = os.path.join(HERE, "..", "audio", "sfx") +SA3 = "/home/xsl/SoundGame/tools/stable-audio-3" +FFMPEG = "/home/xsl/SoundGame/tools/bin/ffmpeg" +SEED = 20260909 + +# (id, gen 秒, prompt, 模式 oneshot/amb, 定档 dBFS, trim 上限 s) +ITEMS = [ + ("echo_metal", 3.0, "Sonar echo returning from a steel submarine hull, resonant metallic ping with slow decay, panned slightly left, underwater, 2 seconds", "oneshot", -5.0, 2.4), + ("echo_debris", 3.0, "Sonar echo returning from an underwater rubble and debris field, diffuse granular reverberation, 2 seconds", "oneshot", -7.0, 2.6), + ("echo_groan", 5.0, "Stressed submarine hull groaning deep underwater, long metallic moan under immense pressure, 4 seconds", "oneshot", -6.0, 4.5), + ("static_burst", 3.0, "Shortwave radio static burst with faint ghost of a distorted voice, 2 seconds", "oneshot", -8.0, 2.4), + ("amb_deep", 12.0, "Deep ocean ambient loop, low pressure hum, distant current wash, occasional far creak, seamless loop", "amb", -26.0, 0), + ("amb_deck", 12.0, "Night ship deck ambience, wind, gentle waves against hull, distant rigging creak and engine hum, seamless loop", "amb", -26.0, 0), + ("breath_loop", 12.0, "Slow scuba regulator breathing loop, calm but slightly labored, underwater, seamless loop", "amb", -26.0, 0), + ("spark_flash", 3.0, "Underwater cutting torch sparks with a brief muffled gas pop, 2 seconds", "oneshot", -4.0, 2.6), + ("valve_close", 3.0, "Heavy submarine valve wheel shutting, metal rotation clunk and water seal, 2 seconds", "oneshot", -5.0, 2.4), + ("landslide", 6.0, "Underwater landslide rumble, rolling sediment and rocks, ominous low roar building, 4 seconds", "oneshot", -4.0, 5.5), + ("surf_splash", 3.0, "Diver breaking through the sea surface, splash and one gasp, 2 seconds", "oneshot", -6.0, 2.6), + ("bell_toll", 4.0, "Old ship brass bell, single clean toll with warm resonance, 3 seconds", "oneshot", -5.0, 3.5), +] + +# 事件窗(SA3 raw 为定长 latent 床,与请求时长无关;None=按 -50dBFS 活动窗自然修剪)。 +# 首轮先自然修剪,实测事件位置偏离设计时再回填窗(radio arg_muffled 同法)。 +WIN = {} + + +def emit(): + items = [it for it in ITEMS if it[3] != "skip"] + durs = " ".join(str(d) for _, d, *_ in items) + ps = " ".join(f'"{p}"' for _, _, p, *_ in items) + print(f"cd {SA3} && flock /home/xsl/SoundGame/.gpu.lock .venv/bin/stable-audio " + f"--model small-sfx --steps 8 --seed {SEED} " + f"-p {ps} --duration {durs} -o {STAGING}/abyss.wav") + + +def trim_oneshot(y, floor_db=-50.0, head_ms=50, tail_ms=140, max_s=None): + env = np.max(np.abs(y), axis=1) + act = np.where(env > 10 ** (floor_db / 20))[0] + a = max(0, act[0] - int(SR * head_ms / 1000)) + b = min(len(y), act[-1] + int(SR * tail_ms / 1000)) + if max_s: + b = min(b, a + int(SR * max_s)) + return y[a:b] + + +def prep_oneshot(y, target_db, win=None, max_s=None): + if win: + a, b = int(SR * win[0]), int(SR * win[1]) + y = y[a:b] + nf2 = max(1, int(SR * 0.050)) # 窗切在持续声中,切点 50ms 缓出 + y[-nf2:] *= (0.5 * (1 + np.cos(np.pi * np.arange(nf2) / nf2)))[:, None] + else: + y = trim_oneshot(y, max_s=max_s) + y = y - np.mean(y, axis=0, keepdims=True) # 去直流 + nf = max(1, int(SR * 0.008)) + fade = 0.5 * (1 - np.cos(np.pi * np.arange(nf) / nf)) + y[:nf] *= fade[:, None] # 8ms cos² 淡入淡出 + y[-nf:] *= fade[::-1][:, None] + y = y / (np.max(np.abs(y)) + 1e-9) * 10 ** (target_db / 20) + pad_h = np.zeros((int(SR * 0.06), y.shape[1])) # 编码器软着陆 + pad_t = np.zeros((int(SR * 0.12), y.shape[1])) + return np.concatenate([pad_h, y, pad_t]) + + +def prep_amb(y, rms_db, peak_db): + """环境床:只去直流,不淡化(游戏层 buffer.loop 循环播放);RMS 定档 + 峰值帽。""" + y = y - np.mean(y, axis=0, keepdims=True) + rms = 20 * np.log10(np.sqrt(np.mean(y ** 2)) + 1e-9) + peak = 20 * np.log10(np.max(np.abs(y)) + 1e-9) + s = min(10 ** ((rms_db - rms) / 20), 10 ** ((peak_db - peak) / 20)) + return y * s + + +def encode(name, sig): + base = sig.astype(np.float64) + vol, lpf, dc_adj = 0.0, False, 0.0 + tmp = os.path.join(STAGING, "_enc.wav") + chk = os.path.join(STAGING, "_qc.wav") + res = {} + for attempt in range(4): # 首发 + 最多 3 轮补偿(首轮重编加 18kHz 低通) + x = base - dc_adj + if vol < 0: + x = x * (10 ** (vol / 20)) + sf.write(tmp, x.astype(np.float32), SR) + af = "highpass=f=30,lowpass=f=18000" if lpf else "highpass=f=30" + for ext, cmd in [("ogg", ["-c:a", "libvorbis", "-q:a", "4"]), + ("m4a", ["-c:a", "aac", "-b:a", "128k"])]: + out = os.path.join(DST, f"{name}.{ext}") + subprocess.run([FFMPEG, "-y", "-v", "error", "-i", tmp, "-af", af, + "-ar", "44100", *cmd, out], check=True) + subprocess.run([FFMPEG, "-y", "-v", "error", "-i", out, "-c:a", "pcm_f32le", chk], check=True) + y, sr = sf.read(chk, always_2d=True) + tp = 20 * np.log10(np.max(np.abs(y)) + 1e-9) + dcv = float(np.mean(y.mean(axis=1))) + rms = 20 * np.log10(np.sqrt(np.mean(y ** 2)) + 1e-9) + ok_tp, ok_dc = tp <= -3.0, abs(dcv) <= 0.001 + res[ext] = (tp, dcv, rms, y.shape[0] / sr) + if not (ok_tp and ok_dc): + if not ok_tp: + vol -= (tp + 3.0) + 0.25 + lpf = True # 首轮重编起加 18kHz 低通压 MDCT 过冲 + if not ok_dc: + dc_adj -= dcv + break + else: + break + else: + raise AssertionError(f"{name}: 补偿 3 轮后仍超限 {res}") + return res + + +def pan_derive(name, sig_mono): + """mono 源 -> 预 pan 立体声 _l/_r(能量只在 L 或 R 声道),ffmpeg pan 派生 + (dungeon side_* 工艺复用:引擎 play() 无实时 pan)。""" + src = os.path.join(STAGING, name + "_mono.wav") + sf.write(src, sig_mono.astype(np.float32), SR) + for side, out_ch in [("l", "c0"), ("r", "c1")]: + out = os.path.join(STAGING, f"{name}_{side}.wav") + subprocess.run([FFMPEG, "-y", "-v", "error", "-i", src, + "-af", f"pan=stereo|{out_ch}=c0", "-c:a", "pcm_f32le", out], check=True) + # 极性断言(staging 源级):能量只在声像侧声道 + for side, ch in [("l", 0), ("r", 1)]: + y, _ = sf.read(os.path.join(STAGING, f"{name}_{side}.wav"), always_2d=True) + assert np.max(np.abs(y[:, 1 - ch])) < 1e-6, f"{name}_{side}: 非目标声道有能量" + assert np.max(np.abs(y[:, ch])) > 0.1, f"{name}_{side}: 目标声道无能量" + + +def main(): + os.makedirs(STAGING, exist_ok=True) + if "--emit" in sys.argv: + emit() + return + print(f"| {'id':14s} | {'时长s':>6s} | {'ogg峰dBFS':>9s} | {'m4a峰dBFS':>9s} | {'RMS dBFS':>8s} | 直流 |") + idx = 0 + for name, _d, _p, mode, tgt, max_s in ITEMS: + if mode == "skip": + continue + raw = os.path.join(STAGING, f"abyss_{idx}.wav") + y, sr = sf.read(raw, always_2d=True, dtype="float64") + assert sr == SR, f"{name}: 采样率 {sr}" + if y.shape[1] == 1: + y = np.stack([y[:, 0], y[:, 0]], axis=1) + idx += 1 + if mode == "oneshot": + sig = prep_oneshot(y, tgt, win=WIN.get(name), max_s=max_s) + else: + sig = prep_amb(y, tgt, -8.0) + if name == "echo_metal": + # mono 下混 -> pan 派生 _l/_r(保留 oneshot 修整/淡化/定档后的信号) + mono = sig.mean(axis=1) + pad_h = np.zeros((int(SR * 0.06), 2)) + pad_t = np.zeros((int(SR * 0.12), 2)) + pan_derive(name, mono) + for side in ("l", "r"): + s2, _ = sf.read(os.path.join(STAGING, f"{name}_{side}.wav"), always_2d=True) + res = encode(f"echo_metal_{side}", np.concatenate([pad_h, s2, pad_t])) + tp_o, dc, rms, dur = res["ogg"] + print(f"| echo_metal_{side:<3s} | {dur:6.2f} | {tp_o:9.2f} | {res['m4a'][0]:9.2f} | {rms:8.2f} | {dc:+.4f} |") + continue + res = encode(name, sig) + tp_o, dc, rms, dur = res["ogg"] + tp_m = res["m4a"][0] + print(f"| {name:14s} | {dur:6.2f} | {tp_o:9.2f} | {tp_m:9.2f} | {rms:8.2f} | {dc:+.4f} |") + print("abyss SA3 sfx done -> web/abyss/audio/sfx/") + + +if __name__ == "__main__": + main() diff --git a/web/abyss/index.html b/web/abyss/index.html new file mode 100644 index 00000000..79bf1eda --- /dev/null +++ b/web/abyss/index.html @@ -0,0 +1,37 @@ + + + + + + + 深海回音 · SoundGame Web + + + + +
+ +
+ + + + diff --git a/web/abyss/test/playtest.mjs b/web/abyss/test/playtest.mjs new file mode 100644 index 00000000..cbe75aef --- /dev/null +++ b/web/abyss/test/playtest.mjs @@ -0,0 +1,1367 @@ +/** + * 《深海回音》无头试玩仿真测试(docs/design/17-abyss.md §9 playtest 断言清单)。 + * + * 方案: + * - mock 浏览器环境:window/localStorage/navigator/fetch/AudioContext + * (decodeAudioData 返回固定 2.0s 时长的假 buffer,语音链靠引擎 endTimer + + * onComplete 推进,虚拟时钟驱动,不真实等待);资产缺失时靠游戏层 30s 单句 + * 看门狗自动续链(N1 机制)——测试对「资产已落地 / 未落地」两种状态都成立; + * - 使用**真实** engine/audio.js + settings.js + guide.js; + * - 手势直接注入 game.onGesture(与 attachGestures 回调同路径); + * - 虚拟时钟:patch setTimeout/clearTimeout/performance.now; + * - 环境音循环(amb_deck/amb_deep/breath_loop)为 buffer.loop 句柄(无重触发 + * 定时器),全程虚拟时钟可空闲、可复现; + * - navigator.maxTouchPoints=1(手机 persona:inputMode() 判 touch;键盘路由 + * 由本文件「键盘变体」块按 settings.input 覆盖验证)。 + * + * 覆盖:剧本 A-F 六条路径到 4 结局(oxy 逐检查点验算 §4.5)、氧气门三形态、 + * pendingChoice 盲玩公平、sonarGate 仲裁(危机拍前置/空闲门/零重叠)、 + * 结算死窗(幽灵 tap 不截链)、看门狗双轨(乱按 96s 抢救 + 双轨去重)、 + * 中断复述(F4 锁)、存档 schema、菜单循环、键盘变体路由、静态审计。 + * + * 运行:tools/node/bin/node web/abyss/test/playtest.mjs + */ +import fs from 'node:fs'; +import path from 'node:path'; +import url from 'node:url'; + +const GAME_DIR = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..'); +const AUDIO_DIR = path.join(GAME_DIR, 'audio'); +const SAVE_KEY = 'soundgame.progress.abyss'; + +// ---------------------------------------------------------------- 断言收集 + +const realLog = console.log.bind(console); +let passCount = 0; +const failures = []; +function assert(cond, msg) { + if (cond) { passCount++; realLog(` ✓ ${msg}`); } + else { failures.push(msg); realLog(` ✗ ${msg}`); } +} + +// console 捕获(引擎/游戏的 warn/error 进日志,测试输出用 realLog) +const consoleErrs = []; +const consoleWarns = []; +console.error = (...a) => { consoleErrs.push(a.join(' ')); }; +console.warn = (...a) => { consoleWarns.push(a.join(' ')); }; +console.log = () => {}; + +// ---------------------------------------------------------------- 虚拟时钟 + +let now = 0; // 虚拟毫秒 +let timerSeq = 1; +const timers = new Map(); // id -> { fireAt, cb, args } +const realSetImmediate = setImmediate; + +globalThis.setTimeout = (cb, ms = 0, ...args) => { + const id = timerSeq++; + timers.set(id, { fireAt: now + Math.max(0, Number(ms) || 0), cb, args }); + return id; +}; +globalThis.clearTimeout = (id) => { timers.delete(id); }; +globalThis.setInterval = (cb, ms = 0, ...args) => { + const id = timerSeq++; + const step = (...a) => { + if (!intervals.has(id)) return; + cb(...a); + if (intervals.has(id)) timers.set(id, { fireAt: now + Math.max(0, Number(ms) || 0), cb: step, args }); + }; + timers.set(id, { fireAt: now + Math.max(0, Number(ms) || 0), cb: step, args }); + return id; +}; +const intervals = new Set(); +globalThis.clearInterval = (id) => { intervals.delete(id); timers.delete(id); }; +Object.defineProperty(globalThis, 'performance', { + value: { now: () => now }, configurable: true, +}); + +/** 排空微任务(fetch/decode 的 promise 链)。 */ +async function flush(rounds = 8) { + for (let i = 0; i < rounds; i++) await new Promise((r) => realSetImmediate(r)); +} + +/** 推进虚拟时钟:逐个触发到期定时器,直到没有待触发定时器(= 游戏在等手势) + * 或 cond() 满足。返回 'cond' | 'idle'。 */ +async function advance(cond = null) { + for (let n = 0; n < 50000; n++) { + await flush(2); + if (cond && cond()) return 'cond'; + let nextId = -1; let nextAt = Infinity; + for (const [id, t] of timers) if (t.fireAt < nextAt) { nextAt = t.fireAt; nextId = id; } + if (nextId < 0) return 'idle'; + now = nextAt; + const t = timers.get(nextId); + timers.delete(nextId); + t.cb(...t.args); + } + throw new Error('虚拟时钟定时器超上限(疑似死循环)'); +} + +// ---------------------------------------------------------------- 浏览器 mock + +const winListeners = new Map(); +globalThis.window = { + addEventListener: (type, fn) => { + if (!winListeners.has(type)) winListeners.set(type, []); + winListeners.get(type).push(fn); + }, + AudioContext: null, // 下方赋值 +}; +function fireWindowEvent(type) { + for (const fn of winListeners.get(type) || []) fn(); +} + +const storageData = new Map(); +globalThis.localStorage = { + getItem: (k) => (storageData.has(k) ? storageData.get(k) : null), + setItem: (k, v) => { storageData.set(k, String(v)); }, + removeItem: (k) => { storageData.delete(k); }, +}; + +const vibrateLog = []; +Object.defineProperty(globalThis, 'navigator', { + // maxTouchPoints: 1 —— 手机 persona:inputMode() 判 touch,语音走触屏措辞主资产 + value: { maxTouchPoints: 1, vibrate: (...a) => { vibrateLog.push({ pattern: a[0], t: now }); return true; } }, + configurable: true, +}); + +class MockParam { + constructor(v = 1) { this.value = v; } + setValueAtTime() {} + linearRampToValueAtTime() {} + cancelScheduledValues() {} +} +class MockGainNode { + constructor() { this.gain = new MockParam(); } + connect() {} + disconnect() {} +} +function fakeBuffer() { + return { + duration: 2.0, // 固定句长 2s(虚拟时钟推进语音链;与资产是否落地无关) + numberOfChannels: 1, + sampleRate: 44100, + getChannelData: () => new Float32Array(16), + }; +} +class MockAudioContext { + constructor() { this.state = 'running'; this.currentTime = 0; this.destination = {}; } + createGain() { return new MockGainNode(); } + createDynamicsCompressor() { + const n = new MockGainNode(); + n.threshold = new MockParam(); n.ratio = new MockParam(); + n.attack = new MockParam(); n.release = new MockParam(); + return n; + } + createBufferSource() { + return { buffer: null, loop: false, connect() {}, start() {}, stop() {} }; + } + createScriptProcessor() { return { connect() {}, disconnect() {}, onaudioprocess: null }; } + createOscillator() { + return { type: 'sine', frequency: new MockParam(440), connect() {}, start() {}, stop() {} }; + } + decodeAudioData() { return Promise.resolve(fakeBuffer()); } + resume() { this.state = 'running'; return Promise.resolve(); } +} +globalThis.window.AudioContext = MockAudioContext; + +// fetch:读真实资产目录;blockedAssets 中的片段强制 404(资产缺失测试用) +const requestedUrls = []; +const blockedAssets = new Set(); +globalThis.fetch = async (u, opts = {}) => { + const s = String(u); + requestedUrls.push(s); + if (s.startsWith('/api/')) { + if ((opts.method || 'GET') === 'PUT') { + return { ok: true, status: 200, json: async () => ({ ok: true, game: 'abyss' }) }; + } + return { ok: false, status: 401, json: async () => ({ error: 'unauthorized' }) }; + } + const blocked = [...blockedAssets].some((b) => s.includes(b)); + const p = path.join(GAME_DIR, decodeURIComponent(s)); + if (blocked || !fs.existsSync(p)) { + return { ok: false, status: 404, arrayBuffer: async () => new ArrayBuffer(0) }; + } + const data = fs.readFileSync(p); + return { + ok: true, status: 200, + arrayBuffer: async () => data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength), + }; +}; + +// ---------------------------------------------------------------- 加载引擎与游戏 + +const { WebAudioEngine } = await import('../../engine/audio.js'); +const { settings } = await import('../../engine/settings.js'); +const { AbyssGame } = await import('../game.js'); + +function resetSettings(guide) { + settings.set({ guide, voice: 'female', speed: 1.0, bgm: 1.0, input: 'touch' }); +} +resetSettings('off'); + +function clearSave() { storageData.delete(SAVE_KEY); } +function saveData() { + const raw = storageData.get(SAVE_KEY); + if (!raw) return null; + try { return JSON.parse(raw); } catch (_) { return null; } +} + +const voicePlays = (playLog, id) => playLog.filter((e) => e.id === id).length; +const urlHits = (frag) => requestedUrls.filter((u) => u.includes(frag)).length; +const lastIndexOf = (playLog, id) => playLog.map((e) => e.id).lastIndexOf(id); +const ERR_PATTERN = JSON.stringify([60, 40, 60]); // error 震型 +const WARN_PATTERN = JSON.stringify([30, 50, 30, 50, 30]); // warning 震型 +const BEAT_PATTERN = JSON.stringify([8]); // beat 震型(G4 轻震) +const DANGER_PATTERN = JSON.stringify([25, 40, 25, 40, 25]); // danger 震型 + +/** 声呐信息音与语音零重叠校验(§6.2 R1;sonarGate 排队语义)。 */ +function sonarVoiceOverlaps(game) { + const bad = []; + for (const sp of game._sonarPlays) { + for (const rec of game._voiceIntervals) { + if (rec.start <= sp.t && sp.t < rec.end) bad.push(`${sp.id}@${sp.t}ms 撞 ${rec.id}`); + } + } + return bad; +} + +function makeRig() { + const audio = new WebAudioEngine('audio/'); + const playLog = []; + const origPlay = audio.play.bind(audio); + audio.play = (id, opts) => { playLog.push({ id, t: now, opts }); return origPlay(id, opts); }; + const game = new AbyssGame(audio, {}); + return { audio, playLog, game }; +} + +let gestureCount = 0; +async function g(game, gesture) { + gestureCount++; + game.onGesture(gesture); + await flush(); +} + +/** 结束一个测试块:作废在途链,避免污染后续块的虚拟时钟。 */ +function teardown(game) { + game.guide.disarm(); + game.stop(); +} + +/** + * 救援推进器:语音在播时等它自然播完(不跳句);搜索等待单击发脉冲;抉择武装 + * 即选;链末等待态单击继续——直到 ENDING 结算态(或 stop() 条件命中,返回 + * 已消耗的抉择数,供续跑)。 + */ +async function playRun(game, choices, opts = {}) { + let i = opts.from || 0; + const stop = opts.stop || (() => false); + let guard = 0; + while (!(game.phase === 'ENDING' && game.awaitingRestart)) { + if (++guard > 900) throw new Error(`playRun 卡住(node=${game.nodeId} phase=${game.phase} vp=${game.voicePlaying} search=${game.searchArmed})`); + if (stop()) return i; + if (game.phase === 'MENU') { await g(game, 'down'); continue; } // 安全网:误入菜单即关闭 + if (game.searchArmed) { await g(game, 'tap'); continue; } // 单击=发脉冲 + if (game.choiceArmed) { + const c = choices[i++]; + if (!c) throw new Error(`playRun 抉择脚本耗尽(node=${game.nodeId})`); + await g(game, c); + continue; + } + if (!game.voicePlaying) { await g(game, 'tap'); continue; } // 链末=继续 + await advance(() => stop() || !game.voicePlaying || game.choiceArmed || game.searchArmed); + } + return i; +} + +// ---- 抉择脚本(p1..p9 遭遇顺序;§9 剧本 A-E + 补剧本 F 单载右路) +// A:全右 + p9 左 → e1(saved=5) +const SCRIPT_A = ['right', 'right', 'right', 'right', 'right', 'right', 'right', 'right', 'left']; +// B:同 A 但 p9 右 → e2 +const SCRIPT_B = [...SCRIPT_A.slice(0, 8), 'right']; +// C:p1左 p2左 p4左,余右 → 氧气门走终局取消链 → p9 左(oxy=2≥1)→ e3(saved=3) +const SCRIPT_C = ['left', 'left', 'right', 'left', 'right', 'right', 'left']; +// D:p1左 p4左,余右 → 单载(stayer=chen → p7_chen)→ p7 左=带船长 → chen_last +// → p8 右 → oxy_end=0 → p9 左封锁(g9_dry 自动右)→ e4(saved=4) +const SCRIPT_D = ['left', 'right', 'right', 'left', 'right', 'right', 'left', 'right', 'left']; +// E:p2左 p3[无knowsPipe]左 p4左 p6左 → mid-dive floor 至 0 干涸链 → e4(saved=3) +const SCRIPT_E = ['right', 'left', 'left', 'left', 'right', 'left', 'left']; +// F:p1左 p4左 p5左,余右 → 单载(stayer=zheng → p7_zheng)→ p7 右=带小郑 +// → gao_last、p8 跳过 → p9 左封锁 → e4(saved=4) +const SCRIPT_F = ['left', 'right', 'right', 'left', 'left', 'right', 'right', 'left']; + +/** + * 手动推进到(第 n 潜)搜索等待:INTRO 链末逐节点单击、语音自然播完—— + * 纯 advance 不出手(虚拟时钟会停在链末等待态 idle,测试会走偏)。 + */ +async function reachSearch(game, n = 1) { + let guard = 0; + while (!(game.searchArmed && game.diveN === n)) { + if (++guard > 300) throw new Error(`reachSearch 卡住(node=${game.nodeId} phase=${game.phase})`); + if (game.searchArmed) return; // 已在(更早的)搜索等待 + if (game.choiceArmed) throw new Error('reachSearch 意外遇到抉择(node=' + game.nodeId + ')'); + if (!game.voicePlaying) { await g(game, 'tap'); continue; } + await advance(() => game.searchArmed || !game.voicePlaying); + } +} + +/** + * 叙事链推进器:语音在播时等它自然播完(不跳句),链末等待态单击继续, + * 搜索等待单击发脉冲——直到 cond() 满足。遇到未应答抉择抛错(调用方先选)。 + */ +async function tapTo(game, cond, maxSteps = 80) { + for (let i = 0; i < maxSteps; i++) { + if (cond()) return; + if (game.searchArmed) { await g(game, 'tap'); continue; } + if (game.choiceArmed) throw new Error(`tapTo 遇到未应答的抉择(node=${game.nodeId})——调用方需先做选择`); + if (game.voicePlaying) { + await advance(() => cond() || !game.voicePlaying); + continue; + } + await g(game, 'tap'); // 链末单击 = 继续 + } + throw new Error(`tapTo 超步限(node=${game.nodeId} phase=${game.phase})`); +} + +// voice 资产是否已落地(测试对两种状态都成立;缺失时走 30s 单句看门狗续链) +const voiceLanded = ['ogg', 'm4a'].some((ext) => + fs.existsSync(path.join(AUDIO_DIR, 'voice/female/b_open.' + ext))); + +// ================================================================ 主路径试玩 +// (§9 关键路径:剧本 A 全程 e1,oxy 逐点与 §4.5 公式一致,≤70 手势) + +realLog('== 主路径 · 剧本 A(e1,guide=off)=='); +{ + clearSave(); + resetSettings('off'); + const g0 = gestureCount; + const { audio, playLog, game } = makeRig(); + + // §7 全局:解锁前 play 为 no-op + audio.play('voice/b_open'); + await flush(); + assert(audio._voice === null && consoleWarns.some((w) => w.includes('尚未解锁')), + '启动遮罩点按前无音频播放(play 为 no-op)'); + await audio.unlock(); + playLog.length = 0; + + game.start(); + await flush(); + assert(playLog.some((e) => e.id === 'bgm/deck'), '开局播放 bgm/deck(甲板,开局淡入)'); + assert(voicePlays(playLog, 'voice/sys_new_dive') === 1, '新救援 sys_new_dive 先行'); + assert(game.phase === 'INTRO' && game.nodeId === 'b_open', '进入宏状态 INTRO(b_open 起播)'); + + // 1. INTRO 链(链末等待单击):四句逐句 + sys_skip 一次;guide=off 零 gd_brief + await advance(() => game.currentVoiceId === 'voice/b_open'); + assert(game.voicePlaying && game.currentVoiceId === 'voice/b_open', 'sys_new_dive 播完接 b_open(voice 层有播放记录)'); + assert(voicePlays(playLog, 'voice/gd_brief') === 0, 'guide=off:开场不插 gd_brief'); + // 逐节点推进(每节点链末等待单击,§2.2「链完单击」) + for (const [node, vid] of [['b_rule', 'voice/b_rule'], ['b_goal', 'voice/b_goal'], ['b_go', 'voice/b_go']]) { + await advance(() => !game.voicePlaying); + await g(game, 'tap'); + await advance(() => game.currentVoiceId === vid); + assert(game.nodeId === node, `INTRO 链末单击推进至 ${node}`); + } + assert(voicePlays(playLog, 'voice/sys_skip') === 1, '开场 sys_skip 提示播一次(每实例一次)'); + + // 2. b_go 链末单击 → DIVE(1):下潜起播扣 3(账目断言)+ 下潜链 auto → 搜索武装 + await advance(() => !game.voicePlaying); + assert(game.phase === 'INTRO' && !game.voicePlaying, 'b_go 链末等待单击(INTRO ──链完单击──> DIVE)'); + await g(game, 'tap'); + assert(game.phase === 'DIVE' && game.diveN === 1 && game.oxy === 9, + '单击开始下潜 → DIVE(1),oxy 12-3=9(每次下潜开始扣 3,§6.3 承诺)'); + assert(playLog.some((e) => e.id === 'bgm/dive'), '进 DIVE crossfade bgm/dive'); + await reachSearch(game); + assert(game.searchArmed && game.nodeId === 'dv1_teach', '下潜链 auto → 搜索等待武装(dv1_down→dv1_teach)'); + for (const id of ['voice/dv1_down', 'voice/dv1_teach']) { + assert(voicePlays(playLog, id) === 1, `下潜链播出 ${id}`); + } + // guide=off:搜索等待挂起零定时器(虚拟时钟可空闲)、零提示、零超时惩罚 + assert((await advance()) === 'idle', 'guide=off 搜索等待零定时器(虚拟时钟可空闲)'); + assert(voicePlays(playLog, 'voice/gd_ping1') === 0 && voicePlays(playLog, 'voice/gd_ping2') === 0, + 'guide=off:搜索等待零向导提示(无超时惩罚)'); + + // 3. 搜索单击 → sonar_ping → 静默一拍 → echo_metal_l → dv1_metal(承诺音)→ + // dv1_link(link_beep)→ COMMS(1):z1_a/b/knock(垫 pipe_knock 两遍) + const sonar0 = voicePlays(playLog, 'sfx/sonar_ping'); + await g(game, 'tap'); + assert(voicePlays(playLog, 'sfx/sonar_ping') === sonar0 + 1, '搜索等待单击恒=发一枚声呐脉冲(非跳句、非无效)'); + await advance(() => game.phase === 'COMMS'); + assert(voicePlays(playLog, 'sfx/echo_metal_l') === 1, '回波链播出 echo_metal_l(金属回波=海燕号船身承诺音)'); + for (const id of ['voice/dv1_metal', 'voice/dv1_link', 'voice/z1_a']) { + assert(voicePlays(playLog, id) === 1, `回波→接通→COMMS 链播出 ${id}`); + } + assert(playLog.some((e) => e.id === 'sfx/link_beep'), '电台接通 link_beep'); + assert(playLog.some((e) => e.id === 'sfx/static_burst'), 'z1_a 垫 static_burst(电台杂音)'); + // R3:语音在播时 amb 常驻声呐元素降 -6dB(duck 标记) + await tapTo(game, () => game.currentVoiceId === 'voice/z1_b'); + assert(voicePlays(playLog, 'voice/z1_b') === 1, '回波→接通→COMMS 链播出 voice/z1_b'); + assert(game._ambDucked === true, 'R3:语音在播时 amb_deep 降 -6dB(duck 标记)'); + // z1_knock 播完垫 pipe_knock 两遍(信息层,语音播完才响,R1) + await tapTo(game, () => game.currentVoiceId === 'voice/z1_knock'); + await tapTo(game, () => game.promptPlaying); + const iKnockV = lastIndexOf(playLog, 'voice/z1_knock'); + const knocks = playLog.filter((e) => e.id === 'sfx/pipe_knock' && e.t > 0) + .filter((e) => playLog.findIndex((x) => x === e) > iKnockV).length; + assert(knocks >= 2, `z1_knock 语音播完垫 pipe_knock 两遍(实际 ${knocks},R1 零重叠)`); + + // 4. p1 不可跳 + pendingChoice(V1 时序:播完才生效、连点不丢、连滑取最后) + const vib0 = vibrateLog.length; + await g(game, 'tap'); + assert(game.promptPlaying && game.currentVoiceId === 'voice/p1', 'p* 播放中单击不跳句(PROMPT_RE 门)'); + await g(game, 'left'); + assert(game.pendingChoice === 'left', 'p* 播放中左滑缓存 pendingChoice(盲玩公平)'); + await g(game, 'right'); + assert(game.pendingChoice === 'right', 'p* 播放中再滑覆盖缓存(最后一次为准)'); + const iP1Last = lastIndexOf(playLog, 'voice/p1'); + await advance(() => game.nodeId === 'd1_up'); + const idsSoFar = playLog.map((e) => e.id); + const iConfirm = idsSoFar.indexOf('sfx/ui_confirm'); + assert(iConfirm > iP1Last && idsSoFar.indexOf('voice/d1_up') > iConfirm, + 'V1:生效时序 = p1 播完 → ui_confirm 结算音 → 结果句(播完才生效,零插播)'); + assert(game.pendingChoice === null && game.flags.knowsPipe === false && game.oxy === 9, + 'V1:p1 右 0 气(knowsPipe=false,oxy=9 不动)'); + assert(vibrateLog.slice(vib0).some((v) => JSON.stringify(v.pattern) === BEAT_PATTERN), + 'G4:p* 不可跳句空点单击播 beat 轻震(可区分于真确认 [20])'); + + // 5. p2 右 → SURFACE(1):出水 sfx + d1_surf + 档位句(账目断言)→ 存档 + await tapTo(game, () => game.choiceArmed); // d1_up → z1_bye → p2 + await g(game, 'right'); + assert(game.flags.hasLog === false && game.bond.zhou === 0, 'p2 右:hasLog=false、bond_zhou=0'); + await tapTo(game, () => game.phase === 'SURFACE' && game.currentVoiceId === 'voice/d1_surf'); + assert(playLog.some((e) => e.id === 'sfx/surf_splash') && playLog.some((e) => e.id === 'sfx/ascent_chime'), + '上浮结算垫 surf_splash + ascent_chime(破水出水+确认音)'); + await advance(() => game.phase === 'DIVE' && game.diveN === 2); + assert(game.oxy === 6, 'SURFACE(1) 后 DIVE(2):oxy 9-3=6(§4.5 逐点验算)'); + assert(voicePlays(playLog, 'voice/oxy_full') === 1, '首次结算播 oxy_full(oxy=9 ≥3 潜气量,账目真实)'); + assert(voicePlays(playLog, 'voice/oxy_ok') === 0 && voicePlays(playLog, 'voice/oxy_low') === 0 + && voicePlays(playLog, 'voice/oxy_crit') === 0, '未跨档不播其他档位句'); + const sd1 = saveData(); + assert(sd1 && sd1.phase === 'DIVE' && sd1.dive === 2 && sd1.oxy === 6 && sd1.node === 'dv2_down', + 'DIVE 进入即存档:phase=DIVE / dive=2 / oxy=6 / node=dv2_down'); + const keysNeeded = ['schema', 'updatedAt', 'summary', 'phase', 'node', 'dive', 'oxy', + 'supply', 'saved', 'stayer', 'flags', 'endings', 'runs']; + assert(keysNeeded.every((k) => k in sd1), '存档 data 覆盖 §8.1 全部字段(含 summary 字符串)'); + assert(sd1.saved.zhou === 1 && sd1.saved.bai === 0, 'saved 标量:小舟救上(教学潜保底)'); + assert(sd1.summary === '第2潜·机舱 · 氧气6格 · 结局0/4', `summary 格式(实际: ${sd1.summary})`); + assert(Number.isFinite(Date.parse(sd1.updatedAt)), 'updatedAt 为 ISO 时间戳'); + + // 6. DIVE(2):脉冲 → echo_debris+pipe_knock(D1 暗号兑现)→ p3 右危机拍 + await reachSearch(game); + await g(game, 'tap'); + await advance(() => game.phase === 'COMMS'); + const iDebris = lastIndexOf(playLog, 'sfx/echo_debris'); + const iKnockD2 = playLog.map((e) => e.id).lastIndexOf('sfx/pipe_knock'); + const iFound = lastIndexOf(playLog, 'voice/dv2_found'); + assert(iDebris >= 0 && iDebris < iKnockD2 && iKnockD2 < iFound, + 'D2 搜索:echo_debris → pipe_knock → dv2_found(敲管=老白的信号,D1 教学兑现)'); + const danger0 = vibrateLog.filter((v) => JSON.stringify(v.pattern) === DANGER_PATTERN).length; + await tapTo(game, () => game.choiceArmed); // bai_a → bai_b → p3 + await g(game, 'right'); // p3 右 = 切割快切 → 危机拍 + await advance(() => game.currentVoiceId === 'voice/d2_flash'); + assert(voicePlays(playLog, 'sfx/spark_flash') === 1, 'D2 危机拍:spark_flash 前置起播'); + assert(vibrateLog.filter((v) => JSON.stringify(v.pattern) === DANGER_PATTERN).length === danger0 + 1, + 'D2 危机拍 danger 震型(签名+震同窗)'); + const iSpark = lastIndexOf(playLog, 'sfx/spark_flash'); + const iCut = lastIndexOf(playLog, 'voice/d2_cut'); + assert(iCut < iSpark, 'spark_flash 在 d2_cut 语音播完后才起(R1:危机音不叠语音)'); + await tapTo(game, () => game.choiceArmed); // d2_flash → bai_out → p4 + await g(game, 'right'); // p4 右 = 直接上浮 + await tapTo(game, () => game.phase === 'SURFACE' && game.diveN === 2); + assert(game.oxy === 9 && game.supply === true, + 'SURFACE(2):supply 后 oxy 6+3=9(应急瓶全局唯一一次 +3)'); + await advance(() => voicePlays(playLog, 'voice/oxy_gain') === 1); + assert(voicePlays(playLog, 'voice/d2_supply') === 1 && voicePlays(playLog, 'voice/oxy_gain') === 1, + '补给链播出 d2_supply → oxy_gain(恰一次)'); + assert(voicePlays(playLog, 'voice/oxy_full') === 1, '补给后 tier=full 与已播档一致 → 不重播(账目真实)'); + + // 7. DIVE(3):脉冲 → echo_groan(预警)→ p5 右 / p6 右 → SURFACE(3) ok 档 → 氧气门双载 + await reachSearch(game); + await g(game, 'tap'); + await advance(() => game.phase === 'COMMS'); + assert(voicePlays(playLog, 'sfx/echo_groan') === 1 && voicePlays(playLog, 'voice/dv3_groan') === 1, + 'D3 搜索:echo_groan 船体呻吟预警 + dv3_groan 解读'); + await tapTo(game, () => game.choiceArmed); + await g(game, 'right'); // p5 右 = 先带小郑 + assert(game.stayer === 'chen' && game.saved.zheng === 1 && game.saved.chen === 0, + 'p5 右:小郑上(saved.zheng=1),stayer=chen'); + for (const id of ['voice/d3_zheng', 'voice/d3_zheng_e', 'voice/chen_stay']) { + await advance(() => voicePlays(playLog, id) === 1); + } + await tapTo(game, () => game.choiceArmed); // d3_zheng → d3_zheng_e → chen_stay → p6 + await g(game, 'right'); // p6 右 = 约定来接 + await tapTo(game, () => game.phase === 'SURFACE' && game.diveN === 3); + assert(game.oxy === 6, 'SURFACE(3):oxy=6(§4.5 逐点验算)'); + await advance(() => voicePlays(playLog, 'voice/oxy_ok') === 1); + assert(voicePlays(playLog, 'voice/oxy_ok') === 1, 'SURFACE(3):oxy=6 → ok 档跨档播报(§4.5)'); + await tapTo(game, () => game.phase === 'DIVE' && game.diveN === 4); + assert(game.diveForm === 'double', '氧气门:oxy=6 ≥4 → DIVE(4) 双载形态'); + assert(game.oxy === 2, 'DIVE(4) 双载起播 oxy 6-3-1=2(双载额外 +1,§4.3)'); + + // 8. DIVE(4):滑坡危机拍(landslide 前置 2s + danger 震循环)→ p7 双载提示 + await reachSearch(game); + await g(game, 'tap'); + await advance(() => game.phase === 'COMMS'); + assert(voicePlays(playLog, 'sfx/echo_groan') === 2 && voicePlays(playLog, 'sfx/echo_metal_r') === 1, + 'D4 搜索:echo_groan 渐强 + echo_metal_r(§6.3:接近指挥舱必播金属回波)'); + assert(voicePlays(playLog, 'sfx/landslide') === 1, 'D4 危机拍:landslide 前置起播'); + const dVibs = vibrateLog.filter((v) => JSON.stringify(v.pattern) === DANGER_PATTERN).length; + assert(dVibs >= 3, `D4 danger 震循环(每遍 ≥400ms,实际 ${dVibs} 遍)`); + const iSlide = lastIndexOf(playLog, 'sfx/landslide'); + const iSlideV = lastIndexOf(playLog, 'voice/dv4_slide'); + assert(iSlide < iSlideV, 'landslide 前置 2s 后才接 dv4_slide 语音(R4 危机拍优先)'); + await tapTo(game, () => game.choiceArmed); // gao_a → gao_b → p7 + assert(voicePlays(playLog, 'voice/p7') === 1 && voicePlays(playLog, 'voice/p7_chen') === 0 + && voicePlays(playLog, 'voice/p7_zheng') === 0, '双载形态播 p7(快慢浮)'); + await g(game, 'right'); // p7 右 = 快浮 + await tapTo(game, () => game.choiceArmed); // d4_fast → d4_fast_e → gao_c → p8 + assert(voicePlays(playLog, 'voice/gao_c') === 1 && voicePlays(playLog, 'voice/gao_confess') === 0, + 'hasLog=false → 心结引入播 gao_c(条件差分)'); + await g(game, 'right'); // p8 右 = 不问 + await tapTo(game, () => game.phase === 'SURFACE' && game.diveN === 4); + await advance(() => voicePlays(playLog, 'voice/oxy_crit') === 1); + assert(game.oxy === 2 && voicePlays(playLog, 'voice/oxy_crit') === 1, + 'SURFACE(4):oxy=2 → crit 档跨档播报(§4.5:双载收队 2-lefts)'); + assert(vibrateLog.some((v) => JSON.stringify(v.pattern) === WARN_PATTERN), + 'G2:oxy_crit 播报位附 warning 震'); + + // 9. 签收:g9_in → g9_knot → p9 左(oxy=2≥1 可用)→ d9_bell + bell_toll → ENDING(e1) + await tapTo(game, () => game.promptPlaying); + assert(game.nodeId === 'p9' && voicePlays(playLog, 'voice/g9_in') === 1 + && voicePlays(playLog, 'voice/g9_knot') === 1, '签收链 g9_in → g9_knot → p9'); + await g(game, 'left'); // p9 左(可用)——提示播放中缓存,播完生效 + await advance(() => game.phase === 'ENDING'); + assert(game.flags.bell === true, 'p9 左(oxy≥1):bell=true'); + assert(game.endingId === 'e1', 'saved=5 且 bell → ENDING(e1 海燕归钟)'); + await advance(() => game.awaitingRestart); + for (const id of ['voice/e1_a', 'voice/e1_b', 'voice/ep_lead', 'voice/ep_zhou_no', + 'voice/ep_bai_no', 'voice/ep_chen_ok', 'voice/ep_zheng_ok', 'voice/ep_gao_up', + 'voice/ep_trench_bell']) { + assert(voicePlays(playLog, id) === 1, `结局链播出 ${id}`); + } + assert(voicePlays(playLog, 'sfx/bell_toll') === 1, '船钟 payoff 音 bell_toll(p9 左兑现)'); + const sd2 = saveData(); + assert(sd2 && sd2.phase === 'ENDING' && sd2.endings.e1 === true && sd2.summary === '救援收队·海燕归钟 · 结局1/4', + `ENDING 存档:endings 收集 e1,summary 收队格式(实际: ${sd2 && sd2.summary})`); + assert(game._breathLog.some((e) => e.on === true) && game._breathLog[game._breathLog.length - 1].on === false, + '低氧混音签名:D4(oxy=2 crit)期间 breath_loop 在播、收队即停'); + assert(game._ambDucked === false, 'R3:链歇后 amb 恢复全响(duck 解除)'); + + // 双通道总账(§9 失败即不通过项):全程信息音与语音零重叠 + 声呐账目 + const overlaps = sonarVoiceOverlaps(game); + assert(overlaps.length === 0, `全程 echo_*/pipe_knock/groan 与语音零重叠(${overlaps.length} 处违规)`); + assert(voicePlays(playLog, 'sfx/sonar_ping') === 4, '四潜各恰一枚玩家脉冲(sonar_ping 计数=4)'); + assert(voicePlays(playLog, 'sfx/surf_splash') === 4 && voicePlays(playLog, 'sfx/ascent_chime') === 4, + '每潜上浮出水 sfx 各 4 次'); + assert(voicePlays(playLog, 'voice/d1_surf') === 1 && voicePlays(playLog, 'voice/d2_surf') === 1 + && voicePlays(playLog, 'voice/d3_surf') === 1 && voicePlays(playLog, 'voice/d4_surf') === 1, + '四条上浮结算句各一次(d1..d4_surf)'); + assert(['voice/dry_a', 'voice/dry_b', 'voice/dv4_none', 'voice/gao_final', + 'voice/g9_dry', 'voice/p7_chen', 'voice/chen_last', 'voice/gao_last'] + .every((id) => voicePlays(playLog, id) === 0), '剧本 A 全程零软失败/差分句(成功不教学)'); + + // 资产缺失兼容:未落地走 30s 单句看门狗自动续链;已落地则真实播放 + if (!voiceLanded) { + assert(consoleWarns.some((w) => w.includes('看门狗') && w.includes('自动续链')), + '资产未落地:单句 30s 看门狗自动续链(N1 兜底,不卡死)'); + assert(consoleErrs.some((e) => e.includes('资产缺失')), '资产缺失仅 console.error,不抛异常'); + } else { + assert(game.endingId === 'e1' && game.awaitingRestart, '资产已落地:真实播放推进到 e1 结算态'); + } + + // ENDING 双击重听结局链(清结算态)+ 重播期间首句可跳(G5:F4 锁不随重开复活) + const e1aBefore = voicePlays(playLog, 'voice/e1_a'); + await g(game, 'doubletap'); + assert(game.awaitingRestart === false && game.nodeId === 'e1', '结局链播完双击 → 重听结局链(清结算态)'); + await g(game, 'tap'); // 重播 e1_a 中单击 = 跳本句 + await advance(() => game.currentVoiceId === 'voice/e1_b'); + assert(voicePlays(playLog, 'voice/e1_b') === 2, '重听期间单击跳本句,链继续(复播可即掐)'); + await advance(() => game.awaitingRestart); + assert(voicePlays(playLog, 'voice/e1_a') === e1aBefore + 1, '结局链同 id 序列重播完成,回结算态'); + + // 结算态单击 → 新救援 + await g(game, 'tap'); + assert(game.phase === 'INTRO' && game.runs === 2 && game.oxy === 12 && game.diveN === 0 + && !game.flags.knowsPipe && game.stayer === null && game.saved.gao === 0, + '结算单击 = 新救援:回 INTRO,oxy/saved/stayer/flags 全部归零'); + assert(game.endings.e1 === true, 'endings 结局收集跨局保留'); + assert(voicePlays(playLog, 'voice/sys_new_dive') === 2, 'sys_new_dive 随新救援再播'); + assert(voicePlays(playLog, 'voice/sys_skip') === 1, 'sys_skip 每实例只播一次,重开不重播'); + assert(gestureCount - g0 <= 70, `关键路径手势预算 ≤70(实际 ${gestureCount - g0})`); + teardown(game); +} + +// ================================================================ 四结局矩阵 + 氧气门三形态 +// (§9 边界:B→e2 / C→取消链→e3 / D→单载 p7_chen+chen_last→e4 / +// E→干涸链→e4 / F→单载 p7_zheng+gao_last+p8 跳过→e4;oxy 逐检查点与 §4.5 一致) + +realLog('== 四结局矩阵(剧本 B / C / D / E / F)=='); +{ + // ---- 剧本 B:与 A 同路但 p9 右 → e2 + clearSave(); resetSettings('off'); + { + const { audio, playLog, game } = makeRig(); + await audio.unlock(); + game.start(); await flush(); + await playRun(game, SCRIPT_B); + assert(game.endingId === 'e2' && game.oxy === 2 && game.flags.bell === false, + '剧本 B(p9 右)→ e2 满载的黎明,oxy=2,bell=false'); + assert(voicePlays(playLog, 'voice/e2_a') === 1 && voicePlays(playLog, 'voice/e2_b') === 1, + 'e2 专属结局主句 + 钩子播出'); + assert(voicePlays(playLog, 'voice/ep_trench_deep') === 1 && voicePlays(playLog, 'voice/ep_trench_bell') === 0, + 'bell=false → 尾声沟底播 ep_trench_deep(钟留在沟底)'); + teardown(game); + } + + // ---- 剧本 C:p1左 p2左 p4左 余右 → 终局取消链 → p9 左 → e3 + clearSave(); resetSettings('off'); + { + const { audio, playLog, game } = makeRig(); + await audio.unlock(); + game.start(); await flush(); + // 逐检查点 oxy 验算(§9 边界:D1 后 7 / D2 后 supply 5 / D3 后 2) + await playRun(game, SCRIPT_C, { + stop: () => game.phase === 'SURFACE' && game.diveN === 1, + }); + assert(game.oxy === 7, '剧本 C:D1 后(p1左+p2左)oxy 12-3-1-1=7(ok 档)'); + await playRun(game, SCRIPT_C, { + from: 2, + stop: () => game.phase === 'SURFACE' && game.diveN === 2, + }); + await advance(() => voicePlays(playLog, 'voice/oxy_low') === 1); + assert(game.oxy === 5 && game.supply === true && voicePlays(playLog, 'voice/oxy_low') === 1, + '剧本 C:D2 后 p4左 floor 前 oxy=2 → supply +3=5(low 档播报)'); + const ci = await playRun(game, SCRIPT_C, { + from: 4, + stop: () => game.nodeId === 'c_none', + }); + assert(game.phase === 'SURFACE' && game.nodeId === 'c_none' && game.diveForm === 'cancel', + '剧本 C:SURFACE(3) oxy=2 ≤2 → 氧气门走终局取消链(dv4_none 起播)'); + assert(game.oxy === 2 && voicePlays(playLog, 'voice/oxy_crit') === 1, + '剧本 C:D3 后 oxy=2(crit 档,取消门与 b_rule「气不够一潜必须上浮」一致)'); + await playRun(game, SCRIPT_C, { from: ci }); + assert(voicePlays(playLog, 'voice/dv4_none') === 1 && voicePlays(playLog, 'voice/gao_final') === 1, + '取消链完整播出 dv4_none → gao_final(措辞克制、零描写)'); + assert(voicePlays(playLog, 'voice/dv4_down') === 0 && voicePlays(playLog, 'voice/p7') === 0 + && voicePlays(playLog, 'voice/p8') === 0, '取消路径跳过第四潜(无 p7/p8)'); + assert(game.endingId === 'e3' && game.flags.bell === true, + '剧本 C:p9 左(oxy=2≥1 可用)→ e3 最后一潜(saved=3 × bell)'); + assert(game.saved.zhou === 1 && game.saved.bai === 1 && game.saved.zheng === 1 + && game.saved.chen === 0 && game.saved.gao === 0, '剧本 C saved=3(小舟/老白/小郑)'); + assert(voicePlays(playLog, 'voice/ep_zhou_ok') === 1 && voicePlays(playLog, 'voice/ep_bai_ok') === 1 + && voicePlays(playLog, 'voice/ep_chen_lost') === 1 && voicePlays(playLog, 'voice/ep_gao_lost') === 1, + '剧本 C 尾声:bond_zhou/bai=1 播 ok,陈医官/船长留守未归播 lost(P2 回响点名)'); + teardown(game); + } + + // ---- 剧本 D:p1左 p4左 余右 → 单载 p7_chen → p7 左=带船长 → chen_last → e4 + clearSave(); resetSettings('off'); + { + const { audio, playLog, game } = makeRig(); + await audio.unlock(); + game.start(); await flush(); + await playRun(game, SCRIPT_D, { + stop: () => game.phase === 'SURFACE' && game.diveN === 1, + }); + assert(game.oxy === 8, '剧本 D:D1 后(仅 p1 左)oxy 12-3-1=8(ok 档)'); + await playRun(game, SCRIPT_D, { + from: 2, + stop: () => game.phase === 'SURFACE' && game.diveN === 2, + }); + assert(game.oxy === 6, '剧本 D:D2 后 p4左 oxy 8-3-2=3 → supply +3=6(ok,不重播)'); + await playRun(game, SCRIPT_D, { + from: 4, + stop: () => game.phase === 'SURFACE' && game.diveN === 3, + }); + await advance(() => voicePlays(playLog, 'voice/oxy_low') === 1); + assert(game.oxy === 3 && voicePlays(playLog, 'voice/oxy_low') === 1, + '剧本 D:SURFACE(3) oxy=3(low 档跨档播报)'); + await playRun(game, SCRIPT_D, { + from: 6, + stop: () => game.phase === 'DIVE' && game.diveN === 4 && game.voicePlaying, + }); + assert(game.diveForm === 'single' && game.oxy === 0, + '剧本 D:氧气门 oxy=3 → 单载形态;DIVE(4) oxy 3-3=0'); + await playRun(game, SCRIPT_D, { from: 6 }); + assert(voicePlays(playLog, 'voice/p7_chen') === 1 && voicePlays(playLog, 'voice/p7') === 0 + && voicePlays(playLog, 'voice/p7_zheng') === 0, '单载·留守=陈医官 → 播 p7_chen(按 stayer 选提示)'); + assert(voicePlays(playLog, 'voice/d4_pick_gao') === 1 && voicePlays(playLog, 'voice/chen_last') === 1 + && voicePlays(playLog, 'voice/d4_stay_lost') === 0, 'p7 左=带船长 → d4_pick_gao → chen_last 留别'); + assert(voicePlays(playLog, 'voice/p8') === 1 && voicePlays(playLog, 'voice/gao_c') === 1, + '船长在乘 → p8 可达(gao_c 差分)'); + assert(game.endingId === 'e4' && game.flags.bell === false && game.oxy === 0, + '剧本 D:oxy_end=0 → p9 左封锁 → e4 按规程上浮(saved=4)'); + assert(voicePlays(playLog, 'voice/g9_dry') === 1 && voicePlays(playLog, 'voice/d9_rest') === 1 + && voicePlays(playLog, 'voice/d9_bell') === 0, 'p9 左封锁:g9_dry 播出后自动走右(不摆假选项)'); + assert(game.saved.gao === 1 && game.saved.chen === 0 && game.saved.zheng === 1, + '剧本 D saved=4(小舟/老白/小郑/船长),陈医官留守未归'); + teardown(game); + } + + // ---- 剧本 E:p2左 p3[无knowsPipe]左 p4左 p6左 → mid-dive floor 0 干涸链 → e4 + clearSave(); resetSettings('off'); + { + const { audio, playLog, game } = makeRig(); + await audio.unlock(); + game.start(); await flush(); + const ei = await playRun(game, SCRIPT_E, { + stop: () => game.currentVoiceId === 'voice/dry_a', + }); + assert(game.currentVoiceId === 'voice/dry_a' && game.oxy === 0, + '剧本 E:p6 左 -2 从 2 floor 至 0 → dry_a 当场合播(mid-dive 干涸链)'); + await playRun(game, SCRIPT_E, { from: ei }); + assert(voicePlays(playLog, 'voice/dry_a') === 1 && voicePlays(playLog, 'voice/dry_b') === 1, + '干涸链 dry_a → 上浮结算内 dry_b(归因真话:气花在了人身上)'); + assert(voicePlays(playLog, 'voice/d3_spare_e') === 1 && game.saved.zheng === 1, + '干涸不中断当前救援:小郑照常救上(人已挂上吊索不失去)'); + assert(game.endingId === 'e4' && game.flags.spareGift === true && game.oxy === 0, + '剧本 E:取消链 + p9 左封锁 → e4(saved=3,spareGift=true)'); + assert(voicePlays(playLog, 'voice/dv4_none') === 1, '剧本 E 走终局取消链(gate oxy=0 ≤2)'); + teardown(game); + } + + // ---- 剧本 F:p5左(stayer=zheng)单载右路 → p7_zheng + gao_last + p8 跳过 → e4 + clearSave(); resetSettings('off'); + { + const { audio, playLog, game } = makeRig(); + await audio.unlock(); + game.start(); await flush(); + await playRun(game, SCRIPT_F); + assert(voicePlays(playLog, 'voice/p7_zheng') === 1 && voicePlays(playLog, 'voice/p7') === 0 + && voicePlays(playLog, 'voice/p7_chen') === 0, '剧本 F:单载·留守=小郑 → 播 p7_zheng'); + assert(voicePlays(playLog, 'voice/d4_pick_zheng') === 1 && voicePlays(playLog, 'voice/gao_last') === 1, + 'p7 右=带留守者 → d4_pick_zheng → gao_last 船长留别(p8 跳过,直达上浮)'); + assert(voicePlays(playLog, 'voice/p8') === 0 && voicePlays(playLog, 'voice/gao_c') === 0, + '单载右路少 p8(每局实走 7-9 抉择点验证)'); + assert(game.endingId === 'e4' && game.saved.chen === 1 && game.saved.zheng === 1 + && game.saved.gao === 0, '剧本 F saved=4(陈医官 p5 左救上、小郑单载带回、船长留别)→ e4'); + teardown(game); + } +} + +// ================================================================ 状态播报与无效手势 +// (§2.3 上滑状态链 G6 顺序 + 搜索双击无回波零静默 + onceKey + idle-only F1.4 + G1 去重) + +realLog('== 状态播报与无效手势(guide=full)=='); +{ + // ---- RIG A:搜索等待上滑(prog → oxy → sys_help 顺序;G1 第 2 次纯音频标记) + clearSave(); resetSettings('full'); + { + const { audio, playLog, game } = makeRig(); + const earcons = []; + const origEarcon = game.guide._playEarcon.bind(game.guide); + game.guide._playEarcon = (name) => { earcons.push(name); return origEarcon(name); }; + await audio.unlock(); + game.start(); await flush(); + await reachSearch(game); // D1 搜索 + await g(game, 'up'); + await advance(() => !game.voicePlaying && game.searchArmed); + const iP1 = lastIndexOf(playLog, 'voice/prog_d1'); + const iR1 = lastIndexOf(playLog, 'voice/oxy_full'); + const iH1 = lastIndexOf(playLog, 'voice/sys_help'); + assert(iP1 >= 0 && iP1 < iR1 && iR1 < iH1, + 'G6:搜索上滑状态链顺序 prog_d1 → oxy_full → sys_help(状态在前、帮助垫后)'); + assert(game.searchArmed && game.phase === 'DIVE', '状态播报后留在搜索等待'); + await g(game, 'up'); // 同一等待点第 2 次:纯音频标记 + await advance(() => !game.voicePlaying && game.searchArmed); + assert(voicePlays(playLog, 'voice/sys_help') === 1, + 'G1:同一等待点第 2 次上滑不重播 sys_help 口令(提示永不逐字重复)'); + assert(voicePlays(playLog, 'voice/prog_d1') === 2 && voicePlays(playLog, 'voice/oxy_full') === 2, + 'G1:状态句照播(复述状态=合法帮助,去重只针对口令)'); + assert(vibrateLog.some((v) => JSON.stringify(v.pattern) === BEAT_PATTERN) + && earcons.includes('nudge'), 'G1:第 2 次帮助播 beat 轻震 + nudge earcon(零新增词表)'); + teardown(game); + } + + // ---- RIG B:搜索等待无效手势(左滑/无回波双击)零静默 + onceKey 同局限次 + clearSave(); resetSettings('full'); + { + const { audio, playLog, game } = makeRig(); + const vib0 = vibrateLog.length; + await audio.unlock(); + game.start(); await flush(); + await reachSearch(game); + await g(game, 'left'); + assert(vibrateLog.slice(vib0).some((v) => JSON.stringify(v.pattern) === ERR_PATTERN), + '搜索等待左滑 → error 震(无效手势零静默)'); + assert(voicePlays(playLog, 'voice/gd_invalid') === 1, '搜索等待左滑 → gd_invalid(full 档播出)'); + await g(game, 'doubletap'); // 尚无回波 → 无效(首潜前零静默) + assert(voicePlays(playLog, 'voice/gd_invalid') === 1, + '再次无效手势 gd_invalid 不重播(onceKey 同局限次)'); + const errN = vibrateLog.slice(vib0).filter((v) => JSON.stringify(v.pattern) === ERR_PATTERN).length; + assert(errN === 2, `无效手势震反馈每次都有(零静默,实际 ${errN} 次)`); + teardown(game); + } + + // ---- RIG C:idle-only(F1.4 对白在播时纠错跳过且不占 onceKey) + clearSave(); resetSettings('full'); + { + const { audio, playLog, game } = makeRig(); + const guideEvents = []; + game.guide.setEventHook((ev) => guideEvents.push(ev)); + const vib0 = vibrateLog.length; + await audio.unlock(); + game.start(); await flush(); + await reachSearch(game); + await g(game, 'tap'); // 发脉冲 → 回波链 → COMMS z1_a 播放中 + await advance(() => game.currentVoiceId === 'voice/z1_a'); + const origIvp = audio.isVoicePlaying.bind(audio); + audio.isVoicePlaying = () => true; // 白盒:资产缺失时引擎无真语音,stub 出「在播」语义 + await g(game, 'left'); + audio.isVoicePlaying = origIvp; + assert(guideEvents.some((e) => e.type === 'error' && e.skipped === true), + '对白在播时无效手势 → 纠错句 idle-only 跳过(事件带 skipped)'); + assert(voicePlays(playLog, 'voice/gd_invalid') === 0 && game.voicePlaying, + '在播对白不被纠错句顶掉,gd_invalid 未播出'); + await advance(() => !game.voicePlaying); // z1_a 链末 + await g(game, 'left'); + assert(voicePlays(playLog, 'voice/gd_invalid') === 1, + '语音空闲后再次无效 → gd_invalid 播出(idle 跳过不占 onceKey)'); + assert(vibrateLog.slice(vib0).filter((v) => JSON.stringify(v.pattern) === ERR_PATTERN).length === 2, + '两次无效手势 error 震都发出(零静默)'); + teardown(game); + } + + // ---- RIG D:COMMS 上滑状态链 + 局面复述;SURFACE 上滑自动链继续 + clearSave(); resetSettings('full'); + { + const { audio, playLog, game } = makeRig(); + await audio.unlock(); + game.start(); await flush(); + await reachSearch(game); + await g(game, 'tap'); + await advance(() => game.phase === 'COMMS'); + await advance(() => !game.voicePlaying); // z1_a 链末等待 + const z1aBefore = voicePlays(playLog, 'voice/z1_a'); + await g(game, 'up'); + await advance(() => !game.voicePlaying); + const iP = lastIndexOf(playLog, 'voice/prog_d1'); + const iR = lastIndexOf(playLog, 'voice/oxy_full'); + const iH = lastIndexOf(playLog, 'voice/sys_help'); + assert(iP >= 0 && iP < iR && iR < iH, 'G6:COMMS 上滑状态链 prog_d1 → oxy_full → sys_help'); + await advance(() => voicePlays(playLog, 'voice/z1_a') === z1aBefore + 1); + assert(voicePlays(playLog, 'voice/z1_a') === z1aBefore + 1, '状态播完重播当前节点(局面复述)'); + teardown(game); + } +} + +// ================================================================ 菜单交互 +// (§9 边界:菜单返回不丢节点、循环顺序、帮助、保存并回大厅;queue 不顶句) + +realLog('== 菜单交互(guide=off,p1 期间呼出)=='); +{ + clearSave(); resetSettings('off'); + const { audio, playLog, game } = makeRig(); + await audio.unlock(); + game.start(); await flush(); + await reachSearch(game); + await g(game, 'tap'); + await tapTo(game, () => game.promptPlaying); // p1 提示播放中呼出菜单 + + await g(game, 'down'); + assert(game.phase === 'MENU', '下滑呼出模态菜单'); + await advance(() => !game.voicePlaying); // sys_menu + 首项报名 + await g(game, 'tap'); + await advance(() => !game.voicePlaying); + await g(game, 'tap'); + await advance(() => !game.voicePlaying); + await g(game, 'tap'); + await advance(() => !game.voicePlaying); + const cnt = (id) => voicePlays(playLog, id); + const iMenu = playLog.findIndex((e) => e.id === 'voice/sys_menu'); + const iRes = playLog.findIndex((e) => e.id === 'voice/sys_menu_resume'); + const iRst = playLog.findIndex((e) => e.id === 'voice/sys_menu_restart'); + const iLob = playLog.findIndex((e) => e.id === 'voice/sys_menu_lobby'); + assert(iMenu >= 0 && iMenu < iRes && iRes < iRst && iRst < iLob, + '菜单项 queue 依序播出(sys_menu → 回到水下 → 重新开始这次救援 → 保存并回大厅),在播句不被截断'); + assert(cnt('voice/sys_menu_restart') === 1 && cnt('voice/sys_menu_lobby') === 1 + && cnt('voice/sys_menu_resume') === 2, + '单击循环顺序 = [回到水下, 重新开始这次救援, 保存并回大厅],末项单击回绕首项再报名'); + + await g(game, 'up'); + await advance(() => !game.voicePlaying); + assert(cnt('voice/sys_help') >= 1 && cnt('voice/sys_settings') >= 1, + '菜单内上滑播 sys_help 帮助 + sys_settings 设置提示'); + + await g(game, 'doubletap'); // 当前项 = 回到水下 + assert(game.phase === 'COMMS' && game.nodeId === 'p1', + '双击确认「回到水下」→ phase/node 不变'); + await advance(() => game.choiceArmed); + assert(cnt('voice/p1') >= 2, '菜单关闭后重播当前节点(局面复述,含抉择提示)'); + + await g(game, 'down'); // 再开 + await advance(() => !game.voicePlaying); + await g(game, 'down'); // 下滑关闭 + assert(game.phase === 'COMMS' && game.nodeId === 'p1', '下滑关闭菜单 → 回原状态'); + await advance(() => game.choiceArmed); + + // 保存并回大厅(第三项) + await g(game, 'down'); + await advance(() => !game.voicePlaying); + await g(game, 'tap'); + await advance(() => !game.voicePlaying); + await g(game, 'tap'); // 第二次切换 = 保存并回大厅 + await advance(() => !game.voicePlaying); + const lobBefore = voicePlays(playLog, 'voice/sys_menu_lobby'); + await g(game, 'doubletap'); + assert(cnt('voice/sys_menu_lobby') === lobBefore + 1, '确认回大厅 → sys_menu_lobby 播出'); + assert(game.leaving === true, 'leaving 置位(跳转大厅前先播确认句)'); + const sd = saveData(); + assert(sd && sd.schema === 1 && typeof sd.summary === 'string' && sd.summary.length > 0, + '回大厅确认后 localStorage 有最新 data(含 summary)'); + teardown(game); +} + +// ================================================================ 中断复述 +// (§9 边界:杀进程重进(node=bai_b)→ sys_continue_ask → 单击恢复 +// (恢复首句不可跳,第二句起可跳);右滑=新救援(runs 口径同结算重开)) + +realLog('== 中断复述(guide=off,预置存档 node=bai_b)=='); +{ + clearSave(); resetSettings('off'); + storageData.set(SAVE_KEY, JSON.stringify({ + schema: 1, + updatedAt: new Date().toISOString(), + summary: '第2潜·机舱 · 氧气5格 · 结局0/4', + phase: 'COMMS', + node: 'bai_b', + dive: 2, + oxy: 5, + supply: true, + saved: { zhou: 1, bai: 0, chen: 0, zheng: 0, gao: 0 }, + stayer: null, + flags: { knowsPipe: false, hasLog: false, valve: false, spareGift: false, gaoPeace: false, bell: false }, + bond: { zhou: 0, bai: 0 }, + form: null, + dryPending: false, + endings: {}, + runs: 3, + })); + { + const { audio, playLog, game } = makeRig(); + const vib0 = vibrateLog.length; + await audio.unlock(); + game.start(); await flush(); + assert(game.phase === 'ASK' && game.currentVoiceId === 'voice/sys_continue_ask', + '带存档重进 → sys_continue_ask 询问'); + + await g(game, 'tap'); + assert(game.phase === 'COMMS' && game.nodeId === 'bai_b' && game.diveN === 2, + '单击继续 → 恢复 phase+node+dive'); + assert(game.oxy === 5 && game.supply === true && game.runs === 3, + 'oxy / supply / runs 全部还原'); + assert(game.currentVoiceId === 'voice/bai_b', '恢复重播当前节点链(局面复述,L5)'); + await g(game, 'tap'); // F4 锁:恢复链首句播完前单击不跳 + assert(game.currentVoiceId === 'voice/bai_b', + 'F4:恢复后首句播完前单击不跳句(先听完再武装跳句)'); + assert(vibrateLog.slice(vib0).some((v) => JSON.stringify(v.pattern) === BEAT_PATTERN), + 'G4:F4 锁内空点单击播 beat 轻震'); + await advance(() => !game.voicePlaying); // bai_b 播完 → 链末等待 + await g(game, 'tap'); // 链末继续 → p3 提示句 + await advance(() => game.choiceArmed); + assert(game.nodeId === 'p3' && game.promptPlaying === false, '恢复局内推进正常:bai_b 链末 → p3'); + + // 上滑状态链(G6):prog_d2 → oxy_low → sys_help(oxy=5 → low 档整句) + await g(game, 'up'); + await advance(() => !game.voicePlaying); + const iP = lastIndexOf(playLog, 'voice/prog_d2'); + const iR = lastIndexOf(playLog, 'voice/oxy_low'); + const iH = lastIndexOf(playLog, 'voice/sys_help'); + assert(iP >= 0 && iP < iR && iR < iH, '恢复局上滑状态链顺序(G6):prog_d2 → oxy_low → sys_help'); + await advance(() => game.choiceArmed); // 状态播完局面复述回 p3 + teardown(game); + } + + // ASK 右滑 = 新救援(runs 口径同结算重开:+1) + clearSave(); resetSettings('off'); + storageData.set(SAVE_KEY, JSON.stringify({ + schema: 1, updatedAt: new Date().toISOString(), summary: 'x', + phase: 'COMMS', node: 'bai_b', dive: 2, oxy: 5, supply: true, + saved: { zhou: 1, bai: 0, chen: 0, zheng: 0, gao: 0 }, stayer: null, + flags: { knowsPipe: false, hasLog: false, valve: false, spareGift: false, gaoPeace: false, bell: false }, + bond: { zhou: 0, bai: 0 }, form: null, dryPending: false, endings: { e1: true }, runs: 3, + })); + { + const { audio, game } = makeRig(); + await audio.unlock(); + game.start(); await flush(); + await advance(() => game.phase === 'ASK' && !game.voicePlaying); + await g(game, 'right'); + assert(game.phase === 'INTRO' && game.oxy === 12 && game.runs === 4 + && game.saveStash === null && game.endings.e1 === true, + 'ASK 右滑 = 新救援:跨潜变量重置、runs 3+1(与结算重开同口径)、结局收集保留'); + teardown(game); + } +} + +// ================================================================ 看门狗双轨 +// (§7.2/§9:搜索 10s/25s 两级不同句 + 双轨去重;乱按 96s 抢救(无进展轨 +// 60/120s);COMMS 链末 25s/50s;p* 25s/50s 复述两级;ENDING 25s/50s; +// guide=off 全程零定时器零向导事件(主路径块已断言 idle)) + +realLog('== 看门狗双轨(guide=full)=='); +{ + // ---- A:搜索等待两级不同句 + 双轨同级交叉登记去重(纯干等不轰炸) + clearSave(); resetSettings('full'); + { + const { audio, playLog, game } = makeRig(); + await audio.unlock(); + game.start(); await flush(); + await reachSearch(game); + const tS = now; + await advance(() => voicePlays(playLog, 'voice/gd_ping1') === 1); + assert(now - tS === 10000, `搜索等待 10s → gd_ping1(实际 ${now - tS}ms)`); + await advance(() => voicePlays(playLog, 'voice/gd_ping2') === 1); + assert(now - tS === 25000, `25s → gd_ping2(与第一级不同句,实际 ${now - tS}ms)`); + await advance(() => now >= tS + 80000); + assert(voicePlays(playLog, 'voice/gd_ping1') === 1 && voicePlays(playLog, 'voice/gd_ping2') === 1, + '两级提示各播一次(绝不逐字重播)'); + assert(voicePlays(playLog, 'voice/gd_prog1') === 0 && voicePlays(playLog, 'voice/gd_prog2') === 0, + '双轨去重:无操作轨已播 ping1/ping2 → 无进展轨 60s/120s 同级跳过(不轰炸)'); + teardown(game); + } + + // ---- B:乱按 96s 抢救——每 2s 一个无效滑动:无操作轨零播出(被手势复位), + // 无进展轨 60s→gd_prog1、120s→gd_prog2(乱按饿不死) + clearSave(); resetSettings('full'); + { + const { audio, playLog, game } = makeRig(); + const vib0 = vibrateLog.length; + await audio.unlock(); + game.start(); await flush(); + await reachSearch(game); + const t0 = now; + for (let k = 1; k <= 69; k++) { // 每 1.9s 注入一个无效左滑,覆盖 130s(96s+ 升级窗) + const beat = t0 + k * 1900; + setTimeout(() => {}, Math.max(0, beat - now)); // 节拍锚:虚拟时钟在 beat 处可停 + await advance(() => now >= beat); + await g(game, 'left'); + } + assert(voicePlays(playLog, 'voice/gd_ping1') === 0 && voicePlays(playLog, 'voice/gd_ping2') === 0 + && voicePlays(playLog, 'voice/gd_continue') === 0 && voicePlays(playLog, 'voice/gd_again') === 0, + '乱按 96s+:无操作轨零播出(任何手势复位,两级永不触发)'); + assert(voicePlays(playLog, 'voice/gd_prog1') === 1, + `无进展轨 60s → gd_prog1(乱按抢救,实际 ${now}ms 起点 ${t0}ms)`); + assert(voicePlays(playLog, 'voice/gd_prog2') === 1, '无进展轨 120s → gd_prog2(用尽即停)'); + const errN = vibrateLog.slice(vib0).filter((v) => JSON.stringify(v.pattern) === ERR_PATTERN).length; + assert(errN >= 60, `每次乱按都有 error 震反馈(零静默,实际 ${errN} 次)`); + assert(game.searchArmed && game.oxy === 9, '乱按期间剧情零推进(无效手势不作进展)'); + teardown(game); + } + + // ---- C:COMMS 链末 25s/50s gd_continue/gd_again + 双轨去重 + clearSave(); resetSettings('full'); + { + const { audio, playLog, game } = makeRig(); + await audio.unlock(); + game.start(); await flush(); + await reachSearch(game); + await g(game, 'tap'); + await advance(() => game.phase === 'COMMS'); + await advance(() => !game.voicePlaying); // z1_a 链末等待 + const tW = now; + await advance(() => voicePlays(playLog, 'voice/gd_continue') === 1); + assert(now - tW === 25000, `COMMS 链末 25s 静默 → gd_continue(实际 ${now - tW}ms)`); + await advance(() => voicePlays(playLog, 'voice/gd_again') === 1); + assert(now - tW === 50000, `50s → gd_again(第二级新句,实际 ${now - tW}ms)`); + await advance(() => now >= tW + 80000); + assert(voicePlays(playLog, 'voice/gd_again') === 1 && voicePlays(playLog, 'voice/gd_prog1') === 0, + '两级用尽彻底静默;prog1 同级被 gd_continue 交叉登记跳过'); + // F1:链末等待中发无效手势,看门狗被 disarm 拆掉后必须重挂(阈值内触发) + await g(game, 'tap'); // 手势复位 + 继续 → z1_b 链末 + await advance(() => !game.voicePlaying); + const tW2 = now; + await g(game, 'left'); // 无效手势(F1:拆看门狗不补挂 = 34.2s 静默死局的根因) + assert(voicePlays(playLog, 'voice/gd_invalid') === 1, 'F1 前置:链末无效手势照常零静默(gd_invalid)'); + const gc0 = voicePlays(playLog, 'voice/gd_continue'); + await advance(() => voicePlays(playLog, 'voice/gd_continue') === gc0 + 1); + assert(now - tW2 === 25000, `F1:链末等待中发无效手势,看门狗仍在阈值内触发(25s 整,实际 ${now - tW2}ms)`); + teardown(game); + } + + // ---- D:p* 播完 25s/50s 重播提示句两级后静默;ENDING 25s/50s;签名总账 + clearSave(); resetSettings('full'); + { + const { audio, playLog, game } = makeRig(); + const guideEvents = []; + game.guide.setEventHook((ev) => guideEvents.push(ev)); + await audio.unlock(); + game.start(); await flush(); + await advance(() => game.currentVoiceId === 'voice/gd_brief'); + assert(voicePlays(playLog, 'voice/gd_brief') === 1, 'full 档开场链最前插 gd_brief(F8 先例)'); + assert(guideEvents.some((e) => e.type === 'state' && e.name === 'explore'), 'INTRO 进入播 explore 签名'); + await reachSearch(game); + assert(guideEvents.filter((e) => e.type === 'turn').length === 1, '搜索武装 turn 事件(轮到你发脉冲)'); + await g(game, 'tap'); + await advance(() => game.phase === 'COMMS'); + await advance(() => !game.voicePlaying); + await g(game, 'tap'); // → z1_b + await advance(() => !game.voicePlaying); + await g(game, 'tap'); // → z1_knock + await advance(() => !game.voicePlaying); + await g(game, 'tap'); // → p1 提示句 + await advance(() => game.choiceArmed); + const tP = now; + await advance(() => voicePlays(playLog, 'voice/p1') === 2); + assert(now - tP === 25000, `p* 播完 25s 无操作 → 重播当前 p* 提示句(第一级,实际 ${now - tP}ms)`); + await advance(() => voicePlays(playLog, 'voice/p1') === 3); + assert(now - tP === 50000, `50s → 再复述一次(第二级,实际 ${now - tP}ms)`); + await advance(() => now >= tP + 70000); + assert(voicePlays(playLog, 'voice/p1') === 3, 'p* 复述两级后彻底静默'); + await g(game, 'left'); // 抉择后正常生效 + assert(game.flags.knowsPipe === true && game.nodeId === 'd1_listen', '复述期间剧情不推进,玩家抉择后正常生效'); + + // 打到 ENDING(playRun 内含搜索脉冲/链末推进;p1 已消耗 left,余 p2..p9 = 8 抉择) + await playRun(game, SCRIPT_A.slice(1)); + await advance(() => game.phase === 'ENDING' && game.awaitingRestart); + const tE = now; + const gc0 = voicePlays(playLog, 'voice/gd_continue'); + const ga0 = voicePlays(playLog, 'voice/gd_again'); + await advance(() => voicePlays(playLog, 'voice/gd_continue') === gc0 + 1); + assert(now - tE === 25000, `ENDING 结算 25s 静默 → gd_continue(实际 ${now - tE}ms)`); + await advance(() => voicePlays(playLog, 'voice/gd_again') === ga0 + 1); + assert(now - tE === 50000, `50s → gd_again(第二级新句,实际 ${now - tE}ms)`); + await advance(() => now >= tE + 70000); + assert(voicePlays(playLog, 'voice/gd_again') === ga0 + 1, 'ENDING 两级用尽彻底静默'); + await g(game, 'tap'); + assert(game.phase === 'INTRO' && game.runs === 2, '结算单击重开新救援'); + + // 向导签名总账(§7.1):choice ×9(p1..p9 各一次)/ settle / danger ×2 / turn ×4 + const choiceEvents = guideEvents.filter((e) => e.type === 'state' && e.name === 'choice').length; + assert(choiceEvents === 9, `每个 p* 起播发一次 choice 签名(实际 ${choiceEvents})`); + assert(guideEvents.filter((e) => e.type === 'state' && e.name === 'settle').length >= 5, + 'SURFACE×4 + ENDING 进入 settle 签名'); + assert(guideEvents.filter((e) => e.type === 'state' && e.name === 'danger').length === 2, + 'D2/D4 危机拍各一次 danger 签名(全游戏仅两处)'); + assert(guideEvents.filter((e) => e.type === 'turn').length === 4, '四潜搜索武装各一次 turn 签名'); + teardown(game); + } +} + +// ================================================================ 双通道仲裁 +// (sonarGate 空闲门排队补发 / 搜索双击重听回波 / R3 duck 账目) + +realLog('== 双通道仲裁(sonarGate 空闲门 / 回波重听 / R3 duck)=='); +{ + clearSave(); resetSettings('off'); + const { audio, playLog, game } = makeRig(); + await audio.unlock(); + game.start(); await flush(); + await reachSearch(game); + await g(game, 'tap'); + await advance(() => game.currentVoiceId === 'voice/z1_a'); + + // sonarGate:语音在播时信息音挂队、不顶不叠(stub 引擎在播语义,与资产状态无关) + const metal0 = voicePlays(playLog, 'sfx/echo_metal_l'); + const origIvp2 = audio.isVoicePlaying.bind(audio); + audio.isVoicePlaying = () => true; + game._sonar('sfx/echo_metal_l'); // 白盒:在播对白期间请求一枚信息回波 + audio.isVoicePlaying = origIvp2; + await flush(); + assert(voicePlays(playLog, 'sfx/echo_metal_l') === metal0, + 'R1:语音在播时信息回波不立即播出(sonarGate 挂队)'); + await advance(() => !game.voicePlaying); // 链末:voice idle → 补发 + assert(voicePlays(playLog, 'sfx/echo_metal_l') === metal0 + 1, + 'sonarGate:句间空闲补发排队回波(晚于在播句结束,零重叠)'); + assert(game._ambDucked === false, 'R3:链歇后 amb duck 解除'); + teardown(game); + + // 搜索双击=重听上一条回波+其解读句(D2 搜索时重听 D1 的金属回波) + clearSave(); resetSettings('off'); + { + const { audio, playLog, game } = makeRig(); + await audio.unlock(); + game.start(); await flush(); + await playRun(game, SCRIPT_A, { + stop: () => game.searchArmed && game.diveN === 2, + }); + assert(game.searchArmed && game.diveN === 2, '推进到 D2 搜索等待(已有 D1 回波记录)'); + const m0 = voicePlays(playLog, 'sfx/echo_metal_l'); + const v0 = voicePlays(playLog, 'voice/dv1_metal'); + await g(game, 'doubletap'); + await reachSearch(game); + assert(voicePlays(playLog, 'sfx/echo_metal_l') === m0 + 1 + && voicePlays(playLog, 'voice/dv1_metal') === v0 + 1, + 'R5:搜索双击重听上一条回波+其解读句(同序重播:echo_metal_l → dv1_metal)'); + assert(game.searchArmed && game.phase === 'DIVE', '重听后回到搜索等待(turn+看门狗重挂)'); + teardown(game); + } +} + +// ================================================================ 结算死窗(H1 内化) +// (终局取消链 / 干涸链 / e* 结局链开播 800ms 内注入在途手势 → 被吞、链完整有序) + +realLog('== 结算死窗(幽灵手势不截结算链)=='); +{ + // ---- 取消链死窗(剧本 C) + clearSave(); resetSettings('off'); + { + const { audio, playLog, game } = makeRig(); + await audio.unlock(); + game.start(); await flush(); + const ci = await playRun(game, SCRIPT_C, { + stop: () => game.nodeId === 'c_none' && game.voicePlaying, + }); + assert(game.currentVoiceId === 'voice/dv4_none', '前置:取消链首句 dv4_none 在播'); + await g(game, 'down'); // 幽灵手势(800ms 死窗内):不得开菜单截链 + assert(game.phase === 'SURFACE' && game.nodeId === 'c_none', + 'H1:取消链开播 800ms 内的下滑被吞(菜单不开、链不被截)'); + await g(game, 'tap'); + assert(game.currentVoiceId === 'voice/dv4_none' || voicePlays(playLog, 'voice/dv4_none') === 1, + 'H1:窗内 tap 不跳取消链首句'); + await playRun(game, SCRIPT_C, { from: ci }); + const iNone = playLog.findIndex((e) => e.id === 'voice/dv4_none'); + const iFinal = playLog.findIndex((e) => e.id === 'voice/gao_final'); + const iG9 = playLog.findIndex((e) => e.id === 'voice/g9_in'); + assert(iNone >= 0 && iNone < iFinal && iFinal < iG9, '取消链完整有序:dv4_none → gao_final → 签收'); + assert(game.endingId === 'e3', '死窗后照常走到 e3'); + teardown(game); + } + + // ---- 干涸链死窗(剧本 E) + clearSave(); resetSettings('off'); + { + const { audio, playLog, game } = makeRig(); + await audio.unlock(); + game.start(); await flush(); + const ei = await playRun(game, SCRIPT_E, { + stop: () => game.currentVoiceId === 'voice/dry_a', + }); + await g(game, 'down'); + assert(game.phase !== 'MENU', 'H1:干涸链 dry_a 开播 800ms 内的下滑被吞'); + await playRun(game, SCRIPT_E, { from: ei }); + assert(voicePlays(playLog, 'voice/dry_a') === 1 && game.endingId === 'e4', + '干涸链完整播出(dry_a 未被截),照常到 e4'); + teardown(game); + } + + // ---- 结局链死窗(剧本 B)+ 死窗后手势照常 + clearSave(); resetSettings('off'); + { + const { audio, playLog, game } = makeRig(); + await audio.unlock(); + game.start(); await flush(); + await playRun(game, SCRIPT_B, { + stop: () => game.phase === 'ENDING' && game.voicePlaying, + }); + assert(game.currentVoiceId === 'voice/e2_a', '前置:结局链首句 e2_a 在播'); + await g(game, 'down'); + assert(game.phase === 'ENDING', 'H1:结局链开播 800ms 内的下滑被吞(菜单不开)'); + await g(game, 'tap'); + assert(game.currentVoiceId === 'voice/e2_a' || voicePlays(playLog, 'voice/e2_a') === 1, + 'H1:窗内 tap 不跳结局链首句(结算出口手势死窗)'); + await advance(() => now >= game._graceUntil + 50); // 死窗过期 + await g(game, 'down'); + assert(game.phase === 'MENU', '死窗过期后下滑照常开菜单'); + await g(game, 'down'); // 关闭 → 重播结局链(局面复述) + await advance(() => game.awaitingRestart); + assert(game.endingId === 'e2' && voicePlays(playLog, 'voice/e2_a') >= 1, + '死窗后手势照常:菜单往返结局链完整、结算出口正常'); + teardown(game); + } +} + +// ================================================================ 键盘变体路由 +// (settings.input='key' → 引擎候选链前置 voice//key/ 探测;'touch' 不探测) + +realLog('== 键盘变体路由(settings.input)=='); +{ + // key:操作提示句走键盘措辞变体资产位(引擎自动路由,游戏代码用普通 id) + clearSave(); resetSettings('full'); + settings.set({ input: 'key' }); + { + const { audio, playLog, game } = makeRig(); + await audio.unlock(); + game.start(); await flush(); + await advance(() => game.currentVoiceId === 'voice/gd_brief'); + assert(urlHits('audio/voice/female/key/gd_brief') >= 1, + 'input=key:gd_brief 先探测键盘变体位 voice/female/key/(游戏代码用普通 id,引擎路由)'); + assert(urlHits('audio/voice/female/gd_brief') >= 1 || urlHits('audio/voice/female/key/gd_brief') >= 1, + 'input=key:变体缺失自然回退主资产(成对生成由资产侧保证)'); + teardown(game); + } + + // touch:零 key/ 探测 + clearSave(); resetSettings('full'); + settings.set({ input: 'touch' }); + { + const keyHits0 = urlHits('audio/voice/female/key/'); + const { audio, playLog, game } = makeRig(); + await audio.unlock(); + game.start(); await flush(); + await advance(() => game.currentVoiceId === 'voice/gd_brief'); + assert(urlHits('audio/voice/female/key/') === keyHits0, + 'input=touch:不探测键盘变体位(触屏措辞主资产直出)'); + assert(voicePlays(playLog, 'voice/gd_brief') === 1, 'touch 会话照常播出 gd_brief(主资产)'); + teardown(game); + } +} + +// ================================================================ 资产缺失专项 +// (屏蔽任意 voice 资产 → 30s 看门狗自动续链到结局,不卡死;NPC 滤波资产缺失回退原声可播) + +realLog('== 资产缺失专项(guide=off,屏蔽 bai_b)=='); +{ + clearSave(); resetSettings('off'); + blockedAssets.add('voice/female/bai_b'); + blockedAssets.add('voice/bai_b'); // NPC 平铺回退位一并屏蔽(最坏情况) + const { audio, playLog, game } = makeRig(); + await audio.unlock(); + game.start(); await flush(); + await playRun(game, SCRIPT_A); + assert(game.endingId === 'e1' && game.awaitingRestart === true, + '屏蔽任意 voice 资产仍打到 e1 结局,不卡死(30s 单句看门狗续链)'); + assert(voicePlays(playLog, 'voice/gd_ping1') === 0 && voicePlays(playLog, 'voice/gd_continue') === 0 + && voicePlays(playLog, 'voice/gd_prog1') === 0, + 'guide=off 全程零向导语音(gd_*)'); + blockedAssets.delete('voice/female/bai_b'); + blockedAssets.delete('voice/bai_b'); + teardown(game); +} + +// ================================================================ 静态审计 +// (数字规范:分档整句字面量、无运行时 id 拼接;summary 格式) + +realLog('== 静态审计 =='); +{ + const src = fs.readFileSync(path.join(GAME_DIR, 'game.js'), 'utf8'); + for (const id of ['voice/prog_d1', 'voice/prog_d2', 'voice/prog_d3', 'voice/prog_d4', + 'voice/oxy_full', 'voice/oxy_ok', 'voice/oxy_low', 'voice/oxy_crit', 'voice/oxy_gain', + 'voice/dry_a', 'voice/dry_b', 'voice/gd_ping1', 'voice/gd_ping2', + 'voice/gd_prog1', 'voice/gd_prog2', 'voice/gd_continue', 'voice/gd_again', + 'voice/p7', 'voice/p7_chen', 'voice/p7_zheng', 'voice/g9_dry']) { + assert(src.includes(`'${id}'`), `分档/差分整句字面量存在:${id}`); + } + assert(!/voice\/\$\{/.test(src), '无模板字符串拼接 voice id'); + assert(!/['"]voice\/[A-Za-z0-9_]+['"]\s*\+/.test(src), '无字符串相加拼接 voice id'); + assert(!/\+\s*['"]voice\//.test(src), '无前置相加拼接 voice id'); + + // 资产 id 盘点:game.js 提取到的 id 全部在 17-abyss-voice-ids 台词表词形内 + const ids = [...new Set([...src.matchAll(/'((?:voice|sfx|bgm)\/[A-Za-z0-9_.-]+)'/g)].map((m) => m[1]))]; + assert(ids.length >= 60, `game.js 提取到 ${ids.length} 个资产 id(约 70 节点规模)`); + assert(ids.every((id) => /^((voice\/(p[1-9](_chen|_zheng)?|[a-z0-9_]+))|sfx\/[a-z0-9_]+|bgm\/[a-z]+)$/.test(id)), + '资产 id 全部为字面量小写词形(无拼接碎片)'); + + // 存档 summary 人话格式(DIVE 进入时机) + clearSave(); resetSettings('off'); + const { audio, game } = makeRig(); + await audio.unlock(); + game.start(); await flush(); + await reachSearch(game); // D1 搜索(DIVE 存档已落) + const sd = saveData(); + assert(sd && sd.schema === 1, '存档 schema=1'); + assert(sd && sd.summary === '第1潜·找船 · 氧气9格 · 结局0/4', + `summary 格式「第N潜·<舱段> · 氧气K格 · 结局X/4」(实际: ${sd && sd.summary})`); + assert(sd && Number.isFinite(Date.parse(sd.updatedAt)), 'updatedAt 为 ISO 时间戳'); + teardown(game); +} + +// ================================================================ 汇总 + +realLog(`\n通过 ${passCount} 条断言,失败 ${failures.length} 条`); +if (failures.length > 0) { + realLog('失败清单:'); + for (const f of failures) realLog(` - ${f}`); + process.exit(1); +} +realLog('全部断言通过。'); diff --git a/web/detective/audio/bgm/board.m4a b/web/detective/audio/bgm/board.m4a new file mode 100644 index 00000000..2e460b6c Binary files /dev/null and b/web/detective/audio/bgm/board.m4a differ diff --git a/web/detective/audio/bgm/board.ogg b/web/detective/audio/bgm/board.ogg new file mode 100644 index 00000000..b606c495 Binary files /dev/null and b/web/detective/audio/bgm/board.ogg differ diff --git a/web/detective/audio/bgm/menu.m4a b/web/detective/audio/bgm/menu.m4a new file mode 100644 index 00000000..685bf27f Binary files /dev/null and b/web/detective/audio/bgm/menu.m4a differ diff --git a/web/detective/audio/bgm/menu.ogg b/web/detective/audio/bgm/menu.ogg new file mode 100644 index 00000000..8ca9b93a Binary files /dev/null and b/web/detective/audio/bgm/menu.ogg differ diff --git a/web/detective/audio/bgm/verdict.m4a b/web/detective/audio/bgm/verdict.m4a new file mode 100644 index 00000000..ade829ec Binary files /dev/null and b/web/detective/audio/bgm/verdict.m4a differ diff --git a/web/detective/audio/bgm/verdict.ogg b/web/detective/audio/bgm/verdict.ogg new file mode 100644 index 00000000..e6f0b386 Binary files /dev/null and b/web/detective/audio/bgm/verdict.ogg differ diff --git a/web/detective/audio/sfx/contra_hit.m4a b/web/detective/audio/sfx/contra_hit.m4a new file mode 100644 index 00000000..13c15839 Binary files /dev/null and b/web/detective/audio/sfx/contra_hit.m4a differ diff --git a/web/detective/audio/sfx/contra_hit.ogg b/web/detective/audio/sfx/contra_hit.ogg new file mode 100644 index 00000000..540d9beb Binary files /dev/null and b/web/detective/audio/sfx/contra_hit.ogg differ diff --git a/web/detective/audio/sfx/contra_miss.m4a b/web/detective/audio/sfx/contra_miss.m4a new file mode 100644 index 00000000..ae469a53 Binary files /dev/null and b/web/detective/audio/sfx/contra_miss.m4a differ diff --git a/web/detective/audio/sfx/contra_miss.ogg b/web/detective/audio/sfx/contra_miss.ogg new file mode 100644 index 00000000..ff81ba6a Binary files /dev/null and b/web/detective/audio/sfx/contra_miss.ogg differ diff --git a/web/detective/audio/sfx/gavel.m4a b/web/detective/audio/sfx/gavel.m4a new file mode 100644 index 00000000..6ac2e987 Binary files /dev/null and b/web/detective/audio/sfx/gavel.m4a differ diff --git a/web/detective/audio/sfx/gavel.ogg b/web/detective/audio/sfx/gavel.ogg new file mode 100644 index 00000000..8c1ed6b7 Binary files /dev/null and b/web/detective/audio/sfx/gavel.ogg differ diff --git a/web/detective/audio/sfx/paper_turn.m4a b/web/detective/audio/sfx/paper_turn.m4a new file mode 100644 index 00000000..b16c40c6 Binary files /dev/null and b/web/detective/audio/sfx/paper_turn.m4a differ diff --git a/web/detective/audio/sfx/paper_turn.ogg b/web/detective/audio/sfx/paper_turn.ogg new file mode 100644 index 00000000..9d66787f Binary files /dev/null and b/web/detective/audio/sfx/paper_turn.ogg differ diff --git a/web/detective/audio/sfx/rain_swell.m4a b/web/detective/audio/sfx/rain_swell.m4a new file mode 100644 index 00000000..53c12ba6 Binary files /dev/null and b/web/detective/audio/sfx/rain_swell.m4a differ diff --git a/web/detective/audio/sfx/rain_swell.ogg b/web/detective/audio/sfx/rain_swell.ogg new file mode 100644 index 00000000..e6824874 Binary files /dev/null and b/web/detective/audio/sfx/rain_swell.ogg differ diff --git a/web/detective/audio/sfx/sig_fore.m4a b/web/detective/audio/sfx/sig_fore.m4a new file mode 100644 index 00000000..85ed3760 Binary files /dev/null and b/web/detective/audio/sfx/sig_fore.m4a differ diff --git a/web/detective/audio/sfx/sig_fore.ogg b/web/detective/audio/sfx/sig_fore.ogg new file mode 100644 index 00000000..c013cd23 Binary files /dev/null and b/web/detective/audio/sfx/sig_fore.ogg differ diff --git a/web/detective/audio/sfx/sig_ge.m4a b/web/detective/audio/sfx/sig_ge.m4a new file mode 100644 index 00000000..d512b087 Binary files /dev/null and b/web/detective/audio/sfx/sig_ge.m4a differ diff --git a/web/detective/audio/sfx/sig_ge.ogg b/web/detective/audio/sfx/sig_ge.ogg new file mode 100644 index 00000000..6c98556a Binary files /dev/null and b/web/detective/audio/sfx/sig_ge.ogg differ diff --git a/web/detective/audio/sfx/sig_scene.m4a b/web/detective/audio/sfx/sig_scene.m4a new file mode 100644 index 00000000..3eb38ee8 Binary files /dev/null and b/web/detective/audio/sfx/sig_scene.m4a differ diff --git a/web/detective/audio/sfx/sig_scene.ogg b/web/detective/audio/sfx/sig_scene.ogg new file mode 100644 index 00000000..cca70466 Binary files /dev/null and b/web/detective/audio/sfx/sig_scene.ogg differ diff --git a/web/detective/audio/sfx/sig_shun.m4a b/web/detective/audio/sfx/sig_shun.m4a new file mode 100644 index 00000000..91cdd1f2 Binary files /dev/null and b/web/detective/audio/sfx/sig_shun.m4a differ diff --git a/web/detective/audio/sfx/sig_shun.ogg b/web/detective/audio/sfx/sig_shun.ogg new file mode 100644 index 00000000..6b80301b Binary files /dev/null and b/web/detective/audio/sfx/sig_shun.ogg differ diff --git a/web/detective/audio/sfx/sig_wen.m4a b/web/detective/audio/sfx/sig_wen.m4a new file mode 100644 index 00000000..482ff083 Binary files /dev/null and b/web/detective/audio/sfx/sig_wen.m4a differ diff --git a/web/detective/audio/sfx/sig_wen.ogg b/web/detective/audio/sfx/sig_wen.ogg new file mode 100644 index 00000000..d7e7d3e7 Binary files /dev/null and b/web/detective/audio/sfx/sig_wen.ogg differ diff --git a/web/detective/audio/sfx/sig_zhou.m4a b/web/detective/audio/sfx/sig_zhou.m4a new file mode 100644 index 00000000..f1d5f512 Binary files /dev/null and b/web/detective/audio/sfx/sig_zhou.m4a differ diff --git a/web/detective/audio/sfx/sig_zhou.ogg b/web/detective/audio/sfx/sig_zhou.ogg new file mode 100644 index 00000000..e4c15537 Binary files /dev/null and b/web/detective/audio/sfx/sig_zhou.ogg differ diff --git a/web/detective/audio/sfx/turn_tick.m4a b/web/detective/audio/sfx/turn_tick.m4a new file mode 100644 index 00000000..df4b20f2 Binary files /dev/null and b/web/detective/audio/sfx/turn_tick.m4a differ diff --git a/web/detective/audio/sfx/turn_tick.ogg b/web/detective/audio/sfx/turn_tick.ogg new file mode 100644 index 00000000..bceb0a2b Binary files /dev/null and b/web/detective/audio/sfx/turn_tick.ogg differ diff --git a/web/detective/audio/voice/acc_ge.m4a b/web/detective/audio/voice/acc_ge.m4a new file mode 100644 index 00000000..7e2a5222 Binary files /dev/null and b/web/detective/audio/voice/acc_ge.m4a differ diff --git a/web/detective/audio/voice/acc_ge.ogg b/web/detective/audio/voice/acc_ge.ogg new file mode 100644 index 00000000..7512eae4 Binary files /dev/null and b/web/detective/audio/voice/acc_ge.ogg differ diff --git a/web/detective/audio/voice/acc_shun.m4a b/web/detective/audio/voice/acc_shun.m4a new file mode 100644 index 00000000..d77e5f46 Binary files /dev/null and b/web/detective/audio/voice/acc_shun.m4a differ diff --git a/web/detective/audio/voice/acc_shun.ogg b/web/detective/audio/voice/acc_shun.ogg new file mode 100644 index 00000000..d79f3226 Binary files /dev/null and b/web/detective/audio/voice/acc_shun.ogg differ diff --git a/web/detective/audio/voice/acc_wen.m4a b/web/detective/audio/voice/acc_wen.m4a new file mode 100644 index 00000000..6a1ffad9 Binary files /dev/null and b/web/detective/audio/voice/acc_wen.m4a differ diff --git a/web/detective/audio/voice/acc_wen.ogg b/web/detective/audio/voice/acc_wen.ogg new file mode 100644 index 00000000..1ad9cf81 Binary files /dev/null and b/web/detective/audio/voice/acc_wen.ogg differ diff --git a/web/detective/audio/voice/acc_zhou.m4a b/web/detective/audio/voice/acc_zhou.m4a new file mode 100644 index 00000000..1a7fdb6a Binary files /dev/null and b/web/detective/audio/voice/acc_zhou.m4a differ diff --git a/web/detective/audio/voice/acc_zhou.ogg b/web/detective/audio/voice/acc_zhou.ogg new file mode 100644 index 00000000..da560c03 Binary files /dev/null and b/web/detective/audio/voice/acc_zhou.ogg differ diff --git a/web/detective/audio/voice/b_shun_1.m4a b/web/detective/audio/voice/b_shun_1.m4a new file mode 100644 index 00000000..8a640773 Binary files /dev/null and b/web/detective/audio/voice/b_shun_1.m4a differ diff --git a/web/detective/audio/voice/b_shun_1.ogg b/web/detective/audio/voice/b_shun_1.ogg new file mode 100644 index 00000000..3befa503 Binary files /dev/null and b/web/detective/audio/voice/b_shun_1.ogg differ diff --git a/web/detective/audio/voice/b_shun_2.m4a b/web/detective/audio/voice/b_shun_2.m4a new file mode 100644 index 00000000..904c86f6 Binary files /dev/null and b/web/detective/audio/voice/b_shun_2.m4a differ diff --git a/web/detective/audio/voice/b_shun_2.ogg b/web/detective/audio/voice/b_shun_2.ogg new file mode 100644 index 00000000..ed33cc1a Binary files /dev/null and b/web/detective/audio/voice/b_shun_2.ogg differ diff --git a/web/detective/audio/voice/b_shun_3.m4a b/web/detective/audio/voice/b_shun_3.m4a new file mode 100644 index 00000000..daa95a40 Binary files /dev/null and b/web/detective/audio/voice/b_shun_3.m4a differ diff --git a/web/detective/audio/voice/b_shun_3.ogg b/web/detective/audio/voice/b_shun_3.ogg new file mode 100644 index 00000000..444dd102 Binary files /dev/null and b/web/detective/audio/voice/b_shun_3.ogg differ diff --git a/web/detective/audio/voice/b_shun_4.m4a b/web/detective/audio/voice/b_shun_4.m4a new file mode 100644 index 00000000..4f05cacd Binary files /dev/null and b/web/detective/audio/voice/b_shun_4.m4a differ diff --git a/web/detective/audio/voice/b_shun_4.ogg b/web/detective/audio/voice/b_shun_4.ogg new file mode 100644 index 00000000..57efb7cd Binary files /dev/null and b/web/detective/audio/voice/b_shun_4.ogg differ diff --git a/web/detective/audio/voice/b_shun_break_1.m4a b/web/detective/audio/voice/b_shun_break_1.m4a new file mode 100644 index 00000000..b23344f0 Binary files /dev/null and b/web/detective/audio/voice/b_shun_break_1.m4a differ diff --git a/web/detective/audio/voice/b_shun_break_1.ogg b/web/detective/audio/voice/b_shun_break_1.ogg new file mode 100644 index 00000000..61b72cb4 Binary files /dev/null and b/web/detective/audio/voice/b_shun_break_1.ogg differ diff --git a/web/detective/audio/voice/b_shun_break_2.m4a b/web/detective/audio/voice/b_shun_break_2.m4a new file mode 100644 index 00000000..dd357ac4 Binary files /dev/null and b/web/detective/audio/voice/b_shun_break_2.m4a differ diff --git a/web/detective/audio/voice/b_shun_break_2.ogg b/web/detective/audio/voice/b_shun_break_2.ogg new file mode 100644 index 00000000..9ce6b197 Binary files /dev/null and b/web/detective/audio/voice/b_shun_break_2.ogg differ diff --git a/web/detective/audio/voice/b_shun_break_3.m4a b/web/detective/audio/voice/b_shun_break_3.m4a new file mode 100644 index 00000000..78dc66dc Binary files /dev/null and b/web/detective/audio/voice/b_shun_break_3.m4a differ diff --git a/web/detective/audio/voice/b_shun_break_3.ogg b/web/detective/audio/voice/b_shun_break_3.ogg new file mode 100644 index 00000000..16e628d8 Binary files /dev/null and b/web/detective/audio/voice/b_shun_break_3.ogg differ diff --git a/web/detective/audio/voice/b_shun_react_1.m4a b/web/detective/audio/voice/b_shun_react_1.m4a new file mode 100644 index 00000000..ba74f8d5 Binary files /dev/null and b/web/detective/audio/voice/b_shun_react_1.m4a differ diff --git a/web/detective/audio/voice/b_shun_react_1.ogg b/web/detective/audio/voice/b_shun_react_1.ogg new file mode 100644 index 00000000..2d5cdf10 Binary files /dev/null and b/web/detective/audio/voice/b_shun_react_1.ogg differ diff --git a/web/detective/audio/voice/b_shun_react_2.m4a b/web/detective/audio/voice/b_shun_react_2.m4a new file mode 100644 index 00000000..252f996f Binary files /dev/null and b/web/detective/audio/voice/b_shun_react_2.m4a differ diff --git a/web/detective/audio/voice/b_shun_react_2.ogg b/web/detective/audio/voice/b_shun_react_2.ogg new file mode 100644 index 00000000..4a82cbdc Binary files /dev/null and b/web/detective/audio/voice/b_shun_react_2.ogg differ diff --git a/web/detective/audio/voice/b_zhou_1.m4a b/web/detective/audio/voice/b_zhou_1.m4a new file mode 100644 index 00000000..be11823f Binary files /dev/null and b/web/detective/audio/voice/b_zhou_1.m4a differ diff --git a/web/detective/audio/voice/b_zhou_1.ogg b/web/detective/audio/voice/b_zhou_1.ogg new file mode 100644 index 00000000..03552987 Binary files /dev/null and b/web/detective/audio/voice/b_zhou_1.ogg differ diff --git a/web/detective/audio/voice/b_zhou_2.m4a b/web/detective/audio/voice/b_zhou_2.m4a new file mode 100644 index 00000000..b3b13da0 Binary files /dev/null and b/web/detective/audio/voice/b_zhou_2.m4a differ diff --git a/web/detective/audio/voice/b_zhou_2.ogg b/web/detective/audio/voice/b_zhou_2.ogg new file mode 100644 index 00000000..6137559b Binary files /dev/null and b/web/detective/audio/voice/b_zhou_2.ogg differ diff --git a/web/detective/audio/voice/b_zhou_3.m4a b/web/detective/audio/voice/b_zhou_3.m4a new file mode 100644 index 00000000..f65cd52a Binary files /dev/null and b/web/detective/audio/voice/b_zhou_3.m4a differ diff --git a/web/detective/audio/voice/b_zhou_3.ogg b/web/detective/audio/voice/b_zhou_3.ogg new file mode 100644 index 00000000..5ac930eb Binary files /dev/null and b/web/detective/audio/voice/b_zhou_3.ogg differ diff --git a/web/detective/audio/voice/female/b_hint_c1.m4a b/web/detective/audio/voice/female/b_hint_c1.m4a new file mode 100644 index 00000000..f968ba06 Binary files /dev/null and b/web/detective/audio/voice/female/b_hint_c1.m4a differ diff --git a/web/detective/audio/voice/female/b_hint_c1.ogg b/web/detective/audio/voice/female/b_hint_c1.ogg new file mode 100644 index 00000000..4ee270fc Binary files /dev/null and b/web/detective/audio/voice/female/b_hint_c1.ogg differ diff --git a/web/detective/audio/voice/female/b_hint_c2.m4a b/web/detective/audio/voice/female/b_hint_c2.m4a new file mode 100644 index 00000000..7e7ff029 Binary files /dev/null and b/web/detective/audio/voice/female/b_hint_c2.m4a differ diff --git a/web/detective/audio/voice/female/b_hint_c2.ogg b/web/detective/audio/voice/female/b_hint_c2.ogg new file mode 100644 index 00000000..5a67303e Binary files /dev/null and b/web/detective/audio/voice/female/b_hint_c2.ogg differ diff --git a/web/detective/audio/voice/female/cf_confirm.m4a b/web/detective/audio/voice/female/cf_confirm.m4a new file mode 100644 index 00000000..38de7f20 Binary files /dev/null and b/web/detective/audio/voice/female/cf_confirm.m4a differ diff --git a/web/detective/audio/voice/female/cf_confirm.ogg b/web/detective/audio/voice/female/cf_confirm.ogg new file mode 100644 index 00000000..dbec7a52 Binary files /dev/null and b/web/detective/audio/voice/female/cf_confirm.ogg differ diff --git a/web/detective/audio/voice/female/cf_diff.m4a b/web/detective/audio/voice/female/cf_diff.m4a new file mode 100644 index 00000000..dd99c508 Binary files /dev/null and b/web/detective/audio/voice/female/cf_diff.m4a differ diff --git a/web/detective/audio/voice/female/cf_diff.ogg b/web/detective/audio/voice/female/cf_diff.ogg new file mode 100644 index 00000000..4540f33c Binary files /dev/null and b/web/detective/audio/voice/female/cf_diff.ogg differ diff --git a/web/detective/audio/voice/female/cf_hit.m4a b/web/detective/audio/voice/female/cf_hit.m4a new file mode 100644 index 00000000..6a4c062c Binary files /dev/null and b/web/detective/audio/voice/female/cf_hit.m4a differ diff --git a/web/detective/audio/voice/female/cf_hit.ogg b/web/detective/audio/voice/female/cf_hit.ogg new file mode 100644 index 00000000..9c6b02f3 Binary files /dev/null and b/web/detective/audio/voice/female/cf_hit.ogg differ diff --git a/web/detective/audio/voice/female/cf_pick_line.m4a b/web/detective/audio/voice/female/cf_pick_line.m4a new file mode 100644 index 00000000..b61e61e7 Binary files /dev/null and b/web/detective/audio/voice/female/cf_pick_line.m4a differ diff --git a/web/detective/audio/voice/female/cf_pick_line.ogg b/web/detective/audio/voice/female/cf_pick_line.ogg new file mode 100644 index 00000000..6deb3410 Binary files /dev/null and b/web/detective/audio/voice/female/cf_pick_line.ogg differ diff --git a/web/detective/audio/voice/female/cf_same.m4a b/web/detective/audio/voice/female/cf_same.m4a new file mode 100644 index 00000000..207c95e9 Binary files /dev/null and b/web/detective/audio/voice/female/cf_same.m4a differ diff --git a/web/detective/audio/voice/female/cf_same.ogg b/web/detective/audio/voice/female/cf_same.ogg new file mode 100644 index 00000000..f9b376d0 Binary files /dev/null and b/web/detective/audio/voice/female/cf_same.ogg differ diff --git a/web/detective/audio/voice/female/cf_second.m4a b/web/detective/audio/voice/female/cf_second.m4a new file mode 100644 index 00000000..8193d377 Binary files /dev/null and b/web/detective/audio/voice/female/cf_second.m4a differ diff --git a/web/detective/audio/voice/female/cf_second.ogg b/web/detective/audio/voice/female/cf_second.ogg new file mode 100644 index 00000000..72de590d Binary files /dev/null and b/web/detective/audio/voice/female/cf_second.ogg differ diff --git a/web/detective/audio/voice/female/cf_self.m4a b/web/detective/audio/voice/female/cf_self.m4a new file mode 100644 index 00000000..7aad86fd Binary files /dev/null and b/web/detective/audio/voice/female/cf_self.m4a differ diff --git a/web/detective/audio/voice/female/cf_self.ogg b/web/detective/audio/voice/female/cf_self.ogg new file mode 100644 index 00000000..807b0509 Binary files /dev/null and b/web/detective/audio/voice/female/cf_self.ogg differ diff --git a/web/detective/audio/voice/female/cf_start.m4a b/web/detective/audio/voice/female/cf_start.m4a new file mode 100644 index 00000000..d3133609 Binary files /dev/null and b/web/detective/audio/voice/female/cf_start.m4a differ diff --git a/web/detective/audio/voice/female/cf_start.ogg b/web/detective/audio/voice/female/cf_start.ogg new file mode 100644 index 00000000..c1465e34 Binary files /dev/null and b/web/detective/audio/voice/female/cf_start.ogg differ diff --git a/web/detective/audio/voice/female/fin_ask_motive.m4a b/web/detective/audio/voice/female/fin_ask_motive.m4a new file mode 100644 index 00000000..e3a86a0d Binary files /dev/null and b/web/detective/audio/voice/female/fin_ask_motive.m4a differ diff --git a/web/detective/audio/voice/female/fin_ask_motive.ogg b/web/detective/audio/voice/female/fin_ask_motive.ogg new file mode 100644 index 00000000..c1c3d4f0 Binary files /dev/null and b/web/detective/audio/voice/female/fin_ask_motive.ogg differ diff --git a/web/detective/audio/voice/female/fin_ask_weapon.m4a b/web/detective/audio/voice/female/fin_ask_weapon.m4a new file mode 100644 index 00000000..3345ef5c Binary files /dev/null and b/web/detective/audio/voice/female/fin_ask_weapon.m4a differ diff --git a/web/detective/audio/voice/female/fin_ask_weapon.ogg b/web/detective/audio/voice/female/fin_ask_weapon.ogg new file mode 100644 index 00000000..edf6b0f4 Binary files /dev/null and b/web/detective/audio/voice/female/fin_ask_weapon.ogg differ diff --git a/web/detective/audio/voice/female/fin_clear.m4a b/web/detective/audio/voice/female/fin_clear.m4a new file mode 100644 index 00000000..d35c1d4f Binary files /dev/null and b/web/detective/audio/voice/female/fin_clear.m4a differ diff --git a/web/detective/audio/voice/female/fin_clear.ogg b/web/detective/audio/voice/female/fin_clear.ogg new file mode 100644 index 00000000..dd94b884 Binary files /dev/null and b/web/detective/audio/voice/female/fin_clear.ogg differ diff --git a/web/detective/audio/voice/female/fin_mv_right.m4a b/web/detective/audio/voice/female/fin_mv_right.m4a new file mode 100644 index 00000000..0d801e81 Binary files /dev/null and b/web/detective/audio/voice/female/fin_mv_right.m4a differ diff --git a/web/detective/audio/voice/female/fin_mv_right.ogg b/web/detective/audio/voice/female/fin_mv_right.ogg new file mode 100644 index 00000000..56852faf Binary files /dev/null and b/web/detective/audio/voice/female/fin_mv_right.ogg differ diff --git a/web/detective/audio/voice/female/fin_mv_wrong.m4a b/web/detective/audio/voice/female/fin_mv_wrong.m4a new file mode 100644 index 00000000..537371e8 Binary files /dev/null and b/web/detective/audio/voice/female/fin_mv_wrong.m4a differ diff --git a/web/detective/audio/voice/female/fin_mv_wrong.ogg b/web/detective/audio/voice/female/fin_mv_wrong.ogg new file mode 100644 index 00000000..5a8eb576 Binary files /dev/null and b/web/detective/audio/voice/female/fin_mv_wrong.ogg differ diff --git a/web/detective/audio/voice/female/fin_right.m4a b/web/detective/audio/voice/female/fin_right.m4a new file mode 100644 index 00000000..d0d4cb97 Binary files /dev/null and b/web/detective/audio/voice/female/fin_right.m4a differ diff --git a/web/detective/audio/voice/female/fin_right.ogg b/web/detective/audio/voice/female/fin_right.ogg new file mode 100644 index 00000000..738b1d74 Binary files /dev/null and b/web/detective/audio/voice/female/fin_right.ogg differ diff --git a/web/detective/audio/voice/female/fin_right_b.m4a b/web/detective/audio/voice/female/fin_right_b.m4a new file mode 100644 index 00000000..a8b014ae Binary files /dev/null and b/web/detective/audio/voice/female/fin_right_b.m4a differ diff --git a/web/detective/audio/voice/female/fin_right_b.ogg b/web/detective/audio/voice/female/fin_right_b.ogg new file mode 100644 index 00000000..c3dc82ef Binary files /dev/null and b/web/detective/audio/voice/female/fin_right_b.ogg differ diff --git a/web/detective/audio/voice/female/fin_start.m4a b/web/detective/audio/voice/female/fin_start.m4a new file mode 100644 index 00000000..062ea1c8 Binary files /dev/null and b/web/detective/audio/voice/female/fin_start.m4a differ diff --git a/web/detective/audio/voice/female/fin_start.ogg b/web/detective/audio/voice/female/fin_start.ogg new file mode 100644 index 00000000..db49852d Binary files /dev/null and b/web/detective/audio/voice/female/fin_start.ogg differ diff --git a/web/detective/audio/voice/female/fin_who_confirm.m4a b/web/detective/audio/voice/female/fin_who_confirm.m4a new file mode 100644 index 00000000..6552d5bb Binary files /dev/null and b/web/detective/audio/voice/female/fin_who_confirm.m4a differ diff --git a/web/detective/audio/voice/female/fin_who_confirm.ogg b/web/detective/audio/voice/female/fin_who_confirm.ogg new file mode 100644 index 00000000..2b5a3229 Binary files /dev/null and b/web/detective/audio/voice/female/fin_who_confirm.ogg differ diff --git a/web/detective/audio/voice/female/fin_wp_right.m4a b/web/detective/audio/voice/female/fin_wp_right.m4a new file mode 100644 index 00000000..7e57323f Binary files /dev/null and b/web/detective/audio/voice/female/fin_wp_right.m4a differ diff --git a/web/detective/audio/voice/female/fin_wp_right.ogg b/web/detective/audio/voice/female/fin_wp_right.ogg new file mode 100644 index 00000000..ef76972a Binary files /dev/null and b/web/detective/audio/voice/female/fin_wp_right.ogg differ diff --git a/web/detective/audio/voice/female/fin_wp_wrong.m4a b/web/detective/audio/voice/female/fin_wp_wrong.m4a new file mode 100644 index 00000000..5518019c Binary files /dev/null and b/web/detective/audio/voice/female/fin_wp_wrong.m4a differ diff --git a/web/detective/audio/voice/female/fin_wp_wrong.ogg b/web/detective/audio/voice/female/fin_wp_wrong.ogg new file mode 100644 index 00000000..738efdf8 Binary files /dev/null and b/web/detective/audio/voice/female/fin_wp_wrong.ogg differ diff --git a/web/detective/audio/voice/female/gd_board.m4a b/web/detective/audio/voice/female/gd_board.m4a new file mode 100644 index 00000000..428ae1e6 Binary files /dev/null and b/web/detective/audio/voice/female/gd_board.m4a differ diff --git a/web/detective/audio/voice/female/gd_board.ogg b/web/detective/audio/voice/female/gd_board.ogg new file mode 100644 index 00000000..84a1f7ae Binary files /dev/null and b/web/detective/audio/voice/female/gd_board.ogg differ diff --git a/web/detective/audio/voice/female/gd_cf.m4a b/web/detective/audio/voice/female/gd_cf.m4a new file mode 100644 index 00000000..7f7c3449 Binary files /dev/null and b/web/detective/audio/voice/female/gd_cf.m4a differ diff --git a/web/detective/audio/voice/female/gd_cf.ogg b/web/detective/audio/voice/female/gd_cf.ogg new file mode 100644 index 00000000..bb37db9b Binary files /dev/null and b/web/detective/audio/voice/female/gd_cf.ogg differ diff --git a/web/detective/audio/voice/female/gd_fin.m4a b/web/detective/audio/voice/female/gd_fin.m4a new file mode 100644 index 00000000..6ba0c241 Binary files /dev/null and b/web/detective/audio/voice/female/gd_fin.m4a differ diff --git a/web/detective/audio/voice/female/gd_fin.ogg b/web/detective/audio/voice/female/gd_fin.ogg new file mode 100644 index 00000000..f93d3042 Binary files /dev/null and b/web/detective/audio/voice/female/gd_fin.ogg differ diff --git a/web/detective/audio/voice/female/gd_invalid.m4a b/web/detective/audio/voice/female/gd_invalid.m4a new file mode 100644 index 00000000..e546bd6e Binary files /dev/null and b/web/detective/audio/voice/female/gd_invalid.m4a differ diff --git a/web/detective/audio/voice/female/gd_invalid.ogg b/web/detective/audio/voice/female/gd_invalid.ogg new file mode 100644 index 00000000..8645c8d1 Binary files /dev/null and b/web/detective/audio/voice/female/gd_invalid.ogg differ diff --git a/web/detective/audio/voice/female/hint_a.m4a b/web/detective/audio/voice/female/hint_a.m4a new file mode 100644 index 00000000..b270837d Binary files /dev/null and b/web/detective/audio/voice/female/hint_a.m4a differ diff --git a/web/detective/audio/voice/female/hint_a.ogg b/web/detective/audio/voice/female/hint_a.ogg new file mode 100644 index 00000000..121db4d7 Binary files /dev/null and b/web/detective/audio/voice/female/hint_a.ogg differ diff --git a/web/detective/audio/voice/female/hint_b.m4a b/web/detective/audio/voice/female/hint_b.m4a new file mode 100644 index 00000000..13979adc Binary files /dev/null and b/web/detective/audio/voice/female/hint_b.m4a differ diff --git a/web/detective/audio/voice/female/hint_b.ogg b/web/detective/audio/voice/female/hint_b.ogg new file mode 100644 index 00000000..5636af46 Binary files /dev/null and b/web/detective/audio/voice/female/hint_b.ogg differ diff --git a/web/detective/audio/voice/female/hint_c1.m4a b/web/detective/audio/voice/female/hint_c1.m4a new file mode 100644 index 00000000..2ce3eaf9 Binary files /dev/null and b/web/detective/audio/voice/female/hint_c1.m4a differ diff --git a/web/detective/audio/voice/female/hint_c1.ogg b/web/detective/audio/voice/female/hint_c1.ogg new file mode 100644 index 00000000..af393807 Binary files /dev/null and b/web/detective/audio/voice/female/hint_c1.ogg differ diff --git a/web/detective/audio/voice/female/hint_c2.m4a b/web/detective/audio/voice/female/hint_c2.m4a new file mode 100644 index 00000000..10815a0e Binary files /dev/null and b/web/detective/audio/voice/female/hint_c2.m4a differ diff --git a/web/detective/audio/voice/female/hint_c2.ogg b/web/detective/audio/voice/female/hint_c2.ogg new file mode 100644 index 00000000..0f9dedaa Binary files /dev/null and b/web/detective/audio/voice/female/hint_c2.ogg differ diff --git a/web/detective/audio/voice/female/hint_fin.m4a b/web/detective/audio/voice/female/hint_fin.m4a new file mode 100644 index 00000000..646745c7 Binary files /dev/null and b/web/detective/audio/voice/female/hint_fin.m4a differ diff --git a/web/detective/audio/voice/female/hint_fin.ogg b/web/detective/audio/voice/female/hint_fin.ogg new file mode 100644 index 00000000..6607e09c Binary files /dev/null and b/web/detective/audio/voice/female/hint_fin.ogg differ diff --git a/web/detective/audio/voice/female/intro_1.m4a b/web/detective/audio/voice/female/intro_1.m4a new file mode 100644 index 00000000..50ee0b72 Binary files /dev/null and b/web/detective/audio/voice/female/intro_1.m4a differ diff --git a/web/detective/audio/voice/female/intro_1.ogg b/web/detective/audio/voice/female/intro_1.ogg new file mode 100644 index 00000000..86c63c67 Binary files /dev/null and b/web/detective/audio/voice/female/intro_1.ogg differ diff --git a/web/detective/audio/voice/female/intro_2.m4a b/web/detective/audio/voice/female/intro_2.m4a new file mode 100644 index 00000000..34d970d8 Binary files /dev/null and b/web/detective/audio/voice/female/intro_2.m4a differ diff --git a/web/detective/audio/voice/female/intro_2.ogg b/web/detective/audio/voice/female/intro_2.ogg new file mode 100644 index 00000000..4a2ae352 Binary files /dev/null and b/web/detective/audio/voice/female/intro_2.ogg differ diff --git a/web/detective/audio/voice/female/intro_3.m4a b/web/detective/audio/voice/female/intro_3.m4a new file mode 100644 index 00000000..1ca00221 Binary files /dev/null and b/web/detective/audio/voice/female/intro_3.m4a differ diff --git a/web/detective/audio/voice/female/intro_3.ogg b/web/detective/audio/voice/female/intro_3.ogg new file mode 100644 index 00000000..94487c07 Binary files /dev/null and b/web/detective/audio/voice/female/intro_3.ogg differ diff --git a/web/detective/audio/voice/female/intro_w3.m4a b/web/detective/audio/voice/female/intro_w3.m4a new file mode 100644 index 00000000..b032d819 Binary files /dev/null and b/web/detective/audio/voice/female/intro_w3.m4a differ diff --git a/web/detective/audio/voice/female/intro_w3.ogg b/web/detective/audio/voice/female/intro_w3.ogg new file mode 100644 index 00000000..f1bfe4d9 Binary files /dev/null and b/web/detective/audio/voice/female/intro_w3.ogg differ diff --git a/web/detective/audio/voice/female/item_case.m4a b/web/detective/audio/voice/female/item_case.m4a new file mode 100644 index 00000000..711048ef Binary files /dev/null and b/web/detective/audio/voice/female/item_case.m4a differ diff --git a/web/detective/audio/voice/female/item_case.ogg b/web/detective/audio/voice/female/item_case.ogg new file mode 100644 index 00000000..a41db11a Binary files /dev/null and b/web/detective/audio/voice/female/item_case.ogg differ diff --git a/web/detective/audio/voice/female/item_fore.m4a b/web/detective/audio/voice/female/item_fore.m4a new file mode 100644 index 00000000..c1886765 Binary files /dev/null and b/web/detective/audio/voice/female/item_fore.m4a differ diff --git a/web/detective/audio/voice/female/item_fore.ogg b/web/detective/audio/voice/female/item_fore.ogg new file mode 100644 index 00000000..ec5817d6 Binary files /dev/null and b/web/detective/audio/voice/female/item_fore.ogg differ diff --git a/web/detective/audio/voice/female/item_scene.m4a b/web/detective/audio/voice/female/item_scene.m4a new file mode 100644 index 00000000..b49e2688 Binary files /dev/null and b/web/detective/audio/voice/female/item_scene.m4a differ diff --git a/web/detective/audio/voice/female/item_scene.ogg b/web/detective/audio/voice/female/item_scene.ogg new file mode 100644 index 00000000..722e98bd Binary files /dev/null and b/web/detective/audio/voice/female/item_scene.ogg differ diff --git a/web/detective/audio/voice/female/item_w1.m4a b/web/detective/audio/voice/female/item_w1.m4a new file mode 100644 index 00000000..88494046 Binary files /dev/null and b/web/detective/audio/voice/female/item_w1.m4a differ diff --git a/web/detective/audio/voice/female/item_w1.ogg b/web/detective/audio/voice/female/item_w1.ogg new file mode 100644 index 00000000..008c2a03 Binary files /dev/null and b/web/detective/audio/voice/female/item_w1.ogg differ diff --git a/web/detective/audio/voice/female/item_w2.m4a b/web/detective/audio/voice/female/item_w2.m4a new file mode 100644 index 00000000..9188f7f0 Binary files /dev/null and b/web/detective/audio/voice/female/item_w2.m4a differ diff --git a/web/detective/audio/voice/female/item_w2.ogg b/web/detective/audio/voice/female/item_w2.ogg new file mode 100644 index 00000000..795f1c9b Binary files /dev/null and b/web/detective/audio/voice/female/item_w2.ogg differ diff --git a/web/detective/audio/voice/female/item_w3.m4a b/web/detective/audio/voice/female/item_w3.m4a new file mode 100644 index 00000000..edd0e9c3 Binary files /dev/null and b/web/detective/audio/voice/female/item_w3.m4a differ diff --git a/web/detective/audio/voice/female/item_w3.ogg b/web/detective/audio/voice/female/item_w3.ogg new file mode 100644 index 00000000..d866b78d Binary files /dev/null and b/web/detective/audio/voice/female/item_w3.ogg differ diff --git a/web/detective/audio/voice/female/item_w4.m4a b/web/detective/audio/voice/female/item_w4.m4a new file mode 100644 index 00000000..ff43c99f Binary files /dev/null and b/web/detective/audio/voice/female/item_w4.m4a differ diff --git a/web/detective/audio/voice/female/item_w4.ogg b/web/detective/audio/voice/female/item_w4.ogg new file mode 100644 index 00000000..2269c295 Binary files /dev/null and b/web/detective/audio/voice/female/item_w4.ogg differ diff --git a/web/detective/audio/voice/female/key/cf_confirm.m4a b/web/detective/audio/voice/female/key/cf_confirm.m4a new file mode 100644 index 00000000..8837f222 Binary files /dev/null and b/web/detective/audio/voice/female/key/cf_confirm.m4a differ diff --git a/web/detective/audio/voice/female/key/cf_confirm.ogg b/web/detective/audio/voice/female/key/cf_confirm.ogg new file mode 100644 index 00000000..44819f28 Binary files /dev/null and b/web/detective/audio/voice/female/key/cf_confirm.ogg differ diff --git a/web/detective/audio/voice/female/key/cf_pick_line.m4a b/web/detective/audio/voice/female/key/cf_pick_line.m4a new file mode 100644 index 00000000..83e40c07 Binary files /dev/null and b/web/detective/audio/voice/female/key/cf_pick_line.m4a differ diff --git a/web/detective/audio/voice/female/key/cf_pick_line.ogg b/web/detective/audio/voice/female/key/cf_pick_line.ogg new file mode 100644 index 00000000..ee524d87 Binary files /dev/null and b/web/detective/audio/voice/female/key/cf_pick_line.ogg differ diff --git a/web/detective/audio/voice/female/key/gd_board.m4a b/web/detective/audio/voice/female/key/gd_board.m4a new file mode 100644 index 00000000..93bf4199 Binary files /dev/null and b/web/detective/audio/voice/female/key/gd_board.m4a differ diff --git a/web/detective/audio/voice/female/key/gd_board.ogg b/web/detective/audio/voice/female/key/gd_board.ogg new file mode 100644 index 00000000..5123f8a3 Binary files /dev/null and b/web/detective/audio/voice/female/key/gd_board.ogg differ diff --git a/web/detective/audio/voice/female/key/gd_cf.m4a b/web/detective/audio/voice/female/key/gd_cf.m4a new file mode 100644 index 00000000..ef6d2d92 Binary files /dev/null and b/web/detective/audio/voice/female/key/gd_cf.m4a differ diff --git a/web/detective/audio/voice/female/key/gd_cf.ogg b/web/detective/audio/voice/female/key/gd_cf.ogg new file mode 100644 index 00000000..c80da22a Binary files /dev/null and b/web/detective/audio/voice/female/key/gd_cf.ogg differ diff --git a/web/detective/audio/voice/female/key/settle_hook.m4a b/web/detective/audio/voice/female/key/settle_hook.m4a new file mode 100644 index 00000000..4bdd92d0 Binary files /dev/null and b/web/detective/audio/voice/female/key/settle_hook.m4a differ diff --git a/web/detective/audio/voice/female/key/settle_hook.ogg b/web/detective/audio/voice/female/key/settle_hook.ogg new file mode 100644 index 00000000..7b7a8e0c Binary files /dev/null and b/web/detective/audio/voice/female/key/settle_hook.ogg differ diff --git a/web/detective/audio/voice/female/key/sys_help.m4a b/web/detective/audio/voice/female/key/sys_help.m4a new file mode 100644 index 00000000..b7bdb4b3 Binary files /dev/null and b/web/detective/audio/voice/female/key/sys_help.m4a differ diff --git a/web/detective/audio/voice/female/key/sys_help.ogg b/web/detective/audio/voice/female/key/sys_help.ogg new file mode 100644 index 00000000..25729824 Binary files /dev/null and b/web/detective/audio/voice/female/key/sys_help.ogg differ diff --git a/web/detective/audio/voice/female/key/tut_confront.m4a b/web/detective/audio/voice/female/key/tut_confront.m4a new file mode 100644 index 00000000..e916e852 Binary files /dev/null and b/web/detective/audio/voice/female/key/tut_confront.m4a differ diff --git a/web/detective/audio/voice/female/key/tut_confront.ogg b/web/detective/audio/voice/female/key/tut_confront.ogg new file mode 100644 index 00000000..2e75ed5f Binary files /dev/null and b/web/detective/audio/voice/female/key/tut_confront.ogg differ diff --git a/web/detective/audio/voice/female/key/tut_d0.m4a b/web/detective/audio/voice/female/key/tut_d0.m4a new file mode 100644 index 00000000..ee6a2e99 Binary files /dev/null and b/web/detective/audio/voice/female/key/tut_d0.m4a differ diff --git a/web/detective/audio/voice/female/key/tut_d0.ogg b/web/detective/audio/voice/female/key/tut_d0.ogg new file mode 100644 index 00000000..80faaae7 Binary files /dev/null and b/web/detective/audio/voice/female/key/tut_d0.ogg differ diff --git a/web/detective/audio/voice/female/key/tut_listen.m4a b/web/detective/audio/voice/female/key/tut_listen.m4a new file mode 100644 index 00000000..e3025849 Binary files /dev/null and b/web/detective/audio/voice/female/key/tut_listen.m4a differ diff --git a/web/detective/audio/voice/female/key/tut_listen.ogg b/web/detective/audio/voice/female/key/tut_listen.ogg new file mode 100644 index 00000000..93f39a38 Binary files /dev/null and b/web/detective/audio/voice/female/key/tut_listen.ogg differ diff --git a/web/detective/audio/voice/female/key/tut_replay.m4a b/web/detective/audio/voice/female/key/tut_replay.m4a new file mode 100644 index 00000000..7c647490 Binary files /dev/null and b/web/detective/audio/voice/female/key/tut_replay.m4a differ diff --git a/web/detective/audio/voice/female/key/tut_replay.ogg b/web/detective/audio/voice/female/key/tut_replay.ogg new file mode 100644 index 00000000..ee30940e Binary files /dev/null and b/web/detective/audio/voice/female/key/tut_replay.ogg differ diff --git a/web/detective/audio/voice/female/mv_1.m4a b/web/detective/audio/voice/female/mv_1.m4a new file mode 100644 index 00000000..eb8358a6 Binary files /dev/null and b/web/detective/audio/voice/female/mv_1.m4a differ diff --git a/web/detective/audio/voice/female/mv_1.ogg b/web/detective/audio/voice/female/mv_1.ogg new file mode 100644 index 00000000..c74153e7 Binary files /dev/null and b/web/detective/audio/voice/female/mv_1.ogg differ diff --git a/web/detective/audio/voice/female/mv_2.m4a b/web/detective/audio/voice/female/mv_2.m4a new file mode 100644 index 00000000..7f22be22 Binary files /dev/null and b/web/detective/audio/voice/female/mv_2.m4a differ diff --git a/web/detective/audio/voice/female/mv_2.ogg b/web/detective/audio/voice/female/mv_2.ogg new file mode 100644 index 00000000..1703a02d Binary files /dev/null and b/web/detective/audio/voice/female/mv_2.ogg differ diff --git a/web/detective/audio/voice/female/mv_3.m4a b/web/detective/audio/voice/female/mv_3.m4a new file mode 100644 index 00000000..cbe96096 Binary files /dev/null and b/web/detective/audio/voice/female/mv_3.m4a differ diff --git a/web/detective/audio/voice/female/mv_3.ogg b/web/detective/audio/voice/female/mv_3.ogg new file mode 100644 index 00000000..12bb9612 Binary files /dev/null and b/web/detective/audio/voice/female/mv_3.ogg differ diff --git a/web/detective/audio/voice/female/rate_a.m4a b/web/detective/audio/voice/female/rate_a.m4a new file mode 100644 index 00000000..8f61bef0 Binary files /dev/null and b/web/detective/audio/voice/female/rate_a.m4a differ diff --git a/web/detective/audio/voice/female/rate_a.ogg b/web/detective/audio/voice/female/rate_a.ogg new file mode 100644 index 00000000..4798d6e7 Binary files /dev/null and b/web/detective/audio/voice/female/rate_a.ogg differ diff --git a/web/detective/audio/voice/female/rate_b.m4a b/web/detective/audio/voice/female/rate_b.m4a new file mode 100644 index 00000000..847f0266 Binary files /dev/null and b/web/detective/audio/voice/female/rate_b.m4a differ diff --git a/web/detective/audio/voice/female/rate_b.ogg b/web/detective/audio/voice/female/rate_b.ogg new file mode 100644 index 00000000..a5e7610a Binary files /dev/null and b/web/detective/audio/voice/female/rate_b.ogg differ diff --git a/web/detective/audio/voice/female/rate_c.m4a b/web/detective/audio/voice/female/rate_c.m4a new file mode 100644 index 00000000..34c024bd Binary files /dev/null and b/web/detective/audio/voice/female/rate_c.m4a differ diff --git a/web/detective/audio/voice/female/rate_c.ogg b/web/detective/audio/voice/female/rate_c.ogg new file mode 100644 index 00000000..c6984a5d Binary files /dev/null and b/web/detective/audio/voice/female/rate_c.ogg differ diff --git a/web/detective/audio/voice/female/rate_s.m4a b/web/detective/audio/voice/female/rate_s.m4a new file mode 100644 index 00000000..4a1c21ec Binary files /dev/null and b/web/detective/audio/voice/female/rate_s.m4a differ diff --git a/web/detective/audio/voice/female/rate_s.ogg b/web/detective/audio/voice/female/rate_s.ogg new file mode 100644 index 00000000..63941e0d Binary files /dev/null and b/web/detective/audio/voice/female/rate_s.ogg differ diff --git a/web/detective/audio/voice/female/recap_go.m4a b/web/detective/audio/voice/female/recap_go.m4a new file mode 100644 index 00000000..0c5526df Binary files /dev/null and b/web/detective/audio/voice/female/recap_go.m4a differ diff --git a/web/detective/audio/voice/female/recap_go.ogg b/web/detective/audio/voice/female/recap_go.ogg new file mode 100644 index 00000000..620984dd Binary files /dev/null and b/web/detective/audio/voice/female/recap_go.ogg differ diff --git a/web/detective/audio/voice/female/sc_0.m4a b/web/detective/audio/voice/female/sc_0.m4a new file mode 100644 index 00000000..761d88ed Binary files /dev/null and b/web/detective/audio/voice/female/sc_0.m4a differ diff --git a/web/detective/audio/voice/female/sc_0.ogg b/web/detective/audio/voice/female/sc_0.ogg new file mode 100644 index 00000000..363ecdca Binary files /dev/null and b/web/detective/audio/voice/female/sc_0.ogg differ diff --git a/web/detective/audio/voice/female/sc_1.m4a b/web/detective/audio/voice/female/sc_1.m4a new file mode 100644 index 00000000..8aab495f Binary files /dev/null and b/web/detective/audio/voice/female/sc_1.m4a differ diff --git a/web/detective/audio/voice/female/sc_1.ogg b/web/detective/audio/voice/female/sc_1.ogg new file mode 100644 index 00000000..a366b6b8 Binary files /dev/null and b/web/detective/audio/voice/female/sc_1.ogg differ diff --git a/web/detective/audio/voice/female/sc_2.m4a b/web/detective/audio/voice/female/sc_2.m4a new file mode 100644 index 00000000..a4392cd3 Binary files /dev/null and b/web/detective/audio/voice/female/sc_2.m4a differ diff --git a/web/detective/audio/voice/female/sc_2.ogg b/web/detective/audio/voice/female/sc_2.ogg new file mode 100644 index 00000000..7c102da9 Binary files /dev/null and b/web/detective/audio/voice/female/sc_2.ogg differ diff --git a/web/detective/audio/voice/female/sc_3.m4a b/web/detective/audio/voice/female/sc_3.m4a new file mode 100644 index 00000000..aa822030 Binary files /dev/null and b/web/detective/audio/voice/female/sc_3.m4a differ diff --git a/web/detective/audio/voice/female/sc_3.ogg b/web/detective/audio/voice/female/sc_3.ogg new file mode 100644 index 00000000..4702ce69 Binary files /dev/null and b/web/detective/audio/voice/female/sc_3.ogg differ diff --git a/web/detective/audio/voice/female/sc_4.m4a b/web/detective/audio/voice/female/sc_4.m4a new file mode 100644 index 00000000..dc849777 Binary files /dev/null and b/web/detective/audio/voice/female/sc_4.m4a differ diff --git a/web/detective/audio/voice/female/sc_4.ogg b/web/detective/audio/voice/female/sc_4.ogg new file mode 100644 index 00000000..a1edf1eb Binary files /dev/null and b/web/detective/audio/voice/female/sc_4.ogg differ diff --git a/web/detective/audio/voice/female/sc_5.m4a b/web/detective/audio/voice/female/sc_5.m4a new file mode 100644 index 00000000..19050834 Binary files /dev/null and b/web/detective/audio/voice/female/sc_5.m4a differ diff --git a/web/detective/audio/voice/female/sc_5.ogg b/web/detective/audio/voice/female/sc_5.ogg new file mode 100644 index 00000000..8abac0e6 Binary files /dev/null and b/web/detective/audio/voice/female/sc_5.ogg differ diff --git a/web/detective/audio/voice/female/settings_hint.m4a b/web/detective/audio/voice/female/settings_hint.m4a new file mode 100644 index 00000000..f3ea3175 Binary files /dev/null and b/web/detective/audio/voice/female/settings_hint.m4a differ diff --git a/web/detective/audio/voice/female/settings_hint.ogg b/web/detective/audio/voice/female/settings_hint.ogg new file mode 100644 index 00000000..79716e83 Binary files /dev/null and b/web/detective/audio/voice/female/settings_hint.ogg differ diff --git a/web/detective/audio/voice/female/settle_hook.m4a b/web/detective/audio/voice/female/settle_hook.m4a new file mode 100644 index 00000000..3d612189 Binary files /dev/null and b/web/detective/audio/voice/female/settle_hook.m4a differ diff --git a/web/detective/audio/voice/female/settle_hook.ogg b/web/detective/audio/voice/female/settle_hook.ogg new file mode 100644 index 00000000..a75dad6b Binary files /dev/null and b/web/detective/audio/voice/female/settle_hook.ogg differ diff --git a/web/detective/audio/voice/female/settle_open.m4a b/web/detective/audio/voice/female/settle_open.m4a new file mode 100644 index 00000000..388d7c7e Binary files /dev/null and b/web/detective/audio/voice/female/settle_open.m4a differ diff --git a/web/detective/audio/voice/female/settle_open.ogg b/web/detective/audio/voice/female/settle_open.ogg new file mode 100644 index 00000000..eab8e497 Binary files /dev/null and b/web/detective/audio/voice/female/settle_open.ogg differ diff --git a/web/detective/audio/voice/female/st_asked_0.m4a b/web/detective/audio/voice/female/st_asked_0.m4a new file mode 100644 index 00000000..697035a9 Binary files /dev/null and b/web/detective/audio/voice/female/st_asked_0.m4a differ diff --git a/web/detective/audio/voice/female/st_asked_0.ogg b/web/detective/audio/voice/female/st_asked_0.ogg new file mode 100644 index 00000000..0ed863ee Binary files /dev/null and b/web/detective/audio/voice/female/st_asked_0.ogg differ diff --git a/web/detective/audio/voice/female/st_asked_1.m4a b/web/detective/audio/voice/female/st_asked_1.m4a new file mode 100644 index 00000000..161f8120 Binary files /dev/null and b/web/detective/audio/voice/female/st_asked_1.m4a differ diff --git a/web/detective/audio/voice/female/st_asked_1.ogg b/web/detective/audio/voice/female/st_asked_1.ogg new file mode 100644 index 00000000..ee28f7fb Binary files /dev/null and b/web/detective/audio/voice/female/st_asked_1.ogg differ diff --git a/web/detective/audio/voice/female/st_asked_2.m4a b/web/detective/audio/voice/female/st_asked_2.m4a new file mode 100644 index 00000000..cad60dce Binary files /dev/null and b/web/detective/audio/voice/female/st_asked_2.m4a differ diff --git a/web/detective/audio/voice/female/st_asked_2.ogg b/web/detective/audio/voice/female/st_asked_2.ogg new file mode 100644 index 00000000..bb5c8b5a Binary files /dev/null and b/web/detective/audio/voice/female/st_asked_2.ogg differ diff --git a/web/detective/audio/voice/female/st_asked_3.m4a b/web/detective/audio/voice/female/st_asked_3.m4a new file mode 100644 index 00000000..dc3f32e9 Binary files /dev/null and b/web/detective/audio/voice/female/st_asked_3.m4a differ diff --git a/web/detective/audio/voice/female/st_asked_3.ogg b/web/detective/audio/voice/female/st_asked_3.ogg new file mode 100644 index 00000000..50620755 Binary files /dev/null and b/web/detective/audio/voice/female/st_asked_3.ogg differ diff --git a/web/detective/audio/voice/female/st_asked_4.m4a b/web/detective/audio/voice/female/st_asked_4.m4a new file mode 100644 index 00000000..fae01a61 Binary files /dev/null and b/web/detective/audio/voice/female/st_asked_4.m4a differ diff --git a/web/detective/audio/voice/female/st_asked_4.ogg b/web/detective/audio/voice/female/st_asked_4.ogg new file mode 100644 index 00000000..25079456 Binary files /dev/null and b/web/detective/audio/voice/female/st_asked_4.ogg differ diff --git a/web/detective/audio/voice/female/st_asked_5.m4a b/web/detective/audio/voice/female/st_asked_5.m4a new file mode 100644 index 00000000..7f856155 Binary files /dev/null and b/web/detective/audio/voice/female/st_asked_5.m4a differ diff --git a/web/detective/audio/voice/female/st_asked_5.ogg b/web/detective/audio/voice/female/st_asked_5.ogg new file mode 100644 index 00000000..b88291e3 Binary files /dev/null and b/web/detective/audio/voice/female/st_asked_5.ogg differ diff --git a/web/detective/audio/voice/female/st_asked_6.m4a b/web/detective/audio/voice/female/st_asked_6.m4a new file mode 100644 index 00000000..84ad8de2 Binary files /dev/null and b/web/detective/audio/voice/female/st_asked_6.m4a differ diff --git a/web/detective/audio/voice/female/st_asked_6.ogg b/web/detective/audio/voice/female/st_asked_6.ogg new file mode 100644 index 00000000..3aed624a Binary files /dev/null and b/web/detective/audio/voice/female/st_asked_6.ogg differ diff --git a/web/detective/audio/voice/female/st_doubt_0.m4a b/web/detective/audio/voice/female/st_doubt_0.m4a new file mode 100644 index 00000000..ab26c0dc Binary files /dev/null and b/web/detective/audio/voice/female/st_doubt_0.m4a differ diff --git a/web/detective/audio/voice/female/st_doubt_0.ogg b/web/detective/audio/voice/female/st_doubt_0.ogg new file mode 100644 index 00000000..6d673712 Binary files /dev/null and b/web/detective/audio/voice/female/st_doubt_0.ogg differ diff --git a/web/detective/audio/voice/female/st_doubt_1.m4a b/web/detective/audio/voice/female/st_doubt_1.m4a new file mode 100644 index 00000000..0a170214 Binary files /dev/null and b/web/detective/audio/voice/female/st_doubt_1.m4a differ diff --git a/web/detective/audio/voice/female/st_doubt_1.ogg b/web/detective/audio/voice/female/st_doubt_1.ogg new file mode 100644 index 00000000..7bffc88e Binary files /dev/null and b/web/detective/audio/voice/female/st_doubt_1.ogg differ diff --git a/web/detective/audio/voice/female/st_doubt_2.m4a b/web/detective/audio/voice/female/st_doubt_2.m4a new file mode 100644 index 00000000..82ae596d Binary files /dev/null and b/web/detective/audio/voice/female/st_doubt_2.m4a differ diff --git a/web/detective/audio/voice/female/st_doubt_2.ogg b/web/detective/audio/voice/female/st_doubt_2.ogg new file mode 100644 index 00000000..77b35c5b Binary files /dev/null and b/web/detective/audio/voice/female/st_doubt_2.ogg differ diff --git a/web/detective/audio/voice/female/st_doubt_3.m4a b/web/detective/audio/voice/female/st_doubt_3.m4a new file mode 100644 index 00000000..7f7d9296 Binary files /dev/null and b/web/detective/audio/voice/female/st_doubt_3.m4a differ diff --git a/web/detective/audio/voice/female/st_doubt_3.ogg b/web/detective/audio/voice/female/st_doubt_3.ogg new file mode 100644 index 00000000..94086275 Binary files /dev/null and b/web/detective/audio/voice/female/st_doubt_3.ogg differ diff --git a/web/detective/audio/voice/female/st_doubt_4.m4a b/web/detective/audio/voice/female/st_doubt_4.m4a new file mode 100644 index 00000000..d0354655 Binary files /dev/null and b/web/detective/audio/voice/female/st_doubt_4.m4a differ diff --git a/web/detective/audio/voice/female/st_doubt_4.ogg b/web/detective/audio/voice/female/st_doubt_4.ogg new file mode 100644 index 00000000..0ba93d8a Binary files /dev/null and b/web/detective/audio/voice/female/st_doubt_4.ogg differ diff --git a/web/detective/audio/voice/female/st_miss_0.m4a b/web/detective/audio/voice/female/st_miss_0.m4a new file mode 100644 index 00000000..15fa4805 Binary files /dev/null and b/web/detective/audio/voice/female/st_miss_0.m4a differ diff --git a/web/detective/audio/voice/female/st_miss_0.ogg b/web/detective/audio/voice/female/st_miss_0.ogg new file mode 100644 index 00000000..720dc664 Binary files /dev/null and b/web/detective/audio/voice/female/st_miss_0.ogg differ diff --git a/web/detective/audio/voice/female/st_miss_1.m4a b/web/detective/audio/voice/female/st_miss_1.m4a new file mode 100644 index 00000000..81de92b5 Binary files /dev/null and b/web/detective/audio/voice/female/st_miss_1.m4a differ diff --git a/web/detective/audio/voice/female/st_miss_1.ogg b/web/detective/audio/voice/female/st_miss_1.ogg new file mode 100644 index 00000000..bb84b204 Binary files /dev/null and b/web/detective/audio/voice/female/st_miss_1.ogg differ diff --git a/web/detective/audio/voice/female/st_miss_2.m4a b/web/detective/audio/voice/female/st_miss_2.m4a new file mode 100644 index 00000000..dbd5ea5e Binary files /dev/null and b/web/detective/audio/voice/female/st_miss_2.m4a differ diff --git a/web/detective/audio/voice/female/st_miss_2.ogg b/web/detective/audio/voice/female/st_miss_2.ogg new file mode 100644 index 00000000..37166ce6 Binary files /dev/null and b/web/detective/audio/voice/female/st_miss_2.ogg differ diff --git a/web/detective/audio/voice/female/sys_continue_ask.m4a b/web/detective/audio/voice/female/sys_continue_ask.m4a new file mode 100644 index 00000000..7ecfdfc8 Binary files /dev/null and b/web/detective/audio/voice/female/sys_continue_ask.m4a differ diff --git a/web/detective/audio/voice/female/sys_continue_ask.ogg b/web/detective/audio/voice/female/sys_continue_ask.ogg new file mode 100644 index 00000000..35abd299 Binary files /dev/null and b/web/detective/audio/voice/female/sys_continue_ask.ogg differ diff --git a/web/detective/audio/voice/female/sys_help.m4a b/web/detective/audio/voice/female/sys_help.m4a new file mode 100644 index 00000000..12655733 Binary files /dev/null and b/web/detective/audio/voice/female/sys_help.m4a differ diff --git a/web/detective/audio/voice/female/sys_help.ogg b/web/detective/audio/voice/female/sys_help.ogg new file mode 100644 index 00000000..50a8640f Binary files /dev/null and b/web/detective/audio/voice/female/sys_help.ogg differ diff --git a/web/detective/audio/voice/female/sys_menu.m4a b/web/detective/audio/voice/female/sys_menu.m4a new file mode 100644 index 00000000..a98b60bf Binary files /dev/null and b/web/detective/audio/voice/female/sys_menu.m4a differ diff --git a/web/detective/audio/voice/female/sys_menu.ogg b/web/detective/audio/voice/female/sys_menu.ogg new file mode 100644 index 00000000..c4e24fc2 Binary files /dev/null and b/web/detective/audio/voice/female/sys_menu.ogg differ diff --git a/web/detective/audio/voice/female/sys_menu_cf.m4a b/web/detective/audio/voice/female/sys_menu_cf.m4a new file mode 100644 index 00000000..de797096 Binary files /dev/null and b/web/detective/audio/voice/female/sys_menu_cf.m4a differ diff --git a/web/detective/audio/voice/female/sys_menu_cf.ogg b/web/detective/audio/voice/female/sys_menu_cf.ogg new file mode 100644 index 00000000..8cf4720a Binary files /dev/null and b/web/detective/audio/voice/female/sys_menu_cf.ogg differ diff --git a/web/detective/audio/voice/female/sys_menu_help.m4a b/web/detective/audio/voice/female/sys_menu_help.m4a new file mode 100644 index 00000000..eab1623b Binary files /dev/null and b/web/detective/audio/voice/female/sys_menu_help.m4a differ diff --git a/web/detective/audio/voice/female/sys_menu_help.ogg b/web/detective/audio/voice/female/sys_menu_help.ogg new file mode 100644 index 00000000..b9568b42 Binary files /dev/null and b/web/detective/audio/voice/female/sys_menu_help.ogg differ diff --git a/web/detective/audio/voice/female/sys_menu_restart.m4a b/web/detective/audio/voice/female/sys_menu_restart.m4a new file mode 100644 index 00000000..72144c71 Binary files /dev/null and b/web/detective/audio/voice/female/sys_menu_restart.m4a differ diff --git a/web/detective/audio/voice/female/sys_menu_restart.ogg b/web/detective/audio/voice/female/sys_menu_restart.ogg new file mode 100644 index 00000000..ad4e6723 Binary files /dev/null and b/web/detective/audio/voice/female/sys_menu_restart.ogg differ diff --git a/web/detective/audio/voice/female/sys_menu_resume.m4a b/web/detective/audio/voice/female/sys_menu_resume.m4a new file mode 100644 index 00000000..a248ef0d Binary files /dev/null and b/web/detective/audio/voice/female/sys_menu_resume.m4a differ diff --git a/web/detective/audio/voice/female/sys_menu_resume.ogg b/web/detective/audio/voice/female/sys_menu_resume.ogg new file mode 100644 index 00000000..344b533a Binary files /dev/null and b/web/detective/audio/voice/female/sys_menu_resume.ogg differ diff --git a/web/detective/audio/voice/female/sys_menu_save.m4a b/web/detective/audio/voice/female/sys_menu_save.m4a new file mode 100644 index 00000000..62659d27 Binary files /dev/null and b/web/detective/audio/voice/female/sys_menu_save.m4a differ diff --git a/web/detective/audio/voice/female/sys_menu_save.ogg b/web/detective/audio/voice/female/sys_menu_save.ogg new file mode 100644 index 00000000..dcb9f613 Binary files /dev/null and b/web/detective/audio/voice/female/sys_menu_save.ogg differ diff --git a/web/detective/audio/voice/female/sys_new_case.m4a b/web/detective/audio/voice/female/sys_new_case.m4a new file mode 100644 index 00000000..520edf23 Binary files /dev/null and b/web/detective/audio/voice/female/sys_new_case.m4a differ diff --git a/web/detective/audio/voice/female/sys_new_case.ogg b/web/detective/audio/voice/female/sys_new_case.ogg new file mode 100644 index 00000000..b1a90fda Binary files /dev/null and b/web/detective/audio/voice/female/sys_new_case.ogg differ diff --git a/web/detective/audio/voice/female/sys_saved.m4a b/web/detective/audio/voice/female/sys_saved.m4a new file mode 100644 index 00000000..1456a591 Binary files /dev/null and b/web/detective/audio/voice/female/sys_saved.m4a differ diff --git a/web/detective/audio/voice/female/sys_saved.ogg b/web/detective/audio/voice/female/sys_saved.ogg new file mode 100644 index 00000000..41c73f72 Binary files /dev/null and b/web/detective/audio/voice/female/sys_saved.ogg differ diff --git a/web/detective/audio/voice/female/tut_cf_ok.m4a b/web/detective/audio/voice/female/tut_cf_ok.m4a new file mode 100644 index 00000000..0d0accad Binary files /dev/null and b/web/detective/audio/voice/female/tut_cf_ok.m4a differ diff --git a/web/detective/audio/voice/female/tut_cf_ok.ogg b/web/detective/audio/voice/female/tut_cf_ok.ogg new file mode 100644 index 00000000..66e8908d Binary files /dev/null and b/web/detective/audio/voice/female/tut_cf_ok.ogg differ diff --git a/web/detective/audio/voice/female/tut_cf_try.m4a b/web/detective/audio/voice/female/tut_cf_try.m4a new file mode 100644 index 00000000..46173f45 Binary files /dev/null and b/web/detective/audio/voice/female/tut_cf_try.m4a differ diff --git a/web/detective/audio/voice/female/tut_cf_try.ogg b/web/detective/audio/voice/female/tut_cf_try.ogg new file mode 100644 index 00000000..359fc284 Binary files /dev/null and b/web/detective/audio/voice/female/tut_cf_try.ogg differ diff --git a/web/detective/audio/voice/female/tut_confront.m4a b/web/detective/audio/voice/female/tut_confront.m4a new file mode 100644 index 00000000..6d1bcca7 Binary files /dev/null and b/web/detective/audio/voice/female/tut_confront.m4a differ diff --git a/web/detective/audio/voice/female/tut_confront.ogg b/web/detective/audio/voice/female/tut_confront.ogg new file mode 100644 index 00000000..ce68c597 Binary files /dev/null and b/web/detective/audio/voice/female/tut_confront.ogg differ diff --git a/web/detective/audio/voice/female/tut_d0.m4a b/web/detective/audio/voice/female/tut_d0.m4a new file mode 100644 index 00000000..47889288 Binary files /dev/null and b/web/detective/audio/voice/female/tut_d0.m4a differ diff --git a/web/detective/audio/voice/female/tut_d0.ogg b/web/detective/audio/voice/female/tut_d0.ogg new file mode 100644 index 00000000..b021da94 Binary files /dev/null and b/web/detective/audio/voice/female/tut_d0.ogg differ diff --git a/web/detective/audio/voice/female/tut_done.m4a b/web/detective/audio/voice/female/tut_done.m4a new file mode 100644 index 00000000..3056316b Binary files /dev/null and b/web/detective/audio/voice/female/tut_done.m4a differ diff --git a/web/detective/audio/voice/female/tut_done.ogg b/web/detective/audio/voice/female/tut_done.ogg new file mode 100644 index 00000000..452ff99f Binary files /dev/null and b/web/detective/audio/voice/female/tut_done.ogg differ diff --git a/web/detective/audio/voice/female/tut_listen.m4a b/web/detective/audio/voice/female/tut_listen.m4a new file mode 100644 index 00000000..2d0842fb Binary files /dev/null and b/web/detective/audio/voice/female/tut_listen.m4a differ diff --git a/web/detective/audio/voice/female/tut_listen.ogg b/web/detective/audio/voice/female/tut_listen.ogg new file mode 100644 index 00000000..490d2bad Binary files /dev/null and b/web/detective/audio/voice/female/tut_listen.ogg differ diff --git a/web/detective/audio/voice/female/tut_replay.m4a b/web/detective/audio/voice/female/tut_replay.m4a new file mode 100644 index 00000000..56350316 Binary files /dev/null and b/web/detective/audio/voice/female/tut_replay.m4a differ diff --git a/web/detective/audio/voice/female/tut_replay.ogg b/web/detective/audio/voice/female/tut_replay.ogg new file mode 100644 index 00000000..5f0eaaa8 Binary files /dev/null and b/web/detective/audio/voice/female/tut_replay.ogg differ diff --git a/web/detective/audio/voice/female/tut_skip.m4a b/web/detective/audio/voice/female/tut_skip.m4a new file mode 100644 index 00000000..1532fead Binary files /dev/null and b/web/detective/audio/voice/female/tut_skip.m4a differ diff --git a/web/detective/audio/voice/female/tut_skip.ogg b/web/detective/audio/voice/female/tut_skip.ogg new file mode 100644 index 00000000..b94f6f12 Binary files /dev/null and b/web/detective/audio/voice/female/tut_skip.ogg differ diff --git a/web/detective/audio/voice/female/tut_status.m4a b/web/detective/audio/voice/female/tut_status.m4a new file mode 100644 index 00000000..dcda45a4 Binary files /dev/null and b/web/detective/audio/voice/female/tut_status.m4a differ diff --git a/web/detective/audio/voice/female/tut_status.ogg b/web/detective/audio/voice/female/tut_status.ogg new file mode 100644 index 00000000..a3e98d9f Binary files /dev/null and b/web/detective/audio/voice/female/tut_status.ogg differ diff --git a/web/detective/audio/voice/female/wp_1.m4a b/web/detective/audio/voice/female/wp_1.m4a new file mode 100644 index 00000000..e86420ae Binary files /dev/null and b/web/detective/audio/voice/female/wp_1.m4a differ diff --git a/web/detective/audio/voice/female/wp_1.ogg b/web/detective/audio/voice/female/wp_1.ogg new file mode 100644 index 00000000..c996f2ec Binary files /dev/null and b/web/detective/audio/voice/female/wp_1.ogg differ diff --git a/web/detective/audio/voice/female/wp_2.m4a b/web/detective/audio/voice/female/wp_2.m4a new file mode 100644 index 00000000..c4db9c32 Binary files /dev/null and b/web/detective/audio/voice/female/wp_2.m4a differ diff --git a/web/detective/audio/voice/female/wp_2.ogg b/web/detective/audio/voice/female/wp_2.ogg new file mode 100644 index 00000000..9faeb652 Binary files /dev/null and b/web/detective/audio/voice/female/wp_2.ogg differ diff --git a/web/detective/audio/voice/female/wp_3.m4a b/web/detective/audio/voice/female/wp_3.m4a new file mode 100644 index 00000000..b0cd74f4 Binary files /dev/null and b/web/detective/audio/voice/female/wp_3.m4a differ diff --git a/web/detective/audio/voice/female/wp_3.ogg b/web/detective/audio/voice/female/wp_3.ogg new file mode 100644 index 00000000..bd348978 Binary files /dev/null and b/web/detective/audio/voice/female/wp_3.ogg differ diff --git a/web/detective/audio/voice/female/wp_4.m4a b/web/detective/audio/voice/female/wp_4.m4a new file mode 100644 index 00000000..5ad40f0c Binary files /dev/null and b/web/detective/audio/voice/female/wp_4.m4a differ diff --git a/web/detective/audio/voice/female/wp_4.ogg b/web/detective/audio/voice/female/wp_4.ogg new file mode 100644 index 00000000..7e74d9b1 Binary files /dev/null and b/web/detective/audio/voice/female/wp_4.ogg differ diff --git a/web/detective/audio/voice/fo_1.m4a b/web/detective/audio/voice/fo_1.m4a new file mode 100644 index 00000000..94bf46da Binary files /dev/null and b/web/detective/audio/voice/fo_1.m4a differ diff --git a/web/detective/audio/voice/fo_1.ogg b/web/detective/audio/voice/fo_1.ogg new file mode 100644 index 00000000..1eb81800 Binary files /dev/null and b/web/detective/audio/voice/fo_1.ogg differ diff --git a/web/detective/audio/voice/fo_2.m4a b/web/detective/audio/voice/fo_2.m4a new file mode 100644 index 00000000..c0e32916 Binary files /dev/null and b/web/detective/audio/voice/fo_2.m4a differ diff --git a/web/detective/audio/voice/fo_2.ogg b/web/detective/audio/voice/fo_2.ogg new file mode 100644 index 00000000..c22f826e Binary files /dev/null and b/web/detective/audio/voice/fo_2.ogg differ diff --git a/web/detective/audio/voice/fo_3.m4a b/web/detective/audio/voice/fo_3.m4a new file mode 100644 index 00000000..6bd5ed4f Binary files /dev/null and b/web/detective/audio/voice/fo_3.m4a differ diff --git a/web/detective/audio/voice/fo_3.ogg b/web/detective/audio/voice/fo_3.ogg new file mode 100644 index 00000000..a78ec9e7 Binary files /dev/null and b/web/detective/audio/voice/fo_3.ogg differ diff --git a/web/detective/audio/voice/fo_4.m4a b/web/detective/audio/voice/fo_4.m4a new file mode 100644 index 00000000..22a227f2 Binary files /dev/null and b/web/detective/audio/voice/fo_4.m4a differ diff --git a/web/detective/audio/voice/fo_4.ogg b/web/detective/audio/voice/fo_4.ogg new file mode 100644 index 00000000..8351ecaa Binary files /dev/null and b/web/detective/audio/voice/fo_4.ogg differ diff --git a/web/detective/audio/voice/fo_5.m4a b/web/detective/audio/voice/fo_5.m4a new file mode 100644 index 00000000..be8b3930 Binary files /dev/null and b/web/detective/audio/voice/fo_5.m4a differ diff --git a/web/detective/audio/voice/fo_5.ogg b/web/detective/audio/voice/fo_5.ogg new file mode 100644 index 00000000..53efa40a Binary files /dev/null and b/web/detective/audio/voice/fo_5.ogg differ diff --git a/web/detective/audio/voice/male/b_hint_c1.m4a b/web/detective/audio/voice/male/b_hint_c1.m4a new file mode 100644 index 00000000..a4d26a67 Binary files /dev/null and b/web/detective/audio/voice/male/b_hint_c1.m4a differ diff --git a/web/detective/audio/voice/male/b_hint_c1.ogg b/web/detective/audio/voice/male/b_hint_c1.ogg new file mode 100644 index 00000000..f90ffdc2 Binary files /dev/null and b/web/detective/audio/voice/male/b_hint_c1.ogg differ diff --git a/web/detective/audio/voice/male/b_hint_c2.m4a b/web/detective/audio/voice/male/b_hint_c2.m4a new file mode 100644 index 00000000..e4bba569 Binary files /dev/null and b/web/detective/audio/voice/male/b_hint_c2.m4a differ diff --git a/web/detective/audio/voice/male/b_hint_c2.ogg b/web/detective/audio/voice/male/b_hint_c2.ogg new file mode 100644 index 00000000..bd13ddfd Binary files /dev/null and b/web/detective/audio/voice/male/b_hint_c2.ogg differ diff --git a/web/detective/audio/voice/male/cf_confirm.m4a b/web/detective/audio/voice/male/cf_confirm.m4a new file mode 100644 index 00000000..8ca826c2 Binary files /dev/null and b/web/detective/audio/voice/male/cf_confirm.m4a differ diff --git a/web/detective/audio/voice/male/cf_confirm.ogg b/web/detective/audio/voice/male/cf_confirm.ogg new file mode 100644 index 00000000..8639108c Binary files /dev/null and b/web/detective/audio/voice/male/cf_confirm.ogg differ diff --git a/web/detective/audio/voice/male/cf_diff.m4a b/web/detective/audio/voice/male/cf_diff.m4a new file mode 100644 index 00000000..6c7aff7f Binary files /dev/null and b/web/detective/audio/voice/male/cf_diff.m4a differ diff --git a/web/detective/audio/voice/male/cf_diff.ogg b/web/detective/audio/voice/male/cf_diff.ogg new file mode 100644 index 00000000..0654d386 Binary files /dev/null and b/web/detective/audio/voice/male/cf_diff.ogg differ diff --git a/web/detective/audio/voice/male/cf_hit.m4a b/web/detective/audio/voice/male/cf_hit.m4a new file mode 100644 index 00000000..02ab54b8 Binary files /dev/null and b/web/detective/audio/voice/male/cf_hit.m4a differ diff --git a/web/detective/audio/voice/male/cf_hit.ogg b/web/detective/audio/voice/male/cf_hit.ogg new file mode 100644 index 00000000..00af2ad2 Binary files /dev/null and b/web/detective/audio/voice/male/cf_hit.ogg differ diff --git a/web/detective/audio/voice/male/cf_pick_line.m4a b/web/detective/audio/voice/male/cf_pick_line.m4a new file mode 100644 index 00000000..64334c5e Binary files /dev/null and b/web/detective/audio/voice/male/cf_pick_line.m4a differ diff --git a/web/detective/audio/voice/male/cf_pick_line.ogg b/web/detective/audio/voice/male/cf_pick_line.ogg new file mode 100644 index 00000000..b152ba3d Binary files /dev/null and b/web/detective/audio/voice/male/cf_pick_line.ogg differ diff --git a/web/detective/audio/voice/male/cf_same.m4a b/web/detective/audio/voice/male/cf_same.m4a new file mode 100644 index 00000000..927832fc Binary files /dev/null and b/web/detective/audio/voice/male/cf_same.m4a differ diff --git a/web/detective/audio/voice/male/cf_same.ogg b/web/detective/audio/voice/male/cf_same.ogg new file mode 100644 index 00000000..99cdd691 Binary files /dev/null and b/web/detective/audio/voice/male/cf_same.ogg differ diff --git a/web/detective/audio/voice/male/cf_second.m4a b/web/detective/audio/voice/male/cf_second.m4a new file mode 100644 index 00000000..827dc887 Binary files /dev/null and b/web/detective/audio/voice/male/cf_second.m4a differ diff --git a/web/detective/audio/voice/male/cf_second.ogg b/web/detective/audio/voice/male/cf_second.ogg new file mode 100644 index 00000000..067c12df Binary files /dev/null and b/web/detective/audio/voice/male/cf_second.ogg differ diff --git a/web/detective/audio/voice/male/cf_self.m4a b/web/detective/audio/voice/male/cf_self.m4a new file mode 100644 index 00000000..1e5e7510 Binary files /dev/null and b/web/detective/audio/voice/male/cf_self.m4a differ diff --git a/web/detective/audio/voice/male/cf_self.ogg b/web/detective/audio/voice/male/cf_self.ogg new file mode 100644 index 00000000..34324526 Binary files /dev/null and b/web/detective/audio/voice/male/cf_self.ogg differ diff --git a/web/detective/audio/voice/male/cf_start.m4a b/web/detective/audio/voice/male/cf_start.m4a new file mode 100644 index 00000000..8faa22a4 Binary files /dev/null and b/web/detective/audio/voice/male/cf_start.m4a differ diff --git a/web/detective/audio/voice/male/cf_start.ogg b/web/detective/audio/voice/male/cf_start.ogg new file mode 100644 index 00000000..94051bb7 Binary files /dev/null and b/web/detective/audio/voice/male/cf_start.ogg differ diff --git a/web/detective/audio/voice/male/fin_ask_motive.m4a b/web/detective/audio/voice/male/fin_ask_motive.m4a new file mode 100644 index 00000000..cf0ba04c Binary files /dev/null and b/web/detective/audio/voice/male/fin_ask_motive.m4a differ diff --git a/web/detective/audio/voice/male/fin_ask_motive.ogg b/web/detective/audio/voice/male/fin_ask_motive.ogg new file mode 100644 index 00000000..711936e7 Binary files /dev/null and b/web/detective/audio/voice/male/fin_ask_motive.ogg differ diff --git a/web/detective/audio/voice/male/fin_ask_weapon.m4a b/web/detective/audio/voice/male/fin_ask_weapon.m4a new file mode 100644 index 00000000..e99db7b2 Binary files /dev/null and b/web/detective/audio/voice/male/fin_ask_weapon.m4a differ diff --git a/web/detective/audio/voice/male/fin_ask_weapon.ogg b/web/detective/audio/voice/male/fin_ask_weapon.ogg new file mode 100644 index 00000000..a03c9633 Binary files /dev/null and b/web/detective/audio/voice/male/fin_ask_weapon.ogg differ diff --git a/web/detective/audio/voice/male/fin_clear.m4a b/web/detective/audio/voice/male/fin_clear.m4a new file mode 100644 index 00000000..a8bf2b39 Binary files /dev/null and b/web/detective/audio/voice/male/fin_clear.m4a differ diff --git a/web/detective/audio/voice/male/fin_clear.ogg b/web/detective/audio/voice/male/fin_clear.ogg new file mode 100644 index 00000000..b616c321 Binary files /dev/null and b/web/detective/audio/voice/male/fin_clear.ogg differ diff --git a/web/detective/audio/voice/male/fin_mv_right.m4a b/web/detective/audio/voice/male/fin_mv_right.m4a new file mode 100644 index 00000000..532c3a73 Binary files /dev/null and b/web/detective/audio/voice/male/fin_mv_right.m4a differ diff --git a/web/detective/audio/voice/male/fin_mv_right.ogg b/web/detective/audio/voice/male/fin_mv_right.ogg new file mode 100644 index 00000000..650db140 Binary files /dev/null and b/web/detective/audio/voice/male/fin_mv_right.ogg differ diff --git a/web/detective/audio/voice/male/fin_mv_wrong.m4a b/web/detective/audio/voice/male/fin_mv_wrong.m4a new file mode 100644 index 00000000..1e8b4a6f Binary files /dev/null and b/web/detective/audio/voice/male/fin_mv_wrong.m4a differ diff --git a/web/detective/audio/voice/male/fin_mv_wrong.ogg b/web/detective/audio/voice/male/fin_mv_wrong.ogg new file mode 100644 index 00000000..b3e9f7ea Binary files /dev/null and b/web/detective/audio/voice/male/fin_mv_wrong.ogg differ diff --git a/web/detective/audio/voice/male/fin_right.m4a b/web/detective/audio/voice/male/fin_right.m4a new file mode 100644 index 00000000..4214e2fe Binary files /dev/null and b/web/detective/audio/voice/male/fin_right.m4a differ diff --git a/web/detective/audio/voice/male/fin_right.ogg b/web/detective/audio/voice/male/fin_right.ogg new file mode 100644 index 00000000..ac758eb0 Binary files /dev/null and b/web/detective/audio/voice/male/fin_right.ogg differ diff --git a/web/detective/audio/voice/male/fin_right_b.m4a b/web/detective/audio/voice/male/fin_right_b.m4a new file mode 100644 index 00000000..d6cc80e3 Binary files /dev/null and b/web/detective/audio/voice/male/fin_right_b.m4a differ diff --git a/web/detective/audio/voice/male/fin_right_b.ogg b/web/detective/audio/voice/male/fin_right_b.ogg new file mode 100644 index 00000000..7b297a88 Binary files /dev/null and b/web/detective/audio/voice/male/fin_right_b.ogg differ diff --git a/web/detective/audio/voice/male/fin_start.m4a b/web/detective/audio/voice/male/fin_start.m4a new file mode 100644 index 00000000..afa787d6 Binary files /dev/null and b/web/detective/audio/voice/male/fin_start.m4a differ diff --git a/web/detective/audio/voice/male/fin_start.ogg b/web/detective/audio/voice/male/fin_start.ogg new file mode 100644 index 00000000..f6cdc59c Binary files /dev/null and b/web/detective/audio/voice/male/fin_start.ogg differ diff --git a/web/detective/audio/voice/male/fin_who_confirm.m4a b/web/detective/audio/voice/male/fin_who_confirm.m4a new file mode 100644 index 00000000..b1ec9b63 Binary files /dev/null and b/web/detective/audio/voice/male/fin_who_confirm.m4a differ diff --git a/web/detective/audio/voice/male/fin_who_confirm.ogg b/web/detective/audio/voice/male/fin_who_confirm.ogg new file mode 100644 index 00000000..217f6363 Binary files /dev/null and b/web/detective/audio/voice/male/fin_who_confirm.ogg differ diff --git a/web/detective/audio/voice/male/fin_wp_right.m4a b/web/detective/audio/voice/male/fin_wp_right.m4a new file mode 100644 index 00000000..c4e91f5b Binary files /dev/null and b/web/detective/audio/voice/male/fin_wp_right.m4a differ diff --git a/web/detective/audio/voice/male/fin_wp_right.ogg b/web/detective/audio/voice/male/fin_wp_right.ogg new file mode 100644 index 00000000..e3a5da5c Binary files /dev/null and b/web/detective/audio/voice/male/fin_wp_right.ogg differ diff --git a/web/detective/audio/voice/male/fin_wp_wrong.m4a b/web/detective/audio/voice/male/fin_wp_wrong.m4a new file mode 100644 index 00000000..79576805 Binary files /dev/null and b/web/detective/audio/voice/male/fin_wp_wrong.m4a differ diff --git a/web/detective/audio/voice/male/fin_wp_wrong.ogg b/web/detective/audio/voice/male/fin_wp_wrong.ogg new file mode 100644 index 00000000..411f0871 Binary files /dev/null and b/web/detective/audio/voice/male/fin_wp_wrong.ogg differ diff --git a/web/detective/audio/voice/male/gd_board.m4a b/web/detective/audio/voice/male/gd_board.m4a new file mode 100644 index 00000000..7572040c Binary files /dev/null and b/web/detective/audio/voice/male/gd_board.m4a differ diff --git a/web/detective/audio/voice/male/gd_board.ogg b/web/detective/audio/voice/male/gd_board.ogg new file mode 100644 index 00000000..a5e96b1e Binary files /dev/null and b/web/detective/audio/voice/male/gd_board.ogg differ diff --git a/web/detective/audio/voice/male/gd_cf.m4a b/web/detective/audio/voice/male/gd_cf.m4a new file mode 100644 index 00000000..6b78d914 Binary files /dev/null and b/web/detective/audio/voice/male/gd_cf.m4a differ diff --git a/web/detective/audio/voice/male/gd_cf.ogg b/web/detective/audio/voice/male/gd_cf.ogg new file mode 100644 index 00000000..fc61a879 Binary files /dev/null and b/web/detective/audio/voice/male/gd_cf.ogg differ diff --git a/web/detective/audio/voice/male/gd_fin.m4a b/web/detective/audio/voice/male/gd_fin.m4a new file mode 100644 index 00000000..4a07d07a Binary files /dev/null and b/web/detective/audio/voice/male/gd_fin.m4a differ diff --git a/web/detective/audio/voice/male/gd_fin.ogg b/web/detective/audio/voice/male/gd_fin.ogg new file mode 100644 index 00000000..778dbc1a Binary files /dev/null and b/web/detective/audio/voice/male/gd_fin.ogg differ diff --git a/web/detective/audio/voice/male/gd_invalid.m4a b/web/detective/audio/voice/male/gd_invalid.m4a new file mode 100644 index 00000000..abb283ea Binary files /dev/null and b/web/detective/audio/voice/male/gd_invalid.m4a differ diff --git a/web/detective/audio/voice/male/gd_invalid.ogg b/web/detective/audio/voice/male/gd_invalid.ogg new file mode 100644 index 00000000..a39768e9 Binary files /dev/null and b/web/detective/audio/voice/male/gd_invalid.ogg differ diff --git a/web/detective/audio/voice/male/hint_a.m4a b/web/detective/audio/voice/male/hint_a.m4a new file mode 100644 index 00000000..0a238237 Binary files /dev/null and b/web/detective/audio/voice/male/hint_a.m4a differ diff --git a/web/detective/audio/voice/male/hint_a.ogg b/web/detective/audio/voice/male/hint_a.ogg new file mode 100644 index 00000000..d8bf5f7e Binary files /dev/null and b/web/detective/audio/voice/male/hint_a.ogg differ diff --git a/web/detective/audio/voice/male/hint_b.m4a b/web/detective/audio/voice/male/hint_b.m4a new file mode 100644 index 00000000..399442f1 Binary files /dev/null and b/web/detective/audio/voice/male/hint_b.m4a differ diff --git a/web/detective/audio/voice/male/hint_b.ogg b/web/detective/audio/voice/male/hint_b.ogg new file mode 100644 index 00000000..3958a9a2 Binary files /dev/null and b/web/detective/audio/voice/male/hint_b.ogg differ diff --git a/web/detective/audio/voice/male/hint_c1.m4a b/web/detective/audio/voice/male/hint_c1.m4a new file mode 100644 index 00000000..94809bbb Binary files /dev/null and b/web/detective/audio/voice/male/hint_c1.m4a differ diff --git a/web/detective/audio/voice/male/hint_c1.ogg b/web/detective/audio/voice/male/hint_c1.ogg new file mode 100644 index 00000000..de395d0f Binary files /dev/null and b/web/detective/audio/voice/male/hint_c1.ogg differ diff --git a/web/detective/audio/voice/male/hint_c2.m4a b/web/detective/audio/voice/male/hint_c2.m4a new file mode 100644 index 00000000..3fc3eebe Binary files /dev/null and b/web/detective/audio/voice/male/hint_c2.m4a differ diff --git a/web/detective/audio/voice/male/hint_c2.ogg b/web/detective/audio/voice/male/hint_c2.ogg new file mode 100644 index 00000000..970a4a99 Binary files /dev/null and b/web/detective/audio/voice/male/hint_c2.ogg differ diff --git a/web/detective/audio/voice/male/hint_fin.m4a b/web/detective/audio/voice/male/hint_fin.m4a new file mode 100644 index 00000000..9cc3f376 Binary files /dev/null and b/web/detective/audio/voice/male/hint_fin.m4a differ diff --git a/web/detective/audio/voice/male/hint_fin.ogg b/web/detective/audio/voice/male/hint_fin.ogg new file mode 100644 index 00000000..8ce1afd7 Binary files /dev/null and b/web/detective/audio/voice/male/hint_fin.ogg differ diff --git a/web/detective/audio/voice/male/intro_1.m4a b/web/detective/audio/voice/male/intro_1.m4a new file mode 100644 index 00000000..315ea510 Binary files /dev/null and b/web/detective/audio/voice/male/intro_1.m4a differ diff --git a/web/detective/audio/voice/male/intro_1.ogg b/web/detective/audio/voice/male/intro_1.ogg new file mode 100644 index 00000000..e112149f Binary files /dev/null and b/web/detective/audio/voice/male/intro_1.ogg differ diff --git a/web/detective/audio/voice/male/intro_2.m4a b/web/detective/audio/voice/male/intro_2.m4a new file mode 100644 index 00000000..968c7db4 Binary files /dev/null and b/web/detective/audio/voice/male/intro_2.m4a differ diff --git a/web/detective/audio/voice/male/intro_2.ogg b/web/detective/audio/voice/male/intro_2.ogg new file mode 100644 index 00000000..ea8f48ca Binary files /dev/null and b/web/detective/audio/voice/male/intro_2.ogg differ diff --git a/web/detective/audio/voice/male/intro_3.m4a b/web/detective/audio/voice/male/intro_3.m4a new file mode 100644 index 00000000..2b45658e Binary files /dev/null and b/web/detective/audio/voice/male/intro_3.m4a differ diff --git a/web/detective/audio/voice/male/intro_3.ogg b/web/detective/audio/voice/male/intro_3.ogg new file mode 100644 index 00000000..ccc4cfe7 Binary files /dev/null and b/web/detective/audio/voice/male/intro_3.ogg differ diff --git a/web/detective/audio/voice/male/intro_w3.m4a b/web/detective/audio/voice/male/intro_w3.m4a new file mode 100644 index 00000000..9fb51f92 Binary files /dev/null and b/web/detective/audio/voice/male/intro_w3.m4a differ diff --git a/web/detective/audio/voice/male/intro_w3.ogg b/web/detective/audio/voice/male/intro_w3.ogg new file mode 100644 index 00000000..61a9b4ed Binary files /dev/null and b/web/detective/audio/voice/male/intro_w3.ogg differ diff --git a/web/detective/audio/voice/male/item_case.m4a b/web/detective/audio/voice/male/item_case.m4a new file mode 100644 index 00000000..5bf1e9ca Binary files /dev/null and b/web/detective/audio/voice/male/item_case.m4a differ diff --git a/web/detective/audio/voice/male/item_case.ogg b/web/detective/audio/voice/male/item_case.ogg new file mode 100644 index 00000000..a1552660 Binary files /dev/null and b/web/detective/audio/voice/male/item_case.ogg differ diff --git a/web/detective/audio/voice/male/item_fore.m4a b/web/detective/audio/voice/male/item_fore.m4a new file mode 100644 index 00000000..ed3b34fc Binary files /dev/null and b/web/detective/audio/voice/male/item_fore.m4a differ diff --git a/web/detective/audio/voice/male/item_fore.ogg b/web/detective/audio/voice/male/item_fore.ogg new file mode 100644 index 00000000..d4a5a4f8 Binary files /dev/null and b/web/detective/audio/voice/male/item_fore.ogg differ diff --git a/web/detective/audio/voice/male/item_scene.m4a b/web/detective/audio/voice/male/item_scene.m4a new file mode 100644 index 00000000..3479068d Binary files /dev/null and b/web/detective/audio/voice/male/item_scene.m4a differ diff --git a/web/detective/audio/voice/male/item_scene.ogg b/web/detective/audio/voice/male/item_scene.ogg new file mode 100644 index 00000000..5f724762 Binary files /dev/null and b/web/detective/audio/voice/male/item_scene.ogg differ diff --git a/web/detective/audio/voice/male/item_w1.m4a b/web/detective/audio/voice/male/item_w1.m4a new file mode 100644 index 00000000..7183eaa2 Binary files /dev/null and b/web/detective/audio/voice/male/item_w1.m4a differ diff --git a/web/detective/audio/voice/male/item_w1.ogg b/web/detective/audio/voice/male/item_w1.ogg new file mode 100644 index 00000000..66e153ce Binary files /dev/null and b/web/detective/audio/voice/male/item_w1.ogg differ diff --git a/web/detective/audio/voice/male/item_w2.m4a b/web/detective/audio/voice/male/item_w2.m4a new file mode 100644 index 00000000..2d3fbe80 Binary files /dev/null and b/web/detective/audio/voice/male/item_w2.m4a differ diff --git a/web/detective/audio/voice/male/item_w2.ogg b/web/detective/audio/voice/male/item_w2.ogg new file mode 100644 index 00000000..206ad4c2 Binary files /dev/null and b/web/detective/audio/voice/male/item_w2.ogg differ diff --git a/web/detective/audio/voice/male/item_w3.m4a b/web/detective/audio/voice/male/item_w3.m4a new file mode 100644 index 00000000..a3d5d20b Binary files /dev/null and b/web/detective/audio/voice/male/item_w3.m4a differ diff --git a/web/detective/audio/voice/male/item_w3.ogg b/web/detective/audio/voice/male/item_w3.ogg new file mode 100644 index 00000000..7581067b Binary files /dev/null and b/web/detective/audio/voice/male/item_w3.ogg differ diff --git a/web/detective/audio/voice/male/item_w4.m4a b/web/detective/audio/voice/male/item_w4.m4a new file mode 100644 index 00000000..3cef127a Binary files /dev/null and b/web/detective/audio/voice/male/item_w4.m4a differ diff --git a/web/detective/audio/voice/male/item_w4.ogg b/web/detective/audio/voice/male/item_w4.ogg new file mode 100644 index 00000000..4ad9dc07 Binary files /dev/null and b/web/detective/audio/voice/male/item_w4.ogg differ diff --git a/web/detective/audio/voice/male/key/cf_confirm.m4a b/web/detective/audio/voice/male/key/cf_confirm.m4a new file mode 100644 index 00000000..94201dcb Binary files /dev/null and b/web/detective/audio/voice/male/key/cf_confirm.m4a differ diff --git a/web/detective/audio/voice/male/key/cf_confirm.ogg b/web/detective/audio/voice/male/key/cf_confirm.ogg new file mode 100644 index 00000000..d192b39a Binary files /dev/null and b/web/detective/audio/voice/male/key/cf_confirm.ogg differ diff --git a/web/detective/audio/voice/male/key/cf_pick_line.m4a b/web/detective/audio/voice/male/key/cf_pick_line.m4a new file mode 100644 index 00000000..60c807fc Binary files /dev/null and b/web/detective/audio/voice/male/key/cf_pick_line.m4a differ diff --git a/web/detective/audio/voice/male/key/cf_pick_line.ogg b/web/detective/audio/voice/male/key/cf_pick_line.ogg new file mode 100644 index 00000000..d41472c7 Binary files /dev/null and b/web/detective/audio/voice/male/key/cf_pick_line.ogg differ diff --git a/web/detective/audio/voice/male/key/gd_board.m4a b/web/detective/audio/voice/male/key/gd_board.m4a new file mode 100644 index 00000000..c5364977 Binary files /dev/null and b/web/detective/audio/voice/male/key/gd_board.m4a differ diff --git a/web/detective/audio/voice/male/key/gd_board.ogg b/web/detective/audio/voice/male/key/gd_board.ogg new file mode 100644 index 00000000..4cc9a9b9 Binary files /dev/null and b/web/detective/audio/voice/male/key/gd_board.ogg differ diff --git a/web/detective/audio/voice/male/key/gd_cf.m4a b/web/detective/audio/voice/male/key/gd_cf.m4a new file mode 100644 index 00000000..21b85139 Binary files /dev/null and b/web/detective/audio/voice/male/key/gd_cf.m4a differ diff --git a/web/detective/audio/voice/male/key/gd_cf.ogg b/web/detective/audio/voice/male/key/gd_cf.ogg new file mode 100644 index 00000000..816c841c Binary files /dev/null and b/web/detective/audio/voice/male/key/gd_cf.ogg differ diff --git a/web/detective/audio/voice/male/key/settle_hook.m4a b/web/detective/audio/voice/male/key/settle_hook.m4a new file mode 100644 index 00000000..64d34019 Binary files /dev/null and b/web/detective/audio/voice/male/key/settle_hook.m4a differ diff --git a/web/detective/audio/voice/male/key/settle_hook.ogg b/web/detective/audio/voice/male/key/settle_hook.ogg new file mode 100644 index 00000000..0b2c8a8d Binary files /dev/null and b/web/detective/audio/voice/male/key/settle_hook.ogg differ diff --git a/web/detective/audio/voice/male/key/sys_help.m4a b/web/detective/audio/voice/male/key/sys_help.m4a new file mode 100644 index 00000000..6f25819f Binary files /dev/null and b/web/detective/audio/voice/male/key/sys_help.m4a differ diff --git a/web/detective/audio/voice/male/key/sys_help.ogg b/web/detective/audio/voice/male/key/sys_help.ogg new file mode 100644 index 00000000..ec6e8c64 Binary files /dev/null and b/web/detective/audio/voice/male/key/sys_help.ogg differ diff --git a/web/detective/audio/voice/male/key/tut_confront.m4a b/web/detective/audio/voice/male/key/tut_confront.m4a new file mode 100644 index 00000000..ec0e90a5 Binary files /dev/null and b/web/detective/audio/voice/male/key/tut_confront.m4a differ diff --git a/web/detective/audio/voice/male/key/tut_confront.ogg b/web/detective/audio/voice/male/key/tut_confront.ogg new file mode 100644 index 00000000..2101b93d Binary files /dev/null and b/web/detective/audio/voice/male/key/tut_confront.ogg differ diff --git a/web/detective/audio/voice/male/key/tut_d0.m4a b/web/detective/audio/voice/male/key/tut_d0.m4a new file mode 100644 index 00000000..c601f9bf Binary files /dev/null and b/web/detective/audio/voice/male/key/tut_d0.m4a differ diff --git a/web/detective/audio/voice/male/key/tut_d0.ogg b/web/detective/audio/voice/male/key/tut_d0.ogg new file mode 100644 index 00000000..cebdcf11 Binary files /dev/null and b/web/detective/audio/voice/male/key/tut_d0.ogg differ diff --git a/web/detective/audio/voice/male/key/tut_listen.m4a b/web/detective/audio/voice/male/key/tut_listen.m4a new file mode 100644 index 00000000..50c1a125 Binary files /dev/null and b/web/detective/audio/voice/male/key/tut_listen.m4a differ diff --git a/web/detective/audio/voice/male/key/tut_listen.ogg b/web/detective/audio/voice/male/key/tut_listen.ogg new file mode 100644 index 00000000..cf87a087 Binary files /dev/null and b/web/detective/audio/voice/male/key/tut_listen.ogg differ diff --git a/web/detective/audio/voice/male/key/tut_replay.m4a b/web/detective/audio/voice/male/key/tut_replay.m4a new file mode 100644 index 00000000..29572138 Binary files /dev/null and b/web/detective/audio/voice/male/key/tut_replay.m4a differ diff --git a/web/detective/audio/voice/male/key/tut_replay.ogg b/web/detective/audio/voice/male/key/tut_replay.ogg new file mode 100644 index 00000000..6f921fae Binary files /dev/null and b/web/detective/audio/voice/male/key/tut_replay.ogg differ diff --git a/web/detective/audio/voice/male/mv_1.m4a b/web/detective/audio/voice/male/mv_1.m4a new file mode 100644 index 00000000..08bbf65c Binary files /dev/null and b/web/detective/audio/voice/male/mv_1.m4a differ diff --git a/web/detective/audio/voice/male/mv_1.ogg b/web/detective/audio/voice/male/mv_1.ogg new file mode 100644 index 00000000..c7dec861 Binary files /dev/null and b/web/detective/audio/voice/male/mv_1.ogg differ diff --git a/web/detective/audio/voice/male/mv_2.m4a b/web/detective/audio/voice/male/mv_2.m4a new file mode 100644 index 00000000..3b6e4df5 Binary files /dev/null and b/web/detective/audio/voice/male/mv_2.m4a differ diff --git a/web/detective/audio/voice/male/mv_2.ogg b/web/detective/audio/voice/male/mv_2.ogg new file mode 100644 index 00000000..4e6703d0 Binary files /dev/null and b/web/detective/audio/voice/male/mv_2.ogg differ diff --git a/web/detective/audio/voice/male/mv_3.m4a b/web/detective/audio/voice/male/mv_3.m4a new file mode 100644 index 00000000..5e9314c3 Binary files /dev/null and b/web/detective/audio/voice/male/mv_3.m4a differ diff --git a/web/detective/audio/voice/male/mv_3.ogg b/web/detective/audio/voice/male/mv_3.ogg new file mode 100644 index 00000000..4fe55206 Binary files /dev/null and b/web/detective/audio/voice/male/mv_3.ogg differ diff --git a/web/detective/audio/voice/male/rate_a.m4a b/web/detective/audio/voice/male/rate_a.m4a new file mode 100644 index 00000000..062e61a9 Binary files /dev/null and b/web/detective/audio/voice/male/rate_a.m4a differ diff --git a/web/detective/audio/voice/male/rate_a.ogg b/web/detective/audio/voice/male/rate_a.ogg new file mode 100644 index 00000000..3cbc7a82 Binary files /dev/null and b/web/detective/audio/voice/male/rate_a.ogg differ diff --git a/web/detective/audio/voice/male/rate_b.m4a b/web/detective/audio/voice/male/rate_b.m4a new file mode 100644 index 00000000..0bd76980 Binary files /dev/null and b/web/detective/audio/voice/male/rate_b.m4a differ diff --git a/web/detective/audio/voice/male/rate_b.ogg b/web/detective/audio/voice/male/rate_b.ogg new file mode 100644 index 00000000..fa95387f Binary files /dev/null and b/web/detective/audio/voice/male/rate_b.ogg differ diff --git a/web/detective/audio/voice/male/rate_c.m4a b/web/detective/audio/voice/male/rate_c.m4a new file mode 100644 index 00000000..59644029 Binary files /dev/null and b/web/detective/audio/voice/male/rate_c.m4a differ diff --git a/web/detective/audio/voice/male/rate_c.ogg b/web/detective/audio/voice/male/rate_c.ogg new file mode 100644 index 00000000..a4d4f4bc Binary files /dev/null and b/web/detective/audio/voice/male/rate_c.ogg differ diff --git a/web/detective/audio/voice/male/rate_s.m4a b/web/detective/audio/voice/male/rate_s.m4a new file mode 100644 index 00000000..3a9a4514 Binary files /dev/null and b/web/detective/audio/voice/male/rate_s.m4a differ diff --git a/web/detective/audio/voice/male/rate_s.ogg b/web/detective/audio/voice/male/rate_s.ogg new file mode 100644 index 00000000..348c7ec5 Binary files /dev/null and b/web/detective/audio/voice/male/rate_s.ogg differ diff --git a/web/detective/audio/voice/male/recap_go.m4a b/web/detective/audio/voice/male/recap_go.m4a new file mode 100644 index 00000000..ae3a504a Binary files /dev/null and b/web/detective/audio/voice/male/recap_go.m4a differ diff --git a/web/detective/audio/voice/male/recap_go.ogg b/web/detective/audio/voice/male/recap_go.ogg new file mode 100644 index 00000000..bcc1c582 Binary files /dev/null and b/web/detective/audio/voice/male/recap_go.ogg differ diff --git a/web/detective/audio/voice/male/sc_0.m4a b/web/detective/audio/voice/male/sc_0.m4a new file mode 100644 index 00000000..e1b22d96 Binary files /dev/null and b/web/detective/audio/voice/male/sc_0.m4a differ diff --git a/web/detective/audio/voice/male/sc_0.ogg b/web/detective/audio/voice/male/sc_0.ogg new file mode 100644 index 00000000..40b636e1 Binary files /dev/null and b/web/detective/audio/voice/male/sc_0.ogg differ diff --git a/web/detective/audio/voice/male/sc_1.m4a b/web/detective/audio/voice/male/sc_1.m4a new file mode 100644 index 00000000..eb12ff4c Binary files /dev/null and b/web/detective/audio/voice/male/sc_1.m4a differ diff --git a/web/detective/audio/voice/male/sc_1.ogg b/web/detective/audio/voice/male/sc_1.ogg new file mode 100644 index 00000000..a70b2b1d Binary files /dev/null and b/web/detective/audio/voice/male/sc_1.ogg differ diff --git a/web/detective/audio/voice/male/sc_2.m4a b/web/detective/audio/voice/male/sc_2.m4a new file mode 100644 index 00000000..39ed6655 Binary files /dev/null and b/web/detective/audio/voice/male/sc_2.m4a differ diff --git a/web/detective/audio/voice/male/sc_2.ogg b/web/detective/audio/voice/male/sc_2.ogg new file mode 100644 index 00000000..8d8fab8b Binary files /dev/null and b/web/detective/audio/voice/male/sc_2.ogg differ diff --git a/web/detective/audio/voice/male/sc_3.m4a b/web/detective/audio/voice/male/sc_3.m4a new file mode 100644 index 00000000..4e652271 Binary files /dev/null and b/web/detective/audio/voice/male/sc_3.m4a differ diff --git a/web/detective/audio/voice/male/sc_3.ogg b/web/detective/audio/voice/male/sc_3.ogg new file mode 100644 index 00000000..19c957b3 Binary files /dev/null and b/web/detective/audio/voice/male/sc_3.ogg differ diff --git a/web/detective/audio/voice/male/sc_4.m4a b/web/detective/audio/voice/male/sc_4.m4a new file mode 100644 index 00000000..3ffb7a7f Binary files /dev/null and b/web/detective/audio/voice/male/sc_4.m4a differ diff --git a/web/detective/audio/voice/male/sc_4.ogg b/web/detective/audio/voice/male/sc_4.ogg new file mode 100644 index 00000000..2262139f Binary files /dev/null and b/web/detective/audio/voice/male/sc_4.ogg differ diff --git a/web/detective/audio/voice/male/sc_5.m4a b/web/detective/audio/voice/male/sc_5.m4a new file mode 100644 index 00000000..8f41c1ce Binary files /dev/null and b/web/detective/audio/voice/male/sc_5.m4a differ diff --git a/web/detective/audio/voice/male/sc_5.ogg b/web/detective/audio/voice/male/sc_5.ogg new file mode 100644 index 00000000..12380d25 Binary files /dev/null and b/web/detective/audio/voice/male/sc_5.ogg differ diff --git a/web/detective/audio/voice/male/settings_hint.m4a b/web/detective/audio/voice/male/settings_hint.m4a new file mode 100644 index 00000000..91ae0d65 Binary files /dev/null and b/web/detective/audio/voice/male/settings_hint.m4a differ diff --git a/web/detective/audio/voice/male/settings_hint.ogg b/web/detective/audio/voice/male/settings_hint.ogg new file mode 100644 index 00000000..48d186bb Binary files /dev/null and b/web/detective/audio/voice/male/settings_hint.ogg differ diff --git a/web/detective/audio/voice/male/settle_hook.m4a b/web/detective/audio/voice/male/settle_hook.m4a new file mode 100644 index 00000000..944692d8 Binary files /dev/null and b/web/detective/audio/voice/male/settle_hook.m4a differ diff --git a/web/detective/audio/voice/male/settle_hook.ogg b/web/detective/audio/voice/male/settle_hook.ogg new file mode 100644 index 00000000..cd45b20f Binary files /dev/null and b/web/detective/audio/voice/male/settle_hook.ogg differ diff --git a/web/detective/audio/voice/male/settle_open.m4a b/web/detective/audio/voice/male/settle_open.m4a new file mode 100644 index 00000000..eec4f379 Binary files /dev/null and b/web/detective/audio/voice/male/settle_open.m4a differ diff --git a/web/detective/audio/voice/male/settle_open.ogg b/web/detective/audio/voice/male/settle_open.ogg new file mode 100644 index 00000000..400b0b79 Binary files /dev/null and b/web/detective/audio/voice/male/settle_open.ogg differ diff --git a/web/detective/audio/voice/male/st_asked_0.m4a b/web/detective/audio/voice/male/st_asked_0.m4a new file mode 100644 index 00000000..762c6857 Binary files /dev/null and b/web/detective/audio/voice/male/st_asked_0.m4a differ diff --git a/web/detective/audio/voice/male/st_asked_0.ogg b/web/detective/audio/voice/male/st_asked_0.ogg new file mode 100644 index 00000000..08cd9fa3 Binary files /dev/null and b/web/detective/audio/voice/male/st_asked_0.ogg differ diff --git a/web/detective/audio/voice/male/st_asked_1.m4a b/web/detective/audio/voice/male/st_asked_1.m4a new file mode 100644 index 00000000..18acc723 Binary files /dev/null and b/web/detective/audio/voice/male/st_asked_1.m4a differ diff --git a/web/detective/audio/voice/male/st_asked_1.ogg b/web/detective/audio/voice/male/st_asked_1.ogg new file mode 100644 index 00000000..63c907d2 Binary files /dev/null and b/web/detective/audio/voice/male/st_asked_1.ogg differ diff --git a/web/detective/audio/voice/male/st_asked_2.m4a b/web/detective/audio/voice/male/st_asked_2.m4a new file mode 100644 index 00000000..8f3a29d9 Binary files /dev/null and b/web/detective/audio/voice/male/st_asked_2.m4a differ diff --git a/web/detective/audio/voice/male/st_asked_2.ogg b/web/detective/audio/voice/male/st_asked_2.ogg new file mode 100644 index 00000000..149fd011 Binary files /dev/null and b/web/detective/audio/voice/male/st_asked_2.ogg differ diff --git a/web/detective/audio/voice/male/st_asked_3.m4a b/web/detective/audio/voice/male/st_asked_3.m4a new file mode 100644 index 00000000..9d8f63fb Binary files /dev/null and b/web/detective/audio/voice/male/st_asked_3.m4a differ diff --git a/web/detective/audio/voice/male/st_asked_3.ogg b/web/detective/audio/voice/male/st_asked_3.ogg new file mode 100644 index 00000000..c582ebc5 Binary files /dev/null and b/web/detective/audio/voice/male/st_asked_3.ogg differ diff --git a/web/detective/audio/voice/male/st_asked_4.m4a b/web/detective/audio/voice/male/st_asked_4.m4a new file mode 100644 index 00000000..ee8646aa Binary files /dev/null and b/web/detective/audio/voice/male/st_asked_4.m4a differ diff --git a/web/detective/audio/voice/male/st_asked_4.ogg b/web/detective/audio/voice/male/st_asked_4.ogg new file mode 100644 index 00000000..675bee71 Binary files /dev/null and b/web/detective/audio/voice/male/st_asked_4.ogg differ diff --git a/web/detective/audio/voice/male/st_asked_5.m4a b/web/detective/audio/voice/male/st_asked_5.m4a new file mode 100644 index 00000000..4c996229 Binary files /dev/null and b/web/detective/audio/voice/male/st_asked_5.m4a differ diff --git a/web/detective/audio/voice/male/st_asked_5.ogg b/web/detective/audio/voice/male/st_asked_5.ogg new file mode 100644 index 00000000..609558c0 Binary files /dev/null and b/web/detective/audio/voice/male/st_asked_5.ogg differ diff --git a/web/detective/audio/voice/male/st_asked_6.m4a b/web/detective/audio/voice/male/st_asked_6.m4a new file mode 100644 index 00000000..1af8c43a Binary files /dev/null and b/web/detective/audio/voice/male/st_asked_6.m4a differ diff --git a/web/detective/audio/voice/male/st_asked_6.ogg b/web/detective/audio/voice/male/st_asked_6.ogg new file mode 100644 index 00000000..5d74aa43 Binary files /dev/null and b/web/detective/audio/voice/male/st_asked_6.ogg differ diff --git a/web/detective/audio/voice/male/st_doubt_0.m4a b/web/detective/audio/voice/male/st_doubt_0.m4a new file mode 100644 index 00000000..a5ded065 Binary files /dev/null and b/web/detective/audio/voice/male/st_doubt_0.m4a differ diff --git a/web/detective/audio/voice/male/st_doubt_0.ogg b/web/detective/audio/voice/male/st_doubt_0.ogg new file mode 100644 index 00000000..bb73a14b Binary files /dev/null and b/web/detective/audio/voice/male/st_doubt_0.ogg differ diff --git a/web/detective/audio/voice/male/st_doubt_1.m4a b/web/detective/audio/voice/male/st_doubt_1.m4a new file mode 100644 index 00000000..d5e4eb76 Binary files /dev/null and b/web/detective/audio/voice/male/st_doubt_1.m4a differ diff --git a/web/detective/audio/voice/male/st_doubt_1.ogg b/web/detective/audio/voice/male/st_doubt_1.ogg new file mode 100644 index 00000000..8ce2fc4c Binary files /dev/null and b/web/detective/audio/voice/male/st_doubt_1.ogg differ diff --git a/web/detective/audio/voice/male/st_doubt_2.m4a b/web/detective/audio/voice/male/st_doubt_2.m4a new file mode 100644 index 00000000..64ebe3a1 Binary files /dev/null and b/web/detective/audio/voice/male/st_doubt_2.m4a differ diff --git a/web/detective/audio/voice/male/st_doubt_2.ogg b/web/detective/audio/voice/male/st_doubt_2.ogg new file mode 100644 index 00000000..3f99a9a1 Binary files /dev/null and b/web/detective/audio/voice/male/st_doubt_2.ogg differ diff --git a/web/detective/audio/voice/male/st_doubt_3.m4a b/web/detective/audio/voice/male/st_doubt_3.m4a new file mode 100644 index 00000000..380f6c86 Binary files /dev/null and b/web/detective/audio/voice/male/st_doubt_3.m4a differ diff --git a/web/detective/audio/voice/male/st_doubt_3.ogg b/web/detective/audio/voice/male/st_doubt_3.ogg new file mode 100644 index 00000000..94d678cf Binary files /dev/null and b/web/detective/audio/voice/male/st_doubt_3.ogg differ diff --git a/web/detective/audio/voice/male/st_doubt_4.m4a b/web/detective/audio/voice/male/st_doubt_4.m4a new file mode 100644 index 00000000..6b9d292a Binary files /dev/null and b/web/detective/audio/voice/male/st_doubt_4.m4a differ diff --git a/web/detective/audio/voice/male/st_doubt_4.ogg b/web/detective/audio/voice/male/st_doubt_4.ogg new file mode 100644 index 00000000..ed98a556 Binary files /dev/null and b/web/detective/audio/voice/male/st_doubt_4.ogg differ diff --git a/web/detective/audio/voice/male/st_miss_0.m4a b/web/detective/audio/voice/male/st_miss_0.m4a new file mode 100644 index 00000000..a44dc33d Binary files /dev/null and b/web/detective/audio/voice/male/st_miss_0.m4a differ diff --git a/web/detective/audio/voice/male/st_miss_0.ogg b/web/detective/audio/voice/male/st_miss_0.ogg new file mode 100644 index 00000000..1e35ea50 Binary files /dev/null and b/web/detective/audio/voice/male/st_miss_0.ogg differ diff --git a/web/detective/audio/voice/male/st_miss_1.m4a b/web/detective/audio/voice/male/st_miss_1.m4a new file mode 100644 index 00000000..58c8097d Binary files /dev/null and b/web/detective/audio/voice/male/st_miss_1.m4a differ diff --git a/web/detective/audio/voice/male/st_miss_1.ogg b/web/detective/audio/voice/male/st_miss_1.ogg new file mode 100644 index 00000000..c11b92c5 Binary files /dev/null and b/web/detective/audio/voice/male/st_miss_1.ogg differ diff --git a/web/detective/audio/voice/male/st_miss_2.m4a b/web/detective/audio/voice/male/st_miss_2.m4a new file mode 100644 index 00000000..6fc4f71d Binary files /dev/null and b/web/detective/audio/voice/male/st_miss_2.m4a differ diff --git a/web/detective/audio/voice/male/st_miss_2.ogg b/web/detective/audio/voice/male/st_miss_2.ogg new file mode 100644 index 00000000..db9705fa Binary files /dev/null and b/web/detective/audio/voice/male/st_miss_2.ogg differ diff --git a/web/detective/audio/voice/male/sys_continue_ask.m4a b/web/detective/audio/voice/male/sys_continue_ask.m4a new file mode 100644 index 00000000..c2062d68 Binary files /dev/null and b/web/detective/audio/voice/male/sys_continue_ask.m4a differ diff --git a/web/detective/audio/voice/male/sys_continue_ask.ogg b/web/detective/audio/voice/male/sys_continue_ask.ogg new file mode 100644 index 00000000..12375738 Binary files /dev/null and b/web/detective/audio/voice/male/sys_continue_ask.ogg differ diff --git a/web/detective/audio/voice/male/sys_help.m4a b/web/detective/audio/voice/male/sys_help.m4a new file mode 100644 index 00000000..cc038872 Binary files /dev/null and b/web/detective/audio/voice/male/sys_help.m4a differ diff --git a/web/detective/audio/voice/male/sys_help.ogg b/web/detective/audio/voice/male/sys_help.ogg new file mode 100644 index 00000000..fb0ede73 Binary files /dev/null and b/web/detective/audio/voice/male/sys_help.ogg differ diff --git a/web/detective/audio/voice/male/sys_menu.m4a b/web/detective/audio/voice/male/sys_menu.m4a new file mode 100644 index 00000000..c22a6489 Binary files /dev/null and b/web/detective/audio/voice/male/sys_menu.m4a differ diff --git a/web/detective/audio/voice/male/sys_menu.ogg b/web/detective/audio/voice/male/sys_menu.ogg new file mode 100644 index 00000000..09258465 Binary files /dev/null and b/web/detective/audio/voice/male/sys_menu.ogg differ diff --git a/web/detective/audio/voice/male/sys_menu_cf.m4a b/web/detective/audio/voice/male/sys_menu_cf.m4a new file mode 100644 index 00000000..329c5e7a Binary files /dev/null and b/web/detective/audio/voice/male/sys_menu_cf.m4a differ diff --git a/web/detective/audio/voice/male/sys_menu_cf.ogg b/web/detective/audio/voice/male/sys_menu_cf.ogg new file mode 100644 index 00000000..214a53c7 Binary files /dev/null and b/web/detective/audio/voice/male/sys_menu_cf.ogg differ diff --git a/web/detective/audio/voice/male/sys_menu_help.m4a b/web/detective/audio/voice/male/sys_menu_help.m4a new file mode 100644 index 00000000..b6262f51 Binary files /dev/null and b/web/detective/audio/voice/male/sys_menu_help.m4a differ diff --git a/web/detective/audio/voice/male/sys_menu_help.ogg b/web/detective/audio/voice/male/sys_menu_help.ogg new file mode 100644 index 00000000..4ff3f9d7 Binary files /dev/null and b/web/detective/audio/voice/male/sys_menu_help.ogg differ diff --git a/web/detective/audio/voice/male/sys_menu_restart.m4a b/web/detective/audio/voice/male/sys_menu_restart.m4a new file mode 100644 index 00000000..cd557294 Binary files /dev/null and b/web/detective/audio/voice/male/sys_menu_restart.m4a differ diff --git a/web/detective/audio/voice/male/sys_menu_restart.ogg b/web/detective/audio/voice/male/sys_menu_restart.ogg new file mode 100644 index 00000000..c758b72b Binary files /dev/null and b/web/detective/audio/voice/male/sys_menu_restart.ogg differ diff --git a/web/detective/audio/voice/male/sys_menu_resume.m4a b/web/detective/audio/voice/male/sys_menu_resume.m4a new file mode 100644 index 00000000..5bfeb60c Binary files /dev/null and b/web/detective/audio/voice/male/sys_menu_resume.m4a differ diff --git a/web/detective/audio/voice/male/sys_menu_resume.ogg b/web/detective/audio/voice/male/sys_menu_resume.ogg new file mode 100644 index 00000000..9b7d8a27 Binary files /dev/null and b/web/detective/audio/voice/male/sys_menu_resume.ogg differ diff --git a/web/detective/audio/voice/male/sys_menu_save.m4a b/web/detective/audio/voice/male/sys_menu_save.m4a new file mode 100644 index 00000000..3d39342c Binary files /dev/null and b/web/detective/audio/voice/male/sys_menu_save.m4a differ diff --git a/web/detective/audio/voice/male/sys_menu_save.ogg b/web/detective/audio/voice/male/sys_menu_save.ogg new file mode 100644 index 00000000..6532c253 Binary files /dev/null and b/web/detective/audio/voice/male/sys_menu_save.ogg differ diff --git a/web/detective/audio/voice/male/sys_new_case.m4a b/web/detective/audio/voice/male/sys_new_case.m4a new file mode 100644 index 00000000..4454d259 Binary files /dev/null and b/web/detective/audio/voice/male/sys_new_case.m4a differ diff --git a/web/detective/audio/voice/male/sys_new_case.ogg b/web/detective/audio/voice/male/sys_new_case.ogg new file mode 100644 index 00000000..7205e714 Binary files /dev/null and b/web/detective/audio/voice/male/sys_new_case.ogg differ diff --git a/web/detective/audio/voice/male/sys_saved.m4a b/web/detective/audio/voice/male/sys_saved.m4a new file mode 100644 index 00000000..aa019fdb Binary files /dev/null and b/web/detective/audio/voice/male/sys_saved.m4a differ diff --git a/web/detective/audio/voice/male/sys_saved.ogg b/web/detective/audio/voice/male/sys_saved.ogg new file mode 100644 index 00000000..17f25c66 Binary files /dev/null and b/web/detective/audio/voice/male/sys_saved.ogg differ diff --git a/web/detective/audio/voice/male/tut_cf_ok.m4a b/web/detective/audio/voice/male/tut_cf_ok.m4a new file mode 100644 index 00000000..7530ac0e Binary files /dev/null and b/web/detective/audio/voice/male/tut_cf_ok.m4a differ diff --git a/web/detective/audio/voice/male/tut_cf_ok.ogg b/web/detective/audio/voice/male/tut_cf_ok.ogg new file mode 100644 index 00000000..171f4efe Binary files /dev/null and b/web/detective/audio/voice/male/tut_cf_ok.ogg differ diff --git a/web/detective/audio/voice/male/tut_cf_try.m4a b/web/detective/audio/voice/male/tut_cf_try.m4a new file mode 100644 index 00000000..85aacd92 Binary files /dev/null and b/web/detective/audio/voice/male/tut_cf_try.m4a differ diff --git a/web/detective/audio/voice/male/tut_cf_try.ogg b/web/detective/audio/voice/male/tut_cf_try.ogg new file mode 100644 index 00000000..e7ab1305 Binary files /dev/null and b/web/detective/audio/voice/male/tut_cf_try.ogg differ diff --git a/web/detective/audio/voice/male/tut_confront.m4a b/web/detective/audio/voice/male/tut_confront.m4a new file mode 100644 index 00000000..5dd1b8df Binary files /dev/null and b/web/detective/audio/voice/male/tut_confront.m4a differ diff --git a/web/detective/audio/voice/male/tut_confront.ogg b/web/detective/audio/voice/male/tut_confront.ogg new file mode 100644 index 00000000..89400d15 Binary files /dev/null and b/web/detective/audio/voice/male/tut_confront.ogg differ diff --git a/web/detective/audio/voice/male/tut_d0.m4a b/web/detective/audio/voice/male/tut_d0.m4a new file mode 100644 index 00000000..3cca2d0d Binary files /dev/null and b/web/detective/audio/voice/male/tut_d0.m4a differ diff --git a/web/detective/audio/voice/male/tut_d0.ogg b/web/detective/audio/voice/male/tut_d0.ogg new file mode 100644 index 00000000..30f2361b Binary files /dev/null and b/web/detective/audio/voice/male/tut_d0.ogg differ diff --git a/web/detective/audio/voice/male/tut_done.m4a b/web/detective/audio/voice/male/tut_done.m4a new file mode 100644 index 00000000..bb758736 Binary files /dev/null and b/web/detective/audio/voice/male/tut_done.m4a differ diff --git a/web/detective/audio/voice/male/tut_done.ogg b/web/detective/audio/voice/male/tut_done.ogg new file mode 100644 index 00000000..8dc04ab4 Binary files /dev/null and b/web/detective/audio/voice/male/tut_done.ogg differ diff --git a/web/detective/audio/voice/male/tut_listen.m4a b/web/detective/audio/voice/male/tut_listen.m4a new file mode 100644 index 00000000..ad2e91ec Binary files /dev/null and b/web/detective/audio/voice/male/tut_listen.m4a differ diff --git a/web/detective/audio/voice/male/tut_listen.ogg b/web/detective/audio/voice/male/tut_listen.ogg new file mode 100644 index 00000000..6bc04bac Binary files /dev/null and b/web/detective/audio/voice/male/tut_listen.ogg differ diff --git a/web/detective/audio/voice/male/tut_replay.m4a b/web/detective/audio/voice/male/tut_replay.m4a new file mode 100644 index 00000000..7c7259d3 Binary files /dev/null and b/web/detective/audio/voice/male/tut_replay.m4a differ diff --git a/web/detective/audio/voice/male/tut_replay.ogg b/web/detective/audio/voice/male/tut_replay.ogg new file mode 100644 index 00000000..90096695 Binary files /dev/null and b/web/detective/audio/voice/male/tut_replay.ogg differ diff --git a/web/detective/audio/voice/male/tut_skip.m4a b/web/detective/audio/voice/male/tut_skip.m4a new file mode 100644 index 00000000..51c27f8b Binary files /dev/null and b/web/detective/audio/voice/male/tut_skip.m4a differ diff --git a/web/detective/audio/voice/male/tut_skip.ogg b/web/detective/audio/voice/male/tut_skip.ogg new file mode 100644 index 00000000..22f6bc4c Binary files /dev/null and b/web/detective/audio/voice/male/tut_skip.ogg differ diff --git a/web/detective/audio/voice/male/tut_status.m4a b/web/detective/audio/voice/male/tut_status.m4a new file mode 100644 index 00000000..d9a8d44a Binary files /dev/null and b/web/detective/audio/voice/male/tut_status.m4a differ diff --git a/web/detective/audio/voice/male/tut_status.ogg b/web/detective/audio/voice/male/tut_status.ogg new file mode 100644 index 00000000..9c69a19b Binary files /dev/null and b/web/detective/audio/voice/male/tut_status.ogg differ diff --git a/web/detective/audio/voice/male/wp_1.m4a b/web/detective/audio/voice/male/wp_1.m4a new file mode 100644 index 00000000..a4dc4570 Binary files /dev/null and b/web/detective/audio/voice/male/wp_1.m4a differ diff --git a/web/detective/audio/voice/male/wp_1.ogg b/web/detective/audio/voice/male/wp_1.ogg new file mode 100644 index 00000000..a32b8849 Binary files /dev/null and b/web/detective/audio/voice/male/wp_1.ogg differ diff --git a/web/detective/audio/voice/male/wp_2.m4a b/web/detective/audio/voice/male/wp_2.m4a new file mode 100644 index 00000000..8d05a71e Binary files /dev/null and b/web/detective/audio/voice/male/wp_2.m4a differ diff --git a/web/detective/audio/voice/male/wp_2.ogg b/web/detective/audio/voice/male/wp_2.ogg new file mode 100644 index 00000000..d8cc5392 Binary files /dev/null and b/web/detective/audio/voice/male/wp_2.ogg differ diff --git a/web/detective/audio/voice/male/wp_3.m4a b/web/detective/audio/voice/male/wp_3.m4a new file mode 100644 index 00000000..15b7e0bf Binary files /dev/null and b/web/detective/audio/voice/male/wp_3.m4a differ diff --git a/web/detective/audio/voice/male/wp_3.ogg b/web/detective/audio/voice/male/wp_3.ogg new file mode 100644 index 00000000..646e96d9 Binary files /dev/null and b/web/detective/audio/voice/male/wp_3.ogg differ diff --git a/web/detective/audio/voice/male/wp_4.m4a b/web/detective/audio/voice/male/wp_4.m4a new file mode 100644 index 00000000..4e282822 Binary files /dev/null and b/web/detective/audio/voice/male/wp_4.m4a differ diff --git a/web/detective/audio/voice/male/wp_4.ogg b/web/detective/audio/voice/male/wp_4.ogg new file mode 100644 index 00000000..a8489f9e Binary files /dev/null and b/web/detective/audio/voice/male/wp_4.ogg differ diff --git a/web/detective/audio/voice/w1_1.m4a b/web/detective/audio/voice/w1_1.m4a new file mode 100644 index 00000000..4b33a1b3 Binary files /dev/null and b/web/detective/audio/voice/w1_1.m4a differ diff --git a/web/detective/audio/voice/w1_1.ogg b/web/detective/audio/voice/w1_1.ogg new file mode 100644 index 00000000..c06669a8 Binary files /dev/null and b/web/detective/audio/voice/w1_1.ogg differ diff --git a/web/detective/audio/voice/w1_2.m4a b/web/detective/audio/voice/w1_2.m4a new file mode 100644 index 00000000..294d80ff Binary files /dev/null and b/web/detective/audio/voice/w1_2.m4a differ diff --git a/web/detective/audio/voice/w1_2.ogg b/web/detective/audio/voice/w1_2.ogg new file mode 100644 index 00000000..444bb210 Binary files /dev/null and b/web/detective/audio/voice/w1_2.ogg differ diff --git a/web/detective/audio/voice/w1_3.m4a b/web/detective/audio/voice/w1_3.m4a new file mode 100644 index 00000000..33349ddc Binary files /dev/null and b/web/detective/audio/voice/w1_3.m4a differ diff --git a/web/detective/audio/voice/w1_3.ogg b/web/detective/audio/voice/w1_3.ogg new file mode 100644 index 00000000..29477755 Binary files /dev/null and b/web/detective/audio/voice/w1_3.ogg differ diff --git a/web/detective/audio/voice/w1_4.m4a b/web/detective/audio/voice/w1_4.m4a new file mode 100644 index 00000000..a7b71588 Binary files /dev/null and b/web/detective/audio/voice/w1_4.m4a differ diff --git a/web/detective/audio/voice/w1_4.ogg b/web/detective/audio/voice/w1_4.ogg new file mode 100644 index 00000000..f401c0d6 Binary files /dev/null and b/web/detective/audio/voice/w1_4.ogg differ diff --git a/web/detective/audio/voice/w1_5.m4a b/web/detective/audio/voice/w1_5.m4a new file mode 100644 index 00000000..a215fffb Binary files /dev/null and b/web/detective/audio/voice/w1_5.m4a differ diff --git a/web/detective/audio/voice/w1_5.ogg b/web/detective/audio/voice/w1_5.ogg new file mode 100644 index 00000000..c8950813 Binary files /dev/null and b/web/detective/audio/voice/w1_5.ogg differ diff --git a/web/detective/audio/voice/w1_6.m4a b/web/detective/audio/voice/w1_6.m4a new file mode 100644 index 00000000..61b6a874 Binary files /dev/null and b/web/detective/audio/voice/w1_6.m4a differ diff --git a/web/detective/audio/voice/w1_6.ogg b/web/detective/audio/voice/w1_6.ogg new file mode 100644 index 00000000..76ccf681 Binary files /dev/null and b/web/detective/audio/voice/w1_6.ogg differ diff --git a/web/detective/audio/voice/w2_1.m4a b/web/detective/audio/voice/w2_1.m4a new file mode 100644 index 00000000..78955678 Binary files /dev/null and b/web/detective/audio/voice/w2_1.m4a differ diff --git a/web/detective/audio/voice/w2_1.ogg b/web/detective/audio/voice/w2_1.ogg new file mode 100644 index 00000000..04f072ae Binary files /dev/null and b/web/detective/audio/voice/w2_1.ogg differ diff --git a/web/detective/audio/voice/w2_2.m4a b/web/detective/audio/voice/w2_2.m4a new file mode 100644 index 00000000..3700f224 Binary files /dev/null and b/web/detective/audio/voice/w2_2.m4a differ diff --git a/web/detective/audio/voice/w2_2.ogg b/web/detective/audio/voice/w2_2.ogg new file mode 100644 index 00000000..46e7396b Binary files /dev/null and b/web/detective/audio/voice/w2_2.ogg differ diff --git a/web/detective/audio/voice/w2_3.m4a b/web/detective/audio/voice/w2_3.m4a new file mode 100644 index 00000000..3239b9af Binary files /dev/null and b/web/detective/audio/voice/w2_3.m4a differ diff --git a/web/detective/audio/voice/w2_3.ogg b/web/detective/audio/voice/w2_3.ogg new file mode 100644 index 00000000..d6766c5e Binary files /dev/null and b/web/detective/audio/voice/w2_3.ogg differ diff --git a/web/detective/audio/voice/w2_4.m4a b/web/detective/audio/voice/w2_4.m4a new file mode 100644 index 00000000..c8780e75 Binary files /dev/null and b/web/detective/audio/voice/w2_4.m4a differ diff --git a/web/detective/audio/voice/w2_4.ogg b/web/detective/audio/voice/w2_4.ogg new file mode 100644 index 00000000..7c371216 Binary files /dev/null and b/web/detective/audio/voice/w2_4.ogg differ diff --git a/web/detective/audio/voice/w2_5.m4a b/web/detective/audio/voice/w2_5.m4a new file mode 100644 index 00000000..2c66a86d Binary files /dev/null and b/web/detective/audio/voice/w2_5.m4a differ diff --git a/web/detective/audio/voice/w2_5.ogg b/web/detective/audio/voice/w2_5.ogg new file mode 100644 index 00000000..a35615bb Binary files /dev/null and b/web/detective/audio/voice/w2_5.ogg differ diff --git a/web/detective/audio/voice/w2_6.m4a b/web/detective/audio/voice/w2_6.m4a new file mode 100644 index 00000000..b7115e07 Binary files /dev/null and b/web/detective/audio/voice/w2_6.m4a differ diff --git a/web/detective/audio/voice/w2_6.ogg b/web/detective/audio/voice/w2_6.ogg new file mode 100644 index 00000000..e8296862 Binary files /dev/null and b/web/detective/audio/voice/w2_6.ogg differ diff --git a/web/detective/audio/voice/wen_1.m4a b/web/detective/audio/voice/wen_1.m4a new file mode 100644 index 00000000..5bb56536 Binary files /dev/null and b/web/detective/audio/voice/wen_1.m4a differ diff --git a/web/detective/audio/voice/wen_1.ogg b/web/detective/audio/voice/wen_1.ogg new file mode 100644 index 00000000..6a7804a1 Binary files /dev/null and b/web/detective/audio/voice/wen_1.ogg differ diff --git a/web/detective/audio/voice/wen_2.m4a b/web/detective/audio/voice/wen_2.m4a new file mode 100644 index 00000000..b8b0fb75 Binary files /dev/null and b/web/detective/audio/voice/wen_2.m4a differ diff --git a/web/detective/audio/voice/wen_2.ogg b/web/detective/audio/voice/wen_2.ogg new file mode 100644 index 00000000..fb20e599 Binary files /dev/null and b/web/detective/audio/voice/wen_2.ogg differ diff --git a/web/detective/audio/voice/wen_3.m4a b/web/detective/audio/voice/wen_3.m4a new file mode 100644 index 00000000..02a80b73 Binary files /dev/null and b/web/detective/audio/voice/wen_3.m4a differ diff --git a/web/detective/audio/voice/wen_3.ogg b/web/detective/audio/voice/wen_3.ogg new file mode 100644 index 00000000..abd38e49 Binary files /dev/null and b/web/detective/audio/voice/wen_3.ogg differ diff --git a/web/detective/audio/voice/wen_4.m4a b/web/detective/audio/voice/wen_4.m4a new file mode 100644 index 00000000..4f1f9035 Binary files /dev/null and b/web/detective/audio/voice/wen_4.m4a differ diff --git a/web/detective/audio/voice/wen_4.ogg b/web/detective/audio/voice/wen_4.ogg new file mode 100644 index 00000000..d092546f Binary files /dev/null and b/web/detective/audio/voice/wen_4.ogg differ diff --git a/web/detective/audio/voice/wen_5.m4a b/web/detective/audio/voice/wen_5.m4a new file mode 100644 index 00000000..43bbd613 Binary files /dev/null and b/web/detective/audio/voice/wen_5.m4a differ diff --git a/web/detective/audio/voice/wen_5.ogg b/web/detective/audio/voice/wen_5.ogg new file mode 100644 index 00000000..93bced9c Binary files /dev/null and b/web/detective/audio/voice/wen_5.ogg differ diff --git a/web/detective/audio/voice/wen_6.m4a b/web/detective/audio/voice/wen_6.m4a new file mode 100644 index 00000000..67e74af1 Binary files /dev/null and b/web/detective/audio/voice/wen_6.m4a differ diff --git a/web/detective/audio/voice/wen_6.ogg b/web/detective/audio/voice/wen_6.ogg new file mode 100644 index 00000000..c835a04c Binary files /dev/null and b/web/detective/audio/voice/wen_6.ogg differ diff --git a/web/detective/audio/voice/z_1.m4a b/web/detective/audio/voice/z_1.m4a new file mode 100644 index 00000000..800d8079 Binary files /dev/null and b/web/detective/audio/voice/z_1.m4a differ diff --git a/web/detective/audio/voice/z_1.ogg b/web/detective/audio/voice/z_1.ogg new file mode 100644 index 00000000..06cdc8fe Binary files /dev/null and b/web/detective/audio/voice/z_1.ogg differ diff --git a/web/detective/audio/voice/z_2.m4a b/web/detective/audio/voice/z_2.m4a new file mode 100644 index 00000000..02d4b5ac Binary files /dev/null and b/web/detective/audio/voice/z_2.m4a differ diff --git a/web/detective/audio/voice/z_2.ogg b/web/detective/audio/voice/z_2.ogg new file mode 100644 index 00000000..f4e89726 Binary files /dev/null and b/web/detective/audio/voice/z_2.ogg differ diff --git a/web/detective/audio/voice/z_3.m4a b/web/detective/audio/voice/z_3.m4a new file mode 100644 index 00000000..82efa542 Binary files /dev/null and b/web/detective/audio/voice/z_3.m4a differ diff --git a/web/detective/audio/voice/z_3.ogg b/web/detective/audio/voice/z_3.ogg new file mode 100644 index 00000000..6af23e85 Binary files /dev/null and b/web/detective/audio/voice/z_3.ogg differ diff --git a/web/detective/audio/voice/z_4.m4a b/web/detective/audio/voice/z_4.m4a new file mode 100644 index 00000000..b5d23144 Binary files /dev/null and b/web/detective/audio/voice/z_4.m4a differ diff --git a/web/detective/audio/voice/z_4.ogg b/web/detective/audio/voice/z_4.ogg new file mode 100644 index 00000000..722ecc43 Binary files /dev/null and b/web/detective/audio/voice/z_4.ogg differ diff --git a/web/detective/audio/voice/z_5.m4a b/web/detective/audio/voice/z_5.m4a new file mode 100644 index 00000000..4a3036b4 Binary files /dev/null and b/web/detective/audio/voice/z_5.m4a differ diff --git a/web/detective/audio/voice/z_5.ogg b/web/detective/audio/voice/z_5.ogg new file mode 100644 index 00000000..0816b9ae Binary files /dev/null and b/web/detective/audio/voice/z_5.ogg differ diff --git a/web/detective/audio/voice/z_6.m4a b/web/detective/audio/voice/z_6.m4a new file mode 100644 index 00000000..2416e1aa Binary files /dev/null and b/web/detective/audio/voice/z_6.m4a differ diff --git a/web/detective/audio/voice/z_6.ogg b/web/detective/audio/voice/z_6.ogg new file mode 100644 index 00000000..87dfce1a Binary files /dev/null and b/web/detective/audio/voice/z_6.ogg differ diff --git a/web/detective/audio/voice/z_break_1.m4a b/web/detective/audio/voice/z_break_1.m4a new file mode 100644 index 00000000..0e307818 Binary files /dev/null and b/web/detective/audio/voice/z_break_1.m4a differ diff --git a/web/detective/audio/voice/z_break_1.ogg b/web/detective/audio/voice/z_break_1.ogg new file mode 100644 index 00000000..6757a609 Binary files /dev/null and b/web/detective/audio/voice/z_break_1.ogg differ diff --git a/web/detective/audio/voice/z_break_2.m4a b/web/detective/audio/voice/z_break_2.m4a new file mode 100644 index 00000000..0538cccd Binary files /dev/null and b/web/detective/audio/voice/z_break_2.m4a differ diff --git a/web/detective/audio/voice/z_break_2.ogg b/web/detective/audio/voice/z_break_2.ogg new file mode 100644 index 00000000..beef0d8c Binary files /dev/null and b/web/detective/audio/voice/z_break_2.ogg differ diff --git a/web/detective/audio/voice/z_break_3.m4a b/web/detective/audio/voice/z_break_3.m4a new file mode 100644 index 00000000..3dc5bdb5 Binary files /dev/null and b/web/detective/audio/voice/z_break_3.m4a differ diff --git a/web/detective/audio/voice/z_break_3.ogg b/web/detective/audio/voice/z_break_3.ogg new file mode 100644 index 00000000..89f50ee8 Binary files /dev/null and b/web/detective/audio/voice/z_break_3.ogg differ diff --git a/web/detective/audio/voice/z_react_1.m4a b/web/detective/audio/voice/z_react_1.m4a new file mode 100644 index 00000000..0fee6a83 Binary files /dev/null and b/web/detective/audio/voice/z_react_1.m4a differ diff --git a/web/detective/audio/voice/z_react_1.ogg b/web/detective/audio/voice/z_react_1.ogg new file mode 100644 index 00000000..126f6d3a Binary files /dev/null and b/web/detective/audio/voice/z_react_1.ogg differ diff --git a/web/detective/audio/voice/z_react_2.m4a b/web/detective/audio/voice/z_react_2.m4a new file mode 100644 index 00000000..8c996dac Binary files /dev/null and b/web/detective/audio/voice/z_react_2.m4a differ diff --git a/web/detective/audio/voice/z_react_2.ogg b/web/detective/audio/voice/z_react_2.ogg new file mode 100644 index 00000000..b4f17933 Binary files /dev/null and b/web/detective/audio/voice/z_react_2.ogg differ diff --git a/web/detective/audio/voice/z_react_3.m4a b/web/detective/audio/voice/z_react_3.m4a new file mode 100644 index 00000000..177dc8b8 Binary files /dev/null and b/web/detective/audio/voice/z_react_3.m4a differ diff --git a/web/detective/audio/voice/z_react_3.ogg b/web/detective/audio/voice/z_react_3.ogg new file mode 100644 index 00000000..9d34e6ba Binary files /dev/null and b/web/detective/audio/voice/z_react_3.ogg differ diff --git a/web/detective/audio/voice/z_react_4.m4a b/web/detective/audio/voice/z_react_4.m4a new file mode 100644 index 00000000..7b120639 Binary files /dev/null and b/web/detective/audio/voice/z_react_4.m4a differ diff --git a/web/detective/audio/voice/z_react_4.ogg b/web/detective/audio/voice/z_react_4.ogg new file mode 100644 index 00000000..417bef41 Binary files /dev/null and b/web/detective/audio/voice/z_react_4.ogg differ diff --git a/web/detective/cover.png b/web/detective/cover.png new file mode 100644 index 00000000..e8fc3f3e Binary files /dev/null and b/web/detective/cover.png differ diff --git a/web/detective/game.js b/web/detective/game.js new file mode 100644 index 00000000..21722356 --- /dev/null +++ b/web/detective/game.js @@ -0,0 +1,1475 @@ +/** + * 《雨夜专案》—— 纯音频本格推理游戏(按 docs/design/18-detective.md 实现; + * 台词 id 见 18-detective-voice-ids.md,158 句 ≤160 上限)。 + * + * 你是专案组探长:翻听六份卷宗(4 证人证词 + 现场勘验 + 法医报告),逐句比对, + * 指出「同轴异值」的证词对,锁定真凶,结案三问(真凶/凶器/动机)。 + * + * 核心动词=指证(§3):每句证词带线索轴标签(0-2 个),判定 = 两句是否共享轴 + * 且值不同——判定与三档归因(cf_same/cf_diff/cf_self)全部从数据单源推导, + * 机制上不存在「台词说对得上、逻辑上对不上」的假话。矩阵不变式(§6.4)在 + * _buildCase 生成期断言:全案卷同轴异值对集合 ≡ 预设计矛盾表(4 条)。 + * + * 状态机(§4):TITLE / DRILL / BOARD(空闲|播放) / CONFRONT / FINISH / VERDICT / + * SETTLE + MENU 挂起 overlay。证词链 = onComplete 串行(句间 0.8s)+ 单句 8s + * 兜底(资产缺失告警续链,§4.3 契约:屏蔽任意 voice 资产流程不卡死)。 + * + * 手势(§2.5 总表,全游戏零手势偏离,仅 2 条已教学豁免): + * - E1:BOARD 单击=播放/停止(「检查/翻阅」类非即时操作,escape/dungeon 先例); + * - E2:BOARD 空闲长按=进入指证(核心动词快捷入口;绑定条件=无证词链会话在播, + * 与长按快进(仅播放中生效)时间互斥零冲突——播放中长按走引擎快进不受理)。 + * - 上滑状态链 / 下滑菜单 / 双击重听全状态保留(手势宪法三件套不豁免)。 + * + * 随机性(§6.2):mulberry32(seed) 生成期一次性洗牌(filler 句位置 / 凶器·动机 + * 选项序 / 保底提示轮询序),存 run.layout——同 (seed,pack) 两次构造 _case 深相等。 + * pack A 真凶=周先生 / pack B 真凶=小顺(fork 12 句,§6.3);结案后新案强制翻包。 + * + * 评级 S-C(§7.2)六变量条件式(doubts/weaponOk/motiveOk/wrongWho/cfMiss/hints), + * 播报值=运行值(账目真实);VERDICT 认罪链 800ms 手势死窗+链锁(dungeon H1 同款, + * §7.4:链完成前一切手势吞掉,settle_hook「单击接新案」言行一致)。 + * + * 看门狗双轨(§9):无操作轨(guide.watchdog:BOARD [10,25] / CONFRONT [15,30] / + * FINISH [20,40],手势重置)+ 无进展轨(游戏层 [45,90,150],只由进展事件重置—— + * 听完新条目/指证判定/结案相位推进;乱按饿不死,150s L3 直接给出可指证的矛盾对)。 + */ +import { Game } from '../engine/game.js'; +import { Guide } from '../engine/guide.js'; +import { vibrate } from '../engine/haptics.js'; + +// ================================ 常量表 ================================ + +const SENTENCE_GAP_MS = 800; // 证词链播句间停顿(§3.1) +const SENTENCE_FALLBACK_MS = 8000; // 单句 8s 兜底(§4.3:资产缺失续链) +const SIG_LEAD_MS = 450; // 签名 earcon → 条目名语音 串行间隔(§8.2) +const VERDICT_GRACE_MS = 800; // VERDICT 手势死窗(§7.4:450ms 长按判定+300ms 双击窗全盖) +const PROG_LEVELS = [45000, 90000, 150000]; // 无进展轨三级(§9.3:earcon→短句→保底矛盾对) +const PROG_POLL_MS = 5000; // 无进展轨轮询周期 +const HINT_MAX_TIMES = 2; // 每句保底提示全局限 2 次(§9.3:不轰炸) +const DRILL_WATCHDOG_AT = [30000, 60000, 90000]; // 教学步无进展轨(§9.2:nudge→短句→自动滚出) + +/** 6 份听取卷宗(st_asked 计账)+ 结案陈词入口(虚拟条目,单列)。 */ +const LISTEN_ITEMS = ['w1', 'w2', 'w3', 'w4', 'scene', 'fore']; +const ITEM_ORDER = [...LISTEN_ITEMS, 'case']; + +/** 模态菜单(§2.4,5 项;标题/结算用精简 2 项——dungeon 先例)。 */ +const MENU_ITEMS = ['sys_menu_resume', 'sys_menu_cf', 'sys_menu_save', 'sys_menu_help', 'sys_menu_restart']; +const TITLE_MENU = ['sys_menu_help', 'sys_menu_resume']; + +/** 结案三问·真凶问选项 = 四位证人(选项名复用条目名句 item_w1..w4)。 */ +const WHO_ITEMS = ['w1', 'w2', 'w3', 'w4']; + +const GRADE_RANK = { S: 4, A: 3, B: 2, C: 1 }; + +const SAVE_KEY = 'soundgame.progress.detective'; +const AUTH_KEY = 'soundgame.auth'; + +const PREWARM_SFX = [ + 'sfx/paper_turn', + 'sfx/sig_ge', 'sfx/sig_shun', 'sfx/sig_zhou', 'sfx/sig_wen', 'sfx/sig_fore', 'sfx/sig_scene', + 'sfx/contra_hit', 'sfx/contra_miss', 'sfx/gavel', 'sfx/rain_swell', 'sfx/turn_tick', +]; + +// ================================ 案卷数据(§6.1) ================================ + +/** pack A《雨夜·聚宝斋》全量(真凶=周先生)。台词全文见 18-detective-voice-ids.md。 */ +const PACK_A = { + id: 0, name: '雨夜·聚宝斋', culprit: 'w3', culpritName: '周先生', + items: [ + { id: 'w1', kind: 'witness', name: 'item_w1', sig: 'sig_ge', intro: null, lines: [ + { id: 'w1_1', shuffle: false, clues: {} }, + { id: 'w1_2', shuffle: true, clues: { rain: '2140', dep_s: '2140' } }, + { id: 'w1_3', shuffle: false, clues: { visitor: 'male_tall_hunched' } }, + { id: 'w1_4', shuffle: true, clues: { dep_z: '2150' } }, + { id: 'w1_5', shuffle: true, clues: {} }, + { id: 'w1_6', shuffle: false, clues: { motive: 'short' } }, + ] }, + { id: 'w2', kind: 'witness', name: 'item_w2', sig: 'sig_shun', intro: null, lines: [ + { id: 'w2_1', shuffle: false, clues: { away_s: 'south' } }, + { id: 'w2_2', shuffle: true, clues: { away_s: 'south' } }, + { id: 'w2_3', shuffle: false, clues: { key: 'hook' } }, + { id: 'w2_4', shuffle: true, clues: {} }, + { id: 'w2_5', shuffle: false, clues: { door: 'victim_opened' } }, + { id: 'w2_6', shuffle: true, clues: {} }, + ] }, + { id: 'w3', kind: 'witness', name: 'item_w3', sig: 'sig_zhou', intro: 'intro_w3', lines: [ + { id: 'z_1', shuffle: false, clues: { dep_z: '2150' } }, + { id: 'z_2', shuffle: false, clues: { lamp: 'off' } }, + { id: 'z_3', shuffle: false, clues: { key: 'body' } }, + { id: 'z_4', shuffle: false, clues: { weapon: 'wood', scale: 'sold' } }, + { id: 'z_5', shuffle: true, clues: {} }, + { id: 'z_6', shuffle: true, clues: {} }, + ] }, + { id: 'w4', kind: 'witness', name: 'item_w4', sig: 'sig_wen', intro: null, lines: [ + { id: 'wen_1', shuffle: false, clues: { rain: '2140' } }, + { id: 'wen_2', shuffle: false, clues: { lamp: 'on_2220' } }, + { id: 'wen_3', shuffle: false, clues: { wall: 'climbed' } }, + { id: 'wen_4', shuffle: true, clues: {} }, + { id: 'wen_5', shuffle: false, clues: { motive: 'short' } }, + { id: 'wen_6', shuffle: true, clues: {} }, + ] }, + { id: 'scene', kind: 'scene', name: 'item_scene', sig: 'sig_scene', intro: null, lines: [ + { id: 'sc_0', shuffle: false, clues: {} }, + { id: 'sc_1', shuffle: false, clues: { struggle: 'none' } }, + { id: 'sc_2', shuffle: false, clues: { door: 'victim_opened' } }, + { id: 'sc_3', shuffle: false, clues: { wall: 'climbed' } }, + { id: 'sc_4', shuffle: false, clues: { scale: 'present' } }, + { id: 'sc_5', shuffle: false, clues: {} }, + ] }, + { id: 'fore', kind: 'forensic', name: 'item_fore', sig: 'sig_fore', intro: null, lines: [ + { id: 'fo_1', shuffle: false, clues: {} }, + { id: 'fo_2', shuffle: false, clues: { death: '2200_2230' } }, + { id: 'fo_3', shuffle: false, clues: { weapon: 'bronze' } }, + { id: 'fo_4', shuffle: false, clues: { struggle: 'none' } }, + { id: 'fo_5', shuffle: false, clues: {} }, + ] }, + ], + contradictions: [ + { id: 'm1', a: 'z_2', b: 'wen_2', axis: 'lamp', hint: 'hint_c1' }, + { id: 'm2', a: 'z_3', b: 'w2_3', axis: 'key', hint: null }, + { id: 'm3', a: 'z_4', b: 'fo_3', axis: 'weapon', hint: null }, + { id: 'm4', a: 'z_4', b: 'sc_4', axis: 'scale', hint: 'hint_c2' }, + ], + verdict: { accuseLine: 'fin_right', break: ['z_break_1', 'z_break_2', 'z_break_3'] }, + wrongAccuse: { w1: 'acc_ge', w2: 'acc_shun', w4: 'acc_wen' }, + reacts: ['z_react_1', 'z_react_2', 'z_react_3', 'z_react_4'], + questions: { + weapon: { options: ['wp_1', 'wp_2', 'wp_3', 'wp_4'], answer: 'wp_2' }, + motive: { options: ['mv_1', 'mv_2', 'mv_3'], answer: 'mv_1' }, + }, +}; + +/** pack B《雨夜·聚宝斋(变体)》(§6.3:fork 12 句,真凶=学徒小顺)。 */ +const PACK_B = { + id: 1, name: '雨夜·聚宝斋(变体)', culprit: 'w2', culpritName: '小顺', + base: 0, + overrides: { + w2: { lines: [ + { id: 'w2_1', shuffle: false, clues: { away_s: 'south' } }, + { id: 'w2_2', shuffle: true, clues: { away_s: 'south' } }, + { id: 'b_shun_1', shuffle: false, clues: { lamp: 'off' } }, + { id: 'b_shun_2', shuffle: false, clues: { key: 'body' } }, + { id: 'b_shun_3', shuffle: false, clues: { weapon: 'wood', scale: 'sold' } }, + { id: 'b_shun_4', shuffle: true, clues: {} }, + ] }, + w3: { lines: [ + { id: 'z_1', shuffle: false, clues: { dep_z: '2150' } }, + { id: 'b_zhou_1', shuffle: true, clues: {} }, + { id: 'b_zhou_2', shuffle: false, clues: { key: 'hook' } }, + { id: 'b_zhou_3', shuffle: true, clues: {} }, + { id: 'z_5', shuffle: true, clues: {} }, + { id: 'z_6', shuffle: true, clues: {} }, + ] }, + contradictions: [ + { id: 'm1b', a: 'b_shun_1', b: 'wen_2', axis: 'lamp', hint: 'b_hint_c1' }, + { id: 'm2b', a: 'b_shun_2', b: 'b_zhou_2', axis: 'key', hint: null }, + { id: 'm3b', a: 'b_shun_3', b: 'fo_3', axis: 'weapon', hint: null }, + { id: 'm4b', a: 'b_shun_3', b: 'sc_4', axis: 'scale', hint: 'b_hint_c2' }, + ], + verdict: { accuseLine: 'fin_right_b', break: ['b_shun_break_1', 'b_shun_break_2', 'b_shun_break_3'] }, + wrongAccuse: { w1: 'acc_ge', w3: 'acc_zhou', w4: 'acc_wen' }, + reacts: ['b_shun_react_1', 'b_shun_react_2', 'b_shun_react_2', 'b_shun_react_2'], + }, +}; + +function deepClone(o) { return JSON.parse(JSON.stringify(o)); } + +/** 解析案包(pack B = pack A 模板 + overrides fork;§6.1/§6.3)。 */ +function resolvePack(idx) { + if (idx === 0) return deepClone(PACK_A); + const base = deepClone(PACK_A); + const ov = PACK_B.overrides; + return { + ...base, + id: PACK_B.id, name: PACK_B.name, culprit: PACK_B.culprit, culpritName: PACK_B.culpritName, + items: base.items.map((it) => (ov[it.id] && ov[it.id].lines ? { ...it, lines: deepClone(ov[it.id].lines) } : it)), + contradictions: deepClone(ov.contradictions), + verdict: deepClone(ov.verdict), + wrongAccuse: deepClone(ov.wrongAccuse), + reacts: [...ov.reacts], + }; +} + +// ================================ 主类 ================================ + +export class DetectiveGame extends Game { + constructor(audio, root, opts = {}) { + super(audio, root); + this.guide = new Guide(audio); // guide='off' 时全部 API 静默 no-op 且零定时器 + this._fixedSeed = opts.seed != null ? opts.seed : null; // playtest 注入(仅首个新案生效) + this._fixedPack = opts.pack != null ? opts.pack : null; + this.state = 'boot'; // TITLE/DRILL/BOARD/CONFRONT/FINISH/VERDICT/SETTLE + this.menuOpen = false; // MENU = 挂起 overlay(记录返回态 menuFrom) + this.menuFrom = null; + this._menuItems = MENU_ITEMS; + this._menuIdx = 0; + this._session = 0; // 会话令牌:状态大跳转作废旧计时器/链回调 + this._timers = []; + this.meta = this._defaultMeta(); + this.run = null; + this._case = null; // 生成期定局:items/layout/contradictions(§6) + this.itemIdx = 0; // 证词板当前条目(0..6,含结案陈词) + this._play = null; // 证词链播放会话 {itemIdx, cur, seq};null=空闲 + this._cf = null; // 指证子相位(item1/line1/confirm1/item2/line2/confirm2/judged) + this._fin = null; // 结案三问子相位(who/whoConfirm/weapon/motive) + this._drill = null; // 教学步(0..6) + this._annSeq = 0; // 条目名播报令牌(快速滑动时旧报名作废) + this._titleChain = []; + this._settleChain = []; // SETTLE 双击重听=整条结算段 + this._gradeInfo = null; // 本局评级快照(SETTLE 上滑重听评级段) + this._verdictGraceUntil = 0; + this._menuOpenedAt = null; // 菜单挂起计时(_progAdjustMenuPause 消费) + // 无进展轨(§9.3):只由进展事件重置(_progressEvent),任何手势都不重置。 + this._prog = { lastAt: 0, l1: false, l2: false, l3: false, counts: {}, armed: false, armedSession: -1 }; + } + + _defaultMeta() { + return { drillDone: false, casesClosed: 0, bestGrade: null, lastPack: null }; + } + + // ================================ 案卷生成(§6) ================================ + + _mulberry32(a) { + return function () { + a |= 0; a = (a + 0x6D2B79F5) | 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; + } + + /** 生成期一次性洗牌(Fisher-Yates by rng)。 */ + _shuffled(arr, rng) { + const a = [...arr]; + for (let i = a.length - 1; i > 0; i--) { + const j = Math.floor(rng() * (i + 1)); + [a[i], a[j]] = [a[j], a[i]]; + } + return a; + } + + /** + * 构造案局(纯函数 of (seed, pack)):filler 句槽位置换(首句与全部矛盾承载句 + * 固定——数据侧 shuffle 标志即单源)、凶器/动机选项序、保底提示轮询序; + * 随后断言矩阵不变式(§6.4:同轴异值对集合 ≡ 矛盾表,防文案事故变归因假话)。 + */ + _buildCase(seed, packIdx) { + const pack = resolvePack(packIdx); + const rng = this._mulberry32(seed >>> 0); + const items = pack.items.map((it) => ({ + ...it, lines: it.lines.map((l) => ({ id: l.id, shuffle: l.shuffle, clues: { ...l.clues } })), + })); + // 结案陈词 = 虚拟条目(第 7 项,单击进 FINISH;不参与听取计账与判定) + items.push({ id: 'case', kind: 'case', name: 'item_case', sig: null, intro: null, lines: [] }); + const layout = { lines: {}, wpOpts: null, mvOpts: null, hintOrder: null }; + for (const it of items) { + // filler 置换:shuffle:true 的句对象在其槽位集合内置换(线索随句走) + const idxs = []; + for (let i = 0; i < it.lines.length; i++) if (it.lines[i].shuffle) idxs.push(i); + const objs = this._shuffled(idxs.map((i) => it.lines[i]), rng); + idxs.forEach((pos, k) => { it.lines[pos] = objs[k]; }); + layout.lines[it.id] = it.lines.map((l) => l.id); + } + layout.wpOpts = this._shuffled(pack.questions.weapon.options, rng); + layout.mvOpts = this._shuffled(pack.questions.motive.options, rng); + layout.hintOrder = this._shuffled(pack.contradictions.map((c) => c.id), rng); + + // 矩阵不变式(§6.4,dev 断言;playtest 另有独立重算双保险) + const all = items.flatMap((it) => it.lines); + const norm = (x, y, axis) => (x < y ? x + '|' + y + '|' + axis : y + '|' + x + '|' + axis); + const derived = new Set(); + for (let i = 0; i < all.length; i++) { + for (let j = i + 1; j < all.length; j++) { + const A = all[i], B = all[j]; + for (const ax of Object.keys(A.clues)) { + if (B.clues[ax] !== undefined && B.clues[ax] !== A.clues[ax]) derived.add(norm(A.id, B.id, ax)); + } + } + } + const wanted = new Set(pack.contradictions.map((c) => norm(c.a, c.b, c.axis))); + if (derived.size !== wanted.size || [...derived].some((k) => !wanted.has(k))) { + throw new Error(`detective: 矩阵不变式破裂(pack ${packIdx})——derived=${[...derived].join(';')} want=${[...wanted].join(';')}`); + } + // 运行期查表:lineId → 线索 / 所属条目 + const lineClues = {}; + const lineItem = {}; + for (const it of items) for (const l of it.lines) { lineClues[l.id] = l.clues; lineItem[l.id] = it.id; } + return { + seed, pack: packIdx, name: pack.name, culprit: pack.culprit, culpritName: pack.culpritName, + items, layout, lineClues, lineItem, + contradictions: pack.contradictions, verdict: pack.verdict, + wrongAccuse: pack.wrongAccuse, reacts: pack.reacts, questions: pack.questions, + }; + } + + // ---------------- 派生账目(§3.1:播报值=运行值) ---------------- + + _itemAt(idx) { return this._case.items[Math.min(Math.max(idx, 0), this._case.items.length - 1)]; } + _itemOf(itemId) { return this._case.items.find((it) => it.id === itemId); } + _itemName(item) { return 'voice/' + item.name; } + _doubtCount() { return this.run ? this.run.doubts.filter(Boolean).length : 0; } + _heardCount() { return this.run ? this.run.heard.length : 0; } + _lineIds(itemId) { return this._case.layout.lines[itemId] || []; } + + /** 上滑状态链 = 中断复述链同构(§10:st_asked_n → 未听 ≤3 时逐个条目名 → st_doubt_m → recap_go)。 */ + _statusChain() { + const ids = ['voice/st_asked_' + this._heardCount()]; + const unheard = LISTEN_ITEMS.filter((id) => !this.run.heard.includes(id)); + if (unheard.length <= 3) for (const id of unheard) ids.push(this._itemName(this._itemOf(id))); + ids.push('voice/st_doubt_' + this._doubtCount()); + ids.push('voice/recap_go'); + return ids; + } + + // ================================ 生命周期 ================================ + + start() { + this.audio.prewarm(PREWARM_SFX); + const save = this._loadSave(); + if (save && save.meta) this.meta = { ...this._defaultMeta(), ...save.meta }; + this._enterTitle(); + } + + stop() { + this._session++; + this._clearTimers(); + this.audio.stopAll(); + } + + _goHome() { + this.stop(); + window.location.href = '../'; + } + + // ---------------- TITLE(§2.3) ---------------- + + _enterTitle() { + this._session++; + this._clearTimers(); + this._play = null; this._cf = null; this._fin = null; this._drill = null; + this.menuOpen = false; this.menuFrom = null; + this.state = 'TITLE'; + this.audio.stopVoice(); + this.audio.play('bgm/menu'); + this.guide.state('menu'); + this._titleChain = this.run ? ['voice/sys_continue_ask'] : []; + if (this._titleChain.length) this._speakChain(this._titleChain); + } + + _onTitleGesture(g) { + if (g === 'tap') { + if (this.run) this._resumeRun(); + else this._newCase(); + } else if (g === 'right') { + this._newCase(); // 换一桩新案:重掷 seed/pack,meta 保留(§2.3) + } else if (g === 'doubletap') { + this._speakChain([...this._titleChain]); // 重听 continue_ask(无档时静默) + } else if (g === 'up') { + this._speakChain(['voice/intro_1', 'voice/intro_2', 'voice/intro_3']); // 案情简介 + } else if (g === 'down') { + this._openMenu(); + } else { + this._invalid(); + } + } + + // ---------------- 新案 / 读档(§4.2/§10) ---------------- + + _newCase(seedOverride, packOverride) { + this._session++; + this._clearTimers(); + this.menuOpen = false; this.menuFrom = null; + this.audio.stopVoice(); + const seed = seedOverride != null ? seedOverride + : this._fixedSeed != null ? this._fixedSeed : Math.floor(Math.random() * 1e9); + this._fixedSeed = null; + const pack = packOverride != null ? packOverride + : this._fixedPack != null ? this._fixedPack + : this.meta.lastPack == null ? 0 : (Math.random() < 0.5 ? 0 : 1); // 首局=0;换新案重掷 + this._fixedPack = null; + this._case = this._buildCase(seed, pack); + this.run = { + seed, pack, + layout: deepClone(this._case.layout), + heard: [], heardLines: {}, lastLine: {}, + doubts: this._case.contradictions.map(() => false), + cfMiss: 0, wrongWho: 0, zHits: 0, + hintsGiven: [], hintFinShown: false, + weaponAns: null, motiveAns: null, + }; + this.itemIdx = 0; + this._play = null; this._cf = null; this._fin = null; this._drill = null; + this._save(); + this._speakChain(['voice/sys_new_case'], () => { + if (!this.meta.drillDone) this._enterDrill(); + else { this._progressEvent(); this._enterBoard(); } // 新案进板 = 基线重置 + }); + } + + /** 菜单「重新查这桩案子」:同 pack 重掷 seed(换洗牌不换案情)。 */ + _restartCase() { + const pack = this._case ? this._case.pack : (this.meta.lastPack ?? 0); + this.run = null; + this._newCase(null, pack); + } + + _resumeRun() { + const r = this.run; + this._case = this._buildCase(r.seed, r.pack); + // 读档复用存档 layout(确定性生成下二者相等;防御性信任存档,不重洗 §10) + if (r.layout && r.layout.lines) { + this._case.layout = deepClone(r.layout); + this.run.layout = deepClone(r.layout); + } + this.itemIdx = 0; + this._play = null; this._cf = null; this._fin = null; + this._progressEvent(); // 读档进板 = 基线重置 + this._enterBoard(this._statusChain()); // 局面复述链(§10:≤10s 回到可操作) + } + + // ================================ 手势总入口 ================================ + + onGesture(g) { + // VERDICT 链锁 + 800ms 死窗(§7.4):链完成前一切手势吞掉——ghost 手势不算 + // 输入、不碰看门狗(在途 tap 按新状态路由会把认罪链掐死在半句,dungeon H1)。 + if (this.state === 'VERDICT') return; + if (g === 'longpressend') return; // 仅引擎快进收尾信号,不承载语义 + this.guide.disarm(); // 任何手势复位向导看门狗(off 档 no-op) + if (this.menuOpen) { this._onMenuGesture(g); return; } + switch (this.state) { + case 'TITLE': this._onTitleGesture(g); break; + case 'DRILL': this._onDrillGesture(g); break; + case 'BOARD': this._onBoardGesture(g); break; + case 'CONFRONT': this._onConfrontGesture(g); break; + case 'FINISH': this._onFinishGesture(g); break; + case 'SETTLE': this._onSettleGesture(g); break; + default: break; + } + // 各态无操作看门狗重挂(手势即重置起点,§9.2) + if (this.menuOpen) return; + if (this.state === 'BOARD' && !this._play) this._armBoardWatchdog(); + else if (this.state === 'CONFRONT' && this._cf && this._cf.phase !== 'judged') this._armCfWatchdog(); + else if (this.state === 'FINISH') this._armFinWatchdog(); + else if (this.state === 'DRILL' && this._drill) this._armDrillWatchdog(); + } + + _invalid() { + vibrate('error'); + this.guide.error({ voice: 'gd_invalid', onceKey: 'detective_invalid', skipHaptic: true }); + } + + // ================================ BOARD 证词板(§2.1) ================================ + + _enterBoard(chain) { + this._session++; + this._clearTimers(); + this._play = null; this._cf = null; this._fin = null; + this.menuOpen = false; + this.state = 'BOARD'; + this.audio.stopVoice(); + this.audio.play('bgm/board'); + this.guide.state('explore'); + // 无进展轨基线由各入口显式落(新案/读档/出教学=重置;判定回板=判定已重置; + // 菜单回板=只剔除挂起时长不重置——见 _closeMenu/_progAdjustMenuPause) + this._progArm(); + this._armBoardWatchdog(); + if (chain && chain.length) this._speakChain(chain); + } + + /** 判定/错指后回板(§4.2:指证判定后返回,explore 签名同点)。 */ + _backToBoard() { this._enterBoard(); } + + _onBoardGesture(g) { + if (this._play) { // 播放中子行为(§2.1:纯聆听保护) + if (g === 'tap') { this._stopPlayback(); } + else if (g === 'doubletap') { this._replayCurrentInChain(); } + else if (g === 'up') { this._stopPlayback(); this._speakChain(this._statusChain()); } + else if (g === 'down') { this._stopPlayback(); this._openMenu(); } + // left/right 静默忽略(无 error 震);longpress 走引擎快进(E2 时间互斥) + return; + } + if (g === 'tap') { + if (ITEM_ORDER[this.itemIdx] === 'case') { this._enterFinish(); return; } + this._startPlayback(); + } else if (g === 'doubletap') { + this._replayLastLine(); + } else if (g === 'left') { + this._navItem(-1); + } else if (g === 'right') { + this._navItem(1); + } else if (g === 'up') { + this._speakChain(this._statusChain()); + } else if (g === 'down') { + this._openMenu(); + } else if (g === 'longpress') { + // E2 豁免:空闲(无证词链会话)长按=进入指证;与播放中长按快进时间互斥 + if (this.audio.isVoicePlaying()) return; // 报名等在播:不受理,引擎快进接管 + this._enterConfront(); + } + } + + _navItem(d) { + this.itemIdx = (this.itemIdx + d + ITEM_ORDER.length) % ITEM_ORDER.length; + this._announceItem(this.itemIdx); + } + + /** 条目播报:翻页音 + 专属签名 earcon → 条目名整句(串行,§8.2 并发纪律)。 + * 结案陈词无签名音:turn earcon(guide 层,off 档静默)代位——不复用 + * sfx/turn_tick(那是无进展轨 L1 的注意音,语义不可混用)。 */ + _announceItem(idx) { + const item = this._itemAt(idx); + this.audio.play('sfx/paper_turn'); + if (item.sig) this.audio.play('sfx/' + item.sig); + else this.guide.turn({}); + const seq = ++this._annSeq; + this._after(SIG_LEAD_MS, () => { + if (seq !== this._annSeq) return; + this.audio.play(this._itemName(item)); + }); + } + + /** 单击听取当前卷宗:从首个未听句起;全部听过=整体重听(§2.1)。 */ + _startPlayback() { + const item = this._itemAt(this.itemIdx); + if (item.id === 'case' || !item.lines.length) return; + const n = this._lineIds(item.id).length; + const heardN = this.run.heardLines[item.id] || 0; + const from = heardN >= n ? 0 : heardN; + this._play = { itemIdx: this.itemIdx, cur: -1, seq: 0 }; + const p = this._play; + const s = this._session; + const seq0 = ++p.seq; + if (item.sig) this.audio.play('sfx/' + item.sig); + this._after(SIG_LEAD_MS, () => { + if (s !== this._session || this._play !== p || p.seq !== seq0) return; + const lead = (item.intro && from === 0) ? ['voice/' + item.intro] : []; + this._speakChain(lead, () => this._playLine(from)); + }); + } + + /** 证词链步进:句粒度可停(tap)/可重听(doubletap 重播当前句后续链,§2.1)。 */ + _playLine(idx) { + const p = this._play; + if (!p) return; + const item = this._itemAt(p.itemIdx); + const ids = this._lineIds(item.id); + if (idx >= ids.length) { this._finishItemPlayback(); return; } + p.cur = idx; + this.run.lastLine[item.id] = ids[idx]; + const seq = ++p.seq; + this._playSentence(ids[idx], () => { + if (!this._play || this._play !== p || p.seq !== seq) return; + this.run.heardLines[item.id] = Math.max(this.run.heardLines[item.id] || 0, idx + 1); + this._after(SENTENCE_GAP_MS, () => { + if (!this._play || this._play !== p || p.seq !== seq) return; + this._playLine(idx + 1); + }); + }); + } + + _stopPlayback() { + this._play = null; + this.audio.stopVoice(); // E1「单击=停止播报」:真停声(链会话作废 + 当前句静音), + // 也是 E2 的前置——停后即空闲,长按指证立即可受理 + this._armBoardWatchdog(); + } + + _finishItemPlayback() { + const item = this._itemAt(this._play.itemIdx); + this._play = null; + if (item.id !== 'case' && !this.run.heard.includes(item.id)) { + this.run.heard.push(item.id); // 听完新条目 = 进展事件(§9.3) + this._save(); + } + if (this._drill && this._drill.step === 1) { this._drillAdvance(2); return; } // D1 教学=听完整份 + this._progressEvent(); + this._armBoardWatchdog(); + } + + /** 播放中双击:重播当前句,链从当前句继续(seq 递增作废在途句间停顿)。 */ + _replayCurrentInChain() { + const p = this._play; + if (!p || p.cur < 0) return; // 首句起播前无「当前句」 + this._playLine(p.cur); + } + + /** 空闲双击:重新听当前条目最后一句(≤8s,免费零消耗,§2.1)。 */ + _replayLastLine() { + const item = this._itemAt(this.itemIdx); + const ids = this._lineIds(item.id); + if (!ids.length) return; + const last = this.run.lastLine[item.id]; + const lineId = last && ids.includes(last) ? last : ids[0]; + if (this._drill && this._drill.step === 2) { + this._playSentence(lineId, () => this._drillAdvance(3)); + return; + } + this._playSentence(lineId); + } + + // ================================ CONFRONT 指证(§2.2/§3.5) ================================ + + _enterConfront() { + this._session++; + this._clearTimers(); + this._play = null; this._fin = null; + this.menuOpen = false; + this.state = 'CONFRONT'; + this.audio.stopVoice(); + this.guide.state('choice'); + this.guide.turn({ haptic: true }); // 等待点 turn cue(§9.2) + const startIdx = Math.min(this.itemIdx, LISTEN_ITEMS.length - 1); + this._cf = { phase: 'item1', idx: startIdx, lineIdx: 0, fixed: null, pickSaid: false }; + const chain = ['voice/cf_start']; + if (this._drill && this._drill.step === 4) { + this._drill.step = 5; // D4 长按即通过 → D5 练习(tut_cf_try 接在 cf_start 链上) + chain.push('voice/tut_cf_try'); + } + this._armCfWatchdog(); + this._speakChain(chain, () => { + if (this._cf && this._cf.phase === 'item1') this._announceCfItem(); + }); + } + + /** 指证条目选择播报(签名前置,§3.7)。 */ + _announceCfItem() { + const item = this._itemOf(LISTEN_ITEMS[this._cf.idx]); + if (!item) return; + this.audio.play('sfx/' + item.sig); + const seq = ++this._annSeq; + this._after(SIG_LEAD_MS, () => { + if (seq !== this._annSeq) return; + this.audio.play(this._itemName(item)); + }); + } + + _onConfrontGesture(g) { + const cf = this._cf; + if (!cf || cf.phase === 'judged') return; // 判定链播报中:吞掉(§4.3 独占链轻量版) + if (g === 'tap') { + if (cf.phase === 'item1' || cf.phase === 'item2') this._cfConfirmItem(); + else if (cf.phase === 'line1' || cf.phase === 'line2') this._cfConfirmLine(); + else if (cf.phase === 'confirm1' || cf.phase === 'confirm2') this._cfFixLine(); + } else if (g === 'left' || g === 'right') { + const d = g === 'right' ? 1 : -1; + if (cf.phase === 'item1' || cf.phase === 'item2') { + cf.idx = (cf.idx + d + LISTEN_ITEMS.length) % LISTEN_ITEMS.length; + this._announceCfItem(); + } else if (cf.phase === 'line1' || cf.phase === 'line2') { + this._cfMoveLine(d); + } else { + this._invalid(); // 确认态滑动手势无映射(§2.2) + } + } else if (g === 'doubletap') { + this._cfReplayPreview(); + } else if (g === 'up') { + this._speakChain(['voice/st_doubt_' + this._doubtCount()]); // 状态链简版(§2.2) + } else if (g === 'down') { + this._openMenu(); + } else if (g === 'longpress') { + this._invalid(); // 选择语境长按不作输入(§2.2) + } + } + + _cfCurLineId() { + const cf = this._cf; + return this._lineIds(LISTEN_ITEMS[cf.idx])[cf.lineIdx]; + } + + _cfConfirmItem() { + const cf = this._cf; + cf.phase = cf.phase === 'item1' ? 'line1' : 'line2'; + cf.lineIdx = 0; + const lead = cf.pickSaid ? [] : ['voice/cf_pick_line']; // 选句相位入口(首次) + cf.pickSaid = true; + this._speakChain(lead, () => this._playSentence(this._cfCurLineId())); // 播该句全文(句序号不播) + } + + _cfMoveLine(d) { + const cf = this._cf; + const n = this._lineIds(LISTEN_ITEMS[cf.idx]).length; + cf.lineIdx = (cf.lineIdx + d + n) % n; + this._playSentence(this._cfCurLineId()); + } + + _cfConfirmLine() { + const cf = this._cf; + cf.phase = cf.phase === 'line1' ? 'confirm1' : 'confirm2'; + this._speakChain(['voice/' + this._cfCurLineId(), 'voice/cf_confirm']); // 重播该句 + 确认句 + } + + _cfReplayPreview() { + const cf = this._cf; + if (cf.phase === 'item1' || cf.phase === 'item2') { this._announceCfItem(); return; } + this._playSentence(this._cfCurLineId()); // 双击=重听当前预览句(宪法语义保留) + } + + /** 确认态单击=定下这句:第一份定下自动重播一次再选第二份(P1 记忆契约,§3.7)。 */ + _cfFixLine() { + const cf = this._cf; + const itemId = LISTEN_ITEMS[cf.idx]; + const lineId = this._cfCurLineId(); + if (cf.phase === 'confirm1') { + cf.fixed = { item: itemId, line: lineId }; + cf.phase = 'item2'; + cf.lineIdx = 0; + this._speakChain(['voice/cf_second', 'voice/' + lineId]); + return; + } + // 第二份定下 → 判定(教学 D5 截获:练习不算数、不计 cfMiss、不动 doubts,§5) + if (this._drill && this._drill.step === 5) { + cf.phase = 'judged'; + vibrate('success'); + this._speakChain(['voice/tut_cf_ok'], () => { + this._cf = null; + this.state = 'DRILL'; + this._drillAdvance(6); + }); + return; + } + this._judge(cf.fixed.line, lineId); + } + + /** + * 指证判定(§3.5):判定与归因全部从线索轴数据单源推导。 + * hit 链 = contra_hit(sfx)→ cf_hit → react_n(真凶累计被坐实次数递进); + * miss 链 = contra_miss(sfx)→ cf_self / cf_same / cf_diff(三档真话)。 + */ + _judge(lineA, lineB) { + this._cf.phase = 'judged'; + const cl = this._case.lineClues; + if (lineA === lineB) { this._cfMiss('voice/cf_self'); return; } + const ca = cl[lineA] || {}; + const cb = cl[lineB] || {}; + const shared = Object.keys(ca).filter((ax) => cb[ax] !== undefined); + if (!shared.length) { this._cfMiss('voice/cf_diff'); return; } + const conflict = shared.find((ax) => ca[ax] !== cb[ax]); + if (!conflict) { this._cfMiss('voice/cf_same'); return; } + const contra = this._case.contradictions.find((c) => c.axis === conflict + && ((c.a === lineA && c.b === lineB) || (c.a === lineB && c.b === lineA))); + if (!contra) { + // 数据错误(矩阵不变式在生成期已断言排除;此处防御性兜底不崩) + console.error('[detective] 矩阵外冲突对:', lineA, lineB, conflict); + this._cfMiss('voice/cf_diff'); + return; + } + this._cfHit(contra); + } + + _cfMiss(voiceId) { + this.run.cfMiss++; + this._progressEvent(); // 指证判定 = 进展事件(§9.3) + this._save(); + this.audio.play('sfx/contra_miss'); + this._speakChain([voiceId], () => this._backToBoard()); + } + + _cfHit(contra) { + const idx = this._case.contradictions.indexOf(contra); + let newDoubt = false; + if (!this.run.doubts[idx]) { + this.run.doubts[idx] = true; + newDoubt = true; + this.run.zHits++; + } + const reactIdx = Math.max(1, Math.min(this.run.zHits, this._case.reacts.length)); + const chain = ['voice/cf_hit', 'voice/' + this._case.reacts[reactIdx - 1]]; + // 疑点 4/4 齐:自动播一次 hint_fin(引导去结案;庆祝性播报不计入保底提示账,§3.5) + if (newDoubt && this._doubtCount() === 4 && !this.run.hintFinShown) { + this.run.hintFinShown = true; + chain.push('voice/hint_fin'); + } + this._progressEvent(); + this._save(); + this.audio.play('sfx/contra_hit'); // 全游戏最强确认音先响(§8.2) + vibrate('success'); + this._speakChain(chain, () => this._backToBoard()); + } + + // ================================ FINISH 结案三问(§7.1) ================================ + + _enterFinish() { + this._session++; + this._clearTimers(); + this._play = null; this._cf = null; + this.menuOpen = false; + this.state = 'FINISH'; + this.audio.stopVoice(); + this.guide.state('choice'); + this.guide.turn({ haptic: true }); + this._fin = { phase: 'who', optIdx: 0, busy: false }; + this._armFinWatchdog(); + this._speakChain(['voice/fin_start'], () => { + this.audio.play(this._itemName(this._itemOf(WHO_ITEMS[0]))); + }); + } + + _onFinishGesture(g) { + const fin = this._fin; + if (!fin) return; + if (fin.busy) return; // 归因/反馈链播报中:吞掉(防重复答题重复记账) + if (g === 'tap') { + if (fin.phase === 'who') { + fin.phase = 'whoConfirm'; // 两段确认(§2.2:重播选项名 + fin_who_confirm) + this._speakChain([this._itemName(this._itemOf(WHO_ITEMS[fin.optIdx])), 'voice/fin_who_confirm']); + } else if (fin.phase === 'whoConfirm') { + this._resolveWho(WHO_ITEMS[fin.optIdx]); + } else if (fin.phase === 'weapon') { + this._resolveWeapon(this._case.layout.wpOpts[fin.optIdx]); + } else if (fin.phase === 'motive') { + this._resolveMotive(this._case.layout.mvOpts[fin.optIdx]); + } + } else if (g === 'left' || g === 'right') { + const d = g === 'right' ? 1 : -1; + const opts = fin.phase === 'motive' ? this._case.layout.mvOpts + : fin.phase === 'weapon' ? this._case.layout.wpOpts : WHO_ITEMS; + fin.optIdx = (fin.optIdx + d + opts.length) % opts.length; + if (fin.phase === 'who' || fin.phase === 'whoConfirm') { + this.audio.play(this._itemName(this._itemOf(WHO_ITEMS[fin.optIdx]))); + } else { + this.audio.play('voice/' + opts[fin.optIdx]); + } + } else if (g === 'doubletap') { + // 重听当前问句/选项(宪法语义保留) + if (fin.phase === 'who' || fin.phase === 'whoConfirm') { + this.audio.play(this._itemName(this._itemOf(WHO_ITEMS[fin.optIdx]))); + } else if (fin.phase === 'weapon') { + this._speakChain(['voice/fin_ask_weapon', 'voice/' + this._case.layout.wpOpts[fin.optIdx]]); + } else if (fin.phase === 'motive') { + this._speakChain(['voice/fin_ask_motive', 'voice/' + this._case.layout.mvOpts[fin.optIdx]]); + } + } else if (g === 'up') { + this._speakChain(['voice/st_doubt_' + this._doubtCount()]); + } else if (g === 'down') { + this._openMenu(); + } else if (g === 'longpress') { + this._invalid(); + } + } + + _resolveWho(itemId) { + const fin = this._fin; + if (itemId === this._case.culprit) { + // 指认对 → 凶器问(§7.1:三问顺序 真凶→凶器→动机) + fin.busy = true; + this._progressEvent(); // 结案相位推进(§9.3) + this._save(); + fin.phase = 'weapon'; + fin.optIdx = 0; + this.guide.turn({ haptic: true }); + this._armFinWatchdog(); + this._speakChain(['voice/fin_ask_weapon'], () => { + fin.busy = false; + this.audio.play('voice/' + this._case.layout.wpOpts[0]); + }); + return; + } + // 错指:不在场证明复述 + fin_clear → 回 BOARD(无限再机会,§7.1) + fin.busy = true; + this.run.wrongWho++; + this._progressEvent(); + this._save(); + this._speakChain(['voice/' + this._case.wrongAccuse[itemId], 'voice/fin_clear'], () => this._backToBoard()); + } + + _resolveWeapon(optId) { + const fin = this._fin; + fin.busy = true; + this.run.weaponAns = optId; + this._progressEvent(); + this._save(); + const ok = optId === this._case.questions.weapon.answer; + this._speakChain(['voice/' + (ok ? 'fin_wp_right' : 'fin_wp_wrong')], () => { + fin.phase = 'motive'; + fin.optIdx = 0; + fin.busy = false; + this.guide.turn({ haptic: true }); + this._armFinWatchdog(); + this._speakChain(['voice/fin_ask_motive'], () => { + this.audio.play('voice/' + this._case.layout.mvOpts[0]); + }); + }); + } + + _resolveMotive(optId) { + this._fin.busy = true; + this.run.motiveAns = optId; + this._progressEvent(); + this._save(); + const ok = optId === this._case.questions.motive.answer; + this._speakChain(['voice/' + (ok ? 'fin_mv_right' : 'fin_mv_wrong')], () => this._enterVerdict()); + } + + // ================================ 评级与裁决(§7) ================================ + + /** 评级六变量条件式(§7.2,自上而下首个命中;纯函数供测试注入矩阵)。 */ + _gradeOf(o) { + if (o.doubts === 4 && o.weaponOk && o.motiveOk && o.wrongWho === 0 && o.cfMiss <= 2 && o.hints === 0) return 'S'; + if (o.doubts >= 3 && o.weaponOk && o.motiveOk && o.wrongWho <= 1 && o.hints <= 1) return 'A'; + if (o.doubts >= 2 && o.wrongWho <= 2) return 'B'; + return 'C'; + } + + _gradeOutcome() { + return { + doubts: this._doubtCount(), + weaponOk: this.run.weaponAns === this._case.questions.weapon.answer, + motiveOk: this.run.motiveAns === this._case.questions.motive.answer, + wrongWho: this.run.wrongWho, + cfMiss: this.run.cfMiss, + hints: this.run.hintsGiven.length, + }; + } + + /** 结算账目段(§7.3:播报值=运行值,措辞档位与账目一一映射)。 */ + _settlePartVoices(o) { + return [ + 'voice/settle_open', + 'voice/rate_' + this._gradeOf(o).toLowerCase(), + 'voice/st_doubt_' + o.doubts, + 'voice/st_miss_' + (o.cfMiss === 0 ? 0 : o.cfMiss <= 2 ? 1 : 2), + 'voice/settle_hook', // 键盘态引擎自动路由 key/ 变体(游戏代码用普通 id) + ]; + } + + /** 裁决仪式(§7.4):fin_right → rain_swell → 认罪三段 → gavel → 结算链。 */ + _enterVerdict() { + this._session++; + this._clearTimers(); + this._play = null; this._cf = null; this._fin = null; + this.menuOpen = false; + this.state = 'VERDICT'; + this.audio.stopVoice(); + this._verdictGraceUntil = performance.now() + VERDICT_GRACE_MS; // 手势死窗(onGesture 已全吞,双保险留档) + this.audio.play('bgm/verdict', { fadeMs: 800 }); + this.guide.state('settle'); + vibrate('success'); + // 元进度入账(结案必达:无失败态,C 档覆盖一切) + const outcome = this._gradeOutcome(); + const grade = this._gradeOf(outcome); + this.meta.casesClosed++; + this.meta.lastPack = this._case.pack; + this.meta.bestGrade = !this.meta.bestGrade || GRADE_RANK[grade] > GRADE_RANK[this.meta.bestGrade] + ? grade : this.meta.bestGrade; + this._gradeInfo = { outcome, grade }; + this.run = null; + this._save(); + const settlePart = this._settlePartVoices(outcome); + this._settleChain = settlePart; + this._speakChain(['voice/' + this._case.verdict.accuseLine], () => { + this.audio.play('sfx/rain_swell'); // 认罪链垫(6s one-shot) + this._speakChain(this._case.verdict.break.map((x) => 'voice/' + x), () => { + this.audio.play('sfx/gavel'); // 法槌 + this.audio.play('bgm/menu', { volume: 0.5 }); // SETTLE 结算曲 + this._speakChain(settlePart, () => { + this.state = 'SETTLE'; // 链完成才解锁手势(链锁,§7.4) + this.guide.state('menu'); + }); + }); + }); + } + + _onSettleGesture(g) { + if (g === 'tap') { + // 接新案:pack 强制翻转 + seed 重掷(§12 开环钩子:二周目必遇变体真凶) + this._newCase(null, this.meta.lastPack == null ? 1 : 1 - this.meta.lastPack); + } else if (g === 'doubletap') { + this._speakChain([...this._settleChain]); // 重听结算链 + } else if (g === 'up') { + if (this._gradeInfo) this._speakChain(this._settlePartVoices(this._gradeInfo.outcome).slice(1, 4)); // 评级段 + } else if (g === 'down') { + this._openMenu(); + } else { + this._invalid(); + } + } + + // ================================ DRILL 教学(§5) ================================ + + _enterDrill() { + this.state = 'DRILL'; + this._session++; + this._clearTimers(); + this.audio.play('bgm/board'); + this.guide.state('explore'); + this._drill = { step: -1, fails: 0 }; + this._drillEnter(0); + } + + _drillEnter(step) { + const d = this._drill; + d.step = step; + d.fails = 0; + this._armDrillWatchdog(); + if (step === 0) { + // FTUE:intro 三句(~13s)+ D0 指令 → 首输入达成点 ≈t+20s(§5) + this._speakChain(['voice/intro_1', 'voice/intro_2', 'voice/intro_3', 'voice/tut_d0']); + } else if (step === 1) { + // D1 听取老葛真实证词:无论 D0 滑到哪,回正到第一份并重新报名 + this.itemIdx = 0; + this._speakChain(['voice/tut_listen'], () => this._announceItem(0)); + } else if (step === 2) { + this._speakChain(['voice/tut_replay']); + } else if (step === 3) { + this._speakChain(['voice/tut_status']); + } else if (step === 4) { + this._speakChain(['voice/tut_confront']); // E2 豁免教学 + } else if (step === 5) { + // 兜底路径(D4 连错 2 次自动通过):直接开练习局 + this._enterConfront(); + } else if (step === 6) { + this._speakChain(['voice/tut_done'], () => this._drillDone()); + } + // step 5 的旁白(tut_cf_try)由 _enterConfront 接在 cf_start 链上(CONFRONT 态) + } + + _drillWrong() { + const d = this._drill; + if (!d) return; + d.fails++; + vibrate('error'); + if (d.fails >= 2) this._drillPass(); // 做错 ≤2 次后自动演示通过(§5) + } + + _drillPass() { + const d = this._drill; + if (!d) return; + d.fails = 0; + this._drillAdvance(d.step + 1); + } + + _drillAdvance(step) { + if (!this._drill) return; + if (step > 6) { this._drillDone(); return; } + this._drillEnter(step); + } + + _drillDone() { + this.meta.drillDone = true; // 持久化:二周目不再进(§5) + this._drill = null; + this._save(); + this._progressEvent(); // 出教学进板 = 基线重置 + this._enterBoard(); + } + + _drillSkip() { // 下滑=跳过教学(§5) + this.meta.drillDone = true; + this._drill = null; + this._cf = null; + this._save(); + this._speakChain(['voice/tut_skip'], () => { this._progressEvent(); this._enterBoard(); }); + } + + _onDrillGesture(g) { + const d = this._drill; + if (!d) { this._onBoardGesture(g); return; } + if (g === 'down') { this._drillSkip(); return; } + if (this._play) { // D1 老葛证词播放中:播放子行为优先(tap 停/双击重听),其余忽略 + if (g === 'tap') this._stopPlayback(); + else if (g === 'doubletap') this._replayCurrentInChain(); + return; + } + switch (d.step) { + case 0: + if (g === 'left' || g === 'right') { + this._navItem(g === 'right' ? 1 : -1); // 签名 earcon + 条目名(真实内容) + vibrate('confirm'); + this._drillPass(); + } else this._drillWrong(); + break; + case 1: + if (g === 'tap') { + if (ITEM_ORDER[this.itemIdx] === 'case') this._drillWrong(); + else this._startPlayback(); // 单击听取当前卷宗(真实证词链播) + } else if (g === 'doubletap') this._replayLastLine(); + else this._drillWrong(); + break; + case 2: + if (g === 'doubletap') this._replayLastLine(); // 重新听这一句(免费) + else this._drillWrong(); + break; + case 3: + if (g === 'up') { + this._speakChain(this._statusChain(), () => this._drillPass()); // st_asked_1 → st_doubt_0 + } else this._drillWrong(); + break; + case 4: + if (g === 'longpress') this._enterConfront(); // E2:按住进入指证 + else this._drillWrong(); + break; + case 6: + break; // tut_done 链播中,链完自动进 BOARD + default: + this._drillWrong(); + } + } + + // ================================ 模态菜单(§2.4/00 §5.2) ================================ + + _openMenu() { + if (this.menuOpen) return; + this.menuFrom = this.state; + this.menuOpen = true; + this._menuItems = (this.state === 'TITLE' || this.state === 'SETTLE') ? TITLE_MENU : MENU_ITEMS; + this._menuIdx = 0; + this._menuOpenedAt = performance.now(); // 无进展轨挂起计时(关菜单时剔除时长,§9.3) + this._session++; // 打开即停播报/停看门狗(菜单=天然暂停,本作无实时玩法) + this._clearTimers(); + this._play = null; + this.audio.stopVoice(); + this.audio.play('bgm/menu'); + this.guide.state('menu'); + this._speakChain(['voice/sys_menu', 'voice/' + this._menuItems[0]]); + } + + /** 菜单挂起时长从无进展轨剔除:lastAt 前移 = 时钟续走、分级保留—— + * 反复开菜单不再能重置救援轨(§9.3 进展事件清单不含菜单;dungeon R3 E0-1 + * 「菜单时间不计入」的正确形态是剔除时长,不是整轨重置)。 */ + _progAdjustMenuPause() { + if (this._menuOpenedAt == null) return; + this._prog.lastAt += performance.now() - this._menuOpenedAt; + this._menuOpenedAt = null; + } + + _closeMenu() { + this.menuOpen = false; + const from = this.menuFrom; + this.menuFrom = null; + // 关闭回执=状态签名(dungeon R2 D1-1 先例:盲手可感知开关) + if (from === 'TITLE' || from === 'SETTLE') { + this._menuOpenedAt = null; + this.state = from; + this.guide.state('menu'); + this.audio.play('bgm/menu', { volume: from === 'SETTLE' ? 0.5 : 1 }); + return; + } + if (from === 'CONFRONT') { + if (this._drill && (this._drill.step === 4 || this._drill.step === 5)) { + // D4/D5 练习被菜单打断:退回 D4 重新按长按进指证(教学不丢步) + this._menuOpenedAt = null; + this._cf = null; + this.state = 'DRILL'; + this._drill.step = 4; + this.audio.play('bgm/board'); + this.guide.state('explore'); + this._speakChain(['voice/tut_confront']); + return; + } + this._menuOpenedAt = null; + this.state = 'CONFRONT'; + this.audio.play('bgm/board'); + this.guide.state('choice'); + if (this._cf && this._cf.phase !== 'judged') this._armCfWatchdog(); + return; + } + if (from === 'FINISH') { + this._menuOpenedAt = null; + this.state = 'FINISH'; + this.audio.play('bgm/board'); + this.guide.state('choice'); + if (this._fin) this._fin.busy = false; + this._armFinWatchdog(); + return; + } + if (from === 'DRILL') { + this._menuOpenedAt = null; + this.state = 'DRILL'; + this.audio.play('bgm/board'); + this.guide.state('explore'); + this._armDrillWatchdog(); + return; + } + // BOARD:只剔除挂起时长(不重置分级——乱按开菜单饿不死救援,§9.3) + this.state = 'BOARD'; + this.audio.play('bgm/board'); + this.guide.state('explore'); + this._progAdjustMenuPause(); + this._progArm(); + this._armBoardWatchdog(); + } + + _onMenuGesture(g) { + const items = this._menuItems; + if (g === 'tap' || g === 'right') { + this._menuIdx = (this._menuIdx + 1) % items.length; // 单击循环切项(每切播项名) + this.audio.play('voice/' + items[this._menuIdx]); + } else if (g === 'left') { + this._menuIdx = (this._menuIdx + items.length - 1) % items.length; + this.audio.play('voice/' + items[this._menuIdx]); + } else if (g === 'doubletap') { + this._menuExec(items[this._menuIdx]); + } else if (g === 'up') { + this._speakChain(['voice/sys_help', 'voice/settings_hint']); // 帮助(键盘态引擎路由 key 变体) + } else if (g === 'down') { + this._closeMenu(); + } + } + + _menuExec(item) { + if (item === 'sys_menu_resume') { + // 「返回证词板」:从 CONFRONT/FINISH = 退出指证/结案回板(§2.2/§2.4) + if (this.menuFrom === 'CONFRONT' || this.menuFrom === 'FINISH') { + if (this._drill && (this._drill.step === 4 || this._drill.step === 5)) { + // D4/D5 练习被菜单打断:退回 D4 重新按长按进指证(教学不丢步) + this.menuOpen = false; this.menuFrom = null; + this._menuOpenedAt = null; + this._cf = null; + this.state = 'DRILL'; + this._drill.step = 4; + this.audio.play('bgm/board'); + this.guide.state('explore'); + this._speakChain(['voice/tut_confront']); + return; + } + this.menuOpen = false; this.menuFrom = null; + this._cf = null; this._fin = null; + this._progAdjustMenuPause(); // 菜单挂起时长剔除(分级保留,指证耗时照计) + this._backToBoard(); + } else { + this._closeMenu(); + } + } else if (item === 'sys_menu_cf') { + // 去指证 = 长按捷径的菜单冗余入口(P4 容错,§2.4) + if (this.state === 'BOARD' && !this._play) { + this.menuOpen = false; this.menuFrom = null; + this._menuOpenedAt = null; + this._enterConfront(); + } else { + this._closeMenu(); + } + } else if (item === 'sys_menu_save') { + this._save(); + this._speakChain(['voice/sys_saved'], () => this._goHome()); + } else if (item === 'sys_menu_help') { + this._speakChain(['voice/sys_help', 'voice/settings_hint']); + } else if (item === 'sys_menu_restart') { + this.menuOpen = false; this.menuFrom = null; + this._menuOpenedAt = null; + this._restartCase(); + } + } + + // ================================ 看门狗(§9) ================================ + + // ---- 无操作轨(guide.watchdog:手势重置;guide=off 全静零定时器) ---- + + _armBoardWatchdog() { + this.guide.watchdog({ + arm: true, at: [10000, 25000], say: ['gd_board', null], + cond: () => this.state === 'BOARD' && !this.menuOpen && !this._play, + }); + } + + _armCfWatchdog() { + this.guide.watchdog({ + arm: true, at: [15000, 30000], say: ['gd_cf', null], + cond: () => this.state === 'CONFRONT' && !this.menuOpen && this._cf && this._cf.phase !== 'judged', + }); + } + + _armFinWatchdog() { + this.guide.watchdog({ + arm: true, at: [20000, 40000], say: ['gd_fin', null], + cond: () => this.state === 'FINISH' && !this.menuOpen, + }); + } + + _armDrillWatchdog() { // 教学步无进展轨(§9.2:nudge → 重述指令 → 自动滚出) + const d = this._drill; + if (!d) return; + const stepVoice = ['tut_d0', 'tut_listen', 'tut_replay', 'tut_status', 'tut_confront', 'tut_cf_try', 'tut_done'][d.step] || null; + this.guide.watchdog({ + arm: true, at: DRILL_WATCHDOG_AT, say: [null, stepVoice, null], + cond: () => this.state === 'DRILL' && this._drill === d, + onEscalate: (n) => { if (n === 3) this._drillSkip(); }, + }); + } + + // ---- 无进展轨(游戏层 [45/90/150];只由进展事件重置——乱按饿不死,§9.3) ---- + + _progressEvent() { + const p = this._prog; + p.lastAt = performance.now(); + p.l1 = false; p.l2 = false; p.l3 = false; + } + + _progArm() { + if (this._prog.armed && this._prog.armedSession === this._session) return; + this._prog.armed = true; + this._prog.armedSession = this._session; + const s = this._session; + const loop = () => { + if (s !== this._session) { this._prog.armed = false; return; } + // 播放会话期间照常检查(计时挂墙钟,只由 _progressEvent 重置,§9.3)—— + // 听新条目会在链尾自然重置;重听旧条目照样攒时间,乱按饿不死 + if (this.state === 'BOARD' && !this.menuOpen) this._progCheck(); + this._after(PROG_POLL_MS, loop); + }; + this._after(PROG_POLL_MS, loop); + } + + /** + * 无进展轨分级(§9.3):t = 墙钟距上次进展事件(语音在播不冻结)。 + * - L1 turn_tick 是 sfx 层 earcon「不占台词通道」——到点即发(证词播中也照发); + * - L2/L3 是台词句——阈值判定不冻结,但只在**无证词链会话**时发(不掐断玩家 + * 主动听取的证词句;报名/重听类短句可被顶掉——可重复触发,无内容损失。 + * 否则乱按者自己触发的短语音会把救援句饿死,违背「三级必达」)。 + */ + _progCheck() { + const p = this._prog; + const t = performance.now() - p.lastAt; + const chainBusy = !!this._play; // 证词链在播 = 保护;短语音不构成保护 + if (p.l3) { // L3 后每 150s 重复(每句全局限 2 次,之后静默) + if (t >= PROG_LEVELS[2] && !chainBusy) { this._fireL3(); p.lastAt = performance.now(); } + return; + } + if (t >= PROG_LEVELS[2]) { + if (!chainBusy) { this._fireL3(); p.l3 = true; p.lastAt = performance.now(); } + return; + } + if (t >= PROG_LEVELS[1] && !p.l2) { + if (chainBusy) return; // 链空了再发(不标记,顺延不丢级) + p.l2 = true; + const id = LISTEN_ITEMS.some((x) => !this.run.heard.includes(x)) ? 'hint_a' : 'hint_b'; + this._hintSpeak(id); + return; + } + if (t >= PROG_LEVELS[0] && !p.l1) { + p.l1 = true; + this.audio.play('sfx/turn_tick', { volume: 0.7 }); // L1 earcon:sfx 层,语音在播也照发 + } + } + + /** L3 保底:直接给出可指证的矛盾对(seed 定序轮询未发现清单,§9.3)。 */ + _fireL3() { + const run = this.run; + if (this._doubtCount() === 4) { this._hintSpeak('hint_fin'); return; } + for (const cid of this._case.layout.hintOrder) { + const c = this._case.contradictions.find((x) => x.id === cid); + const idx = this._case.contradictions.indexOf(c); + if (run.doubts[idx] || !c.hint) continue; + if ((this._prog.counts[c.hint] || 0) >= HINT_MAX_TIMES) continue; + this._prog.counts[c.hint] = (this._prog.counts[c.hint] || 0) + 1; + this._hintSpeak(c.hint); + return; + } + // 可播提示耗尽(m2/m3 无提示句 / 全局 2 次用完):退回 L2 口径 + if ((this._prog.counts.hint_b || 0) < HINT_MAX_TIMES) { + this._hintSpeak('hint_b'); + } + } + + _hintSpeak(id) { + // L2/L3 每句全局限 2 次(§9.3「不轰炸」口径统一) + if (id === 'hint_a' || id === 'hint_b' || id === 'hint_fin') { + this._prog.counts[id] = (this._prog.counts[id] || 0) + 1; + if (this._prog.counts[id] > HINT_MAX_TIMES) { this._prog.counts[id] = HINT_MAX_TIMES; return; } + } + vibrate('warning'); + this.run.hintsGiven.push(id); // 保底提示入账(评级变量 hints,§7.2) + this._save(); + this._speakChain(['voice/' + id]); + } + + // ================================ 语音链 / 计时器基础设施 ================================ + + /** 单句播放:自然播完推进;8s 兜底(资产缺失告警续链,§4.3 契约)。 */ + _playSentence(lineId, onDone) { + const s = this._session; + let fired = false; + const fin = () => { + if (fired || s !== this._session) return; + fired = true; + if (onDone) onDone(); + }; + this.audio.play('voice/' + lineId, { onComplete: fin }); + this._after(SENTENCE_FALLBACK_MS, fin); + } + + /** 串行链:逐句 onComplete(或 8s 兜底)推进;session 变更(打断)整条作废。 */ + _speakChain(ids, onDone) { + if (!ids || !ids.length) { if (onDone) onDone(); return; } + const s = this._session; + const head = ids[0].replace(/^voice\//, ''); + this._playSentence(head, () => { + if (s !== this._session) return; + this._speakChain(ids.slice(1), onDone); + }); + } + + _after(ms, fn) { + const s = this._session; + const id = setTimeout(() => { + this._timers = this._timers.filter((t) => t !== id); + if (s === this._session) fn(); + }, ms); + this._timers.push(id); + return id; + } + + _clearTimers() { + for (const id of this._timers) clearTimeout(id); + this._timers = []; + } + + // ================================ 存档(§10) ================================ + + _summary() { + if (!this.run) return `已结案${this.meta.casesClosed}起 · 最佳${this.meta.bestGrade ?? '—'}`; + return `${this._case ? this._case.name : '雨夜'} · 已问${this.run.heard.length}/6 · 疑点${this._doubtCount()}/4`; + } + + _save() { + const r = this.run; + const data = { + schema: 1, + game: 'detective', + updatedAt: new Date().toISOString(), + summary: this._summary(), + meta: { + drillDone: this.meta.drillDone, casesClosed: this.meta.casesClosed, + bestGrade: this.meta.bestGrade, lastPack: this.meta.lastPack, + }, + run: r ? { + seed: r.seed, pack: r.pack, layout: deepClone(r.layout), + heard: [...r.heard], heardLines: { ...r.heardLines }, lastLine: { ...r.lastLine }, + doubts: [...r.doubts], cfMiss: r.cfMiss, wrongWho: r.wrongWho, zHits: r.zHits, + hintsGiven: [...r.hintsGiven], hintFinShown: r.hintFinShown, + weaponAns: r.weaponAns, motiveAns: r.motiveAns, + } : null, + }; + try { localStorage.setItem(SAVE_KEY, JSON.stringify(data)); } catch (_) { /* 隐私模式 */ } + try { + const auth = JSON.parse(localStorage.getItem(AUTH_KEY)); + if (!auth || !auth.token) return; + fetch('/api/progress', { + method: 'PUT', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + auth.token }, + body: JSON.stringify({ game: 'detective', data }), + }).catch(() => { /* 离线静默 */ }); + } catch (_) { /* 静默 */ } + } + + _loadSave() { + try { + const data = JSON.parse(localStorage.getItem(SAVE_KEY)); + if (!data || data.schema !== 1 || !data.meta) return null; + if (data.run && typeof data.run.seed === 'number' && Array.isArray(data.run.doubts)) { + this.run = { + seed: data.run.seed, pack: data.run.pack ?? 0, + layout: data.run.layout || null, + heard: [...(data.run.heard || [])], + heardLines: { ...(data.run.heardLines || {}) }, + lastLine: { ...(data.run.lastLine || {}) }, + doubts: [...data.run.doubts], + cfMiss: data.run.cfMiss || 0, wrongWho: data.run.wrongWho || 0, zHits: data.run.zHits || 0, + hintsGiven: [...(data.run.hintsGiven || [])], + hintFinShown: !!data.run.hintFinShown, + weaponAns: data.run.weaponAns ?? null, motiveAns: data.run.motiveAns ?? null, + }; + } + return data; + } catch (_) { + return null; + } + } +} diff --git a/web/detective/gen/gen_bgm.sh b/web/detective/gen/gen_bgm.sh new file mode 100644 index 00000000..0ffd8481 --- /dev/null +++ b/web/detective/gen/gen_bgm.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# 《雨夜专案》BGM 后处理(批次 2):Mureka flac 候选 -> 90s 无缝循环 ogg+m4a。 +# 用法: bash web/detective/gen/gen_bgm.sh (flac 原件在 tools/mureka_out/,不入库) +# +# 工艺(tools/mureka_log.md 批次 1 同款): +# 首个强 onset 起点(silenceremove,-35dB 门限)→ 93s 裁剪 → 首尾 3s acrossfade(tri) +# 无缝循环 → loudnorm I=-20/TP=-2/LRA=11(18-detective.md §8.1 明确 3 首全部 -20 LUFS; +# SETTLE 的 0.5 volume 由游戏层控制)→ aresample 44.1kHz +# → ogg libvorbis q3 + m4a aac 128k,落 web/detective/audio/bgm/。 +# 雨声底按设计烤进曲目(§8.1:省第三声流);无节奏关键曲目,不需要 bpm_precise +# (jazz 的 brushed drums 仅为 prompt 特征词)。 +set -euo pipefail +cd /home/xsl/SoundGame + +TMP=tools/bgm_post_wav/detective # 中间 wav(不入库,可由本脚本复现) +DST=web/detective/audio/bgm +mkdir -p "$TMP" "$DST" + +post() { # post + local src="$1" track="$2" lufs="$3" + local tmp="$TMP/$track.wav" + ffmpeg -y -v error -i "$src" -af \ +"silenceremove=start_periods=1:start_threshold=-35dB:start_silence=0.02,atrim=end=93,asetpts=PTS-STARTPTS,asplit[a][b];[b]atrim=start=90,asetpts=PTS-STARTPTS[t];[a][t]acrossfade=d=3:c1=tri:c2=tri,loudnorm=I=${lufs}:TP=-2:LRA=11,aresample=44100" \ + "$tmp" + ffmpeg -y -v error -i "$tmp" -c:a libvorbis -q:a 3 "$DST/$track.ogg" + ffmpeg -y -v error -i "$tmp" -c:a aac -b:a 128k "$DST/$track.m4a" + echo "$track <- $src (I=${lufs})" +} + +# 选用候选 = 更接近 90s 的那支(拿不准原则),详见 mureka_log.md 批次 2 节 +post tools/mureka_out/detective-menu/160001918435329_1.flac menu -20 # 219.8s +post tools/mureka_out/detective-board/160002065039361_0.flac board -20 # 216.8s +post tools/mureka_out/detective-verdict/160002209873922_1.flac verdict -20 # 223.7s + +echo "== detective bgm done: $(ls "$DST" | wc -l) files ==" diff --git a/web/detective/gen/gen_cover.py b/web/detective/gen/gen_cover.py new file mode 100644 index 00000000..0fa06b98 --- /dev/null +++ b/web/detective/gen/gen_cover.py @@ -0,0 +1,115 @@ +"""web/detective 封面:1080x1920,雨夜推理主题——墨蓝夜雨、一盏路灯暖光、 +雨丝竖线与居中标题《雨夜专案》。可复现脚本(固定 seed,无外部网络依赖)。 + +用法(PIL 探测顺序:系统 python3 → .venv-tts → 装 pillow 后任一): + python3 web/detective/gen/gen_cover.py +""" +import math +import os +import random + +from PIL import Image, ImageDraw, ImageFilter, ImageFont + +W, H = 1080, 1920 +FONT_CANDIDATES = [ + "/home/xsl/SoundGame/web/rhythm/gen/NotoSansSC-Bold.ttf", + "/home/xsl/SoundGame/app/src/reflex/gen/NotoSansSC-Bold.ttf", +] +OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "cover.png") + + +def load_font(size): + for p in FONT_CANDIDATES: + if os.path.exists(p): + return ImageFont.truetype(p, size) + raise SystemExit("未找到中文字体,请放一份 NotoSansSC-Bold.ttf 到 web/rhythm/gen/") + + +def main(): + rng = random.Random(918) # 固定 seed:可复现 + img = Image.new("RGB", (W, H), (10, 12, 20)) + d = ImageDraw.Draw(img) + + # 背景竖向渐变:夜空墨蓝 -> 街面冷灰(雨夜街道) + for y in range(H): + t = y / H + d.line([(0, y), (W, y)], + fill=(int(14 + 10 * t), int(17 + 12 * t), int(26 + 18 * t))) + + # 一盏路灯暖光(左上,光晕压暗)——雨夜里唯一的暖色 + lx, ly = 250, 380 + lamp = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + ld = ImageDraw.Draw(lamp) + for r, alpha in [(420, 26), (300, 40), (190, 58), (110, 80)]: + ld.ellipse([lx - r, ly - r, lx + r, ly + r], fill=(253, 230, 138, alpha)) + lamp = lamp.filter(ImageFilter.GaussianBlur(14)) + img.paste(lamp, (0, 0), lamp) + d = ImageDraw.Draw(img) + d.ellipse([lx - 26, ly - 26, lx + 26, ly + 26], fill=(255, 244, 214)) + + # 雨丝:斜细线两景深(远淡近亮),风斜角 ~8° + rain = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + rd = ImageDraw.Draw(rain) + for _ in range(90): # 远景雨 + x = rng.randint(0, W + 200) + y = rng.randint(0, H) + ln = rng.randint(40, 90) + rd.line([(x, y), (x - ln * 0.14, y + ln)], + fill=(148, 163, 184, rng.randint(16, 34)), width=1) + for _ in range(60): # 近景雨 + x = rng.randint(0, W + 200) + y = rng.randint(0, H) + ln = rng.randint(90, 160) + rd.line([(x, y), (x - ln * 0.14, y + ln)], + fill=(203, 213, 225, rng.randint(28, 52)), width=2) + img.paste(rain, (0, 0), rain) + d = ImageDraw.Draw(img) + + # 中央:案卷卷宗剪影(翻开的两页,暗金色边线)——「卷宗六份」的具象 + cx, cy = W // 2, 640 + doc = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + ddoc = ImageDraw.Draw(doc) + ddoc.polygon([(cx - 40, cy - 120), (cx - 250, cy - 60), (cx - 250, cy + 190), (cx - 40, cy + 130)], + outline=(120, 113, 108, 220), width=4) + ddoc.polygon([(cx + 40, cy - 120), (cx + 250, cy - 60), (cx + 250, cy + 190), (cx + 40, cy + 130)], + outline=(120, 113, 108, 220), width=4) + # 页面行文线(证词的横线,右页夹一条对不上的红线) + for i in range(5): + yy = cy - 26 + i * 40 + ddoc.line([(cx - 215, yy - 28), (cx - 75, yy - 46)], fill=(148, 163, 184, 120), width=3) + ddoc.line([(cx + 75, yy - 46), (cx + 215, yy - 28)], fill=(148, 163, 184, 120), width=3) + ddoc.line([(cx + 92, cy + 62), (cx + 208, cy + 80)], fill=(248, 113, 113, 200), width=4) + img.paste(doc, (0, 0), doc) + d = ImageDraw.Draw(img) + + # 标题(居中)与副题 + f_title = load_font(172) + f_sub = load_font(54) + f_small = load_font(38) + + def ctext(y, s, font, fill, spacing=8): + bbox = d.textbbox((0, 0), s, font=font) + w = bbox[2] - bbox[0] + x = (W - w) / 2 + for ch in s: + d.text((x, y), ch, font=font, fill=fill) + bb = d.textbbox((0, 0), ch, font=font) + x += (bb[2] - bb[0]) + spacing + + ctext(1010, "雨夜专案", f_title, (237, 233, 254), spacing=16) + ctext(1262, "证词会撒谎,证据不会", f_sub, (148, 163, 184)) + + # 手势图例(极简两行) + hints = ["左右滑 换卷宗", "单击 听取", "长按 指证", "上滑 进度", "下滑 菜单"] + for i, s in enumerate(hints): + col, row = i % 3, i // 3 + d.text((110 + col * 300, 1420 + row * 76), s, font=f_small, fill=(104, 122, 152)) + + ctext(1700, "翻听六份卷宗,指出矛盾,锁定真凶。", f_small, (86, 102, 130)) + + img.save(OUT) + print(OUT, img.size) + + +if __name__ == "__main__": + main() diff --git a/web/detective/gen/gen_sfx.py b/web/detective/gen/gen_sfx.py new file mode 100644 index 00000000..ec395cba --- /dev/null +++ b/web/detective/gen/gen_sfx.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""《雨夜专案》SYN 程序合成音效(批次 2,18-detective.md §8.2 标注 SYN 的条目)。 + +6 个 id(其余 6 条为 Stable Audio 3 条目,见 gen_sfx_sa3.py): + sig_shun 小顺签名:布掸子拍柜台两下(双拍软拍,间隔 0.16s) + sig_wen 温太太签名:门环叩两下(金属环叩木门,二连击) + sig_fore 法医签名:纸页翻动 + 钢笔划线(噪声簌 + 笔尖刮擦两段) + contra_hit 指证命中:不和谐金属小二度相击 + 盖章尾(全游戏最强确认音,-4dBFS) + contra_miss 指证落空:钝木鱼一下(非 error 音色、不威吓,-8dBFS) + turn_tick 看门狗 L1:880Hz 木鱼族轻响(dungeon 同配方自存一份,-8dBFS) + +签名 earcon 规格:条目切换/指证选择/状态复述时前置(0.3-0.5s 量级),节拍可数 +(两下=可辨身份),电平中低不抢人声。 + +可复现:本脚本入库,一键出 web/detective/audio/sfx/*.{ogg,m4a};中间 wav 在 +gen/staging/sfx/(不入库)。规格:ogg libvorbis q4 44.1kHz + m4a aac 128k; +one-shot 真峰值 <= -3dBFS(解码回读法)。 + +用法:.venv-tts/bin/python web/detective/gen/gen_sfx.py (纯 CPU,无 GPU 依赖) +""" +import os +import subprocess + +import numpy as np +import soundfile as sf + +SR = 44100 +HERE = os.path.dirname(os.path.abspath(__file__)) +STAGING = os.path.join(HERE, "staging", "sfx") +DST = os.path.join(HERE, "..", "audio", "sfx") +FFMPEG = "/home/xsl/SoundGame/tools/bin/ffmpeg" +os.makedirs(STAGING, exist_ok=True) +os.makedirs(DST, exist_ok=True) + + +def t(d): + return np.arange(int(SR * d)) / SR + + +def env_exp(d, attack, release): + n = int(SR * d) + e = np.ones(n) + na, nr = max(1, int(SR * attack)), max(1, int(SR * release)) + e[:na] = np.linspace(0, 1, na) + e[-nr:] *= np.linspace(1, 0, nr) + return e + + +def lowpass(x, k): + return np.convolve(x, np.ones(k) / k, mode="same") + + +def bandpass(x, lo, hi): + kh, kl = max(1, int(SR / (np.pi * hi))), max(1, int(SR / (np.pi * lo))) + return lowpass(x, kh) - lowpass(x, kl) + + +def save(name, sig, peak_db=-6.0): + """mono 存。工艺同 tools/fix_clicks.py:去直流 → 首尾 8ms cos² 淡入淡出 → + 峰值归一指定 dBFS(签名 earcon 默认 -6,contra_hit -4,miss/tick -8)。""" + sig = sig - np.mean(sig) + nf = max(1, int(SR * 0.008)) + fade = 0.5 * (1 - np.cos(np.pi * np.arange(nf) / nf)) + sig = sig.astype(np.float64).copy() + sig[:nf] *= fade + sig[-nf:] *= fade[::-1] + sig = sig / (np.max(np.abs(sig)) + 1e-9) * 10 ** (peak_db / 20) + sf.write(os.path.join(STAGING, name + ".wav"), sig.astype(np.float32), SR) + + +rng = np.random.default_rng(20260909) + +# ---- sig_shun:布掸子拍柜台两下(软布拍击:中频噪声团 + 轻微柜台木腔共鸣) ---- +def duster_pat(dur=0.10): + x = t(dur) + pat = bandpass(rng.standard_normal(len(x)), 250, 1600) * env_exp(dur, 0.012, 0.05) + wood = 0.4 * np.sin(2 * np.pi * 190 * x) * np.exp(-x * 55) + return pat + wood + +y = np.zeros(int(SR * 0.42), dtype=np.float64) +for i, amp in enumerate([1.0, 0.85]): + k = duster_pat() + s = int(SR * (0.02 + i * 0.16)) + y[s:s + len(k)] += k * amp +save("sig_shun", y) + +# ---- sig_wen:门环叩两下(金属环叩木门:非谐波金属簇 + 门板低频响应,二连击) ---- +def knocker(dur=0.13): + x = t(dur) + ring = sum(a * np.sin(2 * np.pi * fr * x) * np.exp(-x * dc) + for fr, a, dc in [(880, 1.0, 30), (1350, 0.5, 42), (2210, 0.3, 55)]) + door = 0.55 * np.sin(2 * np.pi * 120 * x) * np.exp(-x * 40) + thud = lowpass(rng.standard_normal(len(x)), 80) * np.exp(-x * 70) * 0.45 + return ring + door + thud + +y = np.zeros(int(SR * 0.46), dtype=np.float64) +for i, amp in enumerate([1.0, 0.8]): + k = knocker() + s = int(SR * (0.02 + i * 0.18)) + y[s:s + len(k)] += k * amp +save("sig_wen", y) + +# ---- sig_fore:纸页翻动 + 钢笔划线(簌簌噪声扫频 + 笔尖刮擦两划) ---- +d = 0.30 +x = t(d) +page = bandpass(rng.standard_normal(len(x)), 900, 5000) * env_exp(d, 0.02, 0.10) * 0.8 # 翻页簌 +y = page.copy() +for start, dur2 in [(0.14, 0.10), (0.21, 0.07)]: # 钢笔两划 + seg = t(dur2) + pen = bandpass(np.random.default_rng(int(start * 1000)).standard_normal(len(seg)), + 1500, 4500) * env_exp(dur2, 0.01, 0.03) * 0.5 + s = int(SR * start) + y[s:s + len(pen)] += pen +save("sig_fore", y) + +# ---- contra_hit:不和谐金属小二度相击 + 盖章尾(矛盾证实音,全游戏最强确认音) ---- +# 1250/1320Hz 小二度拍频互殴 0.18s -> 顿 0.05s -> 低频盖章闷击(thunk)收尾。 +d1 = 0.18 +x1 = t(d1) +clash = (np.sin(2 * np.pi * 1250 * x1) + np.sin(2 * np.pi * 1320 * x1) + + 0.4 * np.sin(2 * np.pi * 2500 * x1) + 0.3 * np.sin(2 * np.pi * 2640 * x1)) * np.exp(-x1 * 13) +d2 = 0.16 +x2 = t(d2) +stamp = (np.sin(2 * np.pi * 95 * x2) * np.exp(-x2 * 22) + + 0.6 * lowpass(rng.standard_normal(len(x2)), 70) * np.exp(-x2 * 40)) +save("contra_hit", np.concatenate([clash, np.zeros(int(SR * 0.05)), stamp, np.zeros(int(SR * 0.08))]), + peak_db=-4.0) + +# ---- contra_miss:钝木鱼一下(低频短木腔音,非 error 音色) ---- +d = 0.16 +x = t(d) +save("contra_miss", np.sin(2 * np.pi * 310 * x) * np.exp(-x * 30) + + 0.5 * np.sin(2 * np.pi * 620 * x) * np.exp(-x * 45) + + 0.3 * lowpass(rng.standard_normal(len(x)), 60) * np.exp(-x * 70), + peak_db=-8.0) + +# ---- turn_tick:880Hz 木鱼族轻响(dungeon 同配方自存一份;无进展轨 L1 不占台词通道) ---- +d = 0.16 +x = t(d) +save("turn_tick", np.sin(2 * np.pi * 880 * x) * np.exp(-x * 38) + + 0.35 * np.sin(2 * np.pi * 1760 * x) * np.exp(-x * 52), + peak_db=-8.0) + + +def encode(name): + """staging wav -> ogg q4 + m4a 128k,解码回读校验:逐声道真峰值 <= -3dBFS、|直流| <= 0.001。 + 工艺:源尾部垫 50ms 静音(编码器软着陆)+ highpass=f=30(抑制解码直流再生); + 纯弦 AAC 过冲按实测迭代补 volume(abyss link_beep 同坑,dungeon 补偿同款)。""" + src = os.path.join(STAGING, name + ".wav") + base, _sr0 = sf.read(src, always_2d=True) + pad_t = np.zeros((int(SR * 0.05), base.shape[1]), dtype=np.float64) + base = np.concatenate([base, pad_t]) + for ext, cmd in [("ogg", ["-c:a", "libvorbis", "-q:a", "4"]), + ("m4a", ["-c:a", "aac", "-b:a", "128k"])]: + out = os.path.join(DST, f"{name}.{ext}") + vol, dc_adj = 0.0, 0.0 + tmp = "/tmp/_sfx_enc_det.wav" + for _attempt in range(4): + sig = base - dc_adj + if vol < 0: + sig = sig * (10 ** (vol / 20)) + sf.write(tmp, sig.astype(np.float32), SR) + subprocess.run([FFMPEG, "-y", "-v", "error", "-i", tmp, "-af", "highpass=f=30", + "-ar", "44100", *cmd, out], check=True) + chk = "/tmp/_sfx_qc_det.wav" + subprocess.run([FFMPEG, "-y", "-v", "error", "-i", out, "-c:a", "pcm_f32le", chk], check=True) + y, _sr = sf.read(chk, always_2d=True) + tp = 20 * np.log10(np.max(np.abs(y)) + 1e-9) + dcv = float(np.mean(y.mean(axis=1))) + if tp <= -3.0 and abs(dcv) <= 0.001: + break + if tp > -3.0: + vol -= (tp + 3.0) + 0.25 + if abs(dcv) > 0.001: + dc_adj -= dcv + else: + raise AssertionError(f"{name}.{ext}: 补偿后仍超限 tp={tp:.2f}dBFS dc={dcv:.4f}") + print(f"[ok] {name}.{ext} peak={tp:.2f}dBFS" + (f" (vol {vol:+.2f}dB)" if vol else "")) + + +for _n in ["sig_shun", "sig_wen", "sig_fore", "contra_hit", "contra_miss", "turn_tick"]: + encode(_n) +print("detective SYN sfx done: 6 files x2 formats") diff --git a/web/detective/gen/gen_sfx_sa3.py b/web/detective/gen/gen_sfx_sa3.py new file mode 100644 index 00000000..e5226ed0 --- /dev/null +++ b/web/detective/gen/gen_sfx_sa3.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""《雨夜专案》Stable Audio 3 small-sfx 音效(批次 2,18-detective.md §8.2 SA3 条目)。 + +生成(GPU,flock 串行;模型 gated 走 hf-mirror,见 env.d/sfx.sh): + python3 gen_sfx_sa3.py --emit # 打印生成命令,原样执行 +生成 6 条原始 wav 到 gen/staging/sfx_sa3/detective_.wav(顺序 = 下表 idx)。 +本脚本随后做后处理:修整(签名 earcon 按 max_s 压进 0.3-1.0s 量级——条目切换/ +指证选择前置音,长了拖慢导航)/去直流/8ms cos² 淡化/ 响度定档 → ogg q4 + +m4a 128k(highpass=f=30,one-shot 前后垫 60/120ms 静音)→ 解码回读 QC:逐声道 +真峰值 <= -3dBFS、|直流| <= 0.001;超限降增益 + 首轮重编加 18kHz 低通,最多 3 轮。 + +prompt:paper_turn/gavel 直出文档英文原稿;sig_ge/sig_zhou/sig_scene/rain_swell +为文档中文拟声条的英文化(信源:18-detective.md §8.2)。定档: + gavel -4(裁决仪式拍)、paper_turn -7(导航细节反馈) + sig_* -6(签名前置音,可辨不抢人声)、rain_swell -10(认罪链垫,压在 bgm/verdict 下) + +用法:.venv-tts/bin/python web/detective/gen/gen_sfx_sa3.py [--emit] +""" +import os +import subprocess +import sys + +import numpy as np +import soundfile as sf + +SR = 44100 +HERE = os.path.dirname(os.path.abspath(__file__)) +STAGING = os.path.join(HERE, "staging", "sfx_sa3") +DST = os.path.join(HERE, "..", "audio", "sfx") +SA3 = "/home/xsl/SoundGame/tools/stable-audio-3" +FFMPEG = "/home/xsl/SoundGame/tools/bin/ffmpeg" +SEED = 20260909 + +# (id, gen 秒, prompt, 定档 dBFS, trim 上限 s) +ITEMS = [ + ("paper_turn", 3.0, "single paper page turn on desk, dry, close", -7.0, 0.9), + ("sig_ge", 3.0, "Enamel mug thumped down on a wooden desk, single dull metallic knock, close", -6.0, 0.7), + ("sig_zhou", 3.0, "Wooden abacus beads flicked twice, dry clacking, close up", -6.0, 0.9), + ("sig_scene", 4.0, "Leather shoes stepping through rain puddles twice, then one umbrella shake, night rain, close", -6.0, 1.1), + ("gavel", 4.0, "wooden gavel single strike, small room reverb", -4.0, 3.0), + ("rain_swell", 9.0, "Night rain swelling heavier then gradually settling, steady rain on windows and wet street, 6 seconds", -10.0, 7.0), +] + +WIN = {} # 事件窗(raw 定长 latent 床):首轮自然修剪,实测偏离再回填 + + +def emit(): + durs = " ".join(str(d) for _, d, *_ in ITEMS) + ps = " ".join(f'"{p}"' for _, _, p, *_ in ITEMS) + print(f"cd {SA3} && flock /home/xsl/SoundGame/.gpu.lock .venv/bin/stable-audio " + f"--model small-sfx --steps 8 --seed {SEED} " + f"-p {ps} --duration {durs} -o {STAGING}/detective.wav") + + +def trim_oneshot(y, floor_db=-50.0, head_ms=50, tail_ms=140, max_s=None): + env = np.max(np.abs(y), axis=1) + act = np.where(env > 10 ** (floor_db / 20))[0] + a = max(0, act[0] - int(SR * head_ms / 1000)) + b = min(len(y), act[-1] + int(SR * tail_ms / 1000)) + if max_s: + b = min(b, a + int(SR * max_s)) + return y[a:b] + + +def prep_oneshot(y, target_db, win=None, max_s=None): + if win: + a, b = int(SR * win[0]), int(SR * win[1]) + y = y[a:b] + nf2 = max(1, int(SR * 0.050)) + y[-nf2:] *= (0.5 * (1 + np.cos(np.pi * np.arange(nf2) / nf2)))[:, None] + else: + y = trim_oneshot(y, max_s=max_s) + y = y - np.mean(y, axis=0, keepdims=True) + nf = max(1, int(SR * 0.008)) + fade = 0.5 * (1 - np.cos(np.pi * np.arange(nf) / nf)) + y[:nf] *= fade[:, None] + y[-nf:] *= fade[::-1][:, None] + y = y / (np.max(np.abs(y)) + 1e-9) * 10 ** (target_db / 20) + pad_h = np.zeros((int(SR * 0.06), y.shape[1])) + pad_t = np.zeros((int(SR * 0.12), y.shape[1])) + return np.concatenate([pad_h, y, pad_t]) + + +def encode(name, sig): + base = sig.astype(np.float64) + vol, lpf, dc_adj = 0.0, False, 0.0 + tmp = os.path.join(STAGING, "_enc.wav") + chk = os.path.join(STAGING, "_qc.wav") + res = {} + for attempt in range(4): + x = base - dc_adj + if vol < 0: + x = x * (10 ** (vol / 20)) + sf.write(tmp, x.astype(np.float32), SR) + af = "highpass=f=30,lowpass=f=18000" if lpf else "highpass=f=30" + for ext, cmd in [("ogg", ["-c:a", "libvorbis", "-q:a", "4"]), + ("m4a", ["-c:a", "aac", "-b:a", "128k"])]: + out = os.path.join(DST, f"{name}.{ext}") + subprocess.run([FFMPEG, "-y", "-v", "error", "-i", tmp, "-af", af, + "-ar", "44100", *cmd, out], check=True) + subprocess.run([FFMPEG, "-y", "-v", "error", "-i", out, "-c:a", "pcm_f32le", chk], check=True) + y, sr = sf.read(chk, always_2d=True) + tp = 20 * np.log10(np.max(np.abs(y)) + 1e-9) + dcv = float(np.mean(y.mean(axis=1))) + rms = 20 * np.log10(np.sqrt(np.mean(y ** 2)) + 1e-9) + ok_tp, ok_dc = tp <= -3.0, abs(dcv) <= 0.001 + res[ext] = (tp, dcv, rms, y.shape[0] / sr) + if not (ok_tp and ok_dc): + if not ok_tp: + vol -= (tp + 3.0) + 0.25 + lpf = True + if not ok_dc: + dc_adj -= dcv + break + else: + break + else: + raise AssertionError(f"{name}: 补偿 3 轮后仍超限 {res}") + return res + + +def main(): + os.makedirs(STAGING, exist_ok=True) + if "--emit" in sys.argv: + emit() + return + print(f"| {'id':12s} | {'时长s':>6s} | {'ogg峰dBFS':>9s} | {'m4a峰dBFS':>9s} | {'RMS dBFS':>8s} | 直流 |") + for i, (name, _d, _p, tgt, max_s) in enumerate(ITEMS): + raw = os.path.join(STAGING, f"detective_{i}.wav") + y, sr = sf.read(raw, always_2d=True, dtype="float64") + assert sr == SR, f"{name}: 采样率 {sr}" + if y.shape[1] == 1: + y = np.stack([y[:, 0], y[:, 0]], axis=1) + sig = prep_oneshot(y, tgt, win=WIN.get(name), max_s=max_s) + res = encode(name, sig) + tp_o, dc, rms, dur = res["ogg"] + print(f"| {name:12s} | {dur:6.2f} | {tp_o:9.2f} | {res['m4a'][0]:9.2f} | {rms:8.2f} | {dc:+.4f} |") + print("detective SA3 sfx done -> web/detective/audio/sfx/") + + +if __name__ == "__main__": + main() diff --git a/web/detective/index.html b/web/detective/index.html new file mode 100644 index 00000000..9549c163 --- /dev/null +++ b/web/detective/index.html @@ -0,0 +1,36 @@ + + + + + + + 雨夜专案 · SoundGame Web + + + + +
+ +
+ + + + + diff --git a/web/detective/test/playtest.mjs b/web/detective/test/playtest.mjs new file mode 100644 index 00000000..a2437992 --- /dev/null +++ b/web/detective/test/playtest.mjs @@ -0,0 +1,1273 @@ +/** + * 《雨夜专案》无头试玩测试(docs/design/18-detective.md §11 断言清单 14 条关键路径)。 + * + * 以玩家身份模拟:mock 浏览器环境(window/navigator/localStorage/fetch/虚拟时钟) + * 与音频引擎接口(播放日志 + voice onComplete 按虚拟时钟推进语音链),加载真实 + * game.js,手势直接注入 game.onGesture。mock 层机制对齐 web/dungeon/test/playtest.mjs + * (navigator.maxTouchPoints=1 手机 persona;guide:'off' 回归纯玩法逻辑)。 + * + * 资产契约:引擎对缺失资产只 console.error 不崩——本测试在资产未落盘时也必须 + * 全绿(每句 8s 兜底续链推进),故 plays() 查询不过滤 missing 项;资产齐套后 + * 同样全绿(顺序/次数断言与时长无关)。资产审计只做信息性报告不判失败。 + * + * 运行:tools/node/bin/node web/detective/test/playtest.mjs + * 退出码 0 = 全部断言通过。seed/pack 由 opts 注入复现(§6.2)。 + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const DETECTIVE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +// ================================ 虚拟时钟 ================================ + +let vnow = 0; // 虚拟毫秒 +const timers = new Map(); +let nextTimerId = 1; + +function schedule(ms, fn) { + const id = nextTimerId++; + timers.set(id, { at: vnow + ms, fn }); + return id; +} +function unschedule(id) { timers.delete(id); } + +async function flushMicro() { + for (let i = 0; i < 5; i++) await Promise.resolve(); +} + +/** 推进虚拟时钟 ms 毫秒,期间到期的计时器按序触发(含语音 onComplete 链)。 */ +async function tick(ms) { + const target = vnow + ms; + for (let guard = 0; guard < 200000; guard++) { + await flushMicro(); + let best = null; + for (const [id, t] of timers) { + if (t.at <= target && (!best || t.at < best.t.at)) best = { id, t }; + } + if (!best) break; + timers.delete(best.id); + vnow = best.t.at; + best.t.fn(); + } + vnow = target; + await flushMicro(); +} + +// ================================ 浏览器环境 mock ================================ + +const storageMap = new Map(); +const localStorageMock = { + getItem: (k) => (storageMap.has(k) ? storageMap.get(k) : null), + setItem: (k, v) => { storageMap.set(k, String(v)); }, + removeItem: (k) => { storageMap.delete(k); }, + clear: () => storageMap.clear(), +}; + +const vibrateLog = []; +const fetchLog = []; + +Object.defineProperty(globalThis, 'performance', { value: { now: () => vnow }, configurable: true }); +globalThis.setTimeout = (fn, ms = 0) => schedule(ms, fn); +globalThis.clearTimeout = unschedule; +globalThis.localStorage = localStorageMock; +globalThis.fetch = (url, opts) => { fetchLog.push({ url, opts }); return Promise.resolve({ ok: true, json: async () => ({}) }); }; +Object.defineProperty(globalThis, 'navigator', { + // maxTouchPoints: 1 —— 手机 persona:inputMode() 判 touch(键盘路由由场景 13 专测) + value: { maxTouchPoints: 1, vibrate: (p) => { vibrateLog.push({ t: vnow, p }); return true; } }, + configurable: true, +}); +globalThis.window = { + addEventListener: () => {}, + location: { href: '' }, +}; + +const origConsoleError = console.error.bind(console); +console.error = () => {}; // 资产缺失走信息性报告,不刷屏 +const origConsoleLog = console.log.bind(console); + +// ================================ 资产存在性 ================================ + +function hasPair(base) { + return fs.existsSync(base + '.ogg') && fs.existsSync(base + '.m4a'); +} + +/** voice:旁白双性别齐 或 NPC 平铺(引擎平铺回退命中);sfx/bgm:双格式。 */ +function assetExists(id) { + const audioDir = path.join(DETECTIVE_DIR, 'audio'); + if (id.startsWith('voice/')) { + const name = id.slice(6); + return (hasPair(path.join(audioDir, 'voice', 'female', name)) + && hasPair(path.join(audioDir, 'voice', 'male', name))) + || hasPair(path.join(audioDir, 'voice', name)); + } + return hasPair(path.join(audioDir, id)); +} + +// ================================ 音频引擎 mock ================================ + +class MockAudioEngine { + constructor() { + this.log = []; // {t, layer, id, opts, missing} + this._voice = null; // 当前 voice token(isVoicePlaying 判定依据) + this.bgmNow = null; + this.blocked = new Set(); // 被屏蔽的 voice id(onComplete 永不触发 → 8s 兜底续链) + } + isVoicePlaying() { return this._voice !== null; } + setRateBoostEnabled() {} + _dur() { return 1.0; } // 假 buffer 时长(秒):资产存在时语音链按虚拟时钟推进 + play(id, opts = {}) { + const layer = id.split('/')[0]; + const missing = !assetExists(id); + this.log.push({ t: vnow, layer, id, opts, missing }); + if (missing) return; // 引擎静默跳过语义(缺失时 onComplete 不来,游戏 8s 兜底接管) + if (layer === 'voice') { + this._voice = null; // 顶掉当前句(不触发其 onComplete,对齐引擎语义) + const token = { id, onComplete: opts.onComplete }; + this._voice = token; + if (!this.blocked.has(id)) { + schedule(this._dur(id) * 1000, () => { + if (this._voice === token) { + this._voice = null; + if (token.onComplete) token.onComplete(); + } + }); + } + } else if (layer === 'bgm') { + this.bgmNow = id; + } + // sfx:即发即弃,无后续 + } + stopVoice() { this._voice = null; } + stopBgm() { this.log.push({ t: vnow, layer: 'bgm', id: '(stop)', opts: {} }); this.bgmNow = null; } + stopAll() { this.stopBgm(); this.stopVoice(); } + prewarm() {} +} + +// ================================ 断言设施 ================================ + +let passCount = 0; +const failures = []; +function assert(cond, msg) { + if (cond) { passCount++; } + else { failures.push(msg); origConsoleError(` ✗ ${msg}`); } +} +function ok(msg) { passCount++; origConsoleLog(` ✓ ${msg}`); } + +/** 播放日志查询(含 missing 项——顺序/次数断言与资产落盘与否无关)。 */ +function plays(audio, from = 0) { + const L = audio.log.slice(from); + return { + has: (id) => L.some((e) => e.id === id), + count: (id) => L.filter((e) => e.id === id).length, + hasRe: (re) => L.some((e) => re.test(e.id)), + countRe: (re) => L.filter((e) => re.test(e.id)).length, + orderOk: (...ids) => { + let pos = -1; + for (const id of ids) { + const i = L.findIndex((e, j) => j > pos && e.id === id); + if (i < 0) return false; + pos = i; + } + return true; + }, + entries: () => L, + entryOf: (id) => L.find((e) => e.id === id), + }; +} + +const ERROR_PATTERN = JSON.stringify([60, 40, 60]); +function vibratedError(fromIdx = 0) { + return vibrateLog.slice(fromIdx).some((v) => JSON.stringify(v.p) === ERROR_PATTERN); +} +const SUCCESS_PATTERN = JSON.stringify([100]); +function vibratedSuccess(fromIdx = 0) { + return vibrateLog.slice(fromIdx).some((v) => JSON.stringify(v.p) === SUCCESS_PATTERN); +} + +// ================================ 游戏装配 ================================ + +const { DetectiveGame } = await import('../game.js'); +const { settings } = await import('../../engine/settings.js'); + +settings.set({ guide: 'off', voice: 'female', speed: 1.0, bgm: 1.0, input: 'touch' }); + +const LISTEN_ITEMS = ['w1', 'w2', 'w3', 'w4', 'scene', 'fore']; +const ITEM_ORDER = [...LISTEN_ITEMS, 'case']; + +const audioRegistry = []; +function newGame(opts = {}) { + const audio = new MockAudioEngine(); + audioRegistry.push(audio); + const game = new DetectiveGame(audio, {}, opts); + return { game, audio }; +} +const g = (game, gesture) => game.onGesture(gesture); + +/** 有界等待:条件满足或超时即返回。 */ +async function waitState(pred, maxMs) { + const t0 = vnow; + while (!pred() && vnow - t0 < maxMs) await tick(200); + return pred(); +} + +/** 导航到证词板条目(最短环绕方向;播报 650ms 落声)。 */ +async function navToItem(game, itemId) { + const target = ITEM_ORDER.indexOf(itemId); + let guard = 0; + while (game.itemIdx !== target && guard++ < 10) { + const d = (target - game.itemIdx + ITEM_ORDER.length) % ITEM_ORDER.length; + g(game, d * 2 <= ITEM_ORDER.length ? 'right' : 'left'); + await tick(150); + } + await tick(650); +} + +/** 完整听取一个条目(nav + tap + 等链播完)。 */ +async function listenItem(game, itemId) { + await navToItem(game, itemId); + g(game, 'tap'); + return waitState(() => game._play === null && game.state === 'BOARD', 80000); +} + +/** CONFRONT 内选一句:滑到条目 → 单击进选句 → 滑到句 → 单击确认 → 单击定下。 */ +async function cfSelectLine(game, caseData, lineId) { + const itemId = caseData.lineItem[lineId]; + const target = LISTEN_ITEMS.indexOf(itemId); + let guard = 0; + while ((game._cf?.idx ?? -1) !== target && guard++ < 8) { + const d = (target - game._cf.idx + LISTEN_ITEMS.length) % LISTEN_ITEMS.length; + g(game, d * 2 <= LISTEN_ITEMS.length ? 'right' : 'left'); + await tick(120); + } + await tick(650); + g(game, 'tap'); // 确认条目 → 进选句 + await tick(300); + const ids = caseData.layout.lines[itemId]; + const li = ids.indexOf(lineId); + let lg = 0; + while ((game._cf?.lineIdx ?? -1) !== li && lg++ < 8) { + const d = (li - game._cf.lineIdx + ids.length) % ids.length; + g(game, d * 2 <= ids.length ? 'right' : 'left'); + await tick(120); + } + g(game, 'tap'); // 进确认(重播该句 + cf_confirm) + await tick(17000); // 确认链(句 + cf_confirm)走完——玩家听清「就这句?」再定下 + g(game, 'tap'); // 定下这句 +} + +/** 从 BOARD 空闲发起一次完整指证(长按进入 + 两侧选句),等判定链收尾回板。 + * 长按带重试:E2 受理条件=无语音在播——播报尾音未排空时补一拍重试(与时长解耦)。 */ +async function confront(game, caseData, lineA, lineB, { maxMs = 90000 } = {}) { + for (let attempt = 0; attempt < 3 && game.state !== 'CONFRONT'; attempt++) { + g(game, 'longpress'); + if (game.state !== 'CONFRONT') await tick(1500); + } + await waitState(() => game.state === 'CONFRONT' && game._cf && game._cf.phase === 'item1', 12000); + await tick(9000); // cf_start(缺失时 8s 兜底)→ 条目报名 + await cfSelectLine(game, caseData, lineA); + await tick(17500); // cf_second + 第一份自动重播(P1 记忆契约)走完 + await cfSelectLine(game, caseData, lineB); + return waitState(() => game.state === 'BOARD' && !game._play, maxMs); +} + +/** 从 BOARD 走到 FINISH(滑到结案陈词条目 + 单击)。 */ +async function enterFinish(game) { + await navToItem(game, 'case'); + g(game, 'tap'); + await tick(200); +} + +/** 快速跳过教学直达 BOARD(新案已开)。 */ +async function skipDrill(game) { + await waitState(() => game.state === 'DRILL', 20000); + g(game, 'down'); + return waitState(() => game.state === 'BOARD', 20000); +} + +/** 矩阵不变式独立重算(§6.4 playtest 双保险):同轴异值对集合 ≡ 矛盾表。 */ +function matrixInvariant(c) { + const all = c.items.flatMap((it) => it.lines); + const norm = (x, y, ax) => (x < y ? `${x}|${y}|${ax}` : `${y}|${x}|${ax}`); + const derived = new Set(); + for (let i = 0; i < all.length; i++) { + for (let j = i + 1; j < all.length; j++) { + const A = all[i], B = all[j]; + for (const ax of Object.keys(A.clues)) { + if (B.clues[ax] !== undefined && B.clues[ax] !== A.clues[ax]) derived.add(norm(A.id, B.id, ax)); + } + } + } + const want = new Set(c.contradictions.map((c2) => norm(c2.a, c2.b, c2.axis))); + const extra = [...derived].filter((k) => !want.has(k)); + const missing = [...want].filter((k) => !derived.has(k)); + return { ok: extra.length === 0 && missing.length === 0, extra, missing, size: derived.size }; +} + +// ================================ 场景 1:DRILL 教学全链(§5/§11.1) ================================ + +async function scenario1() { + origConsoleLog('\n=== 场景 1:TITLE→DRILL D0..D6 + D5 练习截获零污染 + 跳过路径 ==='); + storageMap.clear(); + const { game, audio } = newGame({ seed: 42 }); + game.start(); + await tick(400); + assert(game.state === 'TITLE' && !game.run, '启动进 TITLE(无存档)'); + assert(plays(audio).has('bgm/menu'), 'TITLE bgm/menu'); + + let mark = audio.log.length; + g(game, 'tap'); + await waitState(() => game.state === 'DRILL' && game._drill && game._drill.step === 0, 20000); + await tick(36000); // sys_new_case + intro_1..3 + tut_d0 四句整链(缺失时 8s/句兜底) + assert(game.run && game.run.seed === 42, 'opts.seed=42 注入新案'); + assert(plays(audio, mark).orderOk('voice/sys_new_case', 'voice/intro_1', 'voice/intro_2', 'voice/intro_3', 'voice/tut_d0'), + 'D0 序:sys_new_case → intro_1/2/3 → tut_d0(资产缺失时整链仍经 8s 兜底推进——§4.3 契约)'); + assert(plays(audio).has('bgm/board'), 'DRILL bgm/board'); + + // D0:左滑或右滑任一(首输入达成点) + mark = audio.log.length; + const vib0 = vibrateLog.length; + g(game, 'right'); + await tick(800); + assert(game._drill?.step === 1, 'D0 右滑通过 → D1'); + assert(plays(audio, mark).has('sfx/paper_turn') && plays(audio, mark).has('sfx/sig_shun') && plays(audio, mark).has('voice/item_w2'), + 'D0 条目播报:paper_turn + sig_shun + item_w2(右滑换到下一份,签名前置)'); + { + const conf = vibrateLog.slice(vib0).some((v) => JSON.stringify(v.p) === JSON.stringify([20])); + assert(conf, 'D0 通过附 confirm 震'); + } + + // D1:单击 → 老葛真实证词链播(教学寄生于真实案卷) + await tick(9000); // tut_listen(兜底)+ 回正报名 + mark = audio.log.length; + g(game, 'tap'); + assert(game._play !== null, 'D1 单击开始证词链播'); + const w1Ids = game._case.layout.lines.w1; + const done1 = await waitState(() => game._drill && game._drill.step === 2, 80000); + assert(done1, 'D1 证词链播完 → D2'); + { + const seg = plays(audio, mark); + let pos = -1, orderOk1 = true; + for (const id of w1Ids) { + const i = seg.entries().findIndex((e, j) => j > pos && e.id === 'voice/' + id); + if (i < 0) { orderOk1 = false; break; } + pos = i; + } + assert(orderOk1, `D1 句序 = layout.lines.w1(${w1Ids.join(',')})`); + } + assert(game.run.heard.includes('w1'), 'D1 听完 w1 入账 heard(st_asked 真实来源)'); + + // D2:双击重新听最后一句(免费) + await tick(9000); // tut_replay + mark = audio.log.length; + const lastW1 = game.run.lastLine.w1; + g(game, 'doubletap'); + await tick(9000); + assert(plays(audio, mark).has('voice/' + lastW1), `D2 双击重播最后一句(${lastW1})`); + assert(game._drill?.step === 3, 'D2 重播完 → D3'); + + // D3:上滑状态链(st_asked_1 → st_doubt_0 → recap_go) + await tick(9000); // tut_status + mark = audio.log.length; + g(game, 'up'); + await waitState(() => game._drill?.step === 4, 45000); + assert(plays(audio, mark).orderOk('voice/st_asked_1', 'voice/st_doubt_0'), 'D3 状态链:st_asked_1 → st_doubt_0'); + assert(plays(audio, mark).has('voice/recap_go'), 'D3 状态链尾 recap_go'); + + // D4:长按进入指证(E2 豁免教学) + await tick(9000); // tut_confront + mark = audio.log.length; + g(game, 'longpress'); + await tick(200); + assert(game.state === 'CONFRONT' && game._cf, 'D4 长按 → CONFRONT'); + assert(game._drill?.step === 5, 'D4 通过 → D5'); + await waitState(() => plays(audio, mark).has('voice/cf_start'), 12000); + await waitState(() => plays(audio, mark).has('voice/tut_cf_try'), 12000); + assert(plays(audio, mark).has('voice/tut_cf_try'), 'D5 练习句 tut_cf_try 接在 cf_start 链上'); + + // D5:任意选择组合(选 w2 首句 vs scene 首句——真判定必 miss 的组合)只播 tut_cf_ok + const before = { cfMiss: game.run.cfMiss, doubts: [...game.run.doubts], zHits: game.run.zHits }; + await tick(17000); // cf_start + tut_cf_try 链走完 → 条目报名 + await cfSelectLine(game, game._case, game._case.layout.lines.w2[0]); + await tick(17500); // cf_second + 自动重播 + mark = audio.log.length; + const vib5 = vibrateLog.length; + await cfSelectLine(game, game._case, game._case.layout.lines.scene[0]); + const done5 = await waitState(() => game.state === 'DRILL' && game._drill?.step === 6, 30000); + assert(done5, 'D5 判定被教学截获 → 回 DRILL D6'); + assert(plays(audio, mark).has('voice/tut_cf_ok'), 'D5 任意组合只播 tut_cf_ok(练习不算数)'); + assert(!plays(audio, mark).hasRe(/^voice\/(cf_hit|cf_same|cf_diff|cf_self)$/), 'D5 真判定句(hit/三档 miss)零出现'); + assert(!plays(audio, mark).has('sfx/contra_hit') && !plays(audio, mark).has('sfx/contra_miss'), 'D5 判定音零出现(练习不算数)'); + assert(vibratedSuccess(vib5), 'D5 完成附 success 震(首个正反馈 ≤3min)'); + assert(game.run.cfMiss === before.cfMiss && game.run.zHits === before.zHits + && JSON.stringify(game.run.doubts) === JSON.stringify(before.doubts), + 'D5 练习零账目污染(cfMiss/doubts/zHits 不变——教学承诺兑现)'); + + // D6:tut_done → BOARD 自由调查 + await waitState(() => game.state === 'BOARD', 30000); + assert(plays(audio, mark).has('voice/tut_done'), 'D6 tut_done'); + assert(game.meta.drillDone === true, '教学标志入元进度'); + assert(game.run.heard.length === 1, '教学期只真实听取了 w1(账目真实)'); + + // 上滑随时可用(教过后) + mark = audio.log.length; + g(game, 'up'); + await tick(40000); + assert(plays(audio, mark).orderOk('voice/st_asked_1', 'voice/st_doubt_0'), 'BOARD 上滑状态链随时可用'); + game.stop(); + + // ---- 跳过路径 + 二周目不再进 ---- + storageMap.clear(); + const b = newGame({ seed: 7 }); + b.game.start(); + await tick(400); + g(b.game, 'tap'); + await waitState(() => b.game.state === 'DRILL', 20000); + mark = b.audio.log.length; + g(b.game, 'down'); + await waitState(() => b.game.state === 'BOARD', 20000); + assert(plays(b.audio, mark).has('voice/tut_skip'), '下滑跳过教学播 tut_skip'); + assert(b.game.meta.drillDone === true, '跳过也置 drillDone'); + b.game._save(); + b.game.stop(); + + const c = newGame({}); + c.game.start(); + await tick(400); + assert(c.game.state === 'TITLE' && plays(c.audio).has('voice/sys_continue_ask'), '有档重进播 sys_continue_ask'); + mark = c.audio.log.length; + g(c.game, 'tap'); + await waitState(() => c.game.state === 'BOARD', 30000); + assert(c.game.state === 'BOARD', '二周目单击继续直达 BOARD(不再进 DRILL)'); + c.game.stop(); +} + +// ================================ 场景 2:seed 复现(§6.2/§11.2) ================================ + +async function scenario2() { + origConsoleLog('\n=== 场景 2:seed 复现——(42,0) 两次构造 _case 深相等;(42,1)≠(43,1) ==='); + storageMap.clear(); + const a = newGame({ seed: 42 }); + const b = newGame({ seed: 42 }); + const c1 = a.game._buildCase(42, 0); + const c2 = b.game._buildCase(42, 0); + assert(JSON.stringify(c1) === JSON.stringify(c2), '同 (seed=42, pack=0) 两次构造 _case 深相等(布局/选项序/提示序)'); + const d1 = a.game._buildCase(42, 1); + const d2 = a.game._buildCase(43, 1); + assert(JSON.stringify(d1.layout) !== JSON.stringify(d2.layout), '(42,1) 与 (43,1) 布局不同'); + assert(JSON.stringify(c1.layout) !== JSON.stringify(d1.layout), '(42,0) 与 (42,1) 布局不同(包内 fork + 洗牌差)'); + // 洗牌结构断言:filler 置换只动 shuffle 槽位集合(句集合不变) + const fixedLines = ['w1_1', 'w1_3', 'w1_6']; + assert(fixedLines.every((id) => c1.items.find((it) => it.id === 'w1').lines.some((l) => l.id === id)), + '非 shuffle 句全部在场(集合不变,仅槽位置换)'); + const slotsA = c1.items.find((it) => it.id === 'w1').lines.map((l) => l.id).sort().join(','); + const slotsB = c2.items.find((it) => it.id === 'w1').lines.map((l) => l.id).sort().join(','); + assert(slotsA === slotsB, 'w1 句集合不变(置换非增删)'); + ok(`seed 复现三组对比通过(wpOpts=${JSON.stringify(c1.layout.wpOpts)} hintOrder=${JSON.stringify(c1.layout.hintOrder)})`); +} + +// ================================ 场景 3:矩阵不变式(§6.4/§11.3) ================================ + +async function scenario3() { + origConsoleLog('\n=== 场景 3:矩阵不变式——同轴异值对 ≡ 矛盾表(pack A/B 各验一遍) ==='); + const { game } = newGame({}); + for (const [seed, pack] of [[42, 0], [43, 0], [100, 1], [101, 1]]) { + const c = game._buildCase(seed, pack); + const r = matrixInvariant(c); + assert(r.ok, `矩阵不变式 (seed=${seed},pack=${pack}):derived=${r.size} 对 ≡ contradictions(extra=${r.extra.join(',')} missing=${r.missing.join(',')})`); + } + const c = game._buildCase(42, 0); + assert(c.contradictions.length === 4 && c.contradictions.map((x) => x.id).join(',') === 'm1,m2,m3,m4', + 'pack A 矛盾表 = M1-M4(lamp/key/weapon/scale)'); + const cb = game._buildCase(100, 1); + assert(cb.contradictions.map((x) => x.a).join(',').includes('b_shun_1'), 'pack B 矛盾指向 b_shun_*'); + // 三档归因真话来源可达性(§3.4 验算):一致对 / 无共享轴对 / 同句 + const all = c.items.flatMap((it) => it.lines); + let sameAxisDiffVal = 0, sameAxisSameVal = 0, noShared = 0; + for (let i = 0; i < all.length; i++) for (let j = i + 1; j < all.length; j++) { + const A = all[i], B = all[j]; + const shared = Object.keys(A.clues).filter((ax) => B.clues[ax] !== undefined); + if (!shared.length) noShared++; + else if (shared.every((ax) => A.clues[ax] === B.clues[ax])) sameAxisSameVal++; + else sameAxisDiffVal++; + } + assert(sameAxisDiffVal === 4, `全案卷同轴异值对恰 4(M1-M4,实际 ${sameAxisDiffVal})`); + assert(sameAxisSameVal >= 6, `一致对 ≥6 组(cf_same 真话来源,实际 ${sameAxisSameVal})`); + assert(noShared >= 13, `无共享轴句对 ≥13(cf_diff 真话来源,实际 ${noShared})`); + ok('三档归因档位全部真实可达(数据单源推导)'); +} + +// ================================ 场景 4:听取细节(§11.4 + 边界) ================================ + +async function scenario4() { + origConsoleLog('\n=== 场景 4:听取——句序/双击重听/单击停止/断句续听/播放中静默忽略 ==='); + storageMap.clear(); + const { game, audio } = newGame({ seed: 42 }); + game.start(); + await tick(400); + g(game, 'tap'); + assert(await skipDrill(game), '跳教学直达 BOARD'); + assert(game.run.seed === 42, 'seed 注入'); + + // w2 完整听取:句序 = layout、播完 st_asked 升档 + let mark = audio.log.length; + assert(await listenItem(game, 'w2'), 'w2 链播完成'); + { + const ids = game._case.layout.lines.w2; + const seg = plays(audio, mark); + let pos = -1, okOrder = true; + for (const id of ids) { + const i = seg.entries().findIndex((e, j) => j > pos && e.id === 'voice/' + id); + if (i < 0) { okOrder = false; break; } + pos = i; + } + assert(okOrder, `w2 句序=layout(${ids.join(',')})`); + } + assert(game.run.heard.includes('w2') && game.run.heardLines.w2 === game._case.layout.lines.w2.length, + 'w2 入账 heard + heardLines 全量'); + + mark = audio.log.length; + g(game, 'up'); + await tick(40000); + assert(plays(audio, mark).has('voice/st_asked_1'), '听完 1 份 → 上滑 st_asked_1'); + + // w4:播放中双击=当前句重播、左右滑静默忽略、单击停止、断句续听 + await navToItem(game, 'w4'); + mark = audio.log.length; + const vib0 = vibrateLog.length; + g(game, 'tap'); + await waitState(() => game._play && game._play.cur >= 1, 30000); // 第二句起播 + const curId = game._play ? game._case.layout.lines.w4[game._play.cur] : '(未起播)'; + g(game, 'doubletap'); + await tick(300); + assert(plays(audio, mark).count('voice/' + curId) >= 2, `播放中双击重播当前句(${curId} 播 ≥2 次,免费零消耗)`); + const itemIdxBefore = game.itemIdx; + g(game, 'left'); + g(game, 'right'); + await tick(500); + assert(game.itemIdx === itemIdxBefore && !vibratedError(vib0), '播放中左右滑静默忽略(无 error 震、条目不变)'); + g(game, 'tap'); + assert(game._play === null, '播放中单击=停止'); + mark = audio.log.length; + await tick(25000); + assert(!plays(audio, mark).hasRe(/^voice\/(wen_|z_|w2_|b_)/), '停止后无后续证词句播出'); + // 续听:从首个未听句起 + const resumeIdx = game.run.heardLines.w4 || 0; + assert(resumeIdx >= 1 && resumeIdx < game._case.layout.lines.w4.length, `w4 断点在句 ${resumeIdx}(1≤断点<6)`); + mark = audio.log.length; + g(game, 'tap'); + await waitState(() => game._play && game._play.cur === resumeIdx, 10000); + assert(game._play && game._play.cur === resumeIdx, `续听从首个未听句起(cur=${game._play?.cur} = heardLines=${resumeIdx})`); + g(game, 'tap'); // 停止收尾 + + // 播放中上滑=状态链(打断证词),打断后双击可重听当前(最后)句 + await navToItem(game, 'scene'); + g(game, 'tap'); + await waitState(() => game._play && game._play.cur >= 0, 30000); + const scCur = game._play ? game._case.layout.lines.scene[game._play.cur] : '(未起播)'; + mark = audio.log.length; + g(game, 'up'); + await tick(3000); + assert(game._play === null && plays(audio, mark).hasRe(/^voice\/st_asked_/), '播放中上滑打断证词播状态链(播报值=实值)'); + mark = audio.log.length; + g(game, 'doubletap'); + await tick(9000); + assert(plays(audio, mark).has('voice/' + scCur), `打断后双击重听当前句(${scCur})`); + + // 全部听齐 → st_asked_6 + for (const id of ['w1', 'w3', 'w4', 'fore', 'scene']) assert(await listenItem(game, id), `${id} 听取完成`); + assert(game.run.heard.length === 6, '六份卷宗全部入账'); + mark = audio.log.length; + g(game, 'up'); + await tick(40000); + assert(plays(audio, mark).has('voice/st_asked_6'), '六份听齐 → st_asked_6'); + game.stop(); +} + +// ================================ 场景 5:指证 4 矛盾逐条走通(§11.5) ================================ + +async function scenario5() { + origConsoleLog('\n=== 场景 5:M1-M4 指证(hit 链/共用 z_4/自动重播/疑点 4/4 hint_fin/E2 边界) ==='); + storageMap.clear(); + const { game, audio } = newGame({ seed: 42 }); + game.start(); + await tick(400); + g(game, 'tap'); + assert(await skipDrill(game), '跳教学进板'); + + const cs = game._case; + // M1(lamp:z_2 vs wen_2) + let mark = audio.log.length; + let vib = vibrateLog.length; + assert(await confront(game, cs, 'z_2', 'wen_2'), 'M1 指证链收尾回板'); + { + const p = plays(audio, mark); + assert(p.has('sfx/contra_hit') && p.has('voice/cf_hit') && p.has('voice/z_react_1'), + 'M1 hit 链:contra_hit → cf_hit → z_react_1(zHits=1 递进取句)'); + assert(p.orderOk('sfx/contra_hit', 'voice/cf_hit', 'voice/z_react_1'), 'M1 hit 链序:音先响、语音后接'); + assert(p.orderOk('voice/cf_confirm', 'voice/cf_second', 'voice/z_2'), '第二份选择前自动重播第一份选定句(P1 记忆契约)'); + } + assert(vibratedSuccess(vib), 'hit 附 success 震'); + assert(game.run.doubts[0] === true && game.run.zHits === 1, 'M1 doubts[0] 置位 + zHits=1'); + assert(game.run.cfMiss === 0, 'hit 不计 cfMiss(账目语义=冗余/错过)'); + + // M2(key:z_3 vs w2_3) + mark = audio.log.length; + assert(await confront(game, cs, 'z_3', 'w2_3'), 'M2 指证完成'); + assert(plays(audio, mark).has('voice/z_react_2') && game.run.doubts[1] === true, 'M2 → z_react_2 + doubts[1]'); + + // M3/M4 共用 z_4(一句两断言,选 z_4 两次分别对 fo_3 与 sc_4) + mark = audio.log.length; + assert(await confront(game, cs, 'z_4', 'fo_3'), 'M3 指证完成(z_4 vs fo_3)'); + assert(plays(audio, mark).has('voice/z_react_3') && game.run.doubts[2] === true, 'M3 → z_react_3 + doubts[2]'); + mark = audio.log.length; + assert(await confront(game, cs, 'z_4', 'sc_4'), 'M4 指证完成(z_4 vs sc_4,同一句两次)'); + assert(plays(audio, mark).has('voice/z_react_4') && game.run.doubts[3] === true, 'M4 → z_react_4 + doubts[3]'); + + // 疑点 4/4:hit 链尾自动播一次 hint_fin(引导去结案) + assert(game._doubtCount() === 4 && game.run.zHits === 4, '四疑点齐 + zHits=4'); + assert(plays(audio, mark).has('voice/hint_fin'), '4/4 齐时 hit 链尾播 hint_fin(一次)'); + assert(!game.run.hintsGiven.includes('hint_fin'), '庆祝性 hint_fin 不计入保底提示账(S 档可达)'); + mark = audio.log.length; + g(game, 'up'); + await tick(40000); + assert(plays(audio, mark).has('voice/st_doubt_4'), '状态链 st_doubt_4(疑点实数)'); + + // 长按豁免边界(§11 边界):播放中长按=引擎快进不进指证;空闲长按=CONFRONT + await navToItem(game, 'w1'); + g(game, 'tap'); + await waitState(() => game._play && game._play.cur >= 0, 30000); + g(game, 'longpress'); + await tick(600); + assert(game.state === 'BOARD' && game._play !== null, 'E2:播放中长按不进指证(引擎快进接管,时间互斥)'); + g(game, 'tap'); // 停止 + await tick(400); + g(game, 'longpress'); + await tick(200); + assert(game.state === 'CONFRONT', 'E2:空闲长按 → CONFRONT'); + // CONFRONT 长按 = error 震(选择语境不作输入) + vib = vibrateLog.length; + g(game, 'longpress'); + await tick(200); + assert(vibratedError(vib) && game.state === 'CONFRONT', 'CONFRONT 长按 error 震不受理'); + // 菜单「返回证词板」退出指证 + g(game, 'down'); + await tick(10000); // sys_menu + 首项 + mark = audio.log.length; + g(game, 'doubletap'); // 双击确认 sys_menu_resume(=返回证词板) + await waitState(() => game.state === 'BOARD', 15000); + assert(game.state === 'BOARD', '菜单「返回证词板」退出指证'); + assert(!plays(audio).hasRe(/^voice\/gd_/), 'guide=off:全程零 gd_* 向导语音'); + game.stop(); +} + +// ================================ 场景 6:指证 miss 三档真话 + 无序(§11.6) ================================ + +async function scenario6() { + origConsoleLog('\n=== 场景 6:miss 三档(cf_same/cf_diff/cf_self)+ 指证对无序 ==='); + storageMap.clear(); + const { game, audio } = newGame({ seed: 43 }); + game.start(); + await tick(400); + g(game, 'tap'); + assert(await skipDrill(game), '跳教学进板'); + const cs = game._case; + + // 指证对无序:反向选 M1(wen_2 在前)也 hit + let mark = audio.log.length; + assert(await confront(game, cs, 'wen_2', 'z_2'), '反向 M1 指证完成'); + assert(plays(audio, mark).has('sfx/contra_hit') && plays(audio, mark).has('voice/cf_hit'), + '指证对无序(任一端在前都判 hit)'); + assert(game.run.doubts[0] === true && game.run.zHits === 1, '反向 M1 同样置疑点'); + + // 同轴同值(w1_2 vs wen_1:都说雨 21:40)→ cf_same + mark = audio.log.length; + assert(await confront(game, cs, 'w1_2', 'wen_1'), '同轴同值指证完成'); + assert(plays(audio, mark).has('sfx/contra_miss') && plays(audio, mark).has('voice/cf_same'), + 'miss 链:contra_miss → cf_same(说得一致,不矛盾——轴数据同值真话)'); + assert(!plays(audio, mark).has('voice/cf_diff') && !plays(audio, mark).has('voice/cf_self'), '同轴同值只播 cf_same'); + + // 无共享轴(z_5 vs sc_1)→ cf_diff + mark = audio.log.length; + assert(await confront(game, cs, 'z_5', 'sc_1'), '无共享轴指证完成'); + assert(plays(audio, mark).has('voice/cf_diff') && !plays(audio, mark).has('voice/cf_same'), + '无共享轴只播 cf_diff(说的不是一件事)'); + + // 同句自比(z_5 vs z_5)→ cf_self + mark = audio.log.length; + assert(await confront(game, cs, 'z_5', 'z_5'), '同句自比完成'); + assert(plays(audio, mark).has('voice/cf_self'), '同句播 cf_self(和自己比不出矛盾)'); + + assert(game.run.cfMiss === 3, `cfMiss=3(三档 miss 各计一次,实际 ${game.run.cfMiss})`); + assert(game.run.doubts.filter(Boolean).length === 1 && game.run.zHits === 1, 'miss 不动 doubts/zHits'); + + // 无资源/次数惩罚:miss 后仍可继续指证与听取 + assert(await listenItem(game, 'fore'), 'miss 后听取照常(无惩罚性失败)'); + game.stop(); +} + +// ================================ 场景 7+8:结案三问 + VERDICT 死窗(§11.7/11.8) ================================ + +async function scenario7() { + origConsoleLog('\n=== 场景 7+8:错指不在场证明 / 三问 / VERDICT 死窗+链锁 / 接新案翻包 ==='); + storageMap.clear(); + const { game, audio } = newGame({ seed: 42 }); + game.start(); + await tick(400); + g(game, 'tap'); + assert(await skipDrill(game), '跳教学进板'); + const cs = game._case; + + // 四疑点(评级账目素材) + for (const [a, b] of [['z_2', 'wen_2'], ['z_3', 'w2_3'], ['z_4', 'fo_3'], ['z_4', 'sc_4']]) { + assert(await confront(game, cs, a, b), `指证 ${a} vs ${b}`); + } + assert(game._doubtCount() === 4, '四疑点齐'); + + // 错指小顺(optIdx 1)→ acc_shun + fin_clear → 回 BOARD(无限再机会) + let mark = audio.log.length; + await enterFinish(game); + assert(plays(audio, mark).has('voice/fin_start'), 'FINISH 入口播 fin_start'); + await waitState(() => game._fin && game._fin.phase === 'who', 15000); + mark = audio.log.length; + g(game, 'right'); + await tick(650); + assert(plays(audio, mark).has('voice/item_w2'), '真凶问右滑播选项名(item_w2)'); + g(game, 'tap'); + await tick(17500); // 两段确认链(重播选项名 + fin_who_confirm)走完 + assert(game._fin?.phase === 'whoConfirm' && plays(audio, mark).has('voice/fin_who_confirm'), + '真凶问两段确认(重播选项名 + fin_who_confirm)'); + g(game, 'tap'); + assert(await waitState(() => game.state === 'BOARD', 30000), '错指 → 回 BOARD'); + assert(plays(audio, mark).orderOk('voice/acc_shun', 'voice/fin_clear'), '错指链:acc_shun(不在场证明自述)→ fin_clear'); + assert(game.run.wrongWho === 1, 'wrongWho 计数'); + + // 错指老葛(optIdx 0,无需滑动)→ acc_ge(无限再机会) + await enterFinish(game); + await waitState(() => game._fin && game._fin.phase === 'who', 15000); + mark = audio.log.length; + g(game, 'tap'); + await tick(200); + g(game, 'tap'); + assert(await waitState(() => game.state === 'BOARD', 30000), '再错指仍回 BOARD(无限再机会)'); + assert(plays(audio, mark).has('voice/acc_ge'), '错指老葛 → acc_ge'); + assert(game.run.wrongWho === 2, 'wrongWho=2'); + + // FINISH 长按 = error(§11 边界) + await enterFinish(game); + await waitState(() => game._fin && game._fin.phase === 'who', 15000); + const vib = vibrateLog.length; + g(game, 'longpress'); + await tick(200); + assert(vibratedError(vib), 'FINISH 长按 error 震'); + + // 正指周先生(optIdx 2)→ 凶器问(选项序=layout)→ 动机问 → VERDICT + g(game, 'right'); + g(game, 'right'); + await tick(650); + assert(plays(audio, mark).has('voice/item_w3'), '滑到周先生(item_w3)'); + mark = audio.log.length; + g(game, 'tap'); + await tick(200); + g(game, 'tap'); // 正式指认 + assert(await waitState(() => game._fin && game._fin.phase === 'weapon', 25000), '指认对 → 凶器问'); + assert(plays(audio, mark).has('voice/fin_ask_weapon'), '凶器问入口 fin_ask_weapon'); + await tick(10000); // 问句链 + busy 解除 + 首选项报名落声 + // 凶器 4 选项全可预览(左右滑一次听一项,选项序=layout.wpOpts) + for (let i = 0; i < 4; i++) { g(game, 'right'); await tick(300); } + { + const seg = plays(audio, mark); + const heard = cs.layout.wpOpts.filter((id) => seg.has('voice/' + id)); + assert(heard.length === 4, `凶器 4 选项全可预览(${heard.join(',')},序=${cs.layout.wpOpts.join(',')})`); + } + // 答对 wp_2(滑到其 layout 下标;确认选项即答题——凶器问无两段确认) + const wpIdx = cs.layout.wpOpts.indexOf('wp_2'); + for (let i = 0; i < wpIdx; i++) g(game, 'right'); + await tick(300); + g(game, 'tap'); + assert(await waitState(() => game._fin && game._fin.phase === 'motive', 25000), '凶器答完 → 动机问'); + assert(plays(audio).has('voice/fin_wp_right') && !plays(audio).has('voice/fin_wp_wrong'), '答对凶器播 fin_wp_right'); + assert(game.run.weaponAns === 'wp_2', 'weaponAns 入账'); + await tick(10000); // 问句链 + busy 解除 + // 动机答错 mv_2 → fin_mv_wrong(真话归因,不重答)→ VERDICT + const mvIdx = cs.layout.mvOpts.indexOf('mv_2'); + for (let i = 0; i < mvIdx; i++) g(game, 'right'); + await tick(300); + mark = audio.log.length; + g(game, 'tap'); + assert(await waitState(() => game.state === 'VERDICT', 25000), '动机答完 → VERDICT'); + assert(plays(audio, mark).has('voice/fin_mv_wrong'), '动机答错播 fin_mv_wrong(不重答,记账)'); + + // ---- 场景 8:VERDICT 死窗 + 链锁(§7.4:dungeon H1 同款) ---- + g(game, 'tap'); + assert(game.state === 'VERDICT', 'H1: 指认对瞬间同拍在途 tap 被死窗吞掉(仍 VERDICT)'); + await tick(300); + g(game, 'tap'); + assert(game.state === 'VERDICT', 'H1: +300ms 在途 tap 同样被吞(引擎双击窗回调延迟)'); + g(game, 'up'); + g(game, 'down'); + g(game, 'doubletap'); + g(game, 'right'); + g(game, 'longpress'); + assert(game.state === 'VERDICT', '链内上滑/下滑/双击/右滑/长按全部吞掉(链锁)'); + const settleDone = await waitState(() => game.state === 'SETTLE', 140000); + assert(settleDone, 'VERDICT 全链播完 → SETTLE'); + const p = plays(audio, mark); + assert(p.orderOk('voice/fin_right', 'voice/z_break_1', 'voice/z_break_2', 'voice/z_break_3', 'sfx/gavel'), + '认罪链:fin_right → z_break_1..3 → gavel(完整有序,未被手势截杀)'); + assert(p.has('sfx/rain_swell'), '认罪链垫 rain_swell'); + assert(p.orderOk('voice/settle_open', 'voice/rate_b', 'voice/st_doubt_4', 'voice/st_miss_0', 'voice/settle_hook'), + '结算链:settle_open → rate_b(wrongWho=2→B 档)→ st_doubt_4 → st_miss_0 → settle_hook(播报值=运行值)'); + assert(game.meta.casesClosed === 1 && game.meta.bestGrade === 'B', '元进度入账 casesClosed=1 bestGrade=B'); + assert(game.run === null, '结案清 run(summary 切已结案口径)'); + { + const save = JSON.parse(storageMap.get('soundgame.progress.detective')); + assert(save.summary === '已结案1起 · 最佳B', `结案 summary 规则("${save.summary}")`); + } + + // 链后:单击=接新案(pack 翻转、seed 重掷——settle_hook 言行一致) + mark = audio.log.length; + g(game, 'tap'); + const fresh = await waitState(() => game.state === 'BOARD' && game.run && game.run.pack === 1 && game.run.seed !== 42, 30000); + assert(fresh, 'SETTLE 单击=接新案(pack 0→1 强制翻转、seed 重掷、drillDone 不再教学)'); + assert(plays(audio, mark).has('voice/sys_new_case'), '新案播 sys_new_case'); + assert(game._case.contradictions[0].a === 'b_shun_1', '新案即 pack B(真凶=小顺)——二周目必遇变体'); + game.stop(); +} + +// ================================ 场景 9:评级账目矩阵(§7.2/§11.9) ================================ + +async function scenario9() { + origConsoleLog('\n=== 场景 9:评级六变量条件式注入矩阵 + 结算链选句与运行值一致 ==='); + const { game } = newGame({}); + const cases = [ + // [doubts, weaponOk, motiveOk, wrongWho, cfMiss, hints, expect] + [4, true, true, 0, 0, 0, 'S'], // 教科书结案 + [4, true, true, 0, 2, 0, 'S'], // cfMiss 恰在阈值内(≤2) + [4, true, true, 0, 3, 0, 'A'], // cfMiss 超阈值降 A + [4, true, true, 1, 0, 0, 'A'], // 错指一次降 A + [3, true, true, 1, 9, 1, 'A'], // A 档边界(hints=1 wrongWho=1) + [4, true, true, 0, 0, 1, 'A'], // 一次保底提示即失 S + [4, true, false, 1, 0, 0, 'B'], // 动机错失 A + [3, true, true, 2, 0, 0, 'B'], // wrongWho=2 失 A + [2, false, false, 2, 9, 9, 'B'], // B 档下边界(疑点 2 + 错指 2) + [1, true, true, 0, 0, 0, 'C'], // 疑点 <2 → C + [4, false, false, 3, 0, 0, 'C'], // 错指 ≥3 → C(全靠保底也能结案) + [0, false, false, 5, 5, 5, 'C'], // C 兜底(案子必然已结) + ]; + for (const [doubts, weaponOk, motiveOk, wrongWho, cfMiss, hints, expect] of cases) { + const o = { doubts, weaponOk, motiveOk, wrongWho, cfMiss, hints }; + const got = game._gradeOf(o); + assert(got === expect, `评级注入 {d=${doubts},wp=${+weaponOk},mv=${+motiveOk},w=${wrongWho},miss=${cfMiss},h=${hints}} → ${expect}(实际 ${got})`); + const vs = game._settlePartVoices(o); + const rateId = 'voice/rate_' + expect.toLowerCase(); + const missBand = cfMiss === 0 ? 0 : cfMiss <= 2 ? 1 : 2; + assert(vs.includes(rateId) && vs.includes('voice/st_doubt_' + doubts) && vs.includes('voice/st_miss_' + missBand), + `结算段选句与账目一致(${rateId} + st_doubt_${doubts} + st_miss_${missBand})`); + assert(vs[0] === 'voice/settle_open' && vs[4] === 'voice/settle_hook', '结算段骨架 settle_open → … → settle_hook'); + } + ok(`评级矩阵 ${cases.length} 组全对(六变量边界:cfMiss 2/3、wrongWho 0/1/2/3、hints 0/1、doubts 0..4)`); +} + +// ================================ 场景 10:看门狗双轨(§9/§11.10) ================================ + +async function scenario10() { + origConsoleLog('\n=== 场景 10:无操作轨(guide=full)+ 无进展轨 45/90/150(乱按必被抢救) ==='); + + // ---- A. 无操作轨:BOARD 10s/25s、CONFRONT 15s、FINISH 20s(guide=full) ---- + storageMap.clear(); + settings.set({ guide: 'full' }); + let a = null; + try { + a = newGame({ seed: 42 }); + a.game.start(); + await tick(400); + g(a.game, 'tap'); + assert(await skipDrill(a.game), '跳教学进板(guide=full)'); + let mark = a.audio.log.length; + await tick(11000); + assert(plays(a.audio, mark).has('voice/gd_board'), '无操作轨:BOARD 10s 播 gd_board'); + await tick(15000); + assert(plays(a.audio, mark).countRe(/^voice\/gd_board$/) === 1, '25s 第 2 级纯 nudge(不重复播句)'); + // CONFRONT 15s + g(a.game, 'longpress'); + await waitState(() => a.game.state === 'CONFRONT', 5000); + mark = a.audio.log.length; + await tick(16000); + assert(plays(a.audio, mark).has('voice/gd_cf'), '无操作轨:CONFRONT 15s 播 gd_cf'); + g(a.game, 'down'); // 菜单 → 「返回证词板」退出指证 + await tick(10000); + g(a.game, 'doubletap'); + await waitState(() => a.game.state === 'BOARD', 15000); + // FINISH 20s + await enterFinish(a.game); + await waitState(() => a.game._fin && a.game._fin.phase === 'who', 15000); + mark = a.audio.log.length; + await tick(21000); + assert(plays(a.audio, mark).has('voice/gd_fin'), '无操作轨:FINISH 20s 播 gd_fin'); + } finally { + if (a) a.game.stop(); + settings.set({ guide: 'off' }); + } + + // ---- B. 无进展轨:乱按 156s 零进展 → 45s earcon / 90s 短句 / 150s 保底矛盾对 ---- + storageMap.clear(); + const { game, audio } = newGame({ seed: 42 }); + game.start(); + await tick(400); + g(game, 'tap'); + assert(await skipDrill(game), '跳教学进板'); + const t0 = vnow; + // 乱按轮转(与语音时长解耦):起停对 = 同拍双 tap——起播与停止零时距,任何语音 + // 时长下一句都不可能完成 = 确定性零进展(「听完新条目=进展」是设计内行为,测试 + // 不得让它发生);其余为换条目滑动/重听噪声;不含 longpress(不进指证);到 + // case 条目强制左滑(不误入 FINISH)。每轮转歇一口气——给等空闲拍的句子级 + // 提示(L2/L3)留落点。 + const ROT = ['pair', 'right', 'left', 'doubletap', 'left', 'right']; + let ri = 0; + const m0 = audio.log.length; + const heard0 = game.run.heard.length; + while (vnow - t0 < 156000) { + const gg = ROT[ri % ROT.length]; + const atCase = game.itemIdx === ITEM_ORDER.length - 1; + if (gg === 'pair' && !atCase) { + g(game, 'tap'); // 起播 + g(game, 'tap'); // 同拍停止——句完成定时器永不可达(时长无关) + } else if (atCase) { + g(game, 'left'); + } else { + g(game, gg); + } + await tick(ri++ % ROT.length === ROT.length - 1 ? 3600 : 1100); + } + assert(game.run.heard.length === heard0, `乱按 156s 零进展(heard 不变=${heard0},无操作轨被手势持续重置不轰炸)`); + const seg = plays(audio, m0); + const tick45 = seg.entryOf('sfx/turn_tick'); + assert(!!tick45 && tick45.t - t0 >= 45000 && tick45.t - t0 < 60000, + `无进展 45s 级 earcon(实测 +${tick45 ? Math.round((tick45.t - t0) / 1000) : '未触发'}s)`); + const hint90 = seg.entries().find((e) => e.id === 'voice/hint_a' || e.id === 'voice/hint_b'); + assert(!!hint90 && hint90.t - t0 >= 90000 && hint90.t - t0 < 105000, + `无进展 90s 级短句(实测 +${hint90 ? Math.round((hint90.t - t0) / 1000) : '未触发'}s)`); + assert(hint90?.id === 'voice/hint_a', '90s 级选句真实:有未听条目播 hint_a'); + const l3 = seg.entries().find((e) => /^voice\/(hint_c1|hint_c2)$/.test(e.id)); + assert(!!l3 && l3.t - t0 >= 150000 && l3.t - t0 < 165000, + `无进展 150s 级保底矛盾对(${l3 ? l3.id : '未触发'},实测 +${l3 ? Math.round((l3.t - t0) / 1000) : '未触发'}s)`); + assert(game.run.hintsGiven.length >= 2, `保底提示入账 hintsGiven(${game.run.hintsGiven.join(',') || '无'})`); + // 保底 hint 给出的矛盾对可走通指证(失败即不通过项) + if (l3) { + const hintText = l3.id === 'voice/hint_c1' ? 'm1' : 'm4'; + const contra = game._case.contradictions.find((c) => c.id === hintText); + const idxC = game._case.contradictions.indexOf(contra); + assert(await confront(game, game._case, contra.a, contra.b), '保底 hint 对走通指证'); + assert(game.run.doubts[idxC] === true, `保底 hint 对命中(${hintText} 置疑点)`); + } + game.stop(); + + // ---- C. 有真实进展零提示 ---- + storageMap.clear(); + const c2 = newGame({ seed: 45 }); + c2.game.start(); + await tick(400); + g(c2.game, 'tap'); + assert(await skipDrill(c2.game), '跳教学进板'); + { + const mC = c2.audio.log.length; + for (const id of LISTEN_ITEMS) assert(await listenItem(c2.game, id), `真实进展:听取 ${id}`); + assert(!plays(c2.audio, mC).has('sfx/turn_tick') && !plays(c2.audio, mC).hasRe(/^voice\/hint_/), + '持续听完新条目(真实进展)零提示零 earcon(不误伤正常玩家)'); + } + c2.game.stop(); + + // ---- D. 4 疑点齐后 150s 无进展 → hint_fin(去结案,计入保底账) ---- + storageMap.clear(); + const d2 = newGame({ seed: 46 }); + d2.game.start(); + await tick(400); + g(d2.game, 'tap'); + assert(await skipDrill(d2.game), '跳教学进板'); + d2.game.run.doubts = [true, true, true, true]; // 测试钩子:疑点已齐(真实路径由场景 5 覆盖) + const mD = d2.audio.log.length; + await tick(156000); + assert(plays(d2.audio, mD).has('voice/hint_fin'), '4 疑点齐 + 150s 无进展 → hint_fin(去结案)'); + assert(d2.game.run.hintsGiven.includes('hint_fin'), '看门狗 hint_fin 计入保底提示账(与 4/4 庆祝播报区分)'); + d2.game.stop(); +} + +// ================================ 场景 11:中断复述 + TITLE 右滑换新案(§10/§11.11) ================================ + +async function scenario11() { + origConsoleLog('\n=== 场景 11:存档重进 → 复述链(未听 ≤3 名直报)+ 上滑同链重放 ==='); + storageMap.clear(); + const a1 = newGame({ seed: 55 }); + a1.game.start(); + await tick(400); + g(a1.game, 'tap'); + assert(await skipDrill(a1.game), '跳教学进板'); + for (const id of ['w1', 'scene', 'fore']) assert(await listenItem(a1.game, id), `听取 ${id}`); + a1.game._save(); + a1.game.stop(); + + const a2 = newGame({}); + a2.game.start(); + await tick(400); + assert(a2.game.state === 'TITLE' && plays(a2.audio).has('voice/sys_continue_ask'), '重进 TITLE 播 sys_continue_ask'); + let mark = a2.audio.log.length; + g(a2.game, 'tap'); + await waitState(() => a2.game.state === 'BOARD', 30000); + assert(a2.game.run.seed === 55 && a2.game.run.pack === 0, '读档还原 (seed,pack)'); + assert(JSON.stringify(a2.game._case.layout) === JSON.stringify(a1.game._case.layout), '读档 layout 复用存档值(不重洗)'); + const p = plays(a2.audio, mark); + await tick(58000); // 复述链 6 句(缺失时 8s/句兜底)整链走完 + const pAfter = plays(a2.audio, mark); + assert(pAfter.orderOk('voice/st_asked_3', 'voice/item_w2', 'voice/item_w3', 'voice/item_w4', 'voice/st_doubt_0', 'voice/recap_go'), + '复述链序:st_asked_3 → 未听 3 名直报(item_w2/w3/w4)→ st_doubt_0 → recap_go'); + assert(a2.game.run.heard.length === 3, 'heard 还原 3 份'); + mark = a2.audio.log.length; + g(a2.game, 'up'); + await tick(58000); + assert(plays(a2.audio, mark).orderOk('voice/st_asked_3', 'voice/item_w2', 'voice/st_doubt_0'), + '上滑同链可重放(中断复述链=状态链同构)'); + a2.game.stop(); + + // TITLE 右滑 = 换新案(seed/pack 重掷,meta 保留) + const a3 = newGame({}); + a3.game.start(); + await tick(400); + assert(plays(a3.audio).has('voice/sys_continue_ask'), '实例 3 仍在 TITLE 询问继续'); + const oldSeed = JSON.parse(storageMap.get('soundgame.progress.detective')).run.seed; + mark = a3.audio.log.length; + g(a3.game, 'right'); + const fresh = await waitState(() => a3.game.state === 'BOARD' && a3.game.run && a3.game.run.seed !== oldSeed, 30000); + assert(fresh, 'TITLE 右滑 → 新案(seed 重掷)'); + assert(plays(a3.audio, mark).has('voice/sys_new_case'), '新案播 sys_new_case'); + assert(!a3.game.run.heard.includes('w1'), '新案 heard 清空(meta 保留 drillDone 不再教学)'); + a3.game.stop(); +} + +// ================================ 场景 12:二周目变体 pack B(§11.12) ================================ + +async function scenario12() { + origConsoleLog('\n=== 场景 12:pack B 变体——真凶小顺 / acc_zhou / b_shun 链 / b_hint ==='); + storageMap.clear(); + const { game, audio } = newGame({ seed: 100, pack: 1 }); + game.start(); + await tick(400); + g(game, 'tap'); + assert(await skipDrill(game), '跳教学进板'); + const cs = game._case; + assert(cs.pack === 1 && cs.culprit === 'w2' && cs.culpritName === '小顺', 'pack B 真凶=小顺(culprit=w2)'); + assert(cs.contradictions.map((c) => c.id).join(',') === 'm1b,m2b,m3b,m4b', '矛盾表 = m1b..m4b'); + assert(cs.contradictions.every((c) => ['b_shun_1', 'b_shun_2', 'b_shun_3'].includes(c.a)), '矛盾真凶侧 = b_shun_*'); + assert(cs.contradictions[1].a === 'b_shun_2' && cs.contradictions[1].b === 'b_zhou_2', 'M2b = b_shun_2 vs b_zhou_2'); + assert(cs.verdict.accuseLine === 'fin_right_b' && cs.verdict.break[0] === 'b_shun_break_1', '认罪链 = fin_right_b + b_shun_break_*'); + assert(cs.wrongAccuse.w3 === 'acc_zhou', '错指周先生走 acc_zhou'); + assert(matrixInvariant(cs).ok, 'pack B 矩阵不变式复验'); + + // M1b 指证(b_shun_1 vs wen_2)→ b_shun_react_1 + let mark = audio.log.length; + assert(await confront(game, cs, 'b_shun_1', 'wen_2'), 'M1b 指证完成'); + assert(plays(audio, mark).has('voice/cf_hit') && plays(audio, mark).has('voice/b_shun_react_1'), + 'M1b hit → b_shun_react_1'); + + // 错指周先生(optIdx 2)→ acc_zhou + fin_clear + await enterFinish(game); + await waitState(() => game._fin && game._fin.phase === 'who', 15000); + mark = audio.log.length; + g(game, 'right'); + g(game, 'right'); + await tick(650); + g(game, 'tap'); + await tick(200); + g(game, 'tap'); + assert(await waitState(() => game.state === 'BOARD', 30000), '错指周回 BOARD'); + assert(plays(audio, mark).has('voice/acc_zhou') && plays(audio, mark).has('voice/fin_clear'), + '错指周先生 → acc_zhou(不在场证明)+ fin_clear'); + + // 保底提示 = b_hint_c1/c2(m1b 已发现 → 轮询到 m4b) + game.run.doubts = [true, false, false, false]; // 钩子:仅 m1b 已发现(真实路径同上) + const mHint = audio.log.length; + await tick(156000); + const bh = plays(audio, mHint).entries().find((e) => /^voice\/b_hint_c/.test(e.id)); + assert(bh, `pack B 保底提示走 b_hint_c*(${bh ? bh.id : '无'})`); + + // 正指小顺 → 凶器对 → 动机对 → VERDICT(b_shun_break 链) + await enterFinish(game); + await waitState(() => game._fin && game._fin.phase === 'who', 15000); + g(game, 'right'); // optIdx 1 = w2 小顺 + await tick(650); + g(game, 'tap'); + await tick(200); + g(game, 'tap'); + assert(await waitState(() => game._fin && game._fin.phase === 'weapon', 25000), '指认小顺 → 凶器问'); + await tick(10000); // 问句链 + busy 解除 + const wpIdx = cs.layout.wpOpts.indexOf('wp_2'); + for (let i = 0; i < wpIdx; i++) g(game, 'right'); + await tick(300); + g(game, 'tap'); + assert(await waitState(() => game._fin && game._fin.phase === 'motive', 25000), '凶器对 → 动机问'); + await tick(10000); // 问句链 + busy 解除 + const mvIdx = cs.layout.mvOpts.indexOf('mv_1'); + for (let i = 0; i < mvIdx; i++) g(game, 'right'); + await tick(300); + mark = audio.log.length; + g(game, 'tap'); + assert(await waitState(() => game.state === 'VERDICT', 25000), '三问齐 → VERDICT'); + assert(await waitState(() => game.state === 'SETTLE', 140000), '结算链完整(链锁)'); + const p = plays(audio, mark); + assert(p.orderOk('voice/fin_right_b', 'voice/b_shun_break_1', 'voice/b_shun_break_2', 'voice/b_shun_break_3', 'sfx/gavel'), + 'pack B 认罪链:fin_right_b → b_shun_break_1..3 → gavel'); + assert(plays(audio, mark).has('voice/settle_hook'), 'settle_hook 落声'); + g(game, 'tap'); + assert(await waitState(() => game.state === 'BOARD' && game.run && game.run.pack === 0, 30000), '接新案 pack 1→0 翻转'); + game.stop(); +} + +// ================================ 场景 13:键盘变体路由(V 组,§11.13) ================================ + +async function scenario13() { + origConsoleLog('\n=== 场景 13:settings.input 联动(游戏代码用普通 id,_k 变体引擎路由) ==='); + const { inputMode } = await import('../../engine/gesture.js'); + assert(inputMode() === 'touch', '手机 persona 默认 touch'); + const { WebAudioEngine } = await import('../../engine/audio.js'); + const eng = new WebAudioEngine('audio/'); + { + settings.set({ input: 'key' }); + assert(inputMode() === 'key', "settings.input='key' → inputMode()=key"); + const cand = eng._candidates('voice/sys_help'); + assert(cand[0].includes('/voice/female/key/sys_help.'), `key 模式候选链前置 key/ 变体(${cand[0]})`); + assert(cand.some((x) => x.includes('/voice/female/sys_help.')), 'key 变体 404 后自然落回主资产'); + } + { + settings.set({ input: 'touch' }); + assert(inputMode() === 'touch', "回 'touch' → 基础句"); + const cand = eng._candidates('voice/settle_hook'); + assert(cand[0].includes('/voice/female/settle_hook.'), `touch 模式直接主资产(${cand[0]})`); + assert(!cand.some((x) => x.includes('/key/')), 'touch 模式无 key/ 候选'); + } + // 游戏代码零 _k 硬编码(平台约定:提示句 _k 变体引擎自动路由,游戏用普通 id) + const src = fs.readFileSync(path.join(DETECTIVE_DIR, 'game.js'), 'utf8'); + assert(!/'[a-z0-9_]+_k'/.test(src), 'game.js 无 _k 后缀 id 硬编码(V 组由引擎路由)'); +} + +// ================================ 场景 14:菜单/帮助/存档(§11.14) ================================ + +async function scenario14() { + origConsoleLog('\n=== 场景 14:模态菜单循环-确认-关闭 / 去指证=长按同效 / 保存 PUT summary ==='); + storageMap.clear(); + storageMap.set('soundgame.auth', JSON.stringify({ token: 'test-token' })); + const { game, audio } = newGame({ seed: 42 }); + game.start(); + await tick(400); + g(game, 'tap'); + assert(await skipDrill(game), '跳教学进板'); + assert(await listenItem(game, 'w1'), '听取 w1(summary 素材)'); + + let mark = audio.log.length; + g(game, 'down'); + await tick(10000); // sys_menu + 首项(兜底) + assert(game.menuOpen, '下滑呼出模态菜单'); + const p0 = plays(audio, mark); + assert(p0.has('voice/sys_menu') && p0.has('voice/sys_menu_resume'), '菜单开场:sys_menu + 首项 resume'); + // 帮助(上滑) + mark = audio.log.length; + g(game, 'up'); + await tick(20000); + assert(plays(audio, mark).orderOk('voice/sys_help', 'voice/settings_hint'), '菜单上滑=帮助链(sys_help + settings_hint)'); + // 循环切项:单击 resume→cf→save→help→restart→resume(5 项循环回绕) + mark = audio.log.length; + for (let i = 0; i < 5; i++) { g(game, 'tap'); await tick(300); } + const pC = plays(audio, mark); + assert(pC.has('voice/sys_menu_cf') && pC.has('voice/sys_menu_save') && pC.has('voice/sys_menu_restart') + && pC.has('voice/sys_menu_help') && pC.count('voice/sys_menu_resume') === 1, + '单击循环切项(5 项回绕,每切播项名)'); + // 「去指证」双击确认 = 长按同效(P4 冗余入口)——当前项恰回 resume,切一格到 cf + g(game, 'tap'); // → cf + await tick(300); + mark = audio.log.length; + g(game, 'doubletap'); + assert(await waitState(() => game.state === 'CONFRONT', 15000), '菜单「去指证」进 CONFRONT'); + assert(plays(audio, mark).has('voice/cf_start'), '去指证 = 长按同效(cf_start 同点)'); + // 「返回证词板」退出 + g(game, 'down'); + await tick(10000); + mark = audio.log.length; + g(game, 'doubletap'); // resume(=返回证词板) + assert(await waitState(() => game.state === 'BOARD', 15000), '菜单「返回证词板」关闭回板'); + + // 保存并回大厅:PUT data.summary 命中生成规则 + const fetch0 = fetchLog.length; + g(game, 'down'); + await tick(10000); // 开菜单(idx=0 resume) + g(game, 'tap'); // → cf + await tick(300); + g(game, 'tap'); // → save + await tick(300); + mark = audio.log.length; + g(game, 'doubletap'); // 确认保存 + assert(await waitState(() => plays(audio, mark).has('voice/sys_saved'), 15000), '确认保存播 sys_saved'); + const put = fetchLog.slice(fetch0).find((f) => f.url === '/api/progress' && f.opts && f.opts.method === 'PUT'); + assert(put, '保存触发 PUT /api/progress'); + if (put) { + const body = JSON.parse(put.opts.body); + assert(body.game === 'detective' && body.data.schema === 1, 'PUT body game=detective schema=1'); + assert(body.data.summary === '雨夜·聚宝斋 · 已问1/6 · 疑点0/4', + `summary 命中生成规则("${body.data.summary}")`); + } + assert(await waitState(() => window.location.href === '../', 15000), '保存并回大厅跳转'); + game.stop(); +} + +// ================================ 资产引用报告(信息性,不判失败) ================================ + +function assetReport() { + origConsoleLog('\n=== 资产引用报告(并行生产期:缺失不判失败,齐套后应为 0) ==='); + const missing = new Set(); + const allIds = new Set(); + for (const inst of audioRegistry) { + for (const e of inst.log) { + allIds.add(e.id); + if (e.missing) missing.add(e.id); + } + } + const femDir = path.join(DETECTIVE_DIR, 'audio', 'voice', 'female'); + const maleDir = path.join(DETECTIVE_DIR, 'audio', 'voice', 'male'); + if (fs.existsSync(femDir) && fs.existsSync(maleDir)) { + const idsOf = (d) => new Set(fs.readdirSync(d).filter((f) => f.endsWith('.ogg')).map((f) => f.replace(/\.ogg$/, ''))); + const fem = idsOf(femDir); + const male = idsOf(maleDir); + const same = fem.size === male.size && [...fem].every((id) => male.has(id)); + ok(`voice 双性别目录集合${same ? '一致' : '不一致'}(female ${fem.size} / male ${male.size})`); + } else { + ok('voice 双性别目录未落盘(资产 agent 并行生产中)'); + } + ok(`运行期引用 id 共 ${allIds.size} 个,其中缺失 ${missing.size} 个${missing.size ? '(每句 8s 兜底全链推进,流程未卡死——§4.3 契约)' : ''}`); +} + +// ================================ 主流程 ================================ + +try { + await scenario1(); + await scenario2(); + await scenario3(); + await scenario4(); + await scenario5(); + await scenario6(); + await scenario7(); + await scenario9(); + await scenario10(); + await scenario11(); + await scenario12(); + await scenario13(); + await scenario14(); +} catch (e) { + failures.push(`未捕获异常: ${e.stack}`); +} +assetReport(); + +origConsoleLog('\n========================================'); +origConsoleLog(`断言通过 ${passCount} 条,失败 ${failures.length} 条`); +if (failures.length) { + origConsoleLog('失败清单:'); + for (const f of failures) origConsoleLog(` ✗ ${f}`); +} +process.exit(failures.length ? 1 : 0); diff --git a/web/harness/sessions/abyss.mjs b/web/harness/sessions/abyss.mjs new file mode 100644 index 00000000..572ac2d7 --- /dev/null +++ b/web/harness/sessions/abyss.mjs @@ -0,0 +1,228 @@ +/** + * 《深海回音》新手会话剧本。 + * + * 剧本: + * - `session`(主线):一位深潜救援员的完整弧线——听完甲板简报、四潜全下 + * (每潜:单击发脉冲 → 听回波 → 通讯 → 抉择)、上浮结算与补给照单全收、 + * 走双载第四潜,最后用省下的气下去取船钟,以完整 E1「海燕归钟」收尾 + * (全右省气 + p9 左 = §4.5 的 P4 友好默认路径)。 + * - `sessionStress`(乱按压力):一位好奇宝宝乱划乱点、快速跳句、菜单里乱逛, + * 一样要能走到 ENDING,全程零死锁、零静默死局。 + * + * 剧本失真红线(参照 radio 剧本先例): + * - 玩家不抢话:只在语音链播完(waitCond !voicePlaying)或抉择武装后才出手; + * - 玩家不干晾自己:只在 COMMS 叙事等待态偶尔走神触发一次看门狗,链一停就接话; + * - 搜索等待挂着看门狗定时器,禁止裸 waitIdle 空等,必须 waitCond 有界等待; + * - 通讯里每句之间是「链末=继续」的等待态——真人听完一句就点一下,不机械、不抢跑。 + */ + +const DIVE_LABEL = ['一', '二', '三', '四']; + +function diveLabel(n) { + return n >= 1 && n <= 4 ? DIVE_LABEL[n - 1] : '·'; +} + +export function describe(game) { + if (game.leaving) return '已确认保存并回大厅'; + if (game.phase === 'ASK') return '进游戏先听到「单击继续上次,右滑新救援」的询问'; + if (game.phase === 'MENU') { + const names = ['回到水下', '重新开始这次救援', '保存并回大厅']; + return `菜单里,当前项「${names[game.menuIdx]}」:单击切换,双击确认,上滑帮助,下滑关闭`; + } + if (game.phase === 'INTRO') { + return game.voicePlaying ? '甲板简报正在播(规矩就一条)' : '简报播完了,等单击下潜'; + } + if (game.phase === 'DIVE') { + if (game.searchArmed) return '声呐静了,轮到你了:单击发一枚脉冲'; + return game.voicePlaying ? `第${diveLabel(game.diveN)}潜下潜中,调度在报位` : '……'; + } + if (game.choiceArmed) { + return `「请选择」念完了(第${diveLabel(game.diveN)}潜):向左或向右滑做出你的决定`; + } + if (game.voicePlaying) return `第${diveLabel(game.diveN)}潜通讯中,正在听对方说话`; + if (game.phase === 'COMMS') return '对方说完了,等我接话(链末单击继续)'; + if (game.phase === 'SURFACE') { + return game.voicePlaying ? '上浮结算播报中(扣账/补给/档位)' : '结算链间隙,等单击继续'; + } + if (game.phase === 'ENDING') { + return game.awaitingRestart + ? '结局和逐人回响都听完了。单击再来一局,下滑开菜单' + : '结局播报中'; + } + return '……'; +} + +// ---------------------------------------------------------------- 主线剧本 + +export async function session(ctx, mock) { + const { game } = ctx; + + ctx.note('开局:听完甲板简报——每段链末单击继续,第一潜开始后声呐会静下来'); + let firstPing = false; + let firstEcho = false; + let invalidTried = false; + let menuDone = false; + let walkedAway = false; + let choiceIdx = 0; + + // 抉择路径(全右省气 + p9 左取船钟 → e1 海燕归钟): + // p1 直接上 / p2 不等日志 / p3 快切 / p4 直接上浮 / p5 先带小郑 / + // p6 约定来接 / p7 快浮 / p8 不问 / p9 用最后的气下去取钟 + const choices = [ + { dir: 'right', why: '小舟先上来,暗号回头再说' }, + { dir: 'right', why: '日志可以补,人不能等' }, + { dir: 'right', why: '切割枪快切,老白憋住气' }, + { dir: 'right', why: '阀门交给母船,腿要紧' }, + { dir: 'right', why: '小郑重伤先走,陈医官下一潜就是你的' }, + { dir: 'right', why: '四小时内我下来,等我' }, + { dir: 'right', why: '来不及了,直浮!医疗舱的灯先开好' }, + { dir: 'right', why: '纪律就是纪律,拉他上浮' }, + { dir: 'left', why: '最后一潜。不为救人,为个交代——下去取船钟' }, + ]; + + for (let i = 0; i < 300 && !game.leaving; i++) { + // 只等当前语音链播完 / 抉择武装 / 搜索轮到自己;绝不用裸 waitIdle + await ctx.waitCond(() => !game.voicePlaying || game.choiceArmed || game.searchArmed); + + if (game.leaving) break; + if (game.searchArmed && game.phase === 'DIVE') { + if (!firstPing) { + firstPing = true; + ctx.status(`第${diveLabel(game.diveN)}潜到深度了,声呐静了`); + ctx.note('调度说了:单击,发一枚脉冲——耳朵交给声呐'); + } else { + ctx.status(`第${diveLabel(game.diveN)}潜,再点一枚`); + } + await ctx.gesture('tap'); + if (!firstEcho) { + firstEcho = true; + await ctx.waitCond(() => game.phase === 'COMMS' || !game.voicePlaying); + if (game.phase === 'COMMS') ctx.status('金属回波后面有杂音——电台接通了,小舟的声音进来了'); + } + continue; + } + if (game.choiceArmed) { + const pick = choices[choiceIdx]; + if (!pick) break; // 抉择走完(p9 之后应已进 ENDING) + + if (!invalidTried) { + invalidTried = true; + ctx.note(pick.why + '——但先上滑听一遍潜次和氧气'); + await ctx.gesture('up'); + await ctx.waitCond(() => !game.voicePlaying); + ctx.status('听完状态链(第几潜+氧气档+口令),局面复述回来了'); + await ctx.waitCond(() => game.choiceArmed || !game.voicePlaying); + if (!game.choiceArmed) continue; // 复述没回到武装态就再等一轮 + } + if (!menuDone) { + menuDone = true; + ctx.note('下滑看看菜单有什么'); + await ctx.gesture('down'); + await ctx.waitCond(() => !game.voicePlaying); + ctx.status('菜单里,切一项听听'); + await ctx.gesture('tap'); // 切一项听听(报名接在当前报名之后,不截断) + await ctx.waitCond(() => !game.voicePlaying); + ctx.note('听完了,下滑关闭菜单,回到水下'); + await ctx.gesture('down'); // 下滑关闭(不双击,绝不误触「重开/回大厅」) + await ctx.waitCond(() => game.choiceArmed || !game.voicePlaying); + if (!game.choiceArmed) continue; + } + ctx.status('选项念完了,做决定'); + ctx.note(pick.why); + await ctx.gesture(pick.dir); // 念完即选,不干晾(犹豫会在等待态空烧看门狗) + choiceIdx++; + continue; + } + if (game.phase === 'ENDING') { + await ctx.waitCond(() => game.awaitingRestart); + continue; + } + if (game.phase === 'SURFACE') { + // 上浮结算自动链(扣账/补给/档位)——播完自动进下一潜或签收,不插手 + await ctx.waitCond(() => !game.voicePlaying || game.choiceArmed || game.searchArmed); + continue; + } + // COMMS 叙事等待态(链末=继续):真人听完一句就接话,偶尔走神一次。 + // 不要在这里 waitVirtual 小憩——等待态挂着看门狗定时器,waitVirtual 会 + // 一口气跳到下一个定时器 tick(25s 看门狗),剧本就变成「每句干晾 25s」。 + if (!walkedAway && game.phase === 'COMMS' && !game.choiceArmed) { + walkedAway = true; + ctx.silence(25, '听到一半走了神,在想氧气还剩几格'); + await ctx.waitVirtual(25600); // 触发看门狗:nudge +「单击屏幕,继续」 + ctx.status('听到一声轻响和「单击屏幕,继续」,马上点'); + await ctx.gesture('tap'); + continue; + } + await ctx.gesture('tap'); // 链末单击 = 继续 + } + + await ctx.waitCond(() => game.phase === 'ENDING' && game.awaitingRestart); + ctx.status('结局链走完,尾声逐人点到我每一潜的救与错过'); + ctx.note('船钟出水,五个人都在甲板上——会话结束(E1 海燕归钟)'); +} + +// ---------------------------------------------------------------- 乱按压力剧本 +// 目标:全程零死锁、零静默死局,一样能走到 ENDING。 +// 剧本特征:不读提示、快速跳句、菜单里乱逛、错手势试探,但保留「最终能通关」底线。 + +export async function sessionStress(ctx, mock) { + const { game } = ctx; + + let seed = 20260910; + const rng = () => { + seed = (seed * 1103515245 + 12345) & 0x7fffffff; + return seed / 0x7fffffff; + }; + const gestures = ['tap', 'left', 'right', 'up', 'down', 'doubletap']; + const pick = (arr) => arr[Math.floor(rng() * arr.length)]; + + let menuOpenCount = 0; + let choiceCount = 0; + let pingCount = 0; + let noiseCount = 0; + + for (let i = 0; i < 500; i++) { + await ctx.waitCond(() => !game.voicePlaying || game.choiceArmed || game.searchArmed); + + if (game.leaving) break; + if (game.searchArmed && game.phase === 'DIVE') { + pingCount++; + await ctx.gesture('tap'); // 声呐静着就一直点——单击恒=发脉冲 + continue; + } + if (game.phase === 'MENU') { + menuOpenCount++; + await ctx.gesture('tap'); // 乱切换 + await ctx.waitCond(() => !game.voicePlaying); + await ctx.gesture('down'); // 下滑关闭(不双击,避免误触「重开/回大厅」) + continue; + } + if (game.choiceArmed) { + choiceCount++; + if (rng() < 0.35) { + // 好奇宝宝:犹豫时乱试一个不推进剧情的手势 + noiseCount++; + await ctx.gesture(pick(['up', 'tap', 'doubletap'])); + await ctx.waitCond(() => game.choiceArmed || !game.voicePlaying); + } + await ctx.gesture(pick(['left', 'right'])); + continue; + } + if (game.phase === 'ENDING') { + await ctx.waitCond(() => game.awaitingRestart); + break; + } + if (game.phase === 'SURFACE') { + // 结算自动链:等它自己走完(播句中乱点=跳句,也无妨) + await ctx.waitCond(() => !game.voicePlaying || game.choiceArmed || game.searchArmed); + continue; + } + // COMMS 叙事等待态 / 其他静默帧:乱划一下(无效手势也要有反馈,零静默) + noiseCount++; + await ctx.gesture(pick(gestures)); + } + + await ctx.waitCond(() => game.phase === 'ENDING' && game.awaitingRestart); + ctx.status(`乱按剧本收尾:脉冲 ${pingCount} 次、${choiceCount} 个抉择、乱试 ${noiseCount} 次、逛菜单 ${menuOpenCount} 次`); + ctx.note('全程没有任何一次手势被无声吞掉,也没有卡死在某个状态里——结局链照样走完'); +} diff --git a/web/harness/sessions/detective.mjs b/web/harness/sessions/detective.mjs new file mode 100644 index 00000000..e9d13e03 --- /dev/null +++ b/web/harness/sessions/detective.mjs @@ -0,0 +1,382 @@ +/** + * 《雨夜专案》新手会话剧本。 + * + * 剧本: + * - `session`(主线):一位新手探长的完整破案弧线——标题开始 → DRILL 七步跟练 + * (换卷宗/听取/重听/状态/长按指证/练习判定)→ 证词板翻听全部六份卷宗 → + * 逐对比对指证出全部四处矛盾(周先生四连谎)→ 结案陈词先错指小顺听不在场 + * 证明,再正指周先生、答对凶器与动机 → 认罪链与法槌 → 评级结算 → 单击接 + * 新案(pack 翻转,变体真凶小顺)短打一处指证收尾。 + * - `sessionStress`(乱按压力):一位手忙脚乱的好奇宝宝——标题乱划、教学乱按、 + * 证词板狂点乱滑(零进展被看门狗三级抢救)、菜单乱逛只下滑关闭、结案乱答 + * 再修正——一样走到 VERDICT+SETTLE 与新案开局,全程零死锁。 + * + * 剧本失真红线(参照 dungeon/radio 剧本先例): + * - 玩家不抢话:出手前等语音链/判定链收尾(waitStep 有界等待状态谓词); + * - 玩家靠耳朵玩:指证前先翻听卷宗,导航只把「听到的内容」翻译成手势; + * - BOARD 常驻无进展轮询定时器:禁止裸 waitIdle 空等,必须 waitStep/waitVirtual; + * - 菜单里只「下滑关闭」,绝不双击执行(防误触「保存并回大厅」结束会话)。 + */ + +const LISTEN_ITEMS = ['w1', 'w2', 'w3', 'w4', 'scene', 'fore']; +const ITEM_ORDER = [...LISTEN_ITEMS, 'case']; + +const ITEM_LABEL = { + w1: '门卫老葛的证词', w2: '学徒小顺的证词', w3: '账房周先生的证词', w4: '房东温太太的证词', + scene: '现场勘验', fore: '法医报告', case: '结案陈词', +}; + +export function describe(game) { + if (game.menuOpen) return '菜单里:单击切换,双击确认,上滑帮助,下滑关闭'; + if (game._play) return `正在听${ITEM_LABEL[ITEM_ORDER[game._play.itemIdx]]}(单击停止,双击重新听当前句)`; + switch (game.state) { + case 'TITLE': return game.run + ? '检测到未结的案子:单击继续,右滑换一桩新案,上滑听案情简介' + : '标题页:单击开始查案,上滑听案情简介'; + case 'DRILL': return game._drill + ? `教学第 ${game._drill.step} 步,跟着旁白做(下滑可跳过)` + : '教学中'; + case 'BOARD': { + const label = ITEM_LABEL[ITEM_ORDER[game.itemIdx]]; + return `证词板上是「${label}」:左右滑换卷宗,单击听取,按住进入指证,上滑状态,下滑菜单`; + } + case 'CONFRONT': { + const cf = game._cf; + if (!cf) return '指证中'; + if (cf.phase === 'judged') return '判定播报中,先听着'; + const ph = { item1: '选第一份证词', line1: '第一份里逐句听', confirm1: '就这句?单击定下来', + item2: '选与它矛盾的那份', line2: '第二份里逐句听', confirm2: '就这句?单击定下来' }[cf.phase]; + return `指证中:${ph}(左右滑换,单击确认,双击重听)`; + } + case 'FINISH': { + const fin = game._fin; + if (!fin) return '结案中'; + const ph = { who: '指认真凶:四位证人选一个', whoConfirm: '就指认他?单击定下来', + weapon: '第二问:凶器是什么', motive: '第三问:动机是什么' }[fin.phase]; + return `结案三问——${ph}(左右滑选,单击定)`; + } + case 'VERDICT': return '裁决链播报中:认罪三段与法槌,手势全部被吞(链锁)'; + case 'SETTLE': return '结算:评级与账目播完,单击接新案(pack 翻转)'; + default: return '……'; + } +} + +// ---------------------------------------------------------------- 会话工具 + +/** 有界等待:条件满足或超时即返回。 */ +async function waitStep(ctx, mock, cond, maxMs = 30000) { + const t0 = mock.now; + let spins = 0; + const spinCap = Math.max(60, Math.floor(maxMs / 50)); + while (!cond() && mock.now - t0 < maxMs && spins < spinCap) { + spins++; + await ctx.waitVirtual(100); + } +} + +/** 滑到指定卷宗条目(最短环绕方向)。 */ +async function navToItem(ctx, game, itemId) { + const target = ITEM_ORDER.indexOf(itemId); + let guard = 0; + while (game.itemIdx !== target && guard++ < 10) { + const d = (target - game.itemIdx + ITEM_ORDER.length) % ITEM_ORDER.length; + await ctx.gesture(d * 2 <= ITEM_ORDER.length ? 'right' : 'left'); + await ctx.waitVirtual(200); + } + await ctx.waitVirtual(700); // 签名 earcon → 条目名落声 +} + +/** 完整听取当前卷宗(单击 → 等链播完回空闲)。 */ +async function listenItem(ctx, mock, game, itemId) { + await navToItem(ctx, game, itemId); + ctx.note(`单击,听取${ITEM_LABEL[itemId]}`); + await ctx.gesture('tap'); + await waitStep(ctx, mock, () => game._play === null && game.state === 'BOARD', 85000); +} + +/** 指证内选一句:滑到条目 → 单击 → 滑到句 → 单击确认 → 听完「就这句?」单击定下。 */ +async function cfSelectLine(ctx, mock, game, caseData, lineId) { + const itemId = caseData.lineItem[lineId]; + const target = LISTEN_ITEMS.indexOf(itemId); + let guard = 0; + while ((game._cf?.idx ?? -1) !== target && guard++ < 8) { + const d = (target - game._cf.idx + LISTEN_ITEMS.length) % LISTEN_ITEMS.length; + await ctx.gesture(d * 2 <= LISTEN_ITEMS.length ? 'right' : 'left'); + await ctx.waitVirtual(200); + } + await ctx.waitVirtual(700); + ctx.note(`选定「${ITEM_LABEL[itemId]}」——单击确认`); + await ctx.gesture('tap'); + await ctx.waitVirtual(400); + const ids = caseData.layout.lines[itemId]; + const li = ids.indexOf(lineId); + let lg = 0; + while ((game._cf?.lineIdx ?? -1) !== li && lg++ < 8) { + const d = (li - game._cf.lineIdx + ids.length) % ids.length; + await ctx.gesture(d * 2 <= ids.length ? 'right' : 'left'); + await ctx.waitVirtual(400); // 一句一听 + } + ctx.note('听到那句对不上的了——单击,进入确认'); + await ctx.gesture('tap'); + await waitStep(ctx, mock, () => game._cf && (game._cf.phase === 'confirm1' || game._cf.phase === 'confirm2'), 25000); + await ctx.waitVirtual(17000); // 重播该句 + 「就这句?」听完 + ctx.note('单击,定下这句'); + await ctx.gesture('tap'); +} + +/** 从证词板发起一次完整指证(按住进入 + 两侧选句),等判定链收尾回板。 + * 长按带重试:E2 受理条件=无语音在播——播报尾音未排空时补一拍重试。 */ +async function confront(ctx, mock, game, lineA, lineB, noteA, noteB) { + ctx.note(noteA); + for (let attempt = 0; attempt < 3 && game.state !== 'CONFRONT'; attempt++) { + await ctx.gesture('longpress'); + if (game.state !== 'CONFRONT') await ctx.waitVirtual(1500); + } + await waitStep(ctx, mock, () => game.state === 'CONFRONT' && game._cf && game._cf.phase === 'item1', 12000); + await ctx.waitVirtual(9000); // cf_start 听完 + const cs = game._case; + await cfSelectLine(ctx, mock, game, cs, lineA); + await ctx.waitVirtual(17500); // 「再选与它矛盾的那份」+ 第一份自动重播 + ctx.note(noteB); + await cfSelectLine(ctx, mock, game, cs, lineB); + await waitStep(ctx, mock, () => game.state === 'BOARD' && !game._play, 95000); +} + +/** 从证词板走到结案陈词并单击进 FINISH。 */ +async function enterFinish(ctx, mock, game) { + await navToItem(ctx, game, 'case'); + ctx.note('翻到结案陈词——单击,进入结案三问'); + await ctx.gesture('tap'); + await waitStep(ctx, mock, () => game.state === 'FINISH', 12000); +} + +/** 等结案问句链播完(busy 解除 + 首选项报名)。 */ +async function waitQuestion(ctx, mock, game, phase) { + await waitStep(ctx, mock, () => game._fin && game._fin.phase === phase && !game._fin.busy, 30000); + await ctx.waitVirtual(1000); +} + +// ---------------------------------------------------------------- 主线剧本 + +export async function session(ctx, mock) { + const { game } = ctx; + + // —— 标题:单击开始(首局无存档) —— + await waitStep(ctx, mock, () => game.state === 'TITLE'); + ctx.status('雨夜专案:一桩雨夜命案等着一双好耳朵'); + ctx.note('单击,开始'); + await ctx.gesture('tap'); + + // —— DRILL 七步跟练 —— + await waitStep(ctx, mock, () => game.state === 'DRILL' && game._drill?.step === 0, 20000); + ctx.status('开场三句案情简介 + 「先熟悉证词板」'); + await ctx.waitVirtual(37000); + ctx.note('右滑——换一份卷宗听听'); + await ctx.gesture('right'); // D0 → D1 + await waitStep(ctx, mock, () => game._drill?.step === 1, 8000); + await ctx.waitVirtual(10000); // 「单击,听取当前卷宗」+ 回正老葛报名 + ctx.note('单击——听老葛的证词'); + await ctx.gesture('tap'); // D1 真实证词链播 + await waitStep(ctx, mock, () => game._drill?.step === 2, 80000); + ctx.status('一份证词听完了——「已听一份,还有五份」'); + await ctx.waitVirtual(10000); // 「双击,重新听这一句」 + ctx.note('双击——最后一句再过一遍耳朵'); + await ctx.gesture('doubletap'); // D2 → D3 + await waitStep(ctx, mock, () => game._drill?.step === 3, 15000); + await ctx.waitVirtual(10000); // 「上滑,随时听进度」 + ctx.note('上滑——听听问了几份、疑点几处'); + await ctx.gesture('up'); // D3 → D4 + await waitStep(ctx, mock, () => game._drill?.step === 4, 45000); + await ctx.waitVirtual(10000); // 「按住屏幕,进入指证」 + ctx.note('按住屏幕——进入指证'); + await ctx.gesture('longpress'); // D4 → D5(CONFRONT 练习局) + ctx.status('练习一次指证:判定被教学截获,不算数'); + await ctx.waitVirtual(18000); // cf_start + 练习指令 + const cs1 = game._case; + await cfSelectLine(ctx, mock, game, cs1, cs1.layout.lines.w2[0]); + await ctx.waitVirtual(17500); + await cfSelectLine(ctx, mock, game, cs1, cs1.layout.lines.scene[0]); + await waitStep(ctx, mock, () => game.state === 'BOARD', 40000); + ctx.status('教学完了——「指证就是这样,真矛盾要靠耳朵找」'); + + // —— 自由调查:翻听全部六份卷宗 —— + const cs = game._case; + for (const id of LISTEN_ITEMS) { + await listenItem(ctx, mock, game, id); + ctx.status(`已听 ${game.run.heard.length}/6 份——${ITEM_LABEL[id]}过完耳朵`); + } + ctx.note('上滑核对进度:六份都听完了,疑点还没坐实'); + await ctx.gesture('up'); + await ctx.waitVirtual(26000); + + // —— 指证穿插:四处矛盾逐对比对(周先生四连谎) —— + const pairs = [ + ['z_2', 'wen_2', '周先生说「顺手熄了灯」——先定下这句', '温太太说「十点二十灯还亮着」——对上了!'], + ['z_3', 'w2_3', '周先生说「钥匙贴身带着」', '小顺说「钥匙平日挂柴房铜钩」——对不上!'], + ['z_4', 'fo_3', '周先生说「伤是松木劈柴砸的」', '法医说「创口嵌有铜屑,凶器是铜器」——对不上!'], + ['z_4', 'sc_4', '还是周先生那句「铜秤卖给收废品的了」', '现场勘验说「铜秤立在架上」——对不上!'], + ]; + for (let i = 0; i < pairs.length; i++) { + const [a, b, na, nb] = pairs[i]; + await confront(ctx, mock, game, a, b, na, nb); + ctx.status(`疑点 ${game._doubtCount()}/4 坐实——真凶的反应一次比一次慌`); + } + ctx.status('四处疑点齐了——「疑点齐了,去结案陈词」'); + + // —— 结案三问:先错指一次听不在场证明,再正指 —— + await enterFinish(ctx, mock, game); + await waitQuestion(ctx, mock, game, 'who'); + ctx.note('先怀疑小顺——右滑选他,单击'); + await ctx.gesture('right'); + await ctx.waitVirtual(800); + await ctx.gesture('tap'); + await ctx.waitVirtual(18000); // 「就指认他?」听完 + ctx.note('单击,定下来'); + await ctx.gesture('tap'); + await waitStep(ctx, mock, () => game.state === 'BOARD', 35000); + ctx.status('错怪了——小顺宿在城南刘掌柜家,不在场证明成立,回到证词板(无限再机会)'); + + await enterFinish(ctx, mock, game); + await waitQuestion(ctx, mock, game, 'who'); + ctx.note('证据都指向周先生——右滑两下选他'); + await ctx.gesture('right'); + await ctx.gesture('right'); + await ctx.waitVirtual(800); + await ctx.gesture('tap'); + await ctx.waitVirtual(18000); + ctx.note('单击,正式指认'); + await ctx.gesture('tap'); + await waitQuestion(ctx, mock, game, 'weapon'); + ctx.status('第二问:凶器是什么?四个选项一个个听'); + { + const wpIdx = cs.layout.wpOpts.indexOf('wp_2'); // 「铜秤砣」——铜屑与不见了的秤砣都指向它 + for (let i = 0; i < wpIdx; i++) { await ctx.gesture('right'); await ctx.waitVirtual(400); } + ctx.note('铜屑不答应别的答案——选「铜秤砣」,单击'); + await ctx.gesture('tap'); + } + await waitQuestion(ctx, mock, game, 'motive'); + ctx.status('第三问:动机是什么?'); + { + const mvIdx = cs.layout.mvOpts.indexOf('mv_1'); // 「监守自盗」——库里短的东西就是引线 + for (let i = 0; i < mvIdx; i++) { await ctx.gesture('right'); await ctx.waitVirtual(400); } + ctx.note('账上短的东西才是引线——选「监守自盗」,单击'); + await ctx.gesture('tap'); + } + + // —— 裁决:认罪链 + 法槌 + 结算(链锁,手势全吞) —— + await waitStep(ctx, mock, () => game.state === 'VERDICT', 30000); + ctx.status('证据链闭合——周先生认罪三段,雨声渐强又落下,法槌落定'); + await waitStep(ctx, mock, () => game.state === 'SETTLE', 150000); + const gi = game._gradeInfo; + ctx.status(`案件归档,雨停了——评级 ${gi ? gi.grade : '?'}:疑点 ${gi ? gi.outcome.doubts : '?'}/4,错指 ${gi ? gi.outcome.wrongWho : '?'} 次`); + ctx.note('单击——接新案(同一场雨里,还有别的案子)'); + await ctx.gesture('tap'); + await waitStep(ctx, mock, () => game.state === 'BOARD' && game.run?.pack === 1, 30000); + ctx.status('新案开卷:pack 翻转——这回的真凶换了人'); + + // —— 变体案短打:一处指证(小顺的谎)收尾 —— + await confront(ctx, mock, game, 'b_shun_1', 'wen_2', + '小顺说「我走的时候熄了灯」——变体案的第一句谎', '温太太还是那句话「灯还亮着」——对上了!'); + ctx.status(`变体案疑点 ${game._doubtCount()}/4——真凶另有其人,收工`); +} + +// ---------------------------------------------------------------- 乱按压力剧本 + +export async function sessionStress(ctx, mock) { + const { game } = ctx; + let seed = 20260909; + const rng = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; }; + + // ① 标题页乱划,然后开始 + for (let i = 0; i < 8 && game.state === 'TITLE'; i++) { + ctx.status('标题页乱划一气'); + await ctx.gesture(rng() < 0.5 ? 'left' : 'doubletap'); + await ctx.waitVirtual(450); + } + await ctx.gesture('tap'); + + // ② 教学:乱按几下,然后下滑跳过(跟不下来也要能走完) + await waitStep(ctx, mock, () => game.state === 'DRILL' || game.state === 'BOARD', 25000); + let dg = 0; + while (game.state === 'DRILL' && dg++ < 30) { + if (rng() < 0.5) { + await ctx.gesture(['tap', 'doubletap', 'left', 'right', 'up'][Math.floor(rng() * 5)]); + await ctx.waitVirtual(450); + continue; + } + ctx.note('下滑,跳过教学'); + await ctx.gesture('down'); + await ctx.waitVirtual(1200); + } + await waitStep(ctx, mock, () => game.state === 'BOARD', 25000); + ctx.status('总算从教学里滚出来了(可能一路错过来的)'); + + // ③ 证词板乱按 185s(零进展——被看门狗三级抢救:earcon → 短句 → 保底矛盾对; + // 菜单乱逛的挂起时长会被无进展轨剔除,故墙钟跑满 185s 保证 L3 落窗)。 + // 起停对=同拍双 tap(与语音时长解耦:一句都不完成=确定性零进展)。 + const t0 = mock.now; + const ROT = ['pair', 'right', 'left', 'doubletap', 'left', 'right']; + let ri = 0; + while (game.state === 'BOARD' && mock.now - t0 < 185000) { + const gg = ROT[ri % ROT.length]; + const atCase = game.itemIdx === ITEM_ORDER.length - 1; + if (gg === 'pair' && !atCase) { + await ctx.gesture('tap'); // 起播 + await ctx.gesture('tap'); // 立刻停止——一句都听不完 + } else if (atCase) { + await ctx.gesture('left'); // 不误入结案陈词 + } else { + await ctx.gesture(gg); + } + if (rng() < 0.12) { + ctx.note('手痒开菜单乱逛——只敢下滑关掉'); + await ctx.gesture('down'); + await ctx.waitVirtual(1500); + await ctx.gesture('down'); + await ctx.waitVirtual(600); + continue; + } + await ctx.waitVirtual(ri++ % ROT.length === ROT.length - 1 ? 3600 : 1100); + } + ctx.status('乱按也被抢救:听到了保底提示直接给出可指证的矛盾对'); + + // ④ 顺着保底提示真指证一处(零死锁底线:核心动词始终可达) + const cs = game._case; + if (game.state === 'BOARD' && game._doubtCount() < 4) { + const contra = cs.contradictions.find((c) => c.hint) || cs.contradictions[0]; + await confront(ctx, mock, game, contra.a, contra.b, + `照着提示指证:${contra.a} 这句`, `对上 ${contra.b} 这句`); + ctx.status(`乱按局也能坐实疑点(${game._doubtCount()}/4)`); + } + + // ⑤ 结案:乱答一次再答对(错指无限再机会 + 问题不重答只记账) + let fg = 0; + while (game.state !== 'SETTLE' && fg++ < 12) { + await enterFinish(ctx, mock, game); + await waitQuestion(ctx, mock, game, 'who'); + const whoIdx = fg === 1 ? Math.floor(rng() * 4) : LISTEN_ITEMS.indexOf(cs.culprit); + for (let i = 0; i < whoIdx; i++) { await ctx.gesture('right'); await ctx.waitVirtual(300); } + ctx.note(fg === 1 ? '先胡乱指一个听听反应' : '这回指着正主'); + await ctx.gesture('tap'); + await ctx.waitVirtual(18000); + await ctx.gesture('tap'); + if (game.state === 'FINISH' && game._fin?.phase !== 'who') { + for (const [phase, want] of [['weapon', cs.questions.weapon.answer], ['motive', cs.questions.motive.answer]]) { + await waitQuestion(ctx, mock, game, phase); + const opts = phase === 'weapon' ? cs.layout.wpOpts : cs.layout.mvOpts; + const idx = opts.indexOf(want); + for (let i = 0; i < idx; i++) { await ctx.gesture('right'); await ctx.waitVirtual(300); } + await ctx.gesture('tap'); + await ctx.waitVirtual(1500); + } + } + await waitStep(ctx, mock, () => game.state !== 'FINISH', 40000); + } + await waitStep(ctx, mock, () => game.state === 'SETTLE', 150000); + ctx.status('乱按一路也把案子结了:裁决链完整、评级如实记账'); + + // ⑥ 接新案短程收尾(压力全程零死锁) + await ctx.gesture('tap'); + await waitStep(ctx, mock, () => game.state === 'BOARD' && game.run, 30000); + ctx.status('新案照样开局,压力测试收工'); +} diff --git a/web/harness/transcribe.mjs b/web/harness/transcribe.mjs index d0e3c6bd..399b81c4 100644 --- a/web/harness/transcribe.mjs +++ b/web/harness/transcribe.mjs @@ -49,7 +49,7 @@ const estimateDuration = (id) => { const CLASS_NAME = { storyline: 'StorylineGame', escape: 'EscapeGame', simon: 'SimonGame', rhythm: 'RhythmGame', space: 'SpaceGame', rpg: 'RpgGame', radio: 'RadioGame', - dungeon: 'DungeonGame', + dungeon: 'DungeonGame', detective: 'DetectiveGame', abyss: 'AbyssGame', }[GAME_NAME]; const mock = installMock(GAME_DIR); diff --git a/web/harness/voice_text.json b/web/harness/voice_text.json index ec6e527d..7b3366ac 100644 --- a/web/harness/voice_text.json +++ b/web/harness/voice_text.json @@ -721,6 +721,295 @@ "unlock_blade": "解锁武器,开局带着短刃。", "unlock_none": "金币还不够,继续攒。" }, + "abyss": { + "b_open": "风停了。我站在问海号甲板上,下面三千米,是海燕号。", + "d1_listen": "都别出声。管子那头,敲得很稳。", + "d1_up": "小舟,抓住我的肩锁。我们上。", + "d1_nolog": "日志可以补,人不能等。走了。", + "d2_pry": "老白,忍住。先泄压,再撬。", + "d2_cut": "切割枪。老白,闭眼,憋气。", + "d2_valve": "电池舱我陪你去。这片海不能脏。", + "d2_leave": "阀门交给母船。你先上,腿要紧。", + "d3_chen": "陈医官,你的手在抖。这次,听我的。", + "d3_zheng": "小郑先走。陈医官,下一潜,就是你的。", + "d3_spare": "备用瓶留下。灯,给我亮着。", + "d3_promise": "四小时内,我下来。等我。", + "d4_slow": "逐站停,慢慢浮。规矩不能破。", + "d4_fast": "来不及了。直浮!", + "d4_pick_gao": "船长,抓住我。海燕号的事,上面再说。", + "d4_pick_chen": "陈医官,轮到你了。抓住,上浮。", + "d4_pick_zheng": "小郑,吊床连人一起。走了。", + "d4_talk": "日志我听完了。海燕号为什么沉,我会带上去。走吧。", + "d4_silent": "纪律就是纪律。抓住,上浮。", + "g9_knot": "三年前,也是这条沟。我按时上浮了,搭档没有。", + "d9_bell": "最后一潜。不为救人,为个交代。", + "d9_rest": "交代,留给明天的人。我去医疗舱。", + "e1_a": "五个人都在甲板上。船钟的第一声,是替这条沟敲的。", + "e1_b": "三年了,我终于敢往下看。明天,想再潜一次。", + "e2_a": "五个人都救上来了。船钟,留在了沟底。", + "e2_b": "没人怪我。可半夜醒来,总听见钟在响。", + "e3_a": "没能都带回来。可我到底下去了,敲响了那口钟。", + "e3_b": "钟声替他们,在沟底响了一声。这样,也好。", + "e4_a": "我按规矩上浮了。规矩里,没写怎么难过。", + "e4_b": "沟底很安静。安静得,像三年前那天。", + "gd_brief": "单击继续简报。要选择时,向左或向右滑。", + "gd_continue": "单击屏幕,继续。", + "gd_again": "还在吗?单击继续。", + "gd_ping1": "声呐待命。单击,发脉冲。", + "gd_ping2": "回波不会自己来。单击。", + "sys_help": "单击继续,双击重新听。上滑听状态,下滑开菜单。选择时,左右滑。", + "sys_menu": "菜单。单击切换选项,双击确认。", + "sys_skip": "想跳过这句,单击就行。", + "sys_continue_ask": "检测到你的下潜记录。单击继续上次,右滑开始新救援。", + "gd_invalid": "这个手势,水里用不上。", + "gd_prog1": "滑动在这里用不上。想想调度刚说的话。", + "gd_prog2": "别乱。停一下,想想下一步。", + "sys_menu_resume": "回到水下。", + "sys_menu_restart": "重新开始这次救援。", + "sys_menu_lobby": "保存并回大厅。", + "sys_settings": "音色、语速、背景音乐,请回大厅修改。", + "sys_new_dive": "新的一次下潜,开始了。", + "prog_d1": "现在是第一次下潜。", + "prog_d2": "现在是第二次下潜。", + "prog_d3": "现在是第三次下潜。", + "prog_d4": "现在是最后一次下潜。", + "oxy_full": "气瓶还足,救得动。", + "oxy_ok": "氧气还够两潜。", + "oxy_low": "氧气告急,只够最后一潜。", + "oxy_crit": "氧气见底,必须上浮。", + "oxy_gain": "应急瓶接上了,多三格。", + "dry_a": "气瓶用干了。立刻上浮。", + "dry_b": "气花在了人身上。人,都上来了。", + "b_rule": "规矩就一条:气不够一潜,人就必须上浮。", + "b_goal": "海燕号,五个人。你的双瓶,够下四潜。", + "b_go": "第一潜,找船。耳朵交给声呐。", + "dv1_down": "下潜开始。水会越来越黑,声音会越来越多。", + "dv1_teach": "到深度了。声呐静了。单击,发一枚脉冲。", + "dv1_metal": "金属回波,钢板。听见这个,就是海燕号的船身。", + "dv1_link": "回波后面有杂音,是电台!接通了。", + "p1": "管路在响。向左滑,停下来听敲管的暗号。向右滑,先带小舟上浮。", + "d1_code": "是老白的暗号:腿被压住,先泄压,再撬。记住这个敲法。", + "p2": "小舟想回去拿声呐日志。向左滑,等她。向右滑,直接上浮。", + "d1_surf": "小舟出水,第一潜收工。往返,扣三格气。", + "dv2_down": "第二潜,机舱。顺着敲管的声音走。", + "dv2_found": "碎石回波后面,敲管声密了。机舱到了。", + "p3": "钢梁压着腿。向左滑,泄压后慢慢撬。向右滑,切割枪,快切。", + "d2_pry_ok": "暗号没白听。管子一松,钢梁抬起来了。", + "d2_pry_slow": "摸着找泄压阀,多费了一格气。腿,出来了。", + "d2_flash": "火花炸了一下,虚惊。老白咳着,腿抽出来了。", + "p4": "老白要去关电池舱阀门。向左滑,陪他去。向右滑,直接上浮。", + "d2_valve_e": "阀门关死了。两格气,换一片干净海。", + "d2_surf": "老白出水。他一直扒着舱门,往水里看。", + "d2_supply": "潜友阿凯捎来应急瓶。甲板上,粥给你温着。", + "dv3_down": "第三潜,病舱。海燕号在倾斜,别恋战。", + "dv3_groan": "回波发闷。船体在呻吟,它撑不久了。", + "p5": "病舱一次只能走一个。向左滑,先带陈医官。向右滑,先带小郑。", + "d3_chen_e": "陈医官出水。小郑固定在吊床上,呼吸平稳。", + "d3_zheng_e": "小郑吊上去了。他一路,都没敢出声。", + "p6": "留守的人要空气。向左滑,留下你的备用瓶。向右滑,约定下一潜来接。", + "d3_spare_e": "备用瓶挂上了,能多撑十二小时。", + "d3_surf": "病舱的人出水。海燕号的呻吟,隔着水都听得见。", + "dv4_none": "气不够一潜了。第四潜,取消。", + "dv4_down": "最后一潜,指挥舱。海燕号,撑不住了。", + "dv4_slide": "它滑了!抓稳索缆,指挥舱里还有人!", + "p7": "双人上浮。向左滑,逐站减压慢慢浮。向右滑,违规直浮,搏一把。", + "p7_chen": "气只够带一个。向左滑,带船长。向右滑,带陈医官。", + "p7_zheng": "气只够带一个。向左滑,带船长。向右滑,带小郑。", + "d4_slow_e": "减压站一个个过。气瓶在跳,人稳着。", + "d4_fast_e": "违规快浮。医疗舱的灯,先给你们开好。", + "d4_stay_lost": "另一位,留在了倾斜的船舱。航标灯,替所有人亮着。", + "p8": "船长的话没说完。向左滑,把你听到的说出来。向右滑,什么都不问,拉他上浮。", + "d4_surf": "最后一潜收工。问海号的甲板,从没这么亮过。", + "g9_in": "医疗舱躺满了。你的气瓶剩多少,你比我清楚。", + "p9": "海燕号的船钟,还挂在沉船上。向左滑,用最后的气,下去取。向右滑,进医疗舱。", + "g9_dry": "一格气都不剩了。能带的,你都带回来了。", + "d9_bello": "船钟出水。海燕号的声音,回家了。", + "ep_lead": "收队了。调度台给你记一笔——他们,后来怎样了。", + "ep_zhou_ok": "小舟:归队报告打了三页。她说,声音都记得。", + "ep_zhou_no": "小舟:声呐日志没了。她凭记忆,重新描了一遍海图。", + "ep_bai_ok": "老白:腿保住了。阀门那夜之后,见人就夸你。", + "ep_bai_no": "老白:腿打了钢钉。他说,瘸着也值。", + "ep_chen_ok": "陈医官:回了医院。诊室里,挂着一盏海底的灯。", + "ep_chen_lost": "陈医官:她守着灯,到最后一小时。灯,还亮着。", + "ep_zheng_ok": "小郑:拆了石膏,第一件事,去看海。", + "ep_zheng_lost": "小郑:吊床随船,滑进了沟底。他没来得及看日出。", + "ep_gao_peace": "高船长:退役申请自己写的。第一行写着:数据不如人。", + "ep_gao_up": "高船长:上了岸,一直没说话。听说,他在学敲管语。", + "ep_gao_lost": "高船长:随海燕号留在沟底。航标灯,还亮着。", + "ep_trench_bell": "沟底:船钟挂在问海号桅杆上。风一过,就响一声。", + "ep_trench_deep": "沟底:很深,很静。海燕号睡在里面,像睡着了。", + "z1_a": "……有人吗?声呐兵,小舟。我们还活着,全艇五个人。", + "z1_b": "轮机长老白困在机舱,陈医官和小郑在病舱,船长在指挥舱。", + "z1_knock": "等等,你听。管子里,有人在敲。", + "z1_bye": "好。上面……是不是天亮了?", + "d1_log": "日志里有失事前,最后一段录音。给你,替我们收好。", + "bai_a": "哈哈,救援的?我这腿,跟冷却管缠上了。", + "bai_b": "艇上的药板烧了。病舱那两个,比我悬。", + "bai_out": "腿还在!我的腿还在!走,我给你带路。", + "bai_thanks": "这片海养着一村人。这份情,我记一辈子。", + "chen_a": "你来了。小郑股骨断了,我给他固定好了。", + "chen_b": "救援钟一次带一个。我是医生,我最后走。", + "chen_up": "……好。小郑,等我,上面给你热粥。", + "chen_stay": "去吧。我守着灯,灯不灭,人就不散。", + "chen_last": "去吧。灯我留着,人齐了就熄。", + "gao_final": "别下来。带他们回家,就算救完了。海燕号,通话结束。", + "gao_a": "救援员。我船上,还剩两个活人。", + "gao_b": "都带走,我最后上。这是船长的规矩。", + "gao_last": "航标灯替我留着。就说,船长睡了。", + "gao_c": "三年前……没什么。走吧,别耽误。", + "gao_confess": "日志你拿了?里面写着。是我下令,多采了一小时。", + "gao_peace": "……好。这一次,听救援员的。" + }, + "detective": { + "sys_menu": "菜单。单击切换,双击确认。", + "sys_menu_resume": "回到证词板。", + "sys_menu_cf": "去指证。", + "sys_menu_save": "保存并回大厅。", + "sys_menu_help": "重新听帮助。", + "sys_menu_restart": "重新查这桩案子。", + "sys_saved": "进度已保存。", + "settings_hint": "音色、语速、背景音乐,请回大厅修改。", + "sys_help": "左右滑换卷宗,单击听取,双击重新听。长按指证,上滑状态,下滑菜单。", + "sys_continue_ask": "检测到未结的案子。单击继续,右滑换一桩新案。", + "sys_new_case": "新案开卷。", + "intro_1": "雨下了一整夜。清晨,聚宝斋后院,古董商沈万川死了。", + "intro_2": "你是专案组探长。这桩案子,归你。", + "intro_3": "卷宗六份:四位证人的证词,现场勘验,法医报告。", + "tut_d0": "先熟悉证词板。左右滑,换卷宗。", + "tut_listen": "单击,听取当前卷宗。", + "tut_replay": "双击,重新听这一句。", + "tut_status": "上滑,随时听进度:问了几份,疑点几处。", + "tut_confront": "按住屏幕,进入指证。去找两句对不上的证词。", + "tut_cf_try": "练习一次。选一份证词,单击确认;再选一份,任意一句。", + "tut_cf_ok": "指证就是这样。练习不算数,真矛盾要靠耳朵找。", + "tut_done": "教学完了。开始查案。", + "tut_skip": "已跳过教学。左右滑换卷宗,单击听取。", + "st_asked_0": "六份卷宗,一份都没听。", + "st_asked_1": "已听一份,还有五份。", + "st_asked_2": "已听两份,还有四份。", + "st_asked_3": "已听三份,还有三份。", + "st_asked_4": "已听四份,还有两份。", + "st_asked_5": "已听五份,还有一份。", + "st_asked_6": "六份都听完了。", + "st_doubt_0": "疑点,还没有。", + "st_doubt_1": "疑点,一处。", + "st_doubt_2": "疑点,两处。", + "st_doubt_3": "疑点,三处。", + "st_doubt_4": "疑点,四处,齐了。", + "item_w1": "门卫老葛的证词。", + "item_w2": "学徒小顺的证词。", + "item_w3": "账房周先生的证词。", + "item_w4": "房东温太太的证词。", + "item_scene": "现场勘验。", + "item_fore": "法医报告。", + "item_case": "结案陈词。", + "intro_w3": "周先生,管了十年的账,见人三分笑。", + "sc_0": "你打伞进了后院。雨水顺着瓦檐,淌成一道帘子。", + "sc_1": "柴房门口,沈万川面朝下倒着。后脑一处伤,现场没有凶器。", + "sc_2": "后门门闩完好,是死者自己开的门。生人叫不开这扇门。", + "sc_3": "泥地上只有进来的脚印,没有出去的。墙根的花盆,翻了一个。", + "sc_4": "库房铜秤立在架上,秤砣不见了。", + "sc_5": "柴房墙上,挂钥匙的铜钩,空着。", + "cf_start": "指证矛盾。先选第一份证词。左右滑换,单击确认。", + "cf_pick_line": "左右滑,一句一句听。", + "cf_confirm": "就这句?单击,定下来。", + "cf_second": "再选与它矛盾的那份证词。", + "cf_self": "同一句证词,和自己比不出矛盾。", + "cf_same": "这两句说得一致,不矛盾。", + "cf_diff": "这两句,说的不是一件事。", + "cf_hit": "对不上!矛盾成立,记入疑点。", + "fin_start": "结案陈词。先指认真凶:四位证人,选一个。", + "fin_who_confirm": "就指认他?单击,定下来。", + "fin_clear": "不在场证明成立。证据放人,回到证词板。", + "fin_right": "证据链闭合。周先生,你还有什么话说?", + "fin_right_b": "证据链闭合。小顺,你还有什么话说?", + "fin_ask_weapon": "第二问:凶器是什么?", + "wp_1": "松木劈柴。", + "wp_2": "铜秤砣。", + "wp_3": "铁门闩。", + "wp_4": "青瓷花盆。", + "fin_wp_right": "对。创口的铜屑,不见了的秤砣,都指向它。", + "fin_wp_wrong": "不对。铜屑不答应。记下了。", + "fin_ask_motive": "第三问:动机是什么?", + "mv_1": "监守自盗。", + "mv_2": "寻仇泄愤。", + "mv_3": "劫财入户。", + "fin_mv_right": "对。库里短的东西,就是引线。", + "fin_mv_wrong": "不对。账上短的东西,才是引线。记下了。", + "settle_open": "案件归档。雨,停了。", + "rate_s": "教科书结案。四处疑点,一击即中。", + "rate_a": "干净利落的结案。", + "rate_b": "案子结了,卷宗上有疏漏。", + "rate_c": "案子是结了,卷宗上补丁不少。", + "st_miss_0": "指证,一次没错。", + "st_miss_1": "指证,错过一两回。", + "st_miss_2": "指证,错了不少。", + "settle_hook": "同一场雨里,还有别的案子。单击,接新案。", + "gd_board": "还在证词板。左右滑,换卷宗。", + "gd_cf": "还在选证词。左右滑,单击确认。", + "gd_fin": "在想?左右滑选,单击定。", + "gd_invalid": "这个操作,这里用不上。", + "hint_a": "探长,还没听的卷宗,先过一遍耳朵。", + "hint_b": "探长,证词里有对不上的地方,逐句比对。", + "hint_c1": "试试指证:周先生说熄了灯,温太太说灯还亮着。", + "hint_c2": "试试指证:周先生说铜秤卖了,现场铜秤还在。", + "b_hint_c1": "试试指证:小顺说熄了灯,温太太说灯还亮着。", + "b_hint_c2": "试试指证:小顺说铜秤卖了,现场铜秤还在。", + "hint_fin": "疑点齐了。去结案陈词。", + "recap_go": "接着查。", + "w1_1": "昨儿夜里雨大,我守着街门,一步没敢挪窝。", + "w1_2": "九点四十,雨点子刚砸下来,小顺打着伞出的街门。", + "w1_3": "十点一刻,一位先生收着黑伞进了后巷。个儿高,微驼背。", + "w1_4": "九点五十,周账房出的街门,跟我说了声,回家。", + "w1_5": "后半夜,温太太还来门房借过一回热水。", + "w1_6": "听说店里账上短了银子,沈老板撂了话,月底清账。", + "acc_ge": "我守了一夜门!温太太来借热水,她能作证!", + "w2_1": "夜里,师父打发我去城南,给刘掌柜送匣子。", + "w2_2": "雨太大,刘掌柜留我住的,今早才回店。", + "w2_3": "库房钥匙?师父嫌沉,平日就挂在柴房墙上的铜钩。", + "w2_4": "师父的账,向来只信周先生一个人。", + "w2_5": "后门那闩,生人叫不开。熟人来了,师父才亲自去开。", + "w2_6": "周先生常念叨,账房的活,越到夜里越清静。", + "acc_shun": "我宿在城南刘掌柜家,他全家都能作证!", + "b_shun_1": "我走的时候,顺手熄了店堂的灯。", + "b_shun_2": "钥匙,师父贴身带着,从不离身。", + "b_shun_3": "那伤,八成是柴房劈柴砸的。铜秤早卖给收废品的了。", + "b_shun_4": "库房的货,我一件都没碰过。", + "b_shun_react_1": "灯?我、我走的时候明明熄了……", + "b_shun_react_2": "别问了……别问了……", + "b_shun_break_1": "匣子是我送的,可我半路折回来了……", + "b_shun_break_2": "我把棉袄反穿,弓着背,葛大爷没认出我。", + "b_shun_break_3": "库房的货,我拿过三件换了钱。他说要对账……秤砣下去的时候,他看着我。", + "z_1": "我七点进店上夜账。九点五十对完,锁了前门回家。", + "z_2": "临走,我顺手熄了店堂的灯。那会儿,店里就剩老板。", + "z_3": "库房钥匙?沈老板贴身带着,从不离身。", + "z_4": "那伤,是松木劈柴砸的。铜秤?上个月卖给收废品的了。", + "z_5": "我到家倒头就睡,夜里的事,一概不知。", + "z_6": "你们要是不信,去问我家婆娘。", + "z_react_1": "灯?……许是我记岔了。人上了岁数,记性不济。", + "z_react_2": "钥匙的事……他、他有时候,也贴身带着的……", + "z_react_3": "秤!秤的事,你们别再问了……", + "z_react_4": "……随你们怎么问。我要回去了。", + "acc_zhou": "我十点就到家了!我家婆娘能作证!", + "z_break_1": "别念了……我说。库房短的三件货,是我抵了赌债。", + "z_break_2": "他说月底清账,要报官。我求他,他不肯。", + "z_break_3": "我只想把钥匙放回去,把被盗做真。抡起秤砣的时候,他认出了我。", + "b_zhou_1": "我对完账就回家了,夜里雨大,路上没停。", + "b_zhou_2": "钥匙?平时就挂在柴房铜钩上,十年了。", + "b_zhou_3": "柴房我常去,没什么不对劲的。", + "wen_1": "雨是九点四十下的。我正收晾的衣裳,记得真真的。", + "wen_2": "十点二十,我出门倒垃圾。聚宝斋店堂的灯,亮着呢。", + "wen_3": "刚回屋,柴房后墙咯噔一响,有人爬墙!花盆翻了一个。", + "wen_4": "吓得我蒙头半宿没睡着,天亮才敢吱声。", + "wen_5": "店里短银子的事,街坊都有耳闻。上礼拜他还摔了账本。", + "wen_6": "周账房那人,见人三分笑,账算得比谁都精。", + "acc_wen": "我这把年纪翻墙?先问问那墙答不答应!", + "fo_1": "验尸报告,念给你听。", + "fo_2": "死亡时间,昨夜十点到十点半。", + "fo_3": "后脑单次钝器伤,一击毙命。创口嵌有铜屑,凶器是铜器。", + "fo_4": "死者双手干净,没有搏斗痕迹。", + "fo_5": "鞋底是干的。当夜,死者没出过后门。" + }, "__key__": { "storyline": { "help": "操作说明。按空格:继续剧情。按回车:重听刚才的台词。按上方向键:听这段说明和当前进度。按下方向键:呼出菜单,菜单里可以重新开始。当旁白说“请选择”的时候,按左方向键或者按右方向键,做出你的决定。选择无法撤销。", @@ -901,6 +1190,31 @@ "gd_pulse": "按空格发脉冲,听听四周。", "gd_turn": "轮到你了。按空格挥击。", "settle_again": "按空格,再探一次。" + }, + "abyss": { + "gd_brief": "按空格继续简报。要选择时,用左右方向键。", + "gd_continue": "按空格,继续。", + "gd_again": "还在吗?按空格继续。", + "gd_ping1": "声呐待命。按空格,发脉冲。", + "gd_ping2": "回波不会自己来。按空格。", + "sys_help": "空格继续,回车重新听。上箭头听状态,下箭头开菜单。选择时,左右箭头。", + "sys_menu": "菜单。空格切换选项,回车确认。", + "sys_skip": "想跳过这句,按空格。", + "sys_continue_ask": "检测到你的下潜记录。空格继续上次,右箭头开始新救援。", + "dv1_teach": "到深度了。声呐静了。按空格,发一枚脉冲。", + "p1": "管路在响。左箭头,停下来听敲管的暗号。右箭头,先带小舟上浮。" + }, + "detective": { + "sys_help": "方向键换卷宗,空格听取,回车重新听。按住空格指证,上键状态,下键菜单。", + "tut_d0": "先熟悉证词板。左右方向键,换卷宗。", + "tut_listen": "空格,听取当前卷宗。", + "tut_replay": "回车,重新听这一句。", + "tut_confront": "按住空格,进入指证。", + "gd_board": "还在证词板。左右方向键,换卷宗。", + "gd_cf": "还在选证词。左右方向键,空格确认。", + "cf_pick_line": "左右方向键,一句一句听。", + "cf_confirm": "就这句?空格,定下来。", + "settle_hook": "同一场雨里,还有别的案子。按空格,接新案。" } } } \ No newline at end of file diff --git a/web/index.html b/web/index.html index 14058680..20e9de9c 100644 --- a/web/index.html +++ b/web/index.html @@ -279,6 +279,18 @@

听声辨位 Roguelike。单击发出声波脉冲,用回声在黑暗中走出一条生路。

+ +

深海回音 新作

+ ABYSS · DEEP ECHO +

深海救援叙事 RPG。声呐里是被困潜艇的船员,氧气在倒数,每次下潜都是抉择。

+

+
+ +

雨夜专案 新作

+ DETECTIVE · RAINY CASE +

本格推理 RPG。问询证人、比对证词、指出矛盾,锁定雨夜命案的真凶。

+

+

剑与声

RPG · SWORD & SOUND diff --git a/web/profiles.js b/web/profiles.js index 3dbfebc6..62389e9c 100644 --- a/web/profiles.js +++ b/web/profiles.js @@ -30,6 +30,8 @@ export const BUCKETS = [ { key: 'soundgame.progress.rpg', game: 'rpg' }, { key: 'soundgame.progress.dungeon', game: 'dungeon' }, { key: 'soundgame.progress.radio', game: 'radio' }, + { key: 'soundgame.progress.abyss', game: 'abyss' }, + { key: 'soundgame.progress.detective', game: 'detective' }, { key: 'soundgame.rhythm.progress', game: 'rhythm' }, { key: 'soundgame.simon.best', game: 'simon' }, { key: 'soundgame.space.best', game: 'space' }, diff --git a/web/server/test/api-smoke.mjs b/web/server/test/api-smoke.mjs index 36689642..96debd0b 100644 --- a/web/server/test/api-smoke.mjs +++ b/web/server/test/api-smoke.mjs @@ -162,9 +162,9 @@ test('H3. 注册表 / 旧登录态收养 / 本地自动名 / 游戏侧 auth 视 assert.equal(storage.getItem('soundgame.auth'), null); }); -test('H4. 受管桶清单覆盖 8 个可同步游戏 + _settings', () => { +test('H4. 受管桶清单覆盖 10 个可同步游戏 + _settings', () => { const games = BUCKETS.map((b) => b.game).sort(); - assert.deepEqual(games, ['_settings', 'dungeon', 'escape', 'radio', 'rhythm', 'rpg', 'simon', 'space']); + assert.deepEqual(games, ['_settings', 'abyss', 'detective', 'dungeon', 'escape', 'radio', 'rhythm', 'rpg', 'simon', 'space']); for (const b of BUCKETS) assert.ok(b.key.startsWith('soundgame.'), '桶 key 应保持游戏原始 key 名(游戏零改动)'); });