diff --git a/docs/design/26-apply-escape.md b/docs/design/26-apply-escape.md new file mode 100644 index 00000000..23388ecc --- /dev/null +++ b/docs/design/26-apply-escape.md @@ -0,0 +1,96 @@ +# 26-apply · 《暗室》重构设计单(26 号文档落地) + +> 按 26 号文档附录 C 填写。交互规则权威来源:[26-audio-interaction-playbook.md](26-audio-interaction-playbook.md)。 + +## 1. 一句话核心动作 + +**朝四个方向滑动在房间里移动,单击检查和使用,按住屏幕问路。** + +## 2. 玩家可感知的状态(3 个)与声音身份 + +| 状态 | 听感 | 来源 | +|---|---|---| +| 探索(轮到你) | 当前房间的环境音常响(水滴/风/电流) | §4.1 ACTION | +| 描述在播(游戏在说) | 房间描述语音,BGM 压低跨链 | §4.1 SPEAKING | +| 撞墙/无效 | 闷响 + invalid 回执(低「咚」) | §4.2 | + +## 3. 新手手势表(四向滑动是玩法 → §4.3 裁定:帮助换到长按) + +| 手势 | 含义 | 说明 | +|---|---|---| +| 四向滑动 | 移动(核心玩法,不按阶梯锁) | swipeCore | +| 单击 | 检查 + 使用(合并;旧「双击=用物品」废除) | 站对房间即用 | +| 长按 | 我在哪、我该干嘛(目标 + 方位一句话) | longpressCore | +| 双击 | (阶段3解锁后)重听最近描述 | 未解锁按单击处理 | + +## 4. 状态 × 手势矩阵(新手期) + +| 状态 \ 手势 | 单击 | 四向滑动 | 长按 | 双击(未解锁) | +|---|---|---|---|---| +| 引导答题 | 按步骤答(纠错句)/ invalid | 按步骤答(脚步/闷响) | 帮助 | 按单击处理 | +| 探索(ACTION) | 检查+使用(环境音/描述即反馈) | 移动=脚步声(silent-ok);撞墙=闷响+invalid+归因 | 帮助(目标+方位) | 按单击处理 | +| 描述在播 | 跳过这句 + ok | 打断并移动 | 打断,帮助 | 按单击处理 | +| 结算 | 重新开始 + ok | invalid + inv_settle | 重听结算 | 按单击处理 | +| 菜单 | 换一项 + 播报 | 右=确认 / 左=上一项 / 其余 invalid | 重听菜单说明 | 按单击处理 | +| 迷茫 L2 期间 | 照常 | 只放行接管方向,其余 invalid(不移动) | 照常 | 照常 | + +## 5. 等待点配置表(引导期)与探索期接管 + +引导等待点(6s/15s 两档,short=「滑一下。」/「向左滑。」): + +| 步骤 | 期待 | 指令句 | +|---|---|---| +| 0 | 任意滑动 | g0_new「黑暗。朝任意方向,滑一下。」 | +| 1 | 撞墙×2(教无效反馈) | g0b(+ wall 归因) | +| 2 | 左滑 | g3_new「现在,向左滑。」 | +| 3 | 下滑 | g4_down「对。再向下滑。」 | +| 4 | 单击 | g5_inspect → g5_key + g7_goal_new | + +探索期(无固定等待点,静默喂迷茫信号 40s 起每 20s)三级接管: + +| 级 | 做什么 | 台词 | +|---|---|---| +| 1 | 说目标方位(30s 台账去重,说过就提示音) | nav_*「向右滑。」 | +| 2 | 只放行第一导航方向 | sys_only_*「现在,只要向左滑。」 | +| 3 | 听我做一次:游戏自己朝目标走一步 | sys_demo + 脚步声 + sys_now_you | + +## 6. 首 3 分钟逐秒脚本 + +| 时间 | 游戏说 / 播 | 期待玩家做 | 没做 | 做错 | +|---|---|---|---|---| +| 0~3s | 环境水滴声 →「黑暗。朝任意方向,滑一下。」 | 任意滑动 | 6s:「滑一下。」 | 点按:纠错句 | +| 3~12s | 脚步声 →「对。你被困在地下设施里。再滑两下,试试墙。」 | 滑动撞墙 | 同上 | 方向错也有闷响(试错无代价) | +| 12~25s | 闷响 + 「那边是墙,换个方向。」+ 方位句 ×2 →「现在,向左滑。」 | 左滑 | 「向左滑。」 | 其他方向:纠错 | +| 25~40s | 脚步 →「对。再向下滑。」→ 储物间环境音 | 下滑 | 同上 | 纠错 | +| 40~60s | 「听到水声了吗?单击一下,仔细检查这个房间。」→ 拾取音 →「你摸到了一把钥匙!」 | 单击 | 「单击。」 | 滑动:纠错 | +| 60~90s | 「找到能用钥匙打开的东西。迷路了,按住屏幕。」→ 自由探索 | 探索 | 40s 起接管 | 撞墙归因 | +| 90~180s | 首到房间描述、谜题链推进(钥匙→铁柜→保险丝→电箱→大门) | 探索 | 接管三级 | 归因 + 就近重试 | + +节奏检查 ✓:3s 内动手;首成功=第一次滑动(脚步声);每 30s 有可听进展(新房间环境音/描述)。 + +## 7. 失败表 + +| 失败类型 | 判定 | 归因句 | 回到哪里 | 新手期代价 | +|---|---|---|---|---| +| 方向错(撞墙) | 越界 | wall + wall_dir_*(每方向整局一次,之后只闷响+回执) | 原地 | 无 | +| 用错地方 | 站位不对 | (合并进检查语义:描述告诉你这里有什么) | 原地 | 无 | +| 迷路 | 静默/乱点信号 | 接管 L1→L2→L3 | 原地 | 无 | + +## 8. 台词表(类别标注) + +- `critical`:g0_new(开场指令 <3s);结算承诺段 again(+解锁教学) +- `content`:g0b/g3_new/g4_down/g5_*、room_*、empty_*、found_key、unlock_cabinet、fix_box、escape、time_band、replay、start_room、inspect_*、gd_*_dir、gd_next_fuse +- `hint`:lp_*(长按帮助)、nav_*、sys_only_*、sys_continue_ask、g_hint_swipe、menu_help +- `feedback`:wall、wall_dir_*、g_wrong_*、guide_wrong_*、guide_blunt_swipe、inv_*、sys_demo、sys_now_you +- 完整清单见 `tools/gen_voice_dual.py` ESCAPE_LINES(74 句 × 双性别)。 + +## 9. 断点复述句 + +- 保存:每步移动/进展写 `soundgame.escape.save`(坐标 + 谜题状态 + 用时)。 +- 回来:sys_continue_ask;单击继续(恢复坐标 + lp 帮助句作复述),右滑重新开始。 + +## 10. 测试记录 + +- 无头 playtest:`web/escape/test/playtest.mjs`(32 断言,乱点机器人:无声操作 0 / + 被动原句重复 0 / 第一次成功 2.1s;撞墙归因按原则 7 只解释一次)。 +- 真人冷启动:待测。 diff --git a/docs/design/26-apply-simon.md b/docs/design/26-apply-simon.md new file mode 100644 index 00000000..b6e7bb92 --- /dev/null +++ b/docs/design/26-apply-simon.md @@ -0,0 +1,94 @@ +# 26-apply · 《音序回响》重构设计单(26 号文档落地) + +> 按 26 号文档附录 C 填写。交互规则权威来源:[26-audio-interaction-playbook.md](26-audio-interaction-playbook.md)。 + +## 1. 一句话核心动作 + +**听一串音色,原样复现。** + +## 2. 玩家可感知的状态(3 个)与声音身份 + +| 状态 | 听感 | 来源 | +|---|---|---| +| 听序列(游戏在说) | 音色序列 + 轮次播报;手势 = busy 回执(「嗯」) | §4.1 BUSY | +| 轮到你(答题) | input_open 标记音 → 风铃 + 等待声;答题回声即时 | §4.1 YOUR_TURN | +| 结算 | gameover 音 + 结算链 + menu pad | — | + +## 3. 新手手势表与音色阶梯 + +全部六个手势在本作都是**音色**(答题输入,§4.3「滑动是玩法」裁定); +帮助(状态查询)在长按。音色按玩家实力渐进开放(做中学,一次只学一样): + +| 音色 | 解锁条件(历史最佳轮数) | +|---|---| +| 单击 / 左滑 / 右滑 | 新手即有(F3:选项 ≤3) | +| 上滑 | 清过第 1 轮 | +| 下滑 | 清过第 3 轮(同时=菜单手势) | +| 双击 | 清过第 6 轮(同时=重听手势) | + +新音色第一次进序列时现场教一句(「向上滑,是这个声音。」)马上用。 +老玩家(best>0)跳过教学,做对过的事不教第二遍(原则 11)。 + +## 4. 状态 × 手势矩阵(新手期) + +| 状态 \ 手势 | 单击 | 滑动×4 | 长按 | 双击(未解锁) | +|---|---|---|---|---| +| 教学(指令在播,critical) | busy 回执(等指令说完) | busy 回执 | 帮助 | busy | +| 教学答题 | 答题/纠错 | 答题/纠错(回响即反馈) | 状态句 | 按单击处理 | +| 播报/序列播放(BUSY) | busy 回执 | busy 回执 | busy 回执 | busy 回执 | +| 答题(YOUR_TURN) | 作答(音色回声) | 作答 | 状态句(轮次段位,不闭窗) | 未入池=invalid;入池=第 6 音色 | +| 结算 | 重新开始(言行一致) | invalid + 解释 | 重听结算 | (阶段3)重听结算 | +| 菜单 | 换一项 | 右=确认/左=上一项 | 重听菜单说明 | 按单击处理 | + +## 5. 等待点配置表 + +| id | 期待 | 提示 full | 提示 short | 静默阈值 | L2 | L3 | +|---|---|---|---|---|---|---| +| tut0 | tap | (指令句刚说过) | 「点一下。」 | 6s/15s | — | 提示音 | +| tut_ | 当前教的音色 | 教学句 | 「再试一次。」 | 6s/15s | — | 提示音 | +| input | 全部已解锁音色 | 「轮到你了。」 | (提示音) | 10s/25s | 免费重听整串 | 放慢 + 重听 | +| settle | tap | 「单击,再来一局。」 | 「再试一次。」 | 20s/45s | — | 提示音 | +| menu | tap/left/right | menu_help | menu_help | 20s/45s | — | 提示音 | + +## 6. 首 3 分钟逐秒脚本 + +| 时间 | 游戏说 / 播 | 期待玩家做 | 没做 | 做错 | +|---|---|---|---|---| +| 0~3s | 「音序回响。点一下屏幕。」(critical) | 单击 | 6s:「点一下。」 | 滑动:纠错 | +| 3~10s | 单击音色回响 → 「向左滑,是这个声音。」+ 音色 | 左滑 | 「再试一次。」 | 纠错句(30s 内不重播) | +| 10~30s | 「对了。」→ 右滑音、单击音各教一个,马上练 | 左/右/单击 | 同上 | 同上 | +| 30~40s | 「记住顺序,原样复现。答错,我们就再听一遍。」→「第一轮,听好了。」→ 2 音序列 | 听 | — | 播放期手势=busy | +| 40~50s | input_open + 风铃 + 等待声 | 复现 2 音 | 10s:「轮到你了。」25s:提示音 | 见失败表 | +| 50~90s | 每轮 +1 音;答对必有表扬句(≤4 字) | 复现 | 三档 + 接管 | 归因 + 免费重听 | +| 90~180s | 第 4 轮完成 → 解锁菜单教学句;序列渐长 | 复现 | 接管 L2 免费重听 / L3 放慢演示 | 第 3 次失败放慢 | + +节奏检查 ✓:3s 内动手;首成功=第一击音色回响(≤10s);每轮都有进展回报。 + +## 7. 失败表(替代旧「机会账」经济) + +| 失败次数 | 处理 | 台词 | +|---|---|---| +| 1~2 | 2 秒内归因(「错了。是向左滑。」)→ 免费重听整串 | wr_* + relisten | +| 3 | 「我放慢一些。」→ 间隔 ×1.6 + 序列减 1 音 | slow_down | +| 4 | 结算(成绩段位 + 破纪录 + 承诺句) | gameover + score_band + replay | + +新手期失败零资源消耗(原则 15);归因真话(错的音色名如实说)。 + +## 8. 台词表(类别标注) + +- `critical`:t0_new、t7_new、教学指令链(t1~t6 各 <3s)、结算承诺段(replay + 解锁教学) +- `content`:round_01..20、score_band1..6、gameover、new_record、t_ok、correct、gd_clear、gd_record +- `hint`:your_turn、t_retry、st_b1..4、sys_demo、menu_help +- `feedback`:wr_*(归因)、relisten、slow_down、t_wrong_*、inv_busy、inv_generic +- 完整清单见 `tools/gen_voice_dual.py` SIMON_LINES(74 句 × 双性别)。 + +## 9. 断点复述句 + +对局内不设存档(单局 2~5 分钟,微循环自洽);历史最佳跨局持久 +(`soundgame.simon.best`),决定音色池与新音教学。 + +## 10. 测试记录 + +- 无头 playtest:`web/simon/test/playtest.mjs`(21 断言,乱点机器人:无声操作 0 + (播放期也有 busy 回执)/ 被动原句重复 0 / 第一次成功 10.2s)。 +- 真人冷启动:待测。 diff --git a/docs/design/26-apply-storyline.md b/docs/design/26-apply-storyline.md new file mode 100644 index 00000000..6234019c --- /dev/null +++ b/docs/design/26-apply-storyline.md @@ -0,0 +1,96 @@ +# 26-apply · 《声命线》重构设计单(26 号文档落地) + +> 按 26 号文档附录 C 填写。本文是 storyline R3 的施工依据与验收基准; +> 交互规则的权威来源是 [26-audio-interaction-playbook.md](26-audio-interaction-playbook.md)。 + +## 1. 一句话核心动作 + +**听一段故事,单击继续;听到「请选择」时左右滑做决定。** + +## 2. 玩家可感知的状态(3 个)与声音身份 + +| 状态 | 听感 | 来源 | +|---|---|---| +| 听讲(游戏在说) | 旁白语音,BGM 压低且跨整条链 | §4.1 SPEAKING | +| 轮到你(继续/抉择) | 风铃交接音 → 轻的「嘟—嘟」等待声 | §4.1 YOUR_TURN | +| (菜单/结算属元状态,由 bgm/menu 与结算句标识) | | | + +## 3. 新手手势表与解锁阶梯 + +| 手势 | 含义 | 解锁 | +|---|---|---| +| 单击 | 继续 / 跳过这句 | 阶段 0 | +| 左滑 / 右滑 | 抉择 | 阶段 0 | +| 上滑 | 我在哪(一句话状态) | 阶段 1(第一次抉择后,教学句挂链尾) | +| 下滑 | 菜单 | 阶段 2(第一次到达结局后) | +| 双击 | 重听(等待点完整句 / 重播节点) | 阶段 3(累计 3 局);未解锁按单击处理 | + +## 4. 状态 × 手势矩阵(新手期) + +| 状态 \ 手势 | 单击 | 左/右滑 | 上滑 | 下滑(未解锁) | 双击(未解锁) | 连点 | +|---|---|---|---|---|---|---| +| 游戏在说 | 跳过这句 + ok | 跳去选项句(不盲选)/ 非抉择点 invalid+解释 | 打断,答「我在哪」 | invalid + 首次解释 | 按单击处理 | 400ms 内 busy | +| 等你继续 | 继续 + ok | invalid(inv_nosel) | 我在哪 | invalid | (阶段3=重听) | busy | +| 等你选择 | 再听选项(「再说一遍:」+ 完整句,hold) | 抉择 + ok | 「正在抉择。左右滑。」 | invalid | (阶段3=重听) | 计入迷茫 | +| 结算 | 重新开始 + ok | invalid(sys_hint_again_full) | 重播状态句 | 菜单(阶段2) | 按单击处理 | 只处理第一下 | +| 菜单 | 换一项 + 播报 | 右=确认 / 左=上一项 | 重听菜单说明 | invalid | 按单击处理 | busy | + +验收:无空格;无「没有声音」的格子;连点无破坏性效果。 + +## 5. 等待点配置表 + +| id | 期待 | 提示 full | 提示 short | 静默阈值 | L2 只留 | L3 演示 | 失败归因 | +|---|---|---|---|---|---|---|---| +| intro | tap | (指令句刚说过)| 「点一下。」 | 6s / 15s | tap | 「听我做一次。」→ 替玩家走一步 | — | +| continue | tap | 「单击,继续。」 | 「单击。」 | 18s / 40s | tap | 同上 | — | +| choice | left/right | pN(选项句) | 「左右滑,选。」 | 25s / 50s | 左右滑 | 「左右滑,都可以。现在你来。」 | — | +| settle | tap | 「单击,重新开始。」 | 「单击。」 | 20s / 45s | tap | 提示音 | — | +| reopen | tap/right | sys_continue_ask | 「单击。」 | 12s / 30s | — | 提示音 | — | +| menu | tap/left/right | menu_help | menu_help | 20s / 45s | — | 提示音 | — | + +三档选择 + 30s 台账跨等待点去重(说过就更短)在 `engine/hints.js` 统一实现。 + +## 6. 首 3 分钟逐秒脚本 + +| 时间 | 游戏说 / 播 | 期待玩家做 | 没做 | 做错 | +|---|---|---|---|---| +| 0~3s | 心跳声 →「点一下屏幕。」(critical) | 单击 | 6s:「点一下。」15s:提示音 | 滑动:invalid + inv_up_locked 等 | +| 3~12s | ok 回执 → 旁白 n00_a →「听到这个声音,就是轮到你了。」→ 风铃 + 等待声 | 单击 | 18s:「单击,继续。」40s:「单击。」 | 双击:按单击处理 | +| 12~30s | n00_b + 「单击可跳过旁白」→ n01 醒来 | 单击(可跳句) | 三档提示 | 滑动:inv_nosel | +| 30~60s | n02 → p1 选项句 → 交出抉择 | 左/右滑 | 25s 复述 p1;50s「左右滑,选。」 | 滑早了:跳去选项句,不盲选 | +| ≈60s | 第一次抉择完成 → 解锁上滑:「想知道自己在哪,就向上滑。」 | 上滑 | 「向上滑。」 | 单击=再听选项 | +| 60~180s | n03~n07 剧情推进,p2 抉择 | 单击/滑动 | 三档提示 + 迷茫接管 | 归因真话 | +| 乱滑/无效×3 | 迷茫接管 L1「左右滑,选。」→ L2 只留期待手势 → L3 演示 | | | | + +节奏检查:3s 内第一次操作 ✓;10s 内第一次成功(ok 回执在第一下单击)✓;每 30s 有进展(节点推进 + sfx)✓;无纯规则说明句 ✓。 + +## 7. 失败表 + +| 失败类型 | 判定 | 归因句 | 回到哪里 | 新手期代价 | +|---|---|---|---|---| +| 无效滑动(非抉择点) | 手势与状态不符 | inv_nosel「现在不用选。单击继续。」 | 原地 | 无 | +| 静默 | 阈值到点 | 三档提示 | 原地 | 无 | +| 迷茫 | 信号累计 ≥2 | 接管 L1 短句 / L2 缩小 / L3 演示 | 原地 | 无 | + +(叙事游戏无资源失败;旧「双击=整节点重播」为反模式 5,已废除。) + +## 8. 台词表(类别标注) + +- `critical`:sys_first_tap(唯一一次开场指令,<3s) +- `content`:n00_a/b、n01~n25、p1~p8、e1~e4、skip_hint、sys_turn_teach、sys_unlock_*、menu_item* +- `hint`:sys_hint_continue_full/short、sys_hint_choice_short、sys_only_choice、sys_demo_*、status_s1..3、status_choice、sys_continue_ask +- `feedback`:inv_nosel、inv_generic、inv_up_locked、inv_down_locked、sys_hint_again_full +- 主动重听:sys_say_again「再说一遍:」(前缀,全项目一句) +- 完整清单见 `tools/gen_voice_dual.py` STORYLINE_LINES(61 句 × 双性别;角色音沿用平铺资产)。 + +## 9. 断点复述句 + +- 保存:每个节点进入时写 `soundgame.storyline.save`(nodeId + 剧情标记)。 +- 回来:sys_continue_ask「检测到上次的进度。单击,继续上次。右滑,从头开始。」 +- 复述:status_sN(位置 → 目标 → 一个动作,≤20 字)。 + +## 10. 测试记录 + +- 无头 playtest:`web/storyline/test/playtest.mjs`(37 断言,含 §6.2 乱点机器人: + 无声操作 0 / 被动原句重复 0 / 打断失败 0 / 第一次操作 2.1s / 第一次成功 2.1s)。 +- 真人冷启动:待测(§6.3,3~5 人)。 diff --git a/docs/design/README.md b/docs/design/README.md index b9780668..39bf7b70 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -10,6 +10,7 @@ |---|---|---| | [00-global-ux.md](00-global-ux.md) | 全局交互规范(终审修订版):设置系统、haptics 震动语言(10 震型)、旁白整句化、手势约定与模态菜单、用户系统 API | ⑥⑦ + 全局需求 | | [26-audio-interaction-playbook.md](26-audio-interaction-playbook.md) | 纯音频交互第一性原理与落地手册:对话模型、16 原则、发言权管理器/输入回执/新手手势/三档提示/迷茫检测/语音优先级、三张设计单模板、P0-P3 改造顺序、乱点机器人与真人冷启动验证 | 真人试玩「搞不清状态、点两下就重复、不知道怎么玩」 | +| [26-apply-storyline.md](26-apply-storyline.md) / [26-apply-escape.md](26-apply-escape.md) / [26-apply-simon.md](26-apply-simon.md) | 26 号手册首批落地(R3):三款游戏的设计单(附录 C:状态×手势矩阵/等待点/逐秒脚本/失败表/台词类别)+ 引擎新模块(floor/receipts/confusion/hints/novice/pipeline) | 同上 | | [01-escape-redesign.md](01-escape-redesign.md) | 《暗室》重设计:引导式开场、三级卡关提示、线索音可重听 | ⑤ | | [02-simon-redesign.md](02-simon-redesign.md) | 《音序回响》重设计:去 BGM、旁白整句化、交互式教学、复活机制(含双击语义裁定) | ⑥⑦ | | [03-rhythm-rebuild.md](03-rhythm-rebuild.md) + [03-rhythm-voice-ids.md](03-rhythm-voice-ids.md) | 《节拍行者》重构施工稿:跟随领奏玩法、三首完整谱面、判定契约、31 句台词表 | ⑧ | diff --git a/tools/gen_voice_dual.py b/tools/gen_voice_dual.py index f8a2c78f..6a0a6aac 100644 --- a/tools/gen_voice_dual.py +++ b/tools/gen_voice_dual.py @@ -89,30 +89,55 @@ _SL_EPIC = _pair("低沉醇厚的中年男声旁白,悲壮,尾音下沉", "女声旁白,悲壮,尾音下沉") STORYLINE_LINES = [ - # ---- 系统语音 ---- - ("help", _SL_NARR, "操作说明。单击屏幕:继续剧情。双击:重听刚才的台词。向上滑:听这段说明和当前进度。向下滑:呼出菜单,菜单里可以重新开始。当旁白说“请选择”的时候,向左滑或者向右滑,做出你的决定。选择无法撤销。"), - ("status1", _SL_NARR, "当前进度:第一幕,城南地铁站附近。你的目标:活着离开这片废墟,前往北区的广播塔,寻找妹妹林晨的下落。"), - ("status2", _SL_NARR, "当前进度:第二幕,酸雨笼罩的街区与断桥。你的目标:渡过江水,抵达广播塔。小心,猎狗的人,就在附近。"), - ("status3", _SL_NARR, "当前进度:第三幕,广播塔下。你的目标:把警报发出去,找到林晨。这是最后一段路了。"), - ("menu", _SL_NARR, "菜单。向左滑:重新开始整个故事。向右滑,或者单击:回到故事。向上滑:听操作说明。"), - # ---- 选择提示 ---- - ("p1", _SL_NARR, "请选择。向左滑:先检查背包里的收音机。向右滑:先摸黑寻找出口。"), - ("p2", _SL_NARR, "请选择。向左滑:钻进旁边的通风管道。向右滑:躺进路边的尸体堆里装死。"), - ("p3", _SL_NARR, "请选择。向左滑:循着哭声过去看看。向右滑:不去管它,直接去北区。"), - ("p4", _SL_NARR, "请选择。向左滑:带上小满一起走。向右滑:留下食物,独自上路。"), - ("p5", _SL_NARR, "请选择。向左滑:走桥下的污水涵洞。向右滑:爬桥上悬着的钢索。"), - ("p6", _SL_NARR, "请选择。向左滑:交出坐标,保命要紧。向右滑:拒绝他,想办法周旋。"), - ("p7", _SL_NARR, "请选择。向左滑:追上去,警告避难所。向右滑:自己逃命,越远越好。"), - ("p8", _SL_NARR, "请选择。向左滑:用公开频道广播,让全城都听见。向右滑:用加密频段,只发给避难所。"), - # ---- 第一幕 ---- - ("n00_intro", _SL_NARR, "大停电后的第三十天,城市陷入彻底的黑暗。没有电,没有网络,只有收音机里偶尔闪过的沙沙声,像垂死者的呼吸。你是林晚,一名前急救员。在这个故事里,你用声音活下去。请记住:单击屏幕,继续剧情;双击,重听上一句;向上滑,听你的状态和帮助;向下滑,呼出菜单。当我说“请选择”的时候,向左滑,或者向右滑,做出你的决定。选择一旦做出,就无法回头。现在,单击屏幕,睁开眼睛。"), + # ---- 26 号重构:系统/交互层(发言权·三档提示·阶梯·复述)---- + ("sys_first_tap", _SL_NARR, "点一下屏幕。"), + ("sys_first_tap_short", _SL_NARR, "点一下。"), + ("n00_a", _SL_NARR, "大停电后的第三十天。没有电,没有网络,只有收音机里的沙沙声。"), + ("n00_b", _SL_NARR, "你是林晚,前急救员。单击屏幕,睁开眼睛。"), + ("sys_turn_teach", _SL_NARR, "听到这个声音,就是轮到你了。"), + ("skip_hint", _SL_NARR, "单击可跳过旁白。"), + ("sys_hint_continue_full", _SL_NARR, "单击,继续。"), + ("sys_hint_continue_short", _SL_NARR, "单击。"), + ("sys_hint_choice_short", _SL_NARR, "左右滑,选。"), + ("sys_hint_again_full", _SL_NARR, "单击,重新开始。"), + ("sys_say_again", _SL_NARR, "再说一遍:"), + ("status_s1", _SL_NARR, "第一幕,地铁站。找林晨。单击继续。"), + ("status_s2", _SL_NARR, "第二幕,断桥。过江。单击继续。"), + ("status_s3", _SL_NARR, "第三幕,广播塔。发警报。单击继续。"), + ("status_choice", _SL_NARR, "正在抉择。左右滑。"), + ("sys_continue_ask", _SL_NARR, "检测到上次的进度。单击,继续上次。右滑,从头开始。"), + ("inv_nosel", _SL_NARR, "现在不用选。单击继续。"), + ("inv_generic", _SL_NARR, "这个操作用不上。单击继续。"), + ("inv_up_locked", _SL_NARR, "上滑还没解锁。先单击继续。"), + ("inv_down_locked", _SL_NARR, "菜单,玩完一局解锁。单击继续。"), + ("sys_only_choice", _SL_NARR, "现在,只要左右滑。"), + ("sys_demo_continue", _SL_NARR, "听我做一次。"), + ("sys_demo_choice", _SL_NARR, "左右滑,都可以。现在你来。"), + ("sys_unlock_up", _SL_NARR, "想知道自己在哪,就向上滑。"), + ("sys_unlock_down", _SL_NARR, "向下滑,可以打开菜单。"), + ("sys_unlock_dt", _SL_NARR, "没听清,就双击,再听一遍。"), + ("menu_open", _SL_NARR, "菜单。"), + ("menu_item1", _SL_NARR, "重新开始。"), + ("menu_item2", _SL_NARR, "回大厅。"), + ("menu_item3", _SL_NARR, "返回故事。"), + ("menu_help", _SL_NARR, "单击换一项,右滑确认。"), + # ---- 选择提示(动作 + 结果,指令句 ≤12 字)---- + ("p1", _SL_NARR, "请选择。左滑,检查收音机。右滑,摸黑找出口。"), + ("p2", _SL_NARR, "请选择。左滑,钻进通风管道。右滑,躺下装死。"), + ("p3", _SL_NARR, "请选择。左滑,循哭声过去。右滑,直接去北区。"), + ("p4", _SL_NARR, "请选择。左滑,带上小满。右滑,独自上路。"), + ("p5", _SL_NARR, "请选择。左滑,走桥下涵洞。右滑,爬桥上钢索。"), + ("p6", _SL_NARR, "请选择。左滑,交出坐标。右滑,拒绝周旋。"), + ("p7", _SL_NARR, "请选择。左滑,追上去警告。右滑,自己逃命。"), + ("p8", _SL_NARR, "请选择。左滑,公开频道。右滑,加密频段。"), + # ---- 第一幕(旁白,不变)---- ("n01_wake", _SL_NARR, "后脑一阵钝痛,嘴里全是铁锈味。你在废弃的地铁站里醒来,四周黑得像沉在水底。背包还压在身下,手电筒却不知去向。远处滴水的声音,一下,一下,像某种倒计时。"), ("n04_exit", _SL_NARR, "你扶着潮湿的墙,一步一步往外摸。第五十步,脚尖踢到一个硬物——是一支手电,还剩半格电。光柱劈开黑暗的那一瞬,你看见墙上用红漆刷着一行大字:别去北区。"), ("n05_signal", _SL_NARR, "就在这时,背包侧袋里的收音机,沙沙声忽然变得清晰。一个平稳的女声,一遍又一遍地重复着:"), ("n06_whistle", _SL_NARR, "你刚走上街面,黑暗里忽然响起一声口哨。接着是第二声,第三声,从不同的方向,把你围在中间。有人的手电光,扫过你的脸。"), ("n08_vent", _SL_NARR, "你一矮身,钻进了通风管道。铁皮贴着脊背,窄得几乎无法呼吸。身后传来咒骂和铁棍敲击管道口的声音,越来越近,又渐渐远去。十分钟后,你从两条街外的井口爬出来,肺里像塞满了沙子。"), ("n09_dead", _SL_NARR, "你屏住呼吸,把自己埋进路边冰冷的尸体堆里。手电、罐头、半瓶水,被他们一样一样摸走。有人踢了你一脚,你纹丝不动。就在他们离开时,你听见一句不该听见的话——“北区那个陷阱,今晚就收网。”"), - # ---- 第二幕 ---- + # ---- 第二幕(旁白,不变)---- ("n10_rain", _SL_NARR, "酸雨开始下了。街面上升起细密的嘶嘶声,像无数条小蛇在游。雨幕那头,隐约传来一阵哭声——细细的,是个孩子。"), ("n11_child", _SL_NARR, "哭声来自一间塌了一半的便利店。一个小男孩抱着膝盖坐在货架底下,身边躺着一个再也不会醒来的女人。他抬起头,脸上全是灰,小声说:“我叫小满。妈妈说,数到一千,她就会醒。我已经数到……数到好多个一千了。”"), ("n13_leave", _SL_NARR, "你把仅剩的一罐罐头放在他身边,转身走进雨里。身后,孩子重新开始数数:一……二……三……你告诉自己,别回头。千万别回头。"), @@ -122,23 +147,15 @@ STORYLINE_LINES = [ ("n16b_culvert_c", _SL_NARR, "你让小满骑在肩上,自己蹚进没到胸口的污水里。又冷又黏,带着腐烂的甜味。走到一半,水面下有什么东西擦过你的腿,小满吓得一抖,你抬手按住他的膝盖:“数数。”他小声数起来,一,二,三……数到一百七,你们看见了洞口的天光。"), ("n17_cable", _SL_NARR, "钢索比想象中更滑。酸雨把它泡得发黏,你每挪一步,整根钢索就晃一下。江风从断桥的缺口灌进来,像有无数只手在水底下拽你的脚。你不往下看。你只知道,前面就是北岸。"), ("n17b_cable_c", _SL_NARR, "钢索比想象中更滑。小满趴在你背上,一声不吭,两条小胳膊勒得你生疼。走到一半,钢索猛地一沉,你听见身后钢丝崩断的脆响。“闭眼。”你说。然后你用尽全身力气,向前,向前,直到膝盖磕上北岸的石头。"), - # ---- 第三幕 ---- + # ---- 第三幕(旁白,不变)---- ("n20_convoy", _SL_NARR, "猎狗的车队朝北区开走了,探照灯在雨幕里划出一道道口子。你蹲在原地,胃里一阵一阵地泛酸。晨晨在北区。而刚才,是你,亲手把狼,引到了她的门口。"), ("n22_fight", _SL_NARR, "你往后退了半步,忽然笑了:“坐标在我脑子里。杀了我,你什么都拿不到。”猎狗愣了半秒。就是这半秒,你抓起一把沙土扬在他脸上,转身就跑。子弹擦着肩膀打进墙里,火辣辣地疼。但你在跑,你在跑——活着的感觉,从来没有这么清楚过。"), ("n23_tower", _SL_NARR, "广播塔就立在头顶,红色的信号灯在雨里一闪一闪,像一颗还在跳动的心脏。塔底的中控台居然还能用。只要爬上去,你就能接入城市广播网,把警报发给北区的每一个人。问题是——用哪个频道。"), - # ---- 结局 ---- + # ---- 结局(不变)---- ("e1_dawn", _SL_WARM, "加密频道那头沉默了三秒。然后,一个你等了三十天的声音响了起来:“姐?姐!是你吗?”天亮之前,避难所的救援队打开了大门。猎狗的人,在北区的高墙下溃散了。你抱着晨晨,听见广播塔里,重新响起了音乐。后来人们说,这座城市,是从一个女人的声音开始,重新活过来的。——好结局:黎明。感谢你,陪林晚走完了这条路。单击屏幕,可以回到故事开始的地方。"), ("e2_price", _SL_EPIC, "你赶在车队前面,撞进了避难所的大门。“关掉广播!关灯!猎狗来了——!”那一夜,北区守住了。探照灯熄灭的时候,晨晨从人群里扑出来抱住你,才发现你胳膊上的血,已经把半边袖子浸透了。坐标是你交出去的,命,是你抢回来的。你赢了,用一条胳膊的代价。——结局:代价。单击屏幕,重新开始。"), ("e3_alone", _SL_COLD, "你逃了。一路向南,没有回头。三天后,收音机里关于北区的广播,停了。又过了七天,你在一个废弃的加油站里,听见最后一个频段,也变成了沙沙声。你活下来了。只是从此以后的每一个夜里,你都会想起那声口哨,和你报出坐标时,自己发抖的声音。——坏结局:孤声。单击屏幕,重新开始。"), ("e4_static", _SL_EPIC, "公开频道,把所有人都引来了——幸存者,还有猎狗。塔顶的风很大,你看见山下火把连成的长龙,也看见避难所方向亮起的防御火光。猎狗的刀抵上来的时候,你对着麦克风笑了:“听见了吗,晨晨?姐姐的声音,全城都听见了。”信号中断了。但那晚之后,北区,再没有人投降。——结局:静默。单击屏幕,重新开始。"), - ("skip_hint", _SL_NARR, "单击可跳过旁白。"), - # ---- 向导层(docs/design/14-guide-layer.md,schemes §1.5 定稿全文)---- - ("gd_continue", _SL_NARR, "轻触屏幕,继续。"), - ("gd_invalid", _SL_NARR, "这个操作在这里用不上。"), - # ---- 向导层 round2(docs/design/14-guide-layer.md「round2 v1.1」,synthesis §3 定稿全文)---- - ("gd_brief", _SL_NARR, "单击继续剧情。请选择的时候,左右滑动。"), - ("gd_again", _SL_NARR, "还在吗?单击继续。"), - ] # ---------------------------------------------------------------- escape @@ -147,62 +164,63 @@ _ESC = _pair("低沉平稳的男声,冷静、克制,带一点神秘感", "沉稳平静的女声,冷静、克制,带一点神秘感") ESCAPE_LINES = [ - # ---- 开场引导 G0-G7 ---- - ("g0_intro", _ESC, "黑暗。你在废弃的地下设施里醒来,什么都想不起来。你必须找到出路。"), - ("g1_move", _ESC, "先活动一下。朝任意方向滑一下,走一小步。"), - ("g1_done", _ESC, "很好,你迈出了第一步。"), - ("g2_wall", _ESC, "这个房间不大,四面都是墙。再滑两下,试试碰碰墙。"), - ("g3_left", _ESC, "跟着我指的路走。向左滑。"), + # ---- 26 号重构:引导(首 3 分钟逐秒脚本,见 26-apply-escape.md)---- + ("g0_new", _ESC, "黑暗。朝任意方向,滑一下。"), + ("g0b", _ESC, "对。你被困在地下设施里。再滑两下,试试墙。"), + ("g3_new", _ESC, "现在,向左滑。"), ("g4_down", _ESC, "对。再向下滑。"), ("g5_inspect", _ESC, "听到水声了吗?单击一下,仔细检查这个房间。"), ("g5_key", _ESC, "你摸到了一把钥匙!"), - ("g6_doubletap", _ESC, "钥匙能开锁。双击,是把物品用出去的用法。现在,双击试试。"), - ("g6_done", _ESC, "对,就是这样。接下来,靠你自己了。"), - ("g7_goal", _ESC, "找到能用这把钥匙打开的东西。需要帮助时,在原地双击。"), - # ---- 引导纠错与跳过 ---- - ("guide_wrong_up", _ESC, "不是这个方向。向上滑。"), - ("guide_wrong_down", _ESC, "不是这个方向。向下滑。"), + ("g7_goal_new", _ESC, "找到能用钥匙打开的东西。迷路了,按住屏幕。"), + ("g_wrong_gesture", _ESC, "不是这个操作。照我说的做。"), + ("g_wrong_tap", _ESC, "这一步不点。滑动。"), + ("g_hint_swipe", _ESC, "滑一下。"), ("guide_wrong_left", _ESC, "不是这个方向。向左滑。"), - ("guide_wrong_right", _ESC, "不是这个方向。向右滑。"), - ("guide_wrong_gesture", _ESC, "不是这个操作。照我说的做。"), - ("guide_blunt_up", _ESC, "手指贴着屏幕,向上推一下。"), - ("guide_blunt_down", _ESC, "手指贴着屏幕,向下推一下。"), - ("guide_blunt_left", _ESC, "手指贴着屏幕,向左推一下。"), - ("guide_blunt_right", _ESC, "手指贴着屏幕,向右推一下。"), - ("guide_offpath", _ESC, "先跟我走。"), - ("guide_skip", _ESC, "跳过引导,直接开始自由探索。"), - ("reopen_prompt", _ESC, "上滑,重新听一遍引导。单击,直接开始。"), - # ---- 卡关三级提示 + 导航 ---- - ("stuck_ask", _ESC, "需要帮助吗?"), - ("stuck_key_1", _ESC, "铁锈味最重的地方,值得仔细看看。"), - ("stuck_key_2", _ESC, "去西南角的储物间,单击检查,找到钥匙。"), - ("stuck_key_3", _ESC, "跟我说的方向走,去西南角的储物间。"), - ("stuck_cabinet_1", _ESC, "钥匙就在你身上。它能打开某一把锁。"), - ("stuck_cabinet_2", _ESC, "去东边的铁柜房,双击,用钥匙打开铁柜。"), - ("stuck_cabinet_3", _ESC, "跟我说的方向走,去铁柜所在的房间。"), - ("stuck_box_1", _ESC, "保险丝属于电箱。有电流声的地方,就有电箱。"), - ("stuck_box_2", _ESC, "去西北角的电箱房,双击,把保险丝装进电箱。"), - ("stuck_box_3", _ESC, "跟我说的方向走,去电箱所在的房间。"), - ("stuck_gate_1", _ESC, "电已经通了。该去找那扇铁门了。"), - ("stuck_gate_2", _ESC, "去东南角的大门,双击,打开它,离开这里。"), - ("stuck_gate_3", _ESC, "跟我说的方向走,去大门所在的房间。"), + ("guide_wrong_down", _ESC, "不是这个方向。向下滑。"), + ("guide_blunt_swipe", _ESC, "手指贴着屏幕,朝我说的方向推一下。"), + # ---- 撞墙与导航(失败表:方向错)---- + ("wall", _ESC, "那边是墙,换个方向。"), + ("wall_dir_l", _ESC, "左边是墙。"), + ("wall_dir_r", _ESC, "右边是墙。"), + ("wall_dir_u", _ESC, "上边是墙。"), + ("wall_dir_d", _ESC, "下边是墙。"), ("nav_up", _ESC, "向上滑。"), ("nav_down", _ESC, "向下滑。"), ("nav_left", _ESC, "向左滑。"), ("nav_right", _ESC, "向右滑。"), - # ---- 房间描述 ---- + # ---- 长按「我在哪」(目标 + 方位,一句话)---- + ("lp_key", _ESC, "目标,找钥匙。"), + ("lp_cabinet", _ESC, "目标,开铁柜。"), + ("lp_box", _ESC, "目标,修电箱。"), + ("lp_gate", _ESC, "目标,开大门。"), + ("lp_here", _ESC, "就在这儿。单击。"), + ("lp_tap", _ESC, "单击。"), + # ---- 迷茫接管(三级)---- + ("sys_only_up", _ESC, "现在,只要向上滑。"), + ("sys_only_down", _ESC, "现在,只要向下滑。"), + ("sys_only_left", _ESC, "现在,只要向左滑。"), + ("sys_only_right", _ESC, "现在,只要向右滑。"), + ("sys_demo", _ESC, "听我做一次。"), + ("sys_now_you", _ESC, "现在,你来。"), + # ---- 系统层 ---- + ("sys_continue_ask", _ESC, "检测到你的进度。单击,继续上次。右滑,重新开始。"), + ("sys_say_again", _ESC, "再说一遍:"), + ("inv_generic", _ESC, "这里用不上。滑动移动,单击检查。"), + ("inv_settle", _ESC, "单击,重新开始。"), + ("menu_open", _ESC, "菜单。"), + ("menu_item1", _ESC, "重新开始。"), + ("menu_item2", _ESC, "回大厅。"), + ("menu_item3", _ESC, "返回游戏。"), + ("menu_help", _ESC, "单击换一项,右滑确认。"), + # ---- 房间与谜题链 ---- + ("start_room", _ESC, "你站在设施中央的房间里。滑动,移动。单击,检查。"), ("room_storage", _ESC, "一间储物间。货架上堆满杂物,角落里传来水滴声。"), ("room_cabinet", _ESC, "这个房间里,立着一个铁柜。"), ("room_power", _ESC, "这个房间的墙上,有一只电箱。"), ("room_gate", _ESC, "面前,是一扇沉重的铁门。"), - # ---- 用时播报 4 段整句 ---- - ("time_band1", _ESC, "你逃出来了,用时不到两分钟。了不起!"), - ("time_band2", _ESC, "你逃出来了,用时不到五分钟。"), - ("time_band3", _ESC, "你逃出来了,用时不到十分钟。"), - ("time_band4", _ESC, "你逃出来了,用时超过十分钟。"), - # ---- 保留句(文本沿用,按新 instruct 重录) ---- - ("wall", _ESC, "那边是墙,换个方向。"), - ("start_room", _ESC, "你站在设施中央的房间里。滑动移动,单击检查,双击使用物品。"), + ("empty_1", _ESC, "房间里空空荡荡,只有你自己的回声。"), + ("empty_2", _ESC, "你伸出手,摸到的只有冰冷粗糙的墙壁。"), + ("empty_3", _ESC, "除了远处的水滴声,这里什么都没有。"), ("found_key", _ESC, "你蹲下身在储物架的角落里摸索,指尖碰到一个冰凉的金属物件——是一把钥匙!钥匙到手了。"), ("inspect_cabinet_locked", _ESC, "这个房间里立着一个铁柜,柜门上挂着一把锁。也许,有什么钥匙能打开它。"), ("unlock_cabinet", _ESC, "钥匙插进锁孔,缓缓转动。咔哒一声,柜门开了。柜子深处,躺着一枚保险丝。保险丝到手了。"), @@ -210,48 +228,23 @@ ESCAPE_LINES = [ ("box_fixed", _ESC, "电箱运转正常,电流的嗡鸣声平稳而持续。"), ("fix_box", _ESC, "你把保险丝用力按进卡座。火花一闪,嗡鸣声响起——头顶的线路恢复供电了!"), ("inspect_gate", _ESC, "面前是一扇沉重的铁门。门边的电子锁一片漆黑,没有任何反应。它需要电力。"), - ("inspect_gate_ready", _ESC, "电子锁的指示灯亮了,发出微弱的绿光。大门已经通电。双击,打开它,离开这里。"), - ("escape", _ESC, "你扳动开关。电机轰鸣,沉重的铁门缓缓升起,刺眼的光线涌了进来。你眯起眼睛,向前迈出了第一步。自由了。你逃出来了。"), - ("use_fail", _ESC, "这里没有用得上东西的地方。换个房间试试,或者先单击检查一下周围。"), - ("inv_empty", _ESC, "你摸了摸身上,什么也没有。"), - ("inv_key", _ESC, "你身上带着一把钥匙。"), - ("inv_fuse", _ESC, "你身上带着一枚保险丝。"), - ("inv_both", _ESC, "你身上带着一把钥匙和一枚保险丝。"), - ("replay", _ESC, "黑暗再次合拢。你回到了中央的房间。这一次,你会更快。"), - ("again", _ESC, "单击屏幕,再来一次。下滑,打开菜单。"), - ("empty_1", _ESC, "房间里空空荡荡,只有你自己的回声。"), - ("empty_2", _ESC, "你伸出手,摸到的只有冰冷粗糙的墙壁。"), - ("empty_3", _ESC, "除了远处的水滴声,这里什么都没有。"), - # ---- 菜单与帮助 ---- - ("help_ops", _ESC, "滑动,在房间之间移动。单击,检查房间,或者重听房间的声音。双击,使用物品,或者查看背包。"), - ("menu_open", _ESC, "菜单。"), - ("menu_restart", _ESC, "重新开始。"), - ("menu_home", _ESC, "回大厅。"), - ("menu_back", _ESC, "返回游戏。"), - ("skip_hint", _ESC, "单击可跳过旁白。"), - # ---- 向导层 round3(docs/design/14-guide-layer.md「round3 v1.2」,synthesis §3 定稿全文)---- + ("inspect_gate_ready", _ESC, "电子锁的指示灯亮了,发出微弱的绿光。单击,打开它,离开这里。"), ("gd_next_fuse", _ESC, "保险丝到手。接下来,去找电箱。"), - # ---- 向导层 round4(docs/design/14-guide-layer.md「round4 v1.3」,synthesis §3 定稿全文)---- - # H1:目标句后按房间图接播目标房间方位句(移动坐标系:up/down/left/right 滑动方向即方位)。 ("gd_room_l", _ESC, "左面的房间。"), ("gd_room_r", _ESC, "右面的房间。"), ("gd_room_u", _ESC, "上面的房间。"), ("gd_room_d", _ESC, "下面的房间。"), - # ---- escape-state round2(docs/design/25-escape-map-flow.md §4.1 B1/B2/B3,台词与草案逐字一致)---- - # B1(F1):撞墙方位句(陈述式非教学腔),wall 播完后接播,同向 10s 限频。 - ("wall_dir_l", _ESC, "左边是墙。"), - ("wall_dir_r", _ESC, "右边是墙。"), - ("wall_dir_u", _ESC, "上边是墙。"), - ("wall_dir_d", _ESC, "下边是墙。"), - # B2(F2):目标名+方位一体句,替换三处 gd_room_* 接播(原句保留作异常路径兜底)。 ("gd_cabinet_dir", _ESC, "铁柜,在右面的房间。"), ("gd_box_dir", _ESC, "电箱,在左面的房间。"), ("gd_gate_dir", _ESC, "大门,在右面的房间。"), - # B3(F3):无进展 150s 活跃计时器的进度重述(stage=cabinet/box/gate,key 阶段不走本通道)。 - ("gd_prog_1", _ESC, "钥匙已经到手。找到能用它的锁。"), - ("gd_prog_2", _ESC, "铁柜已经打开。电还没有通。"), - ("gd_prog_3", _ESC, "电已经通了。去找那扇大门。"), - + # ---- 结算 ---- + ("escape", _ESC, "你扳动开关。电机轰鸣,沉重的铁门缓缓升起,刺眼的光线涌了进来。你眯起眼睛,向前迈出了第一步。自由了。你逃出来了。"), + ("time_band1", _ESC, "你逃出来了,用时不到两分钟。了不起!"), + ("time_band2", _ESC, "你逃出来了,用时不到五分钟。"), + ("time_band3", _ESC, "你逃出来了,用时不到十分钟。"), + ("time_band4", _ESC, "你逃出来了,用时超过十分钟。"), + ("again", _ESC, "单击屏幕,再来一次。"), + ("replay", _ESC, "黑暗再次合拢。你回到了中央的房间。这一次,你会更快。"), ] # ---------------------------------------------------------------- simon @@ -260,9 +253,8 @@ _SIMON = _pair("平静、清晰,像一个耐心的游戏主持人") _SIMON_ROUNDS = ["第一", "第二", "第三", "第四", "第五", "第六", "第七", "第八", "第九", "第十", "第十一", "第十二", "第十三", "第十四", "第十五", "第十六", "第十七", "第十八", "第十九", "第二十"] SIMON_LINES = [ - # ---- 交互式教学 T0-T7 ---- - ("t0_welcome", _SIMON, "欢迎来到音序回响。先认识六种声音。"), - ("t0_goal", _SIMON, "我会演奏一串声音,请你记住顺序,原样复现。"), + # ---- 26 号重构:教学(3 秒内动手;新手只教三个音色)---- + ("t0_new", _SIMON, "音序回响。点一下屏幕。"), ("t1_up", _SIMON, "向上滑,是这个声音。"), ("t2_down", _SIMON, "向下滑,是这个声音。"), ("t3_left", _SIMON, "向左滑,是这个声音。"), @@ -270,6 +262,7 @@ SIMON_LINES = [ ("t5_tap", _SIMON, "单击,是这个声音。"), ("t6_double", _SIMON, "双击,是这个声音。"), ("t_try", _SIMON, "你试试。"), + ("t_retry", _SIMON, "再试一次。"), ("t_ok", _SIMON, "对了。"), ("t_wrong_up", _SIMON, "不是这个,向上滑试试。"), ("t_wrong_down", _SIMON, "不是这个,向下滑试试。"), @@ -277,16 +270,8 @@ SIMON_LINES = [ ("t_wrong_right", _SIMON, "不是这个,向右滑试试。"), ("t_wrong_tap", _SIMON, "不是这个,单击试试。"), ("t_wrong_double", _SIMON, "不是这个,双击试试。"), - ("t_blunt_up", _SIMON, "手指贴着屏幕,向上推一下。"), - ("t_blunt_down", _SIMON, "手指贴着屏幕,向下推一下。"), - ("t_blunt_left", _SIMON, "手指贴着屏幕,向左推一下。"), - ("t_blunt_right", _SIMON, "手指贴着屏幕,向右推一下。"), - ("t_blunt_tap", _SIMON, "用指尖,轻轻点一下屏幕。"), - ("t_blunt_double", _SIMON, "用指尖,快速点两下屏幕。"), - ("t7_ready", _SIMON, "六种声音都记住了。后面的序列,会越来越长。"), - ("t7_retry", _SIMON, "答错可以消耗机会重听。向上滑,可以随时重听。"), - ("t_skip", _SIMON, "跳过教学,直接开始。"), - # ---- 轮次播报(整句,round_01..round_20 + 兜底) ---- + ("t7_new", _SIMON, "记住顺序,原样复现。答错,我们就再听一遍。"), + # ---- 轮次播报(整句)---- *[(f"round_{i + 1:02d}", _SIMON, f"{w}轮,听好了。") for i, w in enumerate(_SIMON_ROUNDS)], ("round_more", _SIMON, "新的一轮,听好了。"), # ---- 成绩播报 6 段整句 ---- @@ -296,60 +281,42 @@ SIMON_LINES = [ ("score_band4", _SIMON, "你坚持了将近十轮。"), ("score_band5", _SIMON, "你坚持了十轮以上,很厉害。"), ("score_band6", _SIMON, "你坚持了十五轮以上,大师级!"), + # ---- 失败表(归因 2 秒内 + 免费重听 + 放慢)---- + ("wr_up", _SIMON, "错了。是向上滑。"), + ("wr_down", _SIMON, "错了。是向下滑。"), + ("wr_left", _SIMON, "错了。是向左滑。"), + ("wr_right", _SIMON, "错了。是向右滑。"), + ("wr_tap", _SIMON, "错了。是单击。"), + ("wr_dt", _SIMON, "错了。是双击。"), + ("relisten", _SIMON, "再听一遍。"), + ("slow_down", _SIMON, "我放慢一些。"), # ---- 流程句 ---- ("your_turn", _SIMON, "轮到你了。"), - ("chances_out", _SIMON, "重听机会用完了,小心。"), - ("retry_use", _SIMON, "错了。用一次机会,再听一遍。"), - ("relisten", _SIMON, "再听一遍。"), - ("replay", _SIMON, "单击屏幕,再来一局。"), - ("rules_prompt", _SIMON, "上滑,重听规则。下滑,打开菜单。"), - ("rules", _SIMON, "上行音,向上滑。下行音,向下滑。左边的音,向左滑。右边的音,向右滑。一声轻响,单击。两声轻响,双击。"), ("gameover", _SIMON, "游戏结束。"), + ("replay", _SIMON, "单击,再来一局。"), + ("new_record", _SIMON, "新纪录!"), + # ---- 状态查询(长按,一句话)---- + ("st_b1", _SIMON, "前几轮。"), + ("st_b2", _SIMON, "第五轮上下。"), + ("st_b3", _SIMON, "第十轮上下。"), + ("st_b4", _SIMON, "十五轮以上。"), # ---- 反馈短句 ---- ("correct", _SIMON, "答对了!"), - ("wrong", _SIMON, "错了。"), - ("chance_plus", _SIMON, "机会加一。"), - ("new_record", _SIMON, "新纪录!"), + ("gd_clear", _SIMON, "这轮全对。"), + ("gd_record", _SIMON, "破纪录了。"), + # ---- 系统层 ---- + ("sys_say_again", _SIMON, "再说一遍:"), + ("sys_demo", _SIMON, "听我做一次。"), + ("inv_busy", _SIMON, "正在播放。稍等。"), + ("inv_generic", _SIMON, "这个操作用不上。"), + ("sys_unlock_down", _SIMON, "向下滑,可以打开菜单。"), + ("sys_unlock_dt", _SIMON, "没听清,就双击,再听一遍。"), # ---- 菜单 ---- ("menu_open", _SIMON, "菜单。"), - ("menu_restart", _SIMON, "重新开始。"), - ("menu_home", _SIMON, "回大厅。"), - ("menu_back", _SIMON, "返回游戏。"), - ("skip_hint", _SIMON, "单击可跳过旁白。"), - # ---- 向导层(docs/design/14-guide-layer.md,schemes §1.5 定稿全文)---- - ("gd_relisten", _SIMON, "需要的话,向上滑可以重新听一遍序列。"), - # ---- 向导层 round2(docs/design/14-guide-layer.md「round2 v1.1」,synthesis §3 定稿全文)---- - # round3 G5 重录(14 号文档「round3 v1.2」):双击改回纯音色、重听改绑上滑, - # 机会经济句按裁定书新文案一字不改重录;教学链 t7_retry 同步手势口径。 - ("gd_economy", _SIMON, "记不住就向上滑,重新听一遍。免费机会有两次,用完要扣一次机会。"), - # ---- 向导层 round3(docs/design/14-guide-layer.md「round3 v1.2」,synthesis §3 定稿全文)---- - ("gd_clear", _SIMON, "漂亮,过了。"), - ("gd_record", _SIMON, "破纪录了。"), - # ---- 向导层 round4(docs/design/14-guide-layer.md「round4 v1.3」,synthesis §3 定稿全文)---- - # H2:轮次开场占用预告(序列播放后、轮到你提示前;重听键与答案音共用词表的根治形态)。 - ("gd_occ_up", _SIMON, "这一轮,向上滑是作答。"), - ("gd_occ_dt", _SIMON, "这一轮,双击是作答。"), - ("gd_occ_both", _SIMON, "这一轮,向上滑和双击都是作答。"), - # H3:使用时如实报余量(替换主动重听路径的旧「用完了」绝对句)。 - ("gd_chances_left1", _SIMON, "免费重听还剩一次。"), - ("gd_chances_left0", _SIMON, "免费重听用完了。之后重听会花一次机会,别怕,慢慢来。"), - # ---- 状态查询(research/simon-polish round1,S1/S3/S4;input 阶段长按=问状态, - # 13 句全部整句预生成、数字分档,禁运行时拼接;不加 guide 前缀——三档 guide 全可用, - # 属玩家主动查询而非向导插播)---- - ("status_len_short", _SIMON, "这一轮很短。"), - ("status_len_mid", _SIMON, "这一轮不长不短。"), - ("status_len_long", _SIMON, "这一轮挺长。"), - ("status_len_xlong", _SIMON, "这一轮很长,稳住。"), - ("status_prog_start", _SIMON, "刚开头,一个还没对。"), - ("status_prog_early", _SIMON, "刚答了个开头。"), - ("status_prog_half", _SIMON, "答了一半左右。"), - ("status_prog_late", _SIMON, "就剩最后几个了。"), - ("status_free2", _SIMON, "免费重听还有两次。"), - ("status_free1", _SIMON, "免费重听还有一次。"), - ("status_chance2", _SIMON, "免费用完,机会还剩两次。"), - ("status_chance1", _SIMON, "免费用完,机会还剩一次。"), - ("status_out", _SIMON, "免费和机会都用完了,答错就结束。"), - + ("menu_item1", _SIMON, "重新开始。"), + ("menu_item2", _SIMON, "回大厅。"), + ("menu_item3", _SIMON, "返回结算。"), + ("menu_help", _SIMON, "单击换一项,右滑确认。"), ] # ---------------------------------------------------------------- rhythm @@ -2167,89 +2134,92 @@ NPCS = {"rpg": RPG_NPC, "radio": RADIO_NPC, "abyss": ABYSS_NPC, "detective": DET # 双表一致铁律:本表与 docs/design/*-voice-ids.md「键盘变体」小节必须逐字一致。 KEY_LINES = { "storyline": [ - ("help", "操作说明。按空格:继续剧情。按回车:重听刚才的台词。按上方向键:听这段说明和当前进度。按下方向键:呼出菜单,菜单里可以重新开始。当旁白说“请选择”的时候,按左方向键或者按右方向键,做出你的决定。选择无法撤销。"), - ("menu", "菜单。按左方向键:重新开始整个故事。按右方向键,或者按空格:回到故事。按上方向键:听操作说明。"), - ("p1", "请选择。按左方向键:先检查背包里的收音机。按右方向键:先摸黑寻找出口。"), - ("p2", "请选择。按左方向键:钻进旁边的通风管道。按右方向键:躺进路边的尸体堆里装死。"), - ("p3", "请选择。按左方向键:循着哭声过去看看。按右方向键:不去管它,直接去北区。"), - ("p4", "请选择。按左方向键:带上小满一起走。按右方向键:留下食物,独自上路。"), - ("p5", "请选择。按左方向键:走桥下的污水涵洞。按右方向键:爬桥上悬着的钢索。"), - ("p6", "请选择。按左方向键:交出坐标,保命要紧。按右方向键:拒绝他,想办法周旋。"), - ("p7", "请选择。按左方向键:追上去,警告避难所。按右方向键:自己逃命,越远越好。"), - ("p8", "请选择。按左方向键:用公开频道广播,让全城都听见。按右方向键:用加密频段,只发给避难所。"), - ("n00_intro", "大停电后的第三十天,城市陷入彻底的黑暗。没有电,没有网络,只有收音机里偶尔闪过的沙沙声,像垂死者的呼吸。你是林晚,一名前急救员。在这个故事里,你用声音活下去。请记住:按空格,继续剧情;按回车,重听上一句;按上方向键,听你的状态和帮助;按下方向键,呼出菜单。当我说“请选择”的时候,按左方向键,或者按右方向键,做出你的决定。选择一旦做出,就无法回头。现在,按空格,睁开眼睛。"), - ("e1_dawn", "加密频道那头沉默了三秒。然后,一个你等了三十天的声音响了起来:“姐?姐!是你吗?”天亮之前,避难所的救援队打开了大门。猎狗的人,在北区的高墙下溃散了。你抱着晨晨,听见广播塔里,重新响起了音乐。后来人们说,这座城市,是从一个女人的声音开始,重新活过来的。——好结局:黎明。感谢你,陪林晚走完了这条路。按空格,可以回到故事开始的地方。"), - ("e2_price", "你赶在车队前面,撞进了避难所的大门。“关掉广播!关灯!猎狗来了——!”那一夜,北区守住了。探照灯熄灭的时候,晨晨从人群里扑出来抱住你,才发现你胳膊上的血,已经把半边袖子浸透了。坐标是你交出去的,命,是你抢回来的。你赢了,用一条胳膊的代价。——结局:代价。按空格,重新开始。"), - ("e3_alone", "你逃了。一路向南,没有回头。三天后,收音机里关于北区的广播,停了。又过了七天,你在一个废弃的加油站里,听见最后一个频段,也变成了沙沙声。你活下来了。只是从此以后的每一个夜里,你都会想起那声口哨,和你报出坐标时,自己发抖的声音。——坏结局:孤声。按空格,重新开始。"), - ("e4_static", "公开频道,把所有人都引来了——幸存者,还有猎狗。塔顶的风很大,你看见山下火把连成的长龙,也看见避难所方向亮起的防御火光。猎狗的刀抵上来的时候,你对着麦克风笑了:“听见了吗,晨晨?姐姐的声音,全城都听见了。”信号中断了。但那晚之后,北区,再没有人投降。——结局:静默。按空格,重新开始。"), + ("sys_first_tap", "按空格。"), + ("sys_first_tap_short", "按一下空格。"), + ("n00_b", "你是林晚,前急救员。按空格,睁开眼睛。"), + ("sys_hint_continue_full", "按空格,继续。"), + ("sys_hint_continue_short", "按空格。"), + ("sys_hint_choice_short", "左右方向键,选。"), + ("sys_hint_again_full", "按空格,重新开始。"), + ("status_s1", "第一幕,地铁站。找林晨。按空格继续。"), + ("status_s2", "第二幕,断桥。过江。按空格继续。"), + ("status_s3", "第三幕,广播塔。发警报。按空格继续。"), + ("status_choice", "正在抉择。左右方向键。"), + ("sys_continue_ask", "检测到上次的进度。按空格,继续上次。右方向键,从头开始。"), + ("inv_nosel", "现在不用选。按空格继续。"), + ("inv_generic", "这个操作用不上。按空格继续。"), + ("inv_up_locked", "上方向键还没解锁。先按空格继续。"), + ("inv_down_locked", "菜单,玩完一局解锁。按空格继续。"), + ("sys_only_choice", "现在,只要左右方向键。"), + ("sys_unlock_up", "想知道自己在哪,就按上方向键。"), + ("sys_unlock_down", "按下方向键,可以打开菜单。"), + ("sys_unlock_dt", "没听清,就按回车,再听一遍。"), + ("menu_help", "空格换一项,右方向键确认。"), + ("p1", "请选择。左方向键,检查收音机。右方向键,摸黑找出口。"), + ("p2", "请选择。左方向键,钻进通风管道。右方向键,躺下装死。"), + ("p3", "请选择。左方向键,循哭声过去。右方向键,直接去北区。"), + ("p4", "请选择。左方向键,带上小满。右方向键,独自上路。"), + ("p5", "请选择。左方向键,走桥下涵洞。右方向键,爬桥上钢索。"), + ("p6", "请选择。左方向键,交出坐标。右方向键,拒绝周旋。"), + ("p7", "请选择。左方向键,追上去警告。右方向键,自己逃命。"), + ("p8", "请选择。左方向键,公开频道。右方向键,加密频段。"), ("skip_hint", "按空格可跳过旁白。"), - ("gd_continue", "按空格,继续。"), - ("gd_brief", "按空格继续剧情。请选择的时候,按左右方向键。"), - ("gd_again", "还在吗?按空格继续。"), ], "escape": [ - ("g1_move", "先活动一下。按任意方向键,走一小步。"), - ("g2_wall", "这个房间不大,四面都是墙。再按两下方向键,试试碰碰墙。"), - ("g3_left", "跟着我指的路走。按左方向键。"), + ("g0_new", "黑暗。按任意方向键。"), + ("g0b", "对。你被困在地下设施里。再按两下方向键,试试墙。"), + ("g3_new", "现在,按左方向键。"), ("g4_down", "对。再按下方向键。"), ("g5_inspect", "听到水声了吗?按一下空格,仔细检查这个房间。"), - ("g6_doubletap", "钥匙能开锁。按回车,是把物品用出去的用法。现在,按回车试试。"), - ("g7_goal", "找到能用这把钥匙打开的东西。需要帮助时,在原地按回车。"), - ("guide_wrong_up", "不是这个方向。按上方向键。"), - ("guide_wrong_down", "不是这个方向。按下方向键。"), + ("g7_goal_new", "找到能用钥匙打开的东西。迷路了,按住空格。"), + ("g_wrong_tap", "这一步不按空格。用方向键。"), + ("g_hint_swipe", "按一下方向键。"), ("guide_wrong_left", "不是这个方向。按左方向键。"), - ("guide_wrong_right", "不是这个方向。按右方向键。"), - ("guide_blunt_up", "按一下上方向键。"), - ("guide_blunt_down", "按一下下方向键。"), - ("guide_blunt_left", "按一下左方向键。"), - ("guide_blunt_right", "按一下右方向键。"), - ("reopen_prompt", "按上方向键,重新听一遍引导。按空格,直接开始。"), - ("stuck_key_2", "去西南角的储物间,按空格检查,找到钥匙。"), - ("stuck_cabinet_2", "去东边的铁柜房,按回车,用钥匙打开铁柜。"), - ("stuck_box_2", "去西北角的电箱房,按回车,把保险丝装进电箱。"), - ("stuck_gate_2", "去东南角的大门,按回车,打开它,离开这里。"), + ("guide_wrong_down", "不是这个方向。按下方向键。"), + ("guide_blunt_swipe", "按我说的方向键,按一下。"), ("nav_up", "按上方向键。"), ("nav_down", "按下方向键。"), ("nav_left", "按左方向键。"), ("nav_right", "按右方向键。"), - ("start_room", "你站在设施中央的房间里。方向键移动,按空格检查,按回车使用物品。"), - ("inspect_gate_ready", "电子锁的指示灯亮了,发出微弱的绿光。大门已经通电。按回车,打开它,离开这里。"), - ("use_fail", "这里没有用得上东西的地方。换个房间试试,或者先按空格检查一下周围。"), - ("again", "按空格,再来一次。按下方向键,打开菜单。"), - ("help_ops", "方向键,在房间之间移动。按空格,检查房间,或者重听房间的声音。按回车,使用物品,或者查看背包。"), - ("skip_hint", "按空格可跳过旁白。"), + ("lp_tap", "按空格。"), + ("lp_here", "就在这儿。按空格。"), + ("sys_only_up", "现在,只要按上方向键。"), + ("sys_only_down", "现在,只要按下方向键。"), + ("sys_only_left", "现在,只要按左方向键。"), + ("sys_only_right", "现在,只要按右方向键。"), + ("sys_continue_ask", "检测到你的进度。按空格,继续上次。右方向键,重新开始。"), + ("inv_settle", "按空格,重新开始。"), + ("menu_help", "空格换一项,右方向键确认。"), + ("start_room", "你站在设施中央的房间里。方向键,移动。空格,检查。"), + ("inspect_gate_ready", "电子锁的指示灯亮了,发出微弱的绿光。按空格,打开它,离开这里。"), + ("again", "按空格,再来一次。"), ], "simon": [ + ("t0_new", "音序回响。按一下空格。"), ("t1_up", "按上方向键,是这个声音。"), ("t2_down", "按下方向键,是这个声音。"), ("t3_left", "按左方向键,是这个声音。"), ("t4_right", "按右方向键,是这个声音。"), ("t5_tap", "按空格,是这个声音。"), ("t6_double", "按回车,是这个声音。"), + ("t_retry", "再按一次。"), ("t_wrong_up", "不是这个,按上方向键试试。"), ("t_wrong_down", "不是这个,按下方向键试试。"), ("t_wrong_left", "不是这个,按左方向键试试。"), ("t_wrong_right", "不是这个,按右方向键试试。"), ("t_wrong_tap", "不是这个,按空格试试。"), ("t_wrong_double", "不是这个,按回车试试。"), - ("t_blunt_up", "按一下上方向键。"), - ("t_blunt_down", "按一下下方向键。"), - ("t_blunt_left", "按一下左方向键。"), - ("t_blunt_right", "按一下右方向键。"), - ("t_blunt_tap", "轻轻按一下空格。"), - ("t_blunt_double", "按一下回车。"), - ("t7_retry", "答错可以消耗机会重听。按上方向键,可以随时重听。"), + ("wr_up", "错了。是按上方向键。"), + ("wr_down", "错了。是按下方向键。"), + ("wr_left", "错了。是按左方向键。"), + ("wr_right", "错了。是按右方向键。"), + ("wr_tap", "错了。是按空格。"), + ("wr_dt", "错了。是按回车。"), ("replay", "按空格,再来一局。"), - ("rules_prompt", "按上方向键,重听规则。按下方向键,打开菜单。"), - ("rules", "上行音,按上方向键。下行音,按下方向键。左边的音,按左方向键。右边的音,按右方向键。一声轻响,按空格。两声轻响,按回车。"), - ("skip_hint", "按空格可跳过旁白。"), - ("gd_relisten", "需要的话,按上方向键可以重新听一遍序列。"), - ("gd_economy", "记不住就按上方向键,重新听一遍。免费机会有两次,用完要扣一次机会。"), - ("gd_occ_up", "这一轮,按上方向键是作答。"), - ("gd_occ_dt", "这一轮,按回车是作答。"), - ("gd_occ_both", "这一轮,按上方向键和回车都是作答。"), + ("sys_unlock_down", "按下方向键,可以打开菜单。"), + ("sys_unlock_dt", "没听清,就按回车,再听一遍。"), + ("menu_help", "空格换一项,右方向键确认。"), ], - # rhythm:8 对(24-rhythm-voice-ids.md 键盘变体表,措辞含数字键/方向键/空格/回车)。 "rhythm": [ ("tut_map", "先认识你的鼓。屏幕横着分三列,左边、中间、右边;竖着分两行,上面清亮,下面低沉。按数字一键到六键,听六面鼓。"), ("tut_probe", "按出三个不同的键,我们就开始。"), diff --git a/web/engine/audio.js b/web/engine/audio.js index 5ecbe414..ec6bc093 100644 --- a/web/engine/audio.js +++ b/web/engine/audio.js @@ -300,6 +300,18 @@ export class WebAudioEngine { this._unduckBgm(); } + /** + * 语音链级压低持有(26 号文档 §4.1「音乐压低要跨整条链」):游戏层用 + * onComplete 一句句接起来的链,句间引擎队列是空的、会恢复音乐音量, + * 玩家就听到一次「好像轮到我了」。Floor 在整条链期间持有一份压低, + * 链结束/被打断时释放。可重入(计数)。 + * @param {boolean} on true=持有一份压低;false=释放一份 + */ + holdDuck(on) { + this._duckHolds = Math.max(0, (this._duckHolds || 0) + (on ? 1 : -1)); + if (on) this._duckBgm(); + } + /** * 跳过当前对白:停掉当前 voice 并**视为这句自然播完**——触发其 * onComplete(链播下一句)并继续等待队列。与 stopVoice 的差异: @@ -443,6 +455,7 @@ export class WebAudioEngine { } _unduckBgm() { + if (this._duckHolds > 0) return; // Floor 持有链级压低:句间不恢复(26 号 §4.1) if (this._bgmBus) this._fadeGain(this._bgmBus, 1, this.duckMs); } diff --git a/web/engine/confusion.js b/web/engine/confusion.js new file mode 100644 index 00000000..e862fb57 --- /dev/null +++ b/web/engine/confusion.js @@ -0,0 +1,89 @@ +/** + * confusion.js — 迷茫检测(26 号文档 §4.5,P2 第 9 项)。 + * + * 迷茫不只看「不动」,更要从「乱点」里识别。信号(10s 滑动窗口): + * | 信号 | 判定 | 权重 | + * |----------|----------------------------|------| + * | 连续无效 | 10 秒内 ≥3 次 invalid | 2 | + * | 反复重听 | 10 秒内主动重听 ≥2 次 | 1 | + * | 乱滑 | 2 秒内 ≥3 个不同滑动方向 | 1 | + * | 同步失败 | 同一等待点失败 ≥2 次 | 2 | + * | 静默 | 动作类 6s / 剧情类 18s 无操作 | 1 | + * + * 权重累计 ≥2 触发接管,15s 冷却期内不再触发。三级接管由当前等待点实现 + * (onConfused(level)):1 提示(_short 句)→ 2 缩小(只保留一个正确手势)→ + * 3 演示(游戏自己做一遍再交回)。同一等待点每触发一次升一级;第 3 级用过后 + * 该等待点停止接管,只保留等待声。 + */ +const WINDOW_MS = 10000; +const COOLDOWN_MS = 15000; +const SCORE_TRIGGER = 2; +const MAX_LEVEL = 3; + +export class Confusion { + /** + * @param {object} opts + * @param {(level: number) => void} opts.onConfused 三级接管回调(由当前等待点实现) + */ + constructor({ onConfused } = {}) { + this.events = []; // { t, kind, dir? } + this.cooldownUntil = 0; + this.onConfused = onConfused || null; + this.level = 0; // 当前等待点已接管到的级别 + this.silenceMs = 18000; // 静默阈值:动作类 6s / 剧情类 18s(游戏设定) + } + + /** 玩家输入(任意手势;dir=手势名,乱滑检测用)。 */ + record(g) { this._push({ kind: 'input', dir: g }); } + /** 无效操作结果(pipeline 在游戏返回 invalid 时喂入)。 */ + recordInvalid() { this._push({ kind: 'invalid' }); } + /** 玩家主动重听。 */ + recordReplay() { this._push({ kind: 'replay' }); } + /** 同一步骤失败(游戏判定:答错/走错,喂入)。 */ + recordFail() { this._push({ kind: 'fail' }); } + /** 静默(guide.watchdog 到点回调喂入;权重 1)。 */ + recordSilence() { this._push({ kind: 'silence' }); } + + /** 换等待点:清事件、级别归零。 */ + resetForWaitPoint() { + this.events = []; + this.level = 0; + } + + /** 当前级别(等待点第 3 级用过后不再升)。 */ + get exhausted() { return this.level >= MAX_LEVEL; } + + _push(e) { + const now = performance.now(); + this.events.push({ t: now, ...e }); + this.events = this.events.filter((x) => now - x.t < WINDOW_MS); + if (now < this.cooldownUntil) return; + if (this._score(now) >= SCORE_TRIGGER && this.level < MAX_LEVEL) { + this.level++; + this.cooldownUntil = now + COOLDOWN_MS; + if (this.onConfused) this.onConfused(this.level); + } + } + + _score(now) { + const n = (k) => this.events.filter((x) => x.kind === k).length; + const recentDirs = new Set(this.events + .filter((x) => x.kind === 'input' && now - x.t < 2000 && x.dir && x.dir !== 'tap') + .map((x) => x.dir)); + let s = 0; + if (n('invalid') >= 3) s += 2; + if (n('replay') >= 2) s += 1; + if (recentDirs.size >= 3) s += 1; + if (n('fail') >= 2) s += 2; + if (n('silence') >= 1 && this._onlySilenceRecently(now)) s += 1; + return s; + } + + /** 静默只在「窗口内除静默外几乎没有输入」时计分(有输入就不算静默)。 */ + _onlySilenceRecently(now) { + const inputs = this.events.filter((x) => x.kind === 'input' && now - x.t < WINDOW_MS); + return inputs.length === 0; + } +} + +export default Confusion; diff --git a/web/engine/floor.js b/web/engine/floor.js new file mode 100644 index 00000000..346e0cdf --- /dev/null +++ b/web/engine/floor.js @@ -0,0 +1,339 @@ +/** + * floor.js — 发言权管理器(26 号文档 §4.1,P1 第 5 项)。 + * + * 全局只维护一个「现在谁在说话」。四种状态: + * | 状态 | 含义 | 听感 | 玩家操作时 | + * |-----------|--------------------------|----------------------------------|--------------------------------| + * | SPEAKING | 游戏在说一段话(整条链) | 音乐压低;句间不恢复、不起等待声 | tap=跳过这句;滑动=打断并执行;critical=busy 回执 | + * | YOUR_TURN | 在等玩家操作 | 交接提示音 → 持续等待声 | 等待声 80ms 内淡出,交给游戏 | + * | BUSY | 演出动画音效 / 处理中 | 只有内容音效 | busy 回执,不执行 | + * | ACTION | 实时玩法(节奏/答题) | 玩法本身的声音就是状态 | 直接交给游戏,不打断任何东西 | + * + * 硬规则(§4.1 转换规则 + §4.6 语音优先级): + * - 离开 SPEAKING 只能去 YOUR_TURN(yieldTurn)或 BUSY(继续演出),不存在安静态。 + * - 进入 YOUR_TURN 必须经过 yieldTurn(waitPoint):交接提示音 → 400ms 后起等待声 → + * 挂提示计时 + 迷茫检测(waitPoint.arm())。 + * - 语音链音乐压低跨整条链(audio.holdDuck)。 + * - 类别:critical(不可打断,须 <3s)/ content(可打断)/ hint / feedback + * (只在自己空闲时播,播不出去就丢——绝不排到长链后面迟到)。 + * - 单句资产缺失看门狗:引擎对缺失资产不回调 onComplete,Floor 轮询 + * isVoicePlaying(),确认未在播才推进链(对齐 storyline 旧 VOICE_WATCHDOG_MS 语义, + * 真在播的长句续约不掐)。 + * + * 等待声(运行时合成,零资产):三角波 220Hz+330Hz 两声「嘟—嘟」,每声 60ms、 + * 间隔 180ms、每 1.4s 重复;增益 0.03。等待超 20s 间隔拉长到 2.5s(耐心感)。 + * 交接提示音与 guide.js 的 turn 同规格(1318.5Hz 风铃 + 高频泛音)——它是状态信号 + * 不是帮助内容,guide=off 也照播(原则 1/2)。 + */ + +/** 等待声参数(§4.1「等待声怎么做」起步值)。 */ +const BED = { + f1: 220, f2: 330, // 两声「嘟—嘟」 + toneMs: 60, gapMs: 180, // 每声 60ms,两声间隔 180ms + loopMs: 1400, slowMs: 2500,// 默认每 1.4s 重复;20s 后 2.5s + gain: 0.03, // ≈ guide 提示音增益 0.12 的四分之一 + slowAfterMs: 20000, + fadeOutMs: 80, // 玩家一操作,等待声 80ms 内淡出 +}; + +/** 交接提示音(turn 风铃同规格)。 */ +const TURN_SPEC = [ + { f: 1318.5, d: 0.28, wave: 'sine', g: 0.12 }, + { f: 2637.0, d: 0.12, wave: 'sine', g: 0.03 }, +]; + +/** 单句资产缺失看门狗轮询间隔。 */ +const SENTINEL_POLL_MS = 500; +/** play() 发起后允许的网络+解码宽限(本地资产 <100ms;超时未见开播=缺失)。 */ +const SENTINEL_GRACE_MS = 3000; + +export class Floor { + /** + * @param {import('./audio.js').WebAudioEngine} audio + * @param {object} [opts] + * @param {boolean} [opts.turnEarcon=true] yieldTurn 时播交接提示音(测试可关) + * @param {boolean} [opts.bed=true] 等待声(虚拟时钟测试关掉,避免周期定时器不排水) + */ + constructor(audio, opts = {}) { + this.audio = audio; + this.state = 'BUSY'; + this.turnEarcon = opts.turnEarcon !== false; + this.bedEnabled = opts.bed !== false; + + this._chain = null; // { ids, i, critical, kind, onDone, token } + this._token = 0; + this._bed = null; // { timer, nodes, startedAt } + this._waitPoint = null; // 当前等待点(yieldTurn 挂上,输入/完成时摘下) + } + + // ------------------------------------------------ 对外 API + + /** + * 播一段话(整条语音链)。已在播的链被顶掉(打断即作废,不触发其 onDone)。 + * @param {string[]} ids voice id 列表('voice/xxx') + * @param {object} [opts] + * @param {boolean} [opts.critical] 关键句:玩家操作给 busy 回执,不打断(须 <3s) + * @param {'content'|'hint'|'feedback'} [opts.kind='content'] 类别(§4.6): + * hint/feedback 只在空闲时播,SPEAKING 中直接丢弃(不排队迟到) + * @param {() => void} [opts.onDone] 整条链自然播完(不含被打断)后回调; + * 由 onDone 决定 yieldTurn 还是继续演出 + */ + speak(ids, opts = {}) { + const critical = !!opts.critical; + const kind = opts.kind || 'content'; + if (kind === 'hint' || kind === 'feedback') { + // 过期即弃(§4.6):正在说话时不播、不排队 + if (this.state === 'SPEAKING') return false; + } + if (!ids || !ids.length) { + if (opts.onDone) opts.onDone(); + return true; + } + this._stopBed(); + this._killChain(); + this.state = 'SPEAKING'; + const token = ++this._token; + this._chain = { ids, i: 0, critical, kind, onDone: opts.onDone, token }; + this.audio.holdDuck(true); // 压低跨整条链 + this._next(token); + return true; + } + + /** + * 交出话轮:交接提示音 → 400ms 后起等待声 → 挂等待点(提示计时+迷茫检测)。 + * @param {import('./hints.js').WaitPoint|null} [waitPoint] + */ + yieldTurn(waitPoint = null) { + this._stopBed(); + if (this._waitPoint && this._waitPoint !== waitPoint) this._waitPoint.complete(); + this._waitPoint = waitPoint; + this.state = 'YOUR_TURN'; + if (this.turnEarcon) this._playTurnEarcon(); + const token = ++this._token; // 防竞争:400ms 内又 speak 则不起等待声 + setTimeout(() => { + if (token !== this._token || this.state !== 'YOUR_TURN') return; + this._startBed(); + }, 400); + if (waitPoint) waitPoint.arm(); + } + + /** 进入实时玩法(节奏/答题/探索):玩法声音即状态,输入直接给游戏。 */ + enterAction() { + this._stopBed(); + if (this._waitPoint) { this._waitPoint.complete(); this._waitPoint = null; } // 清掉残留提示计时 + this.state = 'ACTION'; + } + + /** 离开实时玩法回 BUSY(游戏演出/播报前调用)。 */ + leaveAction() { + if (this.state === 'ACTION') this.state = 'BUSY'; + } + + /** + * 提示句播完后回到等待态(等待点内部用):不重播交接提示音、不重挂等待点, + * 只恢复 YOUR_TURN + 等待声——档位计数在等待点里保留(原则 9)。 + */ + resumeTurn() { + if (this.state === 'SPEAKING') return; + this.state = 'YOUR_TURN'; + this._startBed(); + } + + /** + * 输入先经过这里(pipeline 调用)。返回: + * 'consumed' = 发言权层已处理(tap 跳句),游戏不必再处理; + * 'busy' = 忽略这次操作,给 busy 回执; + * null = 交给游戏处理。 + * + * 打断语义(§4.1 裁定,写进手势表,所有游戏一致): + * - 单击:只打断 = 跳过当前这句(链继续走下一句); + * - 滑动/上滑:打断并执行(滑动是有明确意图的操作)。 + */ + handleInput(g) { + if (this.state === 'SPEAKING') { + if (this._chain && this._chain.critical) return 'busy'; + if (g === 'tap') { + this.audio.skipVoice(); // 视为这句自然播完:onComplete → 链继续 + return 'consumed'; + } + this._killChain(); // 打断并执行:作废旧链,这次操作照常交给游戏 + this.state = 'BUSY'; // 处理中;游戏决定接下来 yieldTurn / 继续演出 / 回 ACTION + return null; + } + if (this.state === 'BUSY') return 'busy'; + if (this.state === 'YOUR_TURN') { + this._stopBed(); + if (this._waitPoint) this._waitPoint.rearm(); // 计时复位;档位计数保留(原则 9) + } + return null; + } + + /** + * 等待点结算。done=true(有效操作完成这个点):摘下等待点(含迷茫计数清零); + * done=false(无效操作/未完成):等待点保留(玩家还在这个点),只重排静默计时 + * ——档位计数保留(原则 9)。 + */ + settleWaitPoint(done = true) { + const wp = this._waitPoint; + if (!wp) return; + if (done) { + this._waitPoint = null; + wp.complete(); + return; + } + wp.rearm(); + // 打断解释句后(BUSY)也要回到等待态,等待声重新响起 + if (this.state !== 'SPEAKING' && this.state !== 'ACTION') this.resumeTurn(); + } + + /** 当前等待点(供游戏查询限制手势等)。 */ + get waitPoint() { return this._waitPoint; } + + /** 当前链是否在播(含句间)。 */ + get speaking() { return this.state === 'SPEAKING'; } + + /** 当前正在播的句 id(状态播报用)。 */ + currentId() { + return this._chain ? this._chain.ids[this._chain.i] : null; + } + + /** 交接提示音(公开:三档提示的第 3 档只播它,§4.4)。 */ + chime() { this._playTurnEarcon(); } + + /** 彻底打断:杀链 + 停等待声 + 回 BUSY(重开/进菜单等场景,游戏层调用)。 */ + interrupt() { + this._killChain(); + this._stopBed(); + if (this._waitPoint) { this._waitPoint.complete(); this._waitPoint = null; } + this.state = 'BUSY'; + } + + // ------------------------------------------------ 内部实现 + + _killChain() { + const c = this._chain; + this._chain = null; + if (!c) return; + this._token++; // 作废后续推进 + this.audio.holdDuck(false); // 释放链级压低 + this.audio.stopVoice(); // 打断即作废:不触发 onComplete + } + + _next(token) { + const c = this._chain; + if (!c || c.token !== token) return; // 链已被打断/顶掉,作废 + if (c.i >= c.ids.length) { // 整条链播完 + this._chain = null; + this.audio.holdDuck(false); + this.state = 'BUSY'; // 由 onDone 决定 yieldTurn 还是继续演出 + if (c.onDone) c.onDone(); + return; + } + const id = c.ids[c.i]; + let sawPlaying = false; + const poll = () => { + if (!this._chain || this._chain.token !== token) return; + if (this.audio.isVoicePlaying()) { sawPlaying = true; t = setTimeout(poll, SENTINEL_POLL_MS); return; } + if (!sawPlaying && performance.now() - t0 < SENTINEL_GRACE_MS) { t = setTimeout(poll, SENTINEL_POLL_MS); return; } + // 资产缺失(引擎静默跳过、永不回调):告警续链不卡死;真在播的长句不会到这 + console.warn('[Floor] 语音资产缺失,跳过续链:', id); + this._advance(token); + }; + const t0 = performance.now(); + let t = setTimeout(poll, SENTINEL_POLL_MS); + this.audio.play(id, { + onComplete: () => { + clearTimeout(t); + if (!this._chain || this._chain.token !== token) return; + this._advance(token); + }, + }); + } + + _advance(token) { + const c = this._chain; + if (!c || c.token !== token) return; + c.i++; + this._next(token); + } + + /** 交接提示音:风铃质感(guide turn 同规格),guide 设置无关。 */ + _playTurnEarcon() { + const ctx = this.audio && this.audio.ctx; + if (!ctx || !this.audio._master || !this.audio.unlocked) return; + const t0 = ctx.currentTime + 0.01; + for (const n of TURN_SPEC) { + const osc = ctx.createOscillator(); + osc.type = n.wave; + osc.frequency.value = n.f; + const g = ctx.createGain(); + g.gain.setValueAtTime(0.0001, t0); + g.gain.linearRampToValueAtTime(n.g, t0 + 0.008); + g.gain.linearRampToValueAtTime(n.g * 0.55, t0 + n.d * 0.4); + g.gain.linearRampToValueAtTime(0.0001, t0 + n.d); + osc.connect(g); + g.connect(this.audio._master); + osc.start(t0); + osc.stop(t0 + n.d + 0.02); + } + } + + /** 等待声:两声「嘟—嘟」循环。 */ + _startBed() { + if (!this.bedEnabled || this._bed) return; + const startedAt = performance.now(); + const bed = { timer: null, nodes: null, startedAt }; + this._bed = bed; + const cycle = () => { + if (this._bed !== bed) return; + bed.nodes = this._beep(); + const waited = performance.now() - startedAt; + const interval = waited > BED.slowAfterMs ? BED.slowMs : BED.loopMs; + bed.timer = setTimeout(cycle, interval); + }; + cycle(); + } + + _beep() { + const ctx = this.audio && this.audio.ctx; + if (!ctx || !this.audio._master || !this.audio.unlocked) return null; + const mk = (delay, f) => { + const t0 = ctx.currentTime + 0.01 + delay; + const osc = ctx.createOscillator(); + osc.type = 'triangle'; + osc.frequency.value = f; + const g = ctx.createGain(); + g.gain.setValueAtTime(0.0001, t0); + g.gain.linearRampToValueAtTime(BED.gain, t0 + 0.01); + g.gain.setValueAtTime(BED.gain, t0 + BED.toneMs / 1000 - 0.012); + g.gain.linearRampToValueAtTime(0.0001, t0 + BED.toneMs / 1000); + osc.connect(g); + g.connect(this.audio._master); + osc.start(t0); + osc.stop(t0 + BED.toneMs / 1000 + 0.02); + return g; + }; + return [mk(0, BED.f1), mk((BED.toneMs + BED.gapMs) / 1000, BED.f2)]; + } + + _stopBed() { + const bed = this._bed; + this._bed = null; + if (!bed) return; + if (bed.timer !== null) clearTimeout(bed.timer); + if (bed.nodes) { + const ctx = this.audio && this.audio.ctx; + for (const g of bed.nodes) { + if (!ctx) continue; + try { + const t = ctx.currentTime; + g.gain.cancelScheduledValues(t); + g.gain.setValueAtTime(g.gain.value, t); + g.gain.linearRampToValueAtTime(0.0001, t + BED.fadeOutMs / 1000); + } catch (_) { /* 已停 */ } + } + } + } +} + +export default Floor; diff --git a/web/engine/hints.js b/web/engine/hints.js new file mode 100644 index 00000000..5e717891 --- /dev/null +++ b/web/engine/hints.js @@ -0,0 +1,201 @@ +/** + * hints.js — 等待点与三档提示(26 号文档 §4.4 / 附录 A,P1 第 6 项)+ 无效操作 + * 解释一次(原则 7)。 + * + * 等待点 = 需要玩家操作的时刻。配置(附录 A 模板): + * { id, expected: ['left','right'], // 期待的手势(迷茫第 2 级只保留它们) + * full: 'voice/xxx_full', // 完整提示 + * short: 'voice/xxx_short', // 短句(≤6 字,只保留动作) + * silenceMs: [6000, 16000, 30000], // 静默分级阈值(同时喂迷茫检测的静默信号) + * restrictLine: 'voice/xxx_only', // 迷茫第 2 级说明句(缺省用 short) + * onDemo: () => {} } // 迷茫第 3 级演示(游戏自己完成这一步再交回) + * + * 三档选择(§4.4): + * 第 n 次被动提示:0 → full;1 → short;≥2 → 只给交接提示音(earcon)。 + * 游戏主动给的提示永远不原样重复(原则 9)。 + * 玩家主动要求重听(requestReplay):永远给完整句,前缀「再说一遍:」 + * (voice/sys_say_again,全项目一句、每游戏各存一份资产)。 + * + * 计数作用范围:按「这一个等待点」算,离开这个点(complete / 重新 arm)就清零; + * 玩家做了有效操作但没完成目标(rearm),计数不清零,下次提示接着变短。 + */ + +/** 提示台账:id → 最近一次播出时刻(模块级,跨等待点共享)。 */ +const LEDGER_WINDOW_MS = 30000; +const hintLedger = new Map(); +function ledgerSay(id) { hintLedger.set(id, performance.now()); } +function ledgerSaidRecently(id) { + const t = hintLedger.get(id); + return t !== undefined && performance.now() - t < LEDGER_WINDOW_MS; +} +/** 台账(导出):游戏层接管提示等自发句子复用同一去重规则(原则 9)。 */ +export { ledgerSay as markHintSaid, ledgerSaidRecently as hintSaidRecently }; + +export class WaitPoint { + /** + * @param {object} conf 附录 A 配置 + * @param {object} ctx { floor, confusion } + */ + constructor(conf, ctx) { + this.conf = conf; + this.floor = ctx.floor; + this.confusion = ctx.confusion; + this.hintCount = 0; // 本等待点已给的被动提示次数(决定档位) + this._timers = []; + this._armedAt = 0; + this._restricted = false; + } + + /** 挂上(floor.yieldTurn 调用):清迷茫计数 + 按阈值排静默提示(阈值绝对)。 */ + arm() { + this._clearTimers(); + this.hintCount = 0; + this._armedAt = performance.now(); + if (this.confusion) this.confusion.resetForWaitPoint(); + this._scheduleTiers(); + } + + /** 玩家动了但没完成这个点:静默计时复位,档位计数保留。 */ + rearm() { + this._clearTimers(); + this._scheduleTiers(); + } + + _scheduleTiers() { + const th = this.conf.silenceMs || [18000, 40000]; + for (let k = 0; k < th.length; k++) { + this._timers.push(setTimeout(() => this._fireSilence(k), th[k])); + } + this._armTailSilence(th.length ? th[th.length - 1] : 0); + } + + /** 这个等待点完成:清计时、清迷茫计数。 */ + complete() { + this._clearTimers(); + this.hintCount = 0; + this._restricted = false; + if (this.confusion) this.confusion.resetForWaitPoint(); + } + + /** 玩家主动重听:完整句 + 前缀,不重置档位计数(原则 9)。 */ + requestReplay() { + if (this.confusion) this.confusion.recordReplay(); + const full = this.conf.full; + if (!full) return; + ledgerSay(full); // 刚主动听过,30s 内被动提示自动降档 + this.floor.speak(['voice/sys_say_again', full], { + kind: 'hint', + onDone: () => { if (this.floor.waitPoint === this) this.floor.resumeTurn(); }, + }); + } + + /** 迷茫第 2 级:只保留 expected 手势,其他手势只给 invalid 回执。 */ + get restricted() { return this._restricted; } + + /** 手势是否被本等待点放行(未受限时永远 true)。 */ + allows(g) { + if (!this._restricted) return true; + return (this.conf.expected || []).includes(g); + } + + /** 静默到点:喂迷茫静默信号 + 给第 hintCount 档提示。 */ + _fireSilence() { + if (this.confusion) this.confusion.recordSilence(); + this._givePassiveHint(); + } + + /** + * 阈值用尽后的长间隔静默汇报(只喂迷茫信号,不再升提示档)。 + * 最多 4 次(≈2 分钟):等待声仍在标记状态(原则 1),接管三级也用尽后 + * 不再无限催促;有界也让虚拟时钟能排干定时器。 + */ + _armTailSilence(afterMs) { + let left = 4; + const tick = () => { + this._timers = this._timers.filter((t) => t !== id); + if (this.confusion) this.confusion.recordSilence(); + if (--left <= 0) return; + id = setTimeout(tick, 30000); + this._timers.push(id); + }; + let id = setTimeout(tick, afterMs + 30000); + this._timers.push(id); + } + + /** + * 第 hintCount 次被动提示:full → short → earcon(full 缺省时第 0 档就是短句)。 + * 台账跨等待点:30s 内别的等待点刚说过同一句 → 直接降档(被动原句重复 = 0)。 + */ + _givePassiveHint() { + this.hintCount++; + const resume = { kind: 'hint', onDone: () => { if (this.floor.waitPoint === this) this.floor.resumeTurn(); } }; + const use = (id) => { ledgerSay(id); this.floor.speak([id], resume); }; + if (this.conf.full && !ledgerSaidRecently(this.conf.full)) { + use(this.conf.full); + } else if (this.conf.short && !ledgerSaidRecently(this.conf.short)) { + use(this.conf.short); + } else { + this.floor.chime(); // 只给交接提示音 + } + } + + /** 三级接管(confusion.onConfused 接到这里)。 */ + takeover(level) { + const resume = { kind: 'hint', onDone: () => { if (this.floor.waitPoint === this) this.floor.resumeTurn(); } }; + if (level === 1) { + // 只说下一步的一个动作,用 _short 句(30s 内说过就只给提示音) + const s = this.conf.short || this.conf.full; + if (s && !ledgerSaidRecently(s)) { ledgerSay(s); this.floor.speak([s], resume); } + else this.floor.chime(); + } else if (level === 2) { + this._restricted = true; + const line = this.conf.restrictLine || this.conf.short || this.conf.full; + if (line) this.floor.speak([line], resume); + } else if (level === 3 && this.conf.onDemo) { + this._restricted = false; + this.conf.onDemo(); + } + } + + _clearTimers() { + for (const t of this._timers) clearTimeout(t); + this._timers.length = 0; + } +} + +/** + * 无效操作解释一次(原则 7):第一次无效操作说一句原因和正确做法; + * 之后只给回执声。解释句按「状态 × 手势」为键,每键只播一次(同局)。 + */ +export class InvalidHints { + constructor() { + this._said = new Set(); + } + + /** + * @param {string} key 状态×手势 键(游戏提供,如 'story:swipe') + * @param {string|null} voiceId 解释句 id;null = 该格没有解释句,只给回执 + * @returns {boolean} 是否真的播了 + */ + explainOnce(key, voiceId) { + if (!voiceId) return false; + if (this._said.has(key)) return false; + if (ledgerSaidRecently(voiceId)) { this._said.add(key); return false; } // 同句音频 30s 内不重播 + this._said.add(key); + ledgerSay(voiceId); + // 解释句走 idle-only 反馈通道(§4.6):正在说话时丢弃;播完回到等待态 + // (无效操作不推进游戏,话轮仍属玩家) + this._resolveFloor?.speak([voiceId], { + kind: 'feedback', + onDone: () => { if (this._resolveFloor && this._resolveFloor.waitPoint) this._resolveFloor.resumeTurn(); }, + }); + return true; + } + + /** 注入 floor(延迟绑定:构造时游戏可能还没建 floor)。 */ + bind(floor) { this._resolveFloor = floor; return this; } + + reset() { this._said.clear(); } +} + +export { WaitPoint as default }; diff --git a/web/engine/novice.js b/web/engine/novice.js new file mode 100644 index 00000000..0e8cc770 --- /dev/null +++ b/web/engine/novice.js @@ -0,0 +1,97 @@ +/** + * novice.js — 新手手势模式与解锁阶梯(26 号文档 §4.3,P0 第 1 项 + P3 第 11 项)。 + * + * 新手手势表(全部游戏共用,原则 6): + * | 手势 | 唯一含义 | 新手期(阶段 0) | + * |------------|------------------------|---------------------------------| + * | tap | 继续 / 确定 / 听一下 | 开放 | + * | left/right | 选择 / 移动 | 开放 | + * | up | 我在哪、我该干嘛 | 阶段 1 解锁(完成第一个小目标) | + * | down | 菜单 | 阶段 2 解锁(完成第一局) | + * | doubletap | 重听 | 阶段 3 解锁(累计 3 局);未解锁按单击处理 | + * | longpress | 帮助(四向玩法类游戏) | 仅「滑动是玩法」的游戏开放(§4.3 裁定) | + * + * 解锁进度跨局持久(localStorage),全局共享(手势表全项目一致,学会一次就算会)。 + * 游戏在对应时机调 onGoal() / onGameDone(),返回新解锁的手势(null=无新解锁), + * 随后按原则 11 教学:先说一句(teachLine),马上用一次。 + */ +const STORAGE_KEY = 'soundgame.gestures'; + +/** 解锁教学句 id(每游戏目录各存一份同文案资产,§4.3 阶梯表)。 */ +const TEACH_IDS = { + up: 'sys_unlock_up', // 「想知道自己在哪,就向上滑。」 + down: 'sys_unlock_down', // 「向下滑,可以打开菜单。」 + doubletap: 'sys_unlock_dt', // 「没听清,就双击,再听一遍。」 +}; + +const UNLOCK_AT = { 1: 'up', 2: 'down', 3: 'doubletap' }; + +export class GestureLadder { + constructor(key = STORAGE_KEY) { + this.key = key; + } + + _load() { + try { + const raw = localStorage.getItem(this.key); + if (!raw) return { stage: 0, games: 0 }; + const o = JSON.parse(raw); + return { + stage: Math.max(0, Math.min(3, Number(o.stage) || 0)), + games: Math.max(0, Number(o.games) || 0), + }; + } catch (_) { + return { stage: 0, games: 0 }; + } + } + + _save(o) { + try { localStorage.setItem(this.key, JSON.stringify(o)); } catch (_) { /* 隐私模式 */ } + } + + /** 当前阶段 0..3。 */ + stage() { return this._load().stage; } + + /** 手势是否已解锁(tap/left/right 永远开放;longpress 由游戏按玩法裁定)。 */ + has(g) { + if (g === 'tap' || g === 'left' || g === 'right') return true; + if (g === 'up') return this.stage() >= 1; + if (g === 'down') return this.stage() >= 2; + if (g === 'doubletap') return this.stage() >= 3; + return false; // longpress 等:走游戏自己的核心玩法裁定 + } + + /** 完成第一个小目标 → 阶段 1。返回新解锁手势或 null。 */ + onGoal() { return this._unlock(1); } + + /** 完成一局 → games+1;第 1 局解锁阶段 2,第 3 局解锁阶段 3。 */ + onGameDone() { + const o = this._load(); + o.games += 1; + this._save(o); + let got = null; + if (o.games >= 1) got = this._unlock(2) || got; + if (o.games >= 3) got = this._unlock(3) || got; + return got; + } + + _unlock(stage) { + const o = this._load(); + if (o.stage >= stage) return null; + o.stage = stage; + this._save(o); + return UNLOCK_AT[stage]; + } + + /** 老玩家在大厅一键全部解锁。 */ + unlockAll() { + const o = this._load(); + o.stage = 3; + this._save(o); + } + + /** 解锁教学句 voice id。 */ + teachLine(g) { return TEACH_IDS[g] || null; } +} + +export default GestureLadder; diff --git a/web/engine/pipeline.js b/web/engine/pipeline.js new file mode 100644 index 00000000..753c4927 --- /dev/null +++ b/web/engine/pipeline.js @@ -0,0 +1,124 @@ +/** + * pipeline.js — 输入管道(26 号文档 §4.2 接线方式 + §4.3 新手手势模式 + 防连点)。 + * + * 每款改造游戏的 attachGestures 回调都换成这个管道(P0 第 1/2/3/4 项一次落地): + * + * const pipe = makeInputPipeline({ floor, confusion, receipts, invalidHints, + * game, ladder }); + * attachGestures(root, pipe, { doubleTapWindow: 0 }); // 关掉双击 + 去掉 300ms 延迟 + * + * 管道顺序: + * 1. 新手手势门(§4.3):未解锁 doubletap 按一次 tap 处理(键盘回车发的 + * doubletap 同样折算);未解锁 down / 非玩法 longpress → invalid 回执 + + * 首次附一句解释(原则 7)。 + * 2. confusion.record(g)(§4.5 乱点信号)。 + * 3. floor.handleInput(g)(§4.1):SPEAKING 中 tap=跳句(consumed)、滑动=打断并 + * 执行、critical=busy;BUSY=busy 回执;YOUR_TURN 停等待声。 + * 4. 防连点(§4.2):tap 刚执行过「继续/跳过」后的 400ms 内,再来的 tap 只给 + * busy 回执(点两下不连跳两句)。ACTION 态(答题)不设防。 + * 5. 等待点第 2 级限制(§4.5 缩小):只放行 expected 手势,其余 invalid 回执。 + * 6. game.onGesture(g) 返回值收口: + * 'ok' 接受(完成当前等待点) + * 'hold' 接受但等待点继续等(如抉择点单击=再听选项) + * 'silent-ok' 接受且已有明显内容音(省 ok 回执,§4.2) + * 'invalid' 拒绝(invalid 回执 + explainOnce + 迷茫信号;等待点计时复位) + */ +import { GestureLadder } from './novice.js'; + +/** 防连点窗口:tap 执行「继续/跳过」后 400ms 内的 tap 只给 busy。 */ +const TAP_GUARD_MS = 400; + +/** 管道要喂给迷茫检测的手势(收尾/撤销类噪声不计)。 */ +const REAL_GESTURES = new Set(['tap', 'doubletap', 'up', 'down', 'left', 'right', 'longpress']); + +/** + * @param {object} deps + * @param {import('./floor.js').Floor} floor + * @param {import('./confusion.js').Confusion} confusion + * @param {import('./receipts.js').Receipts} receipts + * @param {import('./hints.js').InvalidHints} invalidHints + * @param {{ onGesture: (g: string, info?: object) => string|void, + * stateName?: () => string, + * invalidHint?: (state: string, g: string) => string|null }} game + * @param {import('./novice.js').GestureLadder} [deps.ladder] 手势解锁阶梯(缺省=全新手) + * @param {object} [deps.opts] + * @param {boolean} [deps.opts.longpressCore=false] 本游戏长按是核心玩法(四向滑动类 + * 游戏的帮助键,§4.3 裁定);false 时 longpress → invalid + * @param {boolean} [deps.opts.swipeCore=false] 四向滑动是本游戏核心玩法(§4.3 裁定): + * up/down 不按阶梯锁(它们是移动,不是帮助/菜单);阶梯只管 doubletap + */ +export function makeInputPipeline({ floor, confusion, receipts, invalidHints, game, ladder, opts = {} }) { + const lad = ladder || new GestureLadder(); + const longpressCore = (opts && opts.longpressCore) === true; + const swipeCore = (opts && opts.swipeCore) === true; + let lastTapActedAt = -Infinity; + + return (g, info) => { + if (g === 'longpressend' || g === 'tapcancel') return; // 手势收尾/撤销噪声,不吃回执 + + // ---- 1. 新手手势门(§4.3)---- + if (g === 'doubletap' && !lad.has('doubletap')) g = 'tap'; // 双击按一次单击处理 + if (!swipeCore && g === 'up' && !lad.has('up')) { + receipts.play('invalid'); + invalidHints.explainOnce('locked:up', game.invalidHint ? game.invalidHint('locked', 'up') : null); + return; + } + if (!swipeCore && g === 'down' && !lad.has('down')) { + receipts.play('invalid'); + invalidHints.explainOnce('locked:down', game.invalidHint ? game.invalidHint('locked', 'down') : null); + return; + } + if (g === 'longpress' && !longpressCore) { + receipts.play('invalid'); + invalidHints.explainOnce('locked:longpress', game.invalidHint ? game.invalidHint('locked', 'longpress') : null); + return; + } + + // ---- 2. 迷茫检测(§4.5)---- + if (REAL_GESTURES.has(g)) confusion.record(g); + + // ---- 3. 防连点(§4.2):tap 刚执行过「继续/跳过」后的 400ms 内,再来的 tap + // 只给 busy 回执(无论它将被解释成跳句还是继续——都不执行)---- + if (g === 'tap' && floor.state !== 'ACTION' && performance.now() - lastTapActedAt < TAP_GUARD_MS) { + receipts.play('busy'); + return; + } + + // ---- 4. 发言权(§4.1)---- + const preState = floor.state; + const pre = floor.handleInput(g); + if (pre === 'busy') { receipts.play('busy'); return; } + if (pre === 'consumed') { + receipts.play('ok'); // tap 跳句 = 一次「继续」,进防连点窗口 + lastTapActedAt = performance.now(); + return; + } + + // ---- 5. 迷茫第 2 级:等待点只放行 expected(§4.5 缩小)---- + const wp = floor.waitPoint; + if (wp && wp.restricted && !wp.allows(g)) { + receipts.play('invalid'); + confusion.recordInvalid(); + return; + } + + // ---- 6. 游戏处理 + 回执收口 ---- + const r = game.onGesture(g, info) || 'ok'; + if (r === 'invalid') { + receipts.play('invalid'); + const state = game.stateName ? game.stateName() : ''; + invalidHints.explainOnce(`${state}:${g}`, game.invalidHint ? game.invalidHint(state, g) : null); + confusion.recordInvalid(); + // 等待点仍在(无效操作没完成它):静默计时复位、回到等待态(含打断后的 BUSY) + if (floor.waitPoint && !floor.speaking) floor.settleWaitPoint(false); + return; + } + if (g === 'tap' && r !== 'hold' && preState !== 'ACTION') { + lastTapActedAt = performance.now(); // tap「继续」进防连点窗口 + } + if (r !== 'silent-ok') receipts.play('ok'); + if (floor.state === 'YOUR_TURN') floor.settleWaitPoint(r !== 'hold'); + }; +} + +export default makeInputPipeline; diff --git a/web/engine/receipts.js b/web/engine/receipts.js new file mode 100644 index 00000000..86dd2465 --- /dev/null +++ b/web/engine/receipts.js @@ -0,0 +1,61 @@ +/** + * receipts.js — 输入回执三件套(26 号文档 §4.2,P0 第 2 项)。 + * + * 每次玩家操作 100ms 内出声,告知「这下算没算」。三种回执(合成参数为文档起步值): + * | kind | 用在哪里 | 听感 | + * |----------|------------------------|----------------------| + * | ok | 操作被接受并生效 | 清脆的「嗒」 | + * | invalid | 收到了,但现在用不上 | 低沉柔和的「咚」 | + * | busy | 关键句在说 / 正在处理 | 很轻的「嗯」 | + * + * 规则(文档 §4.2 / 原则 4): + * - 回执由引擎统一播,游戏只告诉管道这次操作的结果是哪一种(见 pipeline.js)。 + * - 游戏已为这次操作播了明显内容音(脚步、开门)时可以省 ok,invalid/busy 永远要播。 + * - 音色避开 guide.js 的 turn(1318Hz 风铃)/ nudge(880Hz 木鱼);系统音一律合成音 + * (原则 10),接 master 总线吃限幅、不被 BGM duck 压低(§4.11 优先级 1)。 + * - 任何设备都出声,不依赖震动;震动由 gesture/haptics 照常叠加。 + */ + +/** 合成参数(26 号文档 §4.2 表:合成参数(起步值))。 */ +const SPECS = { + // 三角波 1600Hz,35ms,起音 2ms,快速衰减,增益 0.08 —— 清脆的「嗒」 + ok: { wave: 'triangle', from: 1600, to: 1600, dur: 0.035, attack: 0.002, gain: 0.08 }, + // 正弦波 260Hz 滑到 200Hz,120ms,起音 10ms,增益 0.10 —— 低沉柔和的「咚」 + invalid: { wave: 'sine', from: 260, to: 200, dur: 0.12, attack: 0.01, gain: 0.10 }, + // 正弦波 520Hz,40ms,增益 0.04 —— 很轻的「嗯」 + busy: { wave: 'sine', from: 520, to: 520, dur: 0.04, attack: 0.005, gain: 0.04 }, +}; + +export class Receipts { + /** @param {import('./audio.js').WebAudioEngine} audio */ + constructor(audio) { + this.audio = audio; + } + + /** + * 播一种回执。未解锁 / 无 ctx(playtest mock)时静默跳过。 + * @param {'ok'|'invalid'|'busy'} kind + */ + play(kind) { + const spec = SPECS[kind]; + const ctx = this.audio && this.audio.ctx; + if (!spec || !ctx || !this.audio._master || !this.audio.unlocked) return; + const t0 = ctx.currentTime + 0.005; + const osc = ctx.createOscillator(); + osc.type = spec.wave; + osc.frequency.setValueAtTime(spec.from, t0); + if (spec.to !== spec.from) osc.frequency.linearRampToValueAtTime(spec.to, t0 + spec.dur); + const g = ctx.createGain(); + g.gain.setValueAtTime(0.0001, t0); + g.gain.linearRampToValueAtTime(spec.gain, t0 + spec.attack); + // 快速衰减:attack 后线性落到接近 0(听感接近指数衰减) + g.gain.linearRampToValueAtTime(spec.gain * 0.35, t0 + spec.attack + (spec.dur - spec.attack) * 0.45); + g.gain.linearRampToValueAtTime(0.0001, t0 + spec.dur); + osc.connect(g); + g.connect(this.audio._master); + osc.start(t0); + osc.stop(t0 + spec.dur + 0.02); + } +} + +export default Receipts; diff --git a/web/escape/audio/voice/female/again.m4a b/web/escape/audio/voice/female/again.m4a index 33284335..acf9c7fd 100644 Binary files a/web/escape/audio/voice/female/again.m4a and b/web/escape/audio/voice/female/again.m4a differ diff --git a/web/escape/audio/voice/female/again.ogg b/web/escape/audio/voice/female/again.ogg index bbc34f60..d53d5658 100644 Binary files a/web/escape/audio/voice/female/again.ogg and b/web/escape/audio/voice/female/again.ogg differ diff --git a/web/escape/audio/voice/female/box_fixed.m4a b/web/escape/audio/voice/female/box_fixed.m4a index be72faa8..df5af806 100644 Binary files a/web/escape/audio/voice/female/box_fixed.m4a and b/web/escape/audio/voice/female/box_fixed.m4a differ diff --git a/web/escape/audio/voice/female/box_fixed.ogg b/web/escape/audio/voice/female/box_fixed.ogg index ccf115cf..cc3dad9d 100644 Binary files a/web/escape/audio/voice/female/box_fixed.ogg and b/web/escape/audio/voice/female/box_fixed.ogg differ diff --git a/web/escape/audio/voice/female/empty_1.m4a b/web/escape/audio/voice/female/empty_1.m4a index 8243acd3..3edd737c 100644 Binary files a/web/escape/audio/voice/female/empty_1.m4a and b/web/escape/audio/voice/female/empty_1.m4a differ diff --git a/web/escape/audio/voice/female/empty_1.ogg b/web/escape/audio/voice/female/empty_1.ogg index 9d0f0229..e80a26d8 100644 Binary files a/web/escape/audio/voice/female/empty_1.ogg and b/web/escape/audio/voice/female/empty_1.ogg differ diff --git a/web/escape/audio/voice/female/empty_2.m4a b/web/escape/audio/voice/female/empty_2.m4a index b54b5be8..9b41ece9 100644 Binary files a/web/escape/audio/voice/female/empty_2.m4a and b/web/escape/audio/voice/female/empty_2.m4a differ diff --git a/web/escape/audio/voice/female/empty_2.ogg b/web/escape/audio/voice/female/empty_2.ogg index 3a2b0e76..fc6cbb9c 100644 Binary files a/web/escape/audio/voice/female/empty_2.ogg and b/web/escape/audio/voice/female/empty_2.ogg differ diff --git a/web/escape/audio/voice/female/empty_3.m4a b/web/escape/audio/voice/female/empty_3.m4a index 95732dbd..ce448634 100644 Binary files a/web/escape/audio/voice/female/empty_3.m4a and b/web/escape/audio/voice/female/empty_3.m4a differ diff --git a/web/escape/audio/voice/female/empty_3.ogg b/web/escape/audio/voice/female/empty_3.ogg index ad6ef599..1938116c 100644 Binary files a/web/escape/audio/voice/female/empty_3.ogg and b/web/escape/audio/voice/female/empty_3.ogg differ diff --git a/web/escape/audio/voice/female/escape.m4a b/web/escape/audio/voice/female/escape.m4a index c1a024a8..c808fe3f 100644 Binary files a/web/escape/audio/voice/female/escape.m4a and b/web/escape/audio/voice/female/escape.m4a differ diff --git a/web/escape/audio/voice/female/escape.ogg b/web/escape/audio/voice/female/escape.ogg index 73dd4b47..cead6be0 100644 Binary files a/web/escape/audio/voice/female/escape.ogg and b/web/escape/audio/voice/female/escape.ogg differ diff --git a/web/escape/audio/voice/female/fix_box.m4a b/web/escape/audio/voice/female/fix_box.m4a index 15bdde8d..dfb7d834 100644 Binary files a/web/escape/audio/voice/female/fix_box.m4a and b/web/escape/audio/voice/female/fix_box.m4a differ diff --git a/web/escape/audio/voice/female/fix_box.ogg b/web/escape/audio/voice/female/fix_box.ogg index b6684966..20ee0b52 100644 Binary files a/web/escape/audio/voice/female/fix_box.ogg and b/web/escape/audio/voice/female/fix_box.ogg differ diff --git a/web/escape/audio/voice/female/found_key.m4a b/web/escape/audio/voice/female/found_key.m4a index 937a83ba..34e92846 100644 Binary files a/web/escape/audio/voice/female/found_key.m4a and b/web/escape/audio/voice/female/found_key.m4a differ diff --git a/web/escape/audio/voice/female/found_key.ogg b/web/escape/audio/voice/female/found_key.ogg index be441555..98bf8ae6 100644 Binary files a/web/escape/audio/voice/female/found_key.ogg and b/web/escape/audio/voice/female/found_key.ogg differ diff --git a/web/escape/audio/voice/female/g0_new.m4a b/web/escape/audio/voice/female/g0_new.m4a new file mode 100644 index 00000000..4f1a5bd0 Binary files /dev/null and b/web/escape/audio/voice/female/g0_new.m4a differ diff --git a/web/escape/audio/voice/female/g0_new.ogg b/web/escape/audio/voice/female/g0_new.ogg new file mode 100644 index 00000000..b4fb5e18 Binary files /dev/null and b/web/escape/audio/voice/female/g0_new.ogg differ diff --git a/web/escape/audio/voice/female/g0b.m4a b/web/escape/audio/voice/female/g0b.m4a new file mode 100644 index 00000000..8bfcc066 Binary files /dev/null and b/web/escape/audio/voice/female/g0b.m4a differ diff --git a/web/escape/audio/voice/female/g0b.ogg b/web/escape/audio/voice/female/g0b.ogg new file mode 100644 index 00000000..ac4e7839 Binary files /dev/null and b/web/escape/audio/voice/female/g0b.ogg differ diff --git a/web/escape/audio/voice/female/g3_new.m4a b/web/escape/audio/voice/female/g3_new.m4a new file mode 100644 index 00000000..a9e9992a Binary files /dev/null and b/web/escape/audio/voice/female/g3_new.m4a differ diff --git a/web/escape/audio/voice/female/g3_new.ogg b/web/escape/audio/voice/female/g3_new.ogg new file mode 100644 index 00000000..0f70f071 Binary files /dev/null and b/web/escape/audio/voice/female/g3_new.ogg differ diff --git a/web/escape/audio/voice/female/g4_down.m4a b/web/escape/audio/voice/female/g4_down.m4a index dfc69b9f..537db811 100644 Binary files a/web/escape/audio/voice/female/g4_down.m4a and b/web/escape/audio/voice/female/g4_down.m4a differ diff --git a/web/escape/audio/voice/female/g4_down.ogg b/web/escape/audio/voice/female/g4_down.ogg index 4c6e6df1..eb105d48 100644 Binary files a/web/escape/audio/voice/female/g4_down.ogg and b/web/escape/audio/voice/female/g4_down.ogg differ diff --git a/web/escape/audio/voice/female/g5_inspect.m4a b/web/escape/audio/voice/female/g5_inspect.m4a index 70687990..779d6100 100644 Binary files a/web/escape/audio/voice/female/g5_inspect.m4a and b/web/escape/audio/voice/female/g5_inspect.m4a differ diff --git a/web/escape/audio/voice/female/g5_inspect.ogg b/web/escape/audio/voice/female/g5_inspect.ogg index 9d8762eb..7ffb5599 100644 Binary files a/web/escape/audio/voice/female/g5_inspect.ogg and b/web/escape/audio/voice/female/g5_inspect.ogg differ diff --git a/web/escape/audio/voice/female/g5_key.m4a b/web/escape/audio/voice/female/g5_key.m4a index 3139e489..2d3e14e5 100644 Binary files a/web/escape/audio/voice/female/g5_key.m4a and b/web/escape/audio/voice/female/g5_key.m4a differ diff --git a/web/escape/audio/voice/female/g5_key.ogg b/web/escape/audio/voice/female/g5_key.ogg index 7ed26752..42e0d1ac 100644 Binary files a/web/escape/audio/voice/female/g5_key.ogg and b/web/escape/audio/voice/female/g5_key.ogg differ diff --git a/web/escape/audio/voice/female/g7_goal_new.m4a b/web/escape/audio/voice/female/g7_goal_new.m4a new file mode 100644 index 00000000..99b5c306 Binary files /dev/null and b/web/escape/audio/voice/female/g7_goal_new.m4a differ diff --git a/web/escape/audio/voice/female/g7_goal_new.ogg b/web/escape/audio/voice/female/g7_goal_new.ogg new file mode 100644 index 00000000..708dfb25 Binary files /dev/null and b/web/escape/audio/voice/female/g7_goal_new.ogg differ diff --git a/web/escape/audio/voice/female/g_hint_swipe.m4a b/web/escape/audio/voice/female/g_hint_swipe.m4a new file mode 100644 index 00000000..8c541009 Binary files /dev/null and b/web/escape/audio/voice/female/g_hint_swipe.m4a differ diff --git a/web/escape/audio/voice/female/g_hint_swipe.ogg b/web/escape/audio/voice/female/g_hint_swipe.ogg new file mode 100644 index 00000000..86289ef9 Binary files /dev/null and b/web/escape/audio/voice/female/g_hint_swipe.ogg differ diff --git a/web/escape/audio/voice/female/g_wrong_gesture.m4a b/web/escape/audio/voice/female/g_wrong_gesture.m4a new file mode 100644 index 00000000..2e054413 Binary files /dev/null and b/web/escape/audio/voice/female/g_wrong_gesture.m4a differ diff --git a/web/escape/audio/voice/female/g_wrong_gesture.ogg b/web/escape/audio/voice/female/g_wrong_gesture.ogg new file mode 100644 index 00000000..94eb240b Binary files /dev/null and b/web/escape/audio/voice/female/g_wrong_gesture.ogg differ diff --git a/web/escape/audio/voice/female/g_wrong_tap.m4a b/web/escape/audio/voice/female/g_wrong_tap.m4a new file mode 100644 index 00000000..afa3157c Binary files /dev/null and b/web/escape/audio/voice/female/g_wrong_tap.m4a differ diff --git a/web/escape/audio/voice/female/g_wrong_tap.ogg b/web/escape/audio/voice/female/g_wrong_tap.ogg new file mode 100644 index 00000000..7b46d82f Binary files /dev/null and b/web/escape/audio/voice/female/g_wrong_tap.ogg differ diff --git a/web/escape/audio/voice/female/gd_box_dir.m4a b/web/escape/audio/voice/female/gd_box_dir.m4a index 559a1d7e..e2771179 100644 Binary files a/web/escape/audio/voice/female/gd_box_dir.m4a and b/web/escape/audio/voice/female/gd_box_dir.m4a differ diff --git a/web/escape/audio/voice/female/gd_box_dir.ogg b/web/escape/audio/voice/female/gd_box_dir.ogg index 7579c597..967cd412 100644 Binary files a/web/escape/audio/voice/female/gd_box_dir.ogg and b/web/escape/audio/voice/female/gd_box_dir.ogg differ diff --git a/web/escape/audio/voice/female/gd_cabinet_dir.m4a b/web/escape/audio/voice/female/gd_cabinet_dir.m4a index 6365ee1c..688074da 100644 Binary files a/web/escape/audio/voice/female/gd_cabinet_dir.m4a and b/web/escape/audio/voice/female/gd_cabinet_dir.m4a differ diff --git a/web/escape/audio/voice/female/gd_cabinet_dir.ogg b/web/escape/audio/voice/female/gd_cabinet_dir.ogg index f68da7bd..a2706e19 100644 Binary files a/web/escape/audio/voice/female/gd_cabinet_dir.ogg and b/web/escape/audio/voice/female/gd_cabinet_dir.ogg differ diff --git a/web/escape/audio/voice/female/gd_gate_dir.m4a b/web/escape/audio/voice/female/gd_gate_dir.m4a index 82f3caff..033c4442 100644 Binary files a/web/escape/audio/voice/female/gd_gate_dir.m4a and b/web/escape/audio/voice/female/gd_gate_dir.m4a differ diff --git a/web/escape/audio/voice/female/gd_gate_dir.ogg b/web/escape/audio/voice/female/gd_gate_dir.ogg index 8b094d6c..6cfb5314 100644 Binary files a/web/escape/audio/voice/female/gd_gate_dir.ogg and b/web/escape/audio/voice/female/gd_gate_dir.ogg differ diff --git a/web/escape/audio/voice/female/gd_next_fuse.m4a b/web/escape/audio/voice/female/gd_next_fuse.m4a index 97a2c887..87122fbb 100644 Binary files a/web/escape/audio/voice/female/gd_next_fuse.m4a and b/web/escape/audio/voice/female/gd_next_fuse.m4a differ diff --git a/web/escape/audio/voice/female/gd_next_fuse.ogg b/web/escape/audio/voice/female/gd_next_fuse.ogg index 9db16340..05a962cf 100644 Binary files a/web/escape/audio/voice/female/gd_next_fuse.ogg and b/web/escape/audio/voice/female/gd_next_fuse.ogg differ diff --git a/web/escape/audio/voice/female/gd_room_d.m4a b/web/escape/audio/voice/female/gd_room_d.m4a index 11a38d00..324b05ba 100644 Binary files a/web/escape/audio/voice/female/gd_room_d.m4a and b/web/escape/audio/voice/female/gd_room_d.m4a differ diff --git a/web/escape/audio/voice/female/gd_room_d.ogg b/web/escape/audio/voice/female/gd_room_d.ogg index 899355a8..aae8169f 100644 Binary files a/web/escape/audio/voice/female/gd_room_d.ogg and b/web/escape/audio/voice/female/gd_room_d.ogg differ diff --git a/web/escape/audio/voice/female/gd_room_l.m4a b/web/escape/audio/voice/female/gd_room_l.m4a index 237b3470..5248abf5 100644 Binary files a/web/escape/audio/voice/female/gd_room_l.m4a and b/web/escape/audio/voice/female/gd_room_l.m4a differ diff --git a/web/escape/audio/voice/female/gd_room_l.ogg b/web/escape/audio/voice/female/gd_room_l.ogg index 77fdc087..b373e6f7 100644 Binary files a/web/escape/audio/voice/female/gd_room_l.ogg and b/web/escape/audio/voice/female/gd_room_l.ogg differ diff --git a/web/escape/audio/voice/female/gd_room_r.m4a b/web/escape/audio/voice/female/gd_room_r.m4a index 3cc63c7f..46ae44ae 100644 Binary files a/web/escape/audio/voice/female/gd_room_r.m4a and b/web/escape/audio/voice/female/gd_room_r.m4a differ diff --git a/web/escape/audio/voice/female/gd_room_r.ogg b/web/escape/audio/voice/female/gd_room_r.ogg index d2ef4489..594f791f 100644 Binary files a/web/escape/audio/voice/female/gd_room_r.ogg and b/web/escape/audio/voice/female/gd_room_r.ogg differ diff --git a/web/escape/audio/voice/female/gd_room_u.m4a b/web/escape/audio/voice/female/gd_room_u.m4a index 15d93996..f3280892 100644 Binary files a/web/escape/audio/voice/female/gd_room_u.m4a and b/web/escape/audio/voice/female/gd_room_u.m4a differ diff --git a/web/escape/audio/voice/female/gd_room_u.ogg b/web/escape/audio/voice/female/gd_room_u.ogg index 35a0e6fc..5db898ac 100644 Binary files a/web/escape/audio/voice/female/gd_room_u.ogg and b/web/escape/audio/voice/female/gd_room_u.ogg differ diff --git a/web/escape/audio/voice/female/guide_blunt_swipe.m4a b/web/escape/audio/voice/female/guide_blunt_swipe.m4a new file mode 100644 index 00000000..abc30ad1 Binary files /dev/null and b/web/escape/audio/voice/female/guide_blunt_swipe.m4a differ diff --git a/web/escape/audio/voice/female/guide_blunt_swipe.ogg b/web/escape/audio/voice/female/guide_blunt_swipe.ogg new file mode 100644 index 00000000..58f9b79e Binary files /dev/null and b/web/escape/audio/voice/female/guide_blunt_swipe.ogg differ diff --git a/web/escape/audio/voice/female/guide_wrong_down.m4a b/web/escape/audio/voice/female/guide_wrong_down.m4a index b1da6a5d..da0e109a 100644 Binary files a/web/escape/audio/voice/female/guide_wrong_down.m4a and b/web/escape/audio/voice/female/guide_wrong_down.m4a differ diff --git a/web/escape/audio/voice/female/guide_wrong_down.ogg b/web/escape/audio/voice/female/guide_wrong_down.ogg index 1a525bbe..14a39bb1 100644 Binary files a/web/escape/audio/voice/female/guide_wrong_down.ogg and b/web/escape/audio/voice/female/guide_wrong_down.ogg differ diff --git a/web/escape/audio/voice/female/guide_wrong_left.m4a b/web/escape/audio/voice/female/guide_wrong_left.m4a index c6a5b69f..92912729 100644 Binary files a/web/escape/audio/voice/female/guide_wrong_left.m4a and b/web/escape/audio/voice/female/guide_wrong_left.m4a differ diff --git a/web/escape/audio/voice/female/guide_wrong_left.ogg b/web/escape/audio/voice/female/guide_wrong_left.ogg index 20c076ea..8842e932 100644 Binary files a/web/escape/audio/voice/female/guide_wrong_left.ogg and b/web/escape/audio/voice/female/guide_wrong_left.ogg differ diff --git a/web/escape/audio/voice/female/inspect_box_dead.m4a b/web/escape/audio/voice/female/inspect_box_dead.m4a index dc9aeca7..ce79a1c6 100644 Binary files a/web/escape/audio/voice/female/inspect_box_dead.m4a and b/web/escape/audio/voice/female/inspect_box_dead.m4a differ diff --git a/web/escape/audio/voice/female/inspect_box_dead.ogg b/web/escape/audio/voice/female/inspect_box_dead.ogg index 1b63a368..67e84a2c 100644 Binary files a/web/escape/audio/voice/female/inspect_box_dead.ogg and b/web/escape/audio/voice/female/inspect_box_dead.ogg differ diff --git a/web/escape/audio/voice/female/inspect_cabinet_locked.m4a b/web/escape/audio/voice/female/inspect_cabinet_locked.m4a index d8ecc256..737703a9 100644 Binary files a/web/escape/audio/voice/female/inspect_cabinet_locked.m4a and b/web/escape/audio/voice/female/inspect_cabinet_locked.m4a differ diff --git a/web/escape/audio/voice/female/inspect_cabinet_locked.ogg b/web/escape/audio/voice/female/inspect_cabinet_locked.ogg index ba820158..fcffa4cc 100644 Binary files a/web/escape/audio/voice/female/inspect_cabinet_locked.ogg and b/web/escape/audio/voice/female/inspect_cabinet_locked.ogg differ diff --git a/web/escape/audio/voice/female/inspect_gate.m4a b/web/escape/audio/voice/female/inspect_gate.m4a index 93dc5a80..eb538c00 100644 Binary files a/web/escape/audio/voice/female/inspect_gate.m4a and b/web/escape/audio/voice/female/inspect_gate.m4a differ diff --git a/web/escape/audio/voice/female/inspect_gate.ogg b/web/escape/audio/voice/female/inspect_gate.ogg index 9776ca59..b9ffd299 100644 Binary files a/web/escape/audio/voice/female/inspect_gate.ogg and b/web/escape/audio/voice/female/inspect_gate.ogg differ diff --git a/web/escape/audio/voice/female/inspect_gate_ready.m4a b/web/escape/audio/voice/female/inspect_gate_ready.m4a index b3593386..4b8e1358 100644 Binary files a/web/escape/audio/voice/female/inspect_gate_ready.m4a and b/web/escape/audio/voice/female/inspect_gate_ready.m4a differ diff --git a/web/escape/audio/voice/female/inspect_gate_ready.ogg b/web/escape/audio/voice/female/inspect_gate_ready.ogg index 2050f9c6..cb1719e9 100644 Binary files a/web/escape/audio/voice/female/inspect_gate_ready.ogg and b/web/escape/audio/voice/female/inspect_gate_ready.ogg differ diff --git a/web/escape/audio/voice/female/inv_generic.m4a b/web/escape/audio/voice/female/inv_generic.m4a new file mode 100644 index 00000000..324ec2c0 Binary files /dev/null and b/web/escape/audio/voice/female/inv_generic.m4a differ diff --git a/web/escape/audio/voice/female/inv_generic.ogg b/web/escape/audio/voice/female/inv_generic.ogg new file mode 100644 index 00000000..90c64810 Binary files /dev/null and b/web/escape/audio/voice/female/inv_generic.ogg differ diff --git a/web/escape/audio/voice/female/inv_settle.m4a b/web/escape/audio/voice/female/inv_settle.m4a new file mode 100644 index 00000000..64414b99 Binary files /dev/null and b/web/escape/audio/voice/female/inv_settle.m4a differ diff --git a/web/escape/audio/voice/female/inv_settle.ogg b/web/escape/audio/voice/female/inv_settle.ogg new file mode 100644 index 00000000..fd1a3f9d Binary files /dev/null and b/web/escape/audio/voice/female/inv_settle.ogg differ diff --git a/web/escape/audio/voice/female/key/again.m4a b/web/escape/audio/voice/female/key/again.m4a index 3688e5cb..b543e013 100644 Binary files a/web/escape/audio/voice/female/key/again.m4a and b/web/escape/audio/voice/female/key/again.m4a differ diff --git a/web/escape/audio/voice/female/key/again.ogg b/web/escape/audio/voice/female/key/again.ogg index 1c75b125..ea901da2 100644 Binary files a/web/escape/audio/voice/female/key/again.ogg and b/web/escape/audio/voice/female/key/again.ogg differ diff --git a/web/escape/audio/voice/female/key/g0_new.m4a b/web/escape/audio/voice/female/key/g0_new.m4a new file mode 100644 index 00000000..e19f1530 Binary files /dev/null and b/web/escape/audio/voice/female/key/g0_new.m4a differ diff --git a/web/escape/audio/voice/female/key/g0_new.ogg b/web/escape/audio/voice/female/key/g0_new.ogg new file mode 100644 index 00000000..9f10dda4 Binary files /dev/null and b/web/escape/audio/voice/female/key/g0_new.ogg differ diff --git a/web/escape/audio/voice/female/key/g0b.m4a b/web/escape/audio/voice/female/key/g0b.m4a new file mode 100644 index 00000000..fe815c01 Binary files /dev/null and b/web/escape/audio/voice/female/key/g0b.m4a differ diff --git a/web/escape/audio/voice/female/key/g0b.ogg b/web/escape/audio/voice/female/key/g0b.ogg new file mode 100644 index 00000000..e6b5712f Binary files /dev/null and b/web/escape/audio/voice/female/key/g0b.ogg differ diff --git a/web/escape/audio/voice/female/key/g3_new.m4a b/web/escape/audio/voice/female/key/g3_new.m4a new file mode 100644 index 00000000..d451d8da Binary files /dev/null and b/web/escape/audio/voice/female/key/g3_new.m4a differ diff --git a/web/escape/audio/voice/female/key/g3_new.ogg b/web/escape/audio/voice/female/key/g3_new.ogg new file mode 100644 index 00000000..529a30a2 Binary files /dev/null and b/web/escape/audio/voice/female/key/g3_new.ogg differ diff --git a/web/escape/audio/voice/female/key/g4_down.m4a b/web/escape/audio/voice/female/key/g4_down.m4a index 8f53b1e6..d0061d64 100644 Binary files a/web/escape/audio/voice/female/key/g4_down.m4a and b/web/escape/audio/voice/female/key/g4_down.m4a differ diff --git a/web/escape/audio/voice/female/key/g4_down.ogg b/web/escape/audio/voice/female/key/g4_down.ogg index fc494aad..7b67d659 100644 Binary files a/web/escape/audio/voice/female/key/g4_down.ogg and b/web/escape/audio/voice/female/key/g4_down.ogg differ diff --git a/web/escape/audio/voice/female/key/g5_inspect.m4a b/web/escape/audio/voice/female/key/g5_inspect.m4a index d4121d48..1dad72bb 100644 Binary files a/web/escape/audio/voice/female/key/g5_inspect.m4a and b/web/escape/audio/voice/female/key/g5_inspect.m4a differ diff --git a/web/escape/audio/voice/female/key/g5_inspect.ogg b/web/escape/audio/voice/female/key/g5_inspect.ogg index 3a08b1fe..9fa68599 100644 Binary files a/web/escape/audio/voice/female/key/g5_inspect.ogg and b/web/escape/audio/voice/female/key/g5_inspect.ogg differ diff --git a/web/escape/audio/voice/female/key/g7_goal_new.m4a b/web/escape/audio/voice/female/key/g7_goal_new.m4a new file mode 100644 index 00000000..a87469a5 Binary files /dev/null and b/web/escape/audio/voice/female/key/g7_goal_new.m4a differ diff --git a/web/escape/audio/voice/female/key/g7_goal_new.ogg b/web/escape/audio/voice/female/key/g7_goal_new.ogg new file mode 100644 index 00000000..32f5d93f Binary files /dev/null and b/web/escape/audio/voice/female/key/g7_goal_new.ogg differ diff --git a/web/escape/audio/voice/female/key/g_hint_swipe.m4a b/web/escape/audio/voice/female/key/g_hint_swipe.m4a new file mode 100644 index 00000000..cf0b5957 Binary files /dev/null and b/web/escape/audio/voice/female/key/g_hint_swipe.m4a differ diff --git a/web/escape/audio/voice/female/key/g_hint_swipe.ogg b/web/escape/audio/voice/female/key/g_hint_swipe.ogg new file mode 100644 index 00000000..963620d3 Binary files /dev/null and b/web/escape/audio/voice/female/key/g_hint_swipe.ogg differ diff --git a/web/escape/audio/voice/female/key/g_wrong_tap.m4a b/web/escape/audio/voice/female/key/g_wrong_tap.m4a new file mode 100644 index 00000000..cde46a36 Binary files /dev/null and b/web/escape/audio/voice/female/key/g_wrong_tap.m4a differ diff --git a/web/escape/audio/voice/female/key/g_wrong_tap.ogg b/web/escape/audio/voice/female/key/g_wrong_tap.ogg new file mode 100644 index 00000000..d81f2ad1 Binary files /dev/null and b/web/escape/audio/voice/female/key/g_wrong_tap.ogg differ diff --git a/web/escape/audio/voice/female/key/guide_blunt_swipe.m4a b/web/escape/audio/voice/female/key/guide_blunt_swipe.m4a new file mode 100644 index 00000000..32250611 Binary files /dev/null and b/web/escape/audio/voice/female/key/guide_blunt_swipe.m4a differ diff --git a/web/escape/audio/voice/female/key/guide_blunt_swipe.ogg b/web/escape/audio/voice/female/key/guide_blunt_swipe.ogg new file mode 100644 index 00000000..a0809144 Binary files /dev/null and b/web/escape/audio/voice/female/key/guide_blunt_swipe.ogg differ diff --git a/web/escape/audio/voice/female/key/guide_wrong_down.m4a b/web/escape/audio/voice/female/key/guide_wrong_down.m4a index 0d7c053a..47c01de0 100644 Binary files a/web/escape/audio/voice/female/key/guide_wrong_down.m4a and b/web/escape/audio/voice/female/key/guide_wrong_down.m4a differ diff --git a/web/escape/audio/voice/female/key/guide_wrong_down.ogg b/web/escape/audio/voice/female/key/guide_wrong_down.ogg index c03eacd9..88d3800f 100644 Binary files a/web/escape/audio/voice/female/key/guide_wrong_down.ogg and b/web/escape/audio/voice/female/key/guide_wrong_down.ogg differ diff --git a/web/escape/audio/voice/female/key/guide_wrong_left.m4a b/web/escape/audio/voice/female/key/guide_wrong_left.m4a index 4de7919e..40a23b59 100644 Binary files a/web/escape/audio/voice/female/key/guide_wrong_left.m4a and b/web/escape/audio/voice/female/key/guide_wrong_left.m4a differ diff --git a/web/escape/audio/voice/female/key/guide_wrong_left.ogg b/web/escape/audio/voice/female/key/guide_wrong_left.ogg index 5825a939..f4edb6fa 100644 Binary files a/web/escape/audio/voice/female/key/guide_wrong_left.ogg and b/web/escape/audio/voice/female/key/guide_wrong_left.ogg differ diff --git a/web/escape/audio/voice/female/key/inspect_gate_ready.m4a b/web/escape/audio/voice/female/key/inspect_gate_ready.m4a index 33bc6515..b8007e70 100644 Binary files a/web/escape/audio/voice/female/key/inspect_gate_ready.m4a and b/web/escape/audio/voice/female/key/inspect_gate_ready.m4a differ diff --git a/web/escape/audio/voice/female/key/inspect_gate_ready.ogg b/web/escape/audio/voice/female/key/inspect_gate_ready.ogg index d6882aa5..7ae1b005 100644 Binary files a/web/escape/audio/voice/female/key/inspect_gate_ready.ogg and b/web/escape/audio/voice/female/key/inspect_gate_ready.ogg differ diff --git a/web/escape/audio/voice/female/key/inv_settle.m4a b/web/escape/audio/voice/female/key/inv_settle.m4a new file mode 100644 index 00000000..b8228c4e Binary files /dev/null and b/web/escape/audio/voice/female/key/inv_settle.m4a differ diff --git a/web/escape/audio/voice/female/key/inv_settle.ogg b/web/escape/audio/voice/female/key/inv_settle.ogg new file mode 100644 index 00000000..57ddcf9a Binary files /dev/null and b/web/escape/audio/voice/female/key/inv_settle.ogg differ diff --git a/web/escape/audio/voice/female/key/lp_here.m4a b/web/escape/audio/voice/female/key/lp_here.m4a new file mode 100644 index 00000000..842cd317 Binary files /dev/null and b/web/escape/audio/voice/female/key/lp_here.m4a differ diff --git a/web/escape/audio/voice/female/key/lp_here.ogg b/web/escape/audio/voice/female/key/lp_here.ogg new file mode 100644 index 00000000..19fc1a52 Binary files /dev/null and b/web/escape/audio/voice/female/key/lp_here.ogg differ diff --git a/web/escape/audio/voice/female/key/lp_tap.m4a b/web/escape/audio/voice/female/key/lp_tap.m4a new file mode 100644 index 00000000..d10ff9ad Binary files /dev/null and b/web/escape/audio/voice/female/key/lp_tap.m4a differ diff --git a/web/escape/audio/voice/female/key/lp_tap.ogg b/web/escape/audio/voice/female/key/lp_tap.ogg new file mode 100644 index 00000000..b5d88ab6 Binary files /dev/null and b/web/escape/audio/voice/female/key/lp_tap.ogg differ diff --git a/web/escape/audio/voice/female/key/menu_help.m4a b/web/escape/audio/voice/female/key/menu_help.m4a new file mode 100644 index 00000000..5b67c03a Binary files /dev/null and b/web/escape/audio/voice/female/key/menu_help.m4a differ diff --git a/web/escape/audio/voice/female/key/menu_help.ogg b/web/escape/audio/voice/female/key/menu_help.ogg new file mode 100644 index 00000000..be04cf33 Binary files /dev/null and b/web/escape/audio/voice/female/key/menu_help.ogg differ diff --git a/web/escape/audio/voice/female/key/nav_down.m4a b/web/escape/audio/voice/female/key/nav_down.m4a index 81868de2..8d34e78a 100644 Binary files a/web/escape/audio/voice/female/key/nav_down.m4a and b/web/escape/audio/voice/female/key/nav_down.m4a differ diff --git a/web/escape/audio/voice/female/key/nav_down.ogg b/web/escape/audio/voice/female/key/nav_down.ogg index 7ea049fb..a85d9e8e 100644 Binary files a/web/escape/audio/voice/female/key/nav_down.ogg and b/web/escape/audio/voice/female/key/nav_down.ogg differ diff --git a/web/escape/audio/voice/female/key/nav_left.m4a b/web/escape/audio/voice/female/key/nav_left.m4a index bbcd4f75..89d71cca 100644 Binary files a/web/escape/audio/voice/female/key/nav_left.m4a and b/web/escape/audio/voice/female/key/nav_left.m4a differ diff --git a/web/escape/audio/voice/female/key/nav_left.ogg b/web/escape/audio/voice/female/key/nav_left.ogg index 7023c7b7..71ecd13a 100644 Binary files a/web/escape/audio/voice/female/key/nav_left.ogg and b/web/escape/audio/voice/female/key/nav_left.ogg differ diff --git a/web/escape/audio/voice/female/key/nav_right.m4a b/web/escape/audio/voice/female/key/nav_right.m4a index 9b37600f..1988ae61 100644 Binary files a/web/escape/audio/voice/female/key/nav_right.m4a and b/web/escape/audio/voice/female/key/nav_right.m4a differ diff --git a/web/escape/audio/voice/female/key/nav_right.ogg b/web/escape/audio/voice/female/key/nav_right.ogg index 46bc9762..804bfede 100644 Binary files a/web/escape/audio/voice/female/key/nav_right.ogg and b/web/escape/audio/voice/female/key/nav_right.ogg differ diff --git a/web/escape/audio/voice/female/key/nav_up.m4a b/web/escape/audio/voice/female/key/nav_up.m4a index 99825b99..22ac8136 100644 Binary files a/web/escape/audio/voice/female/key/nav_up.m4a and b/web/escape/audio/voice/female/key/nav_up.m4a differ diff --git a/web/escape/audio/voice/female/key/nav_up.ogg b/web/escape/audio/voice/female/key/nav_up.ogg index ae118f62..4e8b7d75 100644 Binary files a/web/escape/audio/voice/female/key/nav_up.ogg and b/web/escape/audio/voice/female/key/nav_up.ogg differ diff --git a/web/escape/audio/voice/female/key/start_room.m4a b/web/escape/audio/voice/female/key/start_room.m4a index 76aca0fd..0d1f5192 100644 Binary files a/web/escape/audio/voice/female/key/start_room.m4a and b/web/escape/audio/voice/female/key/start_room.m4a differ diff --git a/web/escape/audio/voice/female/key/start_room.ogg b/web/escape/audio/voice/female/key/start_room.ogg index ef0e59b1..be8a2e22 100644 Binary files a/web/escape/audio/voice/female/key/start_room.ogg and b/web/escape/audio/voice/female/key/start_room.ogg differ diff --git a/web/escape/audio/voice/female/key/sys_continue_ask.m4a b/web/escape/audio/voice/female/key/sys_continue_ask.m4a new file mode 100644 index 00000000..79981dae Binary files /dev/null and b/web/escape/audio/voice/female/key/sys_continue_ask.m4a differ diff --git a/web/escape/audio/voice/female/key/sys_continue_ask.ogg b/web/escape/audio/voice/female/key/sys_continue_ask.ogg new file mode 100644 index 00000000..3fed28c7 Binary files /dev/null and b/web/escape/audio/voice/female/key/sys_continue_ask.ogg differ diff --git a/web/escape/audio/voice/female/key/sys_only_down.m4a b/web/escape/audio/voice/female/key/sys_only_down.m4a new file mode 100644 index 00000000..80d7eeb8 Binary files /dev/null and b/web/escape/audio/voice/female/key/sys_only_down.m4a differ diff --git a/web/escape/audio/voice/female/key/sys_only_down.ogg b/web/escape/audio/voice/female/key/sys_only_down.ogg new file mode 100644 index 00000000..87c6fbc6 Binary files /dev/null and b/web/escape/audio/voice/female/key/sys_only_down.ogg differ diff --git a/web/escape/audio/voice/female/key/sys_only_left.m4a b/web/escape/audio/voice/female/key/sys_only_left.m4a new file mode 100644 index 00000000..fd54c2e9 Binary files /dev/null and b/web/escape/audio/voice/female/key/sys_only_left.m4a differ diff --git a/web/escape/audio/voice/female/key/sys_only_left.ogg b/web/escape/audio/voice/female/key/sys_only_left.ogg new file mode 100644 index 00000000..eedc6aba Binary files /dev/null and b/web/escape/audio/voice/female/key/sys_only_left.ogg differ diff --git a/web/escape/audio/voice/female/key/sys_only_right.m4a b/web/escape/audio/voice/female/key/sys_only_right.m4a new file mode 100644 index 00000000..173488b4 Binary files /dev/null and b/web/escape/audio/voice/female/key/sys_only_right.m4a differ diff --git a/web/escape/audio/voice/female/key/sys_only_right.ogg b/web/escape/audio/voice/female/key/sys_only_right.ogg new file mode 100644 index 00000000..ebdbb36c Binary files /dev/null and b/web/escape/audio/voice/female/key/sys_only_right.ogg differ diff --git a/web/escape/audio/voice/female/key/sys_only_up.m4a b/web/escape/audio/voice/female/key/sys_only_up.m4a new file mode 100644 index 00000000..03f81bd0 Binary files /dev/null and b/web/escape/audio/voice/female/key/sys_only_up.m4a differ diff --git a/web/escape/audio/voice/female/key/sys_only_up.ogg b/web/escape/audio/voice/female/key/sys_only_up.ogg new file mode 100644 index 00000000..19c90bde Binary files /dev/null and b/web/escape/audio/voice/female/key/sys_only_up.ogg differ diff --git a/web/escape/audio/voice/female/lp_box.m4a b/web/escape/audio/voice/female/lp_box.m4a new file mode 100644 index 00000000..ac2e209a Binary files /dev/null and b/web/escape/audio/voice/female/lp_box.m4a differ diff --git a/web/escape/audio/voice/female/lp_box.ogg b/web/escape/audio/voice/female/lp_box.ogg new file mode 100644 index 00000000..acb2f2bd Binary files /dev/null and b/web/escape/audio/voice/female/lp_box.ogg differ diff --git a/web/escape/audio/voice/female/lp_cabinet.m4a b/web/escape/audio/voice/female/lp_cabinet.m4a new file mode 100644 index 00000000..568d8e68 Binary files /dev/null and b/web/escape/audio/voice/female/lp_cabinet.m4a differ diff --git a/web/escape/audio/voice/female/lp_cabinet.ogg b/web/escape/audio/voice/female/lp_cabinet.ogg new file mode 100644 index 00000000..5f83ce03 Binary files /dev/null and b/web/escape/audio/voice/female/lp_cabinet.ogg differ diff --git a/web/escape/audio/voice/female/lp_gate.m4a b/web/escape/audio/voice/female/lp_gate.m4a new file mode 100644 index 00000000..43c9aa17 Binary files /dev/null and b/web/escape/audio/voice/female/lp_gate.m4a differ diff --git a/web/escape/audio/voice/female/lp_gate.ogg b/web/escape/audio/voice/female/lp_gate.ogg new file mode 100644 index 00000000..6d40ef4c Binary files /dev/null and b/web/escape/audio/voice/female/lp_gate.ogg differ diff --git a/web/escape/audio/voice/female/lp_here.m4a b/web/escape/audio/voice/female/lp_here.m4a new file mode 100644 index 00000000..7237d4c0 Binary files /dev/null and b/web/escape/audio/voice/female/lp_here.m4a differ diff --git a/web/escape/audio/voice/female/lp_here.ogg b/web/escape/audio/voice/female/lp_here.ogg new file mode 100644 index 00000000..0f1a6d9f Binary files /dev/null and b/web/escape/audio/voice/female/lp_here.ogg differ diff --git a/web/escape/audio/voice/female/lp_key.m4a b/web/escape/audio/voice/female/lp_key.m4a new file mode 100644 index 00000000..0399c4e3 Binary files /dev/null and b/web/escape/audio/voice/female/lp_key.m4a differ diff --git a/web/escape/audio/voice/female/lp_key.ogg b/web/escape/audio/voice/female/lp_key.ogg new file mode 100644 index 00000000..050aad0f Binary files /dev/null and b/web/escape/audio/voice/female/lp_key.ogg differ diff --git a/web/escape/audio/voice/female/lp_tap.m4a b/web/escape/audio/voice/female/lp_tap.m4a new file mode 100644 index 00000000..5a63a4d4 Binary files /dev/null and b/web/escape/audio/voice/female/lp_tap.m4a differ diff --git a/web/escape/audio/voice/female/lp_tap.ogg b/web/escape/audio/voice/female/lp_tap.ogg new file mode 100644 index 00000000..dae1e297 Binary files /dev/null and b/web/escape/audio/voice/female/lp_tap.ogg differ diff --git a/web/escape/audio/voice/female/menu_help.m4a b/web/escape/audio/voice/female/menu_help.m4a new file mode 100644 index 00000000..c1153e88 Binary files /dev/null and b/web/escape/audio/voice/female/menu_help.m4a differ diff --git a/web/escape/audio/voice/female/menu_help.ogg b/web/escape/audio/voice/female/menu_help.ogg new file mode 100644 index 00000000..2fcf8b02 Binary files /dev/null and b/web/escape/audio/voice/female/menu_help.ogg differ diff --git a/web/escape/audio/voice/female/menu_item1.m4a b/web/escape/audio/voice/female/menu_item1.m4a new file mode 100644 index 00000000..c31f6603 Binary files /dev/null and b/web/escape/audio/voice/female/menu_item1.m4a differ diff --git a/web/escape/audio/voice/female/menu_item1.ogg b/web/escape/audio/voice/female/menu_item1.ogg new file mode 100644 index 00000000..2228bcf0 Binary files /dev/null and b/web/escape/audio/voice/female/menu_item1.ogg differ diff --git a/web/escape/audio/voice/female/menu_item2.m4a b/web/escape/audio/voice/female/menu_item2.m4a new file mode 100644 index 00000000..2cdf9673 Binary files /dev/null and b/web/escape/audio/voice/female/menu_item2.m4a differ diff --git a/web/escape/audio/voice/female/menu_item2.ogg b/web/escape/audio/voice/female/menu_item2.ogg new file mode 100644 index 00000000..0889aa58 Binary files /dev/null and b/web/escape/audio/voice/female/menu_item2.ogg differ diff --git a/web/escape/audio/voice/female/menu_item3.m4a b/web/escape/audio/voice/female/menu_item3.m4a new file mode 100644 index 00000000..682119ca Binary files /dev/null and b/web/escape/audio/voice/female/menu_item3.m4a differ diff --git a/web/escape/audio/voice/female/menu_item3.ogg b/web/escape/audio/voice/female/menu_item3.ogg new file mode 100644 index 00000000..729f2f6a Binary files /dev/null and b/web/escape/audio/voice/female/menu_item3.ogg differ diff --git a/web/escape/audio/voice/female/menu_open.m4a b/web/escape/audio/voice/female/menu_open.m4a index a4839ef5..deeded16 100644 Binary files a/web/escape/audio/voice/female/menu_open.m4a and b/web/escape/audio/voice/female/menu_open.m4a differ diff --git a/web/escape/audio/voice/female/menu_open.ogg b/web/escape/audio/voice/female/menu_open.ogg index d0540b46..9af657e4 100644 Binary files a/web/escape/audio/voice/female/menu_open.ogg and b/web/escape/audio/voice/female/menu_open.ogg differ diff --git a/web/escape/audio/voice/female/nav_down.m4a b/web/escape/audio/voice/female/nav_down.m4a index ef2db1b3..6d1b017a 100644 Binary files a/web/escape/audio/voice/female/nav_down.m4a and b/web/escape/audio/voice/female/nav_down.m4a differ diff --git a/web/escape/audio/voice/female/nav_down.ogg b/web/escape/audio/voice/female/nav_down.ogg index eef5ed96..bbd14b06 100644 Binary files a/web/escape/audio/voice/female/nav_down.ogg and b/web/escape/audio/voice/female/nav_down.ogg differ diff --git a/web/escape/audio/voice/female/nav_left.m4a b/web/escape/audio/voice/female/nav_left.m4a index 92a8e4d5..f47e2c0d 100644 Binary files a/web/escape/audio/voice/female/nav_left.m4a and b/web/escape/audio/voice/female/nav_left.m4a differ diff --git a/web/escape/audio/voice/female/nav_left.ogg b/web/escape/audio/voice/female/nav_left.ogg index ab0f4421..8e79096e 100644 Binary files a/web/escape/audio/voice/female/nav_left.ogg and b/web/escape/audio/voice/female/nav_left.ogg differ diff --git a/web/escape/audio/voice/female/nav_right.m4a b/web/escape/audio/voice/female/nav_right.m4a index f0e3d085..d36ae2ef 100644 Binary files a/web/escape/audio/voice/female/nav_right.m4a and b/web/escape/audio/voice/female/nav_right.m4a differ diff --git a/web/escape/audio/voice/female/nav_right.ogg b/web/escape/audio/voice/female/nav_right.ogg index f85f0d33..7e3f3171 100644 Binary files a/web/escape/audio/voice/female/nav_right.ogg and b/web/escape/audio/voice/female/nav_right.ogg differ diff --git a/web/escape/audio/voice/female/nav_up.m4a b/web/escape/audio/voice/female/nav_up.m4a index 066a315e..44032fe5 100644 Binary files a/web/escape/audio/voice/female/nav_up.m4a and b/web/escape/audio/voice/female/nav_up.m4a differ diff --git a/web/escape/audio/voice/female/nav_up.ogg b/web/escape/audio/voice/female/nav_up.ogg index 8e75a7e1..34944c9f 100644 Binary files a/web/escape/audio/voice/female/nav_up.ogg and b/web/escape/audio/voice/female/nav_up.ogg differ diff --git a/web/escape/audio/voice/female/replay.m4a b/web/escape/audio/voice/female/replay.m4a index 24c33ef7..c5923603 100644 Binary files a/web/escape/audio/voice/female/replay.m4a and b/web/escape/audio/voice/female/replay.m4a differ diff --git a/web/escape/audio/voice/female/replay.ogg b/web/escape/audio/voice/female/replay.ogg index a31a75e9..fb555ece 100644 Binary files a/web/escape/audio/voice/female/replay.ogg and b/web/escape/audio/voice/female/replay.ogg differ diff --git a/web/escape/audio/voice/female/room_cabinet.m4a b/web/escape/audio/voice/female/room_cabinet.m4a index cdb640ed..0bfe57e7 100644 Binary files a/web/escape/audio/voice/female/room_cabinet.m4a and b/web/escape/audio/voice/female/room_cabinet.m4a differ diff --git a/web/escape/audio/voice/female/room_cabinet.ogg b/web/escape/audio/voice/female/room_cabinet.ogg index 3257312c..c7057018 100644 Binary files a/web/escape/audio/voice/female/room_cabinet.ogg and b/web/escape/audio/voice/female/room_cabinet.ogg differ diff --git a/web/escape/audio/voice/female/room_gate.m4a b/web/escape/audio/voice/female/room_gate.m4a index 236b7904..2c384dc0 100644 Binary files a/web/escape/audio/voice/female/room_gate.m4a and b/web/escape/audio/voice/female/room_gate.m4a differ diff --git a/web/escape/audio/voice/female/room_gate.ogg b/web/escape/audio/voice/female/room_gate.ogg index c4275c7e..aa873f13 100644 Binary files a/web/escape/audio/voice/female/room_gate.ogg and b/web/escape/audio/voice/female/room_gate.ogg differ diff --git a/web/escape/audio/voice/female/room_power.m4a b/web/escape/audio/voice/female/room_power.m4a index 9fad87b0..d147c0b6 100644 Binary files a/web/escape/audio/voice/female/room_power.m4a and b/web/escape/audio/voice/female/room_power.m4a differ diff --git a/web/escape/audio/voice/female/room_power.ogg b/web/escape/audio/voice/female/room_power.ogg index 974af2f2..dccffcfd 100644 Binary files a/web/escape/audio/voice/female/room_power.ogg and b/web/escape/audio/voice/female/room_power.ogg differ diff --git a/web/escape/audio/voice/female/room_storage.m4a b/web/escape/audio/voice/female/room_storage.m4a index 03cfedef..719b808e 100644 Binary files a/web/escape/audio/voice/female/room_storage.m4a and b/web/escape/audio/voice/female/room_storage.m4a differ diff --git a/web/escape/audio/voice/female/room_storage.ogg b/web/escape/audio/voice/female/room_storage.ogg index a3f59dc7..5c7f9fbe 100644 Binary files a/web/escape/audio/voice/female/room_storage.ogg and b/web/escape/audio/voice/female/room_storage.ogg differ diff --git a/web/escape/audio/voice/female/start_room.m4a b/web/escape/audio/voice/female/start_room.m4a index 77f6b93b..9c15158d 100644 Binary files a/web/escape/audio/voice/female/start_room.m4a and b/web/escape/audio/voice/female/start_room.m4a differ diff --git a/web/escape/audio/voice/female/start_room.ogg b/web/escape/audio/voice/female/start_room.ogg index 02e665e3..bb6dcf76 100644 Binary files a/web/escape/audio/voice/female/start_room.ogg and b/web/escape/audio/voice/female/start_room.ogg differ diff --git a/web/escape/audio/voice/female/sys_continue_ask.m4a b/web/escape/audio/voice/female/sys_continue_ask.m4a new file mode 100644 index 00000000..c06c7429 Binary files /dev/null and b/web/escape/audio/voice/female/sys_continue_ask.m4a differ diff --git a/web/escape/audio/voice/female/sys_continue_ask.ogg b/web/escape/audio/voice/female/sys_continue_ask.ogg new file mode 100644 index 00000000..1b11a724 Binary files /dev/null and b/web/escape/audio/voice/female/sys_continue_ask.ogg differ diff --git a/web/escape/audio/voice/female/sys_demo.m4a b/web/escape/audio/voice/female/sys_demo.m4a new file mode 100644 index 00000000..7bacdc97 Binary files /dev/null and b/web/escape/audio/voice/female/sys_demo.m4a differ diff --git a/web/escape/audio/voice/female/sys_demo.ogg b/web/escape/audio/voice/female/sys_demo.ogg new file mode 100644 index 00000000..5aeb5f97 Binary files /dev/null and b/web/escape/audio/voice/female/sys_demo.ogg differ diff --git a/web/escape/audio/voice/female/sys_now_you.m4a b/web/escape/audio/voice/female/sys_now_you.m4a new file mode 100644 index 00000000..21efbb06 Binary files /dev/null and b/web/escape/audio/voice/female/sys_now_you.m4a differ diff --git a/web/escape/audio/voice/female/sys_now_you.ogg b/web/escape/audio/voice/female/sys_now_you.ogg new file mode 100644 index 00000000..bd9cc4d4 Binary files /dev/null and b/web/escape/audio/voice/female/sys_now_you.ogg differ diff --git a/web/escape/audio/voice/female/sys_only_down.m4a b/web/escape/audio/voice/female/sys_only_down.m4a new file mode 100644 index 00000000..84ac0a41 Binary files /dev/null and b/web/escape/audio/voice/female/sys_only_down.m4a differ diff --git a/web/escape/audio/voice/female/sys_only_down.ogg b/web/escape/audio/voice/female/sys_only_down.ogg new file mode 100644 index 00000000..31346113 Binary files /dev/null and b/web/escape/audio/voice/female/sys_only_down.ogg differ diff --git a/web/escape/audio/voice/female/sys_only_left.m4a b/web/escape/audio/voice/female/sys_only_left.m4a new file mode 100644 index 00000000..fefadb7e Binary files /dev/null and b/web/escape/audio/voice/female/sys_only_left.m4a differ diff --git a/web/escape/audio/voice/female/sys_only_left.ogg b/web/escape/audio/voice/female/sys_only_left.ogg new file mode 100644 index 00000000..e0188352 Binary files /dev/null and b/web/escape/audio/voice/female/sys_only_left.ogg differ diff --git a/web/escape/audio/voice/female/sys_only_right.m4a b/web/escape/audio/voice/female/sys_only_right.m4a new file mode 100644 index 00000000..97d67a44 Binary files /dev/null and b/web/escape/audio/voice/female/sys_only_right.m4a differ diff --git a/web/escape/audio/voice/female/sys_only_right.ogg b/web/escape/audio/voice/female/sys_only_right.ogg new file mode 100644 index 00000000..10eb0b21 Binary files /dev/null and b/web/escape/audio/voice/female/sys_only_right.ogg differ diff --git a/web/escape/audio/voice/female/sys_only_up.m4a b/web/escape/audio/voice/female/sys_only_up.m4a new file mode 100644 index 00000000..1950a3f8 Binary files /dev/null and b/web/escape/audio/voice/female/sys_only_up.m4a differ diff --git a/web/escape/audio/voice/female/sys_only_up.ogg b/web/escape/audio/voice/female/sys_only_up.ogg new file mode 100644 index 00000000..dbeeb0a4 Binary files /dev/null and b/web/escape/audio/voice/female/sys_only_up.ogg differ diff --git a/web/escape/audio/voice/female/sys_say_again.m4a b/web/escape/audio/voice/female/sys_say_again.m4a new file mode 100644 index 00000000..63807951 Binary files /dev/null and b/web/escape/audio/voice/female/sys_say_again.m4a differ diff --git a/web/escape/audio/voice/female/sys_say_again.ogg b/web/escape/audio/voice/female/sys_say_again.ogg new file mode 100644 index 00000000..595a8707 Binary files /dev/null and b/web/escape/audio/voice/female/sys_say_again.ogg differ diff --git a/web/escape/audio/voice/female/time_band1.m4a b/web/escape/audio/voice/female/time_band1.m4a index 475eb552..92f23f5b 100644 Binary files a/web/escape/audio/voice/female/time_band1.m4a and b/web/escape/audio/voice/female/time_band1.m4a differ diff --git a/web/escape/audio/voice/female/time_band1.ogg b/web/escape/audio/voice/female/time_band1.ogg index e1700705..845b33da 100644 Binary files a/web/escape/audio/voice/female/time_band1.ogg and b/web/escape/audio/voice/female/time_band1.ogg differ diff --git a/web/escape/audio/voice/female/time_band2.m4a b/web/escape/audio/voice/female/time_band2.m4a index abbb474d..976ad6e5 100644 Binary files a/web/escape/audio/voice/female/time_band2.m4a and b/web/escape/audio/voice/female/time_band2.m4a differ diff --git a/web/escape/audio/voice/female/time_band2.ogg b/web/escape/audio/voice/female/time_band2.ogg index de7fd78e..b9353662 100644 Binary files a/web/escape/audio/voice/female/time_band2.ogg and b/web/escape/audio/voice/female/time_band2.ogg differ diff --git a/web/escape/audio/voice/female/time_band3.m4a b/web/escape/audio/voice/female/time_band3.m4a index 302bfa1d..76ddd7f0 100644 Binary files a/web/escape/audio/voice/female/time_band3.m4a and b/web/escape/audio/voice/female/time_band3.m4a differ diff --git a/web/escape/audio/voice/female/time_band3.ogg b/web/escape/audio/voice/female/time_band3.ogg index 3ed99989..8191d2b3 100644 Binary files a/web/escape/audio/voice/female/time_band3.ogg and b/web/escape/audio/voice/female/time_band3.ogg differ diff --git a/web/escape/audio/voice/female/time_band4.m4a b/web/escape/audio/voice/female/time_band4.m4a index c7dc1be5..f034a59f 100644 Binary files a/web/escape/audio/voice/female/time_band4.m4a and b/web/escape/audio/voice/female/time_band4.m4a differ diff --git a/web/escape/audio/voice/female/time_band4.ogg b/web/escape/audio/voice/female/time_band4.ogg index 8a518358..fa05d532 100644 Binary files a/web/escape/audio/voice/female/time_band4.ogg and b/web/escape/audio/voice/female/time_band4.ogg differ diff --git a/web/escape/audio/voice/female/unlock_cabinet.m4a b/web/escape/audio/voice/female/unlock_cabinet.m4a index 8b132e44..09102a95 100644 Binary files a/web/escape/audio/voice/female/unlock_cabinet.m4a and b/web/escape/audio/voice/female/unlock_cabinet.m4a differ diff --git a/web/escape/audio/voice/female/unlock_cabinet.ogg b/web/escape/audio/voice/female/unlock_cabinet.ogg index 99228bf8..a8314ebc 100644 Binary files a/web/escape/audio/voice/female/unlock_cabinet.ogg and b/web/escape/audio/voice/female/unlock_cabinet.ogg differ diff --git a/web/escape/audio/voice/female/wall.m4a b/web/escape/audio/voice/female/wall.m4a index 7f1e11de..535a1651 100644 Binary files a/web/escape/audio/voice/female/wall.m4a and b/web/escape/audio/voice/female/wall.m4a differ diff --git a/web/escape/audio/voice/female/wall.ogg b/web/escape/audio/voice/female/wall.ogg index 029184aa..9a00bd16 100644 Binary files a/web/escape/audio/voice/female/wall.ogg and b/web/escape/audio/voice/female/wall.ogg differ diff --git a/web/escape/audio/voice/female/wall_dir_d.m4a b/web/escape/audio/voice/female/wall_dir_d.m4a index b6196f09..0bab5d6e 100644 Binary files a/web/escape/audio/voice/female/wall_dir_d.m4a and b/web/escape/audio/voice/female/wall_dir_d.m4a differ diff --git a/web/escape/audio/voice/female/wall_dir_d.ogg b/web/escape/audio/voice/female/wall_dir_d.ogg index 9013daf8..0a33d490 100644 Binary files a/web/escape/audio/voice/female/wall_dir_d.ogg and b/web/escape/audio/voice/female/wall_dir_d.ogg differ diff --git a/web/escape/audio/voice/female/wall_dir_l.m4a b/web/escape/audio/voice/female/wall_dir_l.m4a index 29be1d18..491a3242 100644 Binary files a/web/escape/audio/voice/female/wall_dir_l.m4a and b/web/escape/audio/voice/female/wall_dir_l.m4a differ diff --git a/web/escape/audio/voice/female/wall_dir_l.ogg b/web/escape/audio/voice/female/wall_dir_l.ogg index a8147763..4e3e8ce8 100644 Binary files a/web/escape/audio/voice/female/wall_dir_l.ogg and b/web/escape/audio/voice/female/wall_dir_l.ogg differ diff --git a/web/escape/audio/voice/female/wall_dir_r.m4a b/web/escape/audio/voice/female/wall_dir_r.m4a index 141e305f..ad9da18f 100644 Binary files a/web/escape/audio/voice/female/wall_dir_r.m4a and b/web/escape/audio/voice/female/wall_dir_r.m4a differ diff --git a/web/escape/audio/voice/female/wall_dir_r.ogg b/web/escape/audio/voice/female/wall_dir_r.ogg index 121386e5..caa8b966 100644 Binary files a/web/escape/audio/voice/female/wall_dir_r.ogg and b/web/escape/audio/voice/female/wall_dir_r.ogg differ diff --git a/web/escape/audio/voice/female/wall_dir_u.m4a b/web/escape/audio/voice/female/wall_dir_u.m4a index 19669261..4c60e753 100644 Binary files a/web/escape/audio/voice/female/wall_dir_u.m4a and b/web/escape/audio/voice/female/wall_dir_u.m4a differ diff --git a/web/escape/audio/voice/female/wall_dir_u.ogg b/web/escape/audio/voice/female/wall_dir_u.ogg index 7fe8bbc1..a9b7525e 100644 Binary files a/web/escape/audio/voice/female/wall_dir_u.ogg and b/web/escape/audio/voice/female/wall_dir_u.ogg differ diff --git a/web/escape/audio/voice/male/again.m4a b/web/escape/audio/voice/male/again.m4a index a2fbdb6e..28c015dc 100644 Binary files a/web/escape/audio/voice/male/again.m4a and b/web/escape/audio/voice/male/again.m4a differ diff --git a/web/escape/audio/voice/male/again.ogg b/web/escape/audio/voice/male/again.ogg index 9a41eeab..8616731f 100644 Binary files a/web/escape/audio/voice/male/again.ogg and b/web/escape/audio/voice/male/again.ogg differ diff --git a/web/escape/audio/voice/male/box_fixed.m4a b/web/escape/audio/voice/male/box_fixed.m4a index 3ad9d94d..611c908b 100644 Binary files a/web/escape/audio/voice/male/box_fixed.m4a and b/web/escape/audio/voice/male/box_fixed.m4a differ diff --git a/web/escape/audio/voice/male/box_fixed.ogg b/web/escape/audio/voice/male/box_fixed.ogg index c8964569..b0dc6310 100644 Binary files a/web/escape/audio/voice/male/box_fixed.ogg and b/web/escape/audio/voice/male/box_fixed.ogg differ diff --git a/web/escape/audio/voice/male/empty_1.m4a b/web/escape/audio/voice/male/empty_1.m4a index 7617c78c..99c2ba3c 100644 Binary files a/web/escape/audio/voice/male/empty_1.m4a and b/web/escape/audio/voice/male/empty_1.m4a differ diff --git a/web/escape/audio/voice/male/empty_1.ogg b/web/escape/audio/voice/male/empty_1.ogg index 656239c7..54782b77 100644 Binary files a/web/escape/audio/voice/male/empty_1.ogg and b/web/escape/audio/voice/male/empty_1.ogg differ diff --git a/web/escape/audio/voice/male/empty_2.m4a b/web/escape/audio/voice/male/empty_2.m4a index 24b67c5f..258271bc 100644 Binary files a/web/escape/audio/voice/male/empty_2.m4a and b/web/escape/audio/voice/male/empty_2.m4a differ diff --git a/web/escape/audio/voice/male/empty_2.ogg b/web/escape/audio/voice/male/empty_2.ogg index 0f4112b1..ad670fd3 100644 Binary files a/web/escape/audio/voice/male/empty_2.ogg and b/web/escape/audio/voice/male/empty_2.ogg differ diff --git a/web/escape/audio/voice/male/empty_3.m4a b/web/escape/audio/voice/male/empty_3.m4a index bc49d9d1..e0e6a0bd 100644 Binary files a/web/escape/audio/voice/male/empty_3.m4a and b/web/escape/audio/voice/male/empty_3.m4a differ diff --git a/web/escape/audio/voice/male/empty_3.ogg b/web/escape/audio/voice/male/empty_3.ogg index 21f8bbbb..792aa8ab 100644 Binary files a/web/escape/audio/voice/male/empty_3.ogg and b/web/escape/audio/voice/male/empty_3.ogg differ diff --git a/web/escape/audio/voice/male/escape.m4a b/web/escape/audio/voice/male/escape.m4a index a493f120..5275cd97 100644 Binary files a/web/escape/audio/voice/male/escape.m4a and b/web/escape/audio/voice/male/escape.m4a differ diff --git a/web/escape/audio/voice/male/escape.ogg b/web/escape/audio/voice/male/escape.ogg index 93096015..71133f74 100644 Binary files a/web/escape/audio/voice/male/escape.ogg and b/web/escape/audio/voice/male/escape.ogg differ diff --git a/web/escape/audio/voice/male/fix_box.m4a b/web/escape/audio/voice/male/fix_box.m4a index 4098f0ce..74e3c623 100644 Binary files a/web/escape/audio/voice/male/fix_box.m4a and b/web/escape/audio/voice/male/fix_box.m4a differ diff --git a/web/escape/audio/voice/male/fix_box.ogg b/web/escape/audio/voice/male/fix_box.ogg index 46c2511f..19196ce1 100644 Binary files a/web/escape/audio/voice/male/fix_box.ogg and b/web/escape/audio/voice/male/fix_box.ogg differ diff --git a/web/escape/audio/voice/male/found_key.m4a b/web/escape/audio/voice/male/found_key.m4a index 88ed8eb9..1a9376ed 100644 Binary files a/web/escape/audio/voice/male/found_key.m4a and b/web/escape/audio/voice/male/found_key.m4a differ diff --git a/web/escape/audio/voice/male/found_key.ogg b/web/escape/audio/voice/male/found_key.ogg index 8dd2856a..6e2faf15 100644 Binary files a/web/escape/audio/voice/male/found_key.ogg and b/web/escape/audio/voice/male/found_key.ogg differ diff --git a/web/escape/audio/voice/male/g0_new.m4a b/web/escape/audio/voice/male/g0_new.m4a new file mode 100644 index 00000000..b129f2c4 Binary files /dev/null and b/web/escape/audio/voice/male/g0_new.m4a differ diff --git a/web/escape/audio/voice/male/g0_new.ogg b/web/escape/audio/voice/male/g0_new.ogg new file mode 100644 index 00000000..562d4422 Binary files /dev/null and b/web/escape/audio/voice/male/g0_new.ogg differ diff --git a/web/escape/audio/voice/male/g0b.m4a b/web/escape/audio/voice/male/g0b.m4a new file mode 100644 index 00000000..18692256 Binary files /dev/null and b/web/escape/audio/voice/male/g0b.m4a differ diff --git a/web/escape/audio/voice/male/g0b.ogg b/web/escape/audio/voice/male/g0b.ogg new file mode 100644 index 00000000..03b87cdb Binary files /dev/null and b/web/escape/audio/voice/male/g0b.ogg differ diff --git a/web/escape/audio/voice/male/g3_new.m4a b/web/escape/audio/voice/male/g3_new.m4a new file mode 100644 index 00000000..4a617797 Binary files /dev/null and b/web/escape/audio/voice/male/g3_new.m4a differ diff --git a/web/escape/audio/voice/male/g3_new.ogg b/web/escape/audio/voice/male/g3_new.ogg new file mode 100644 index 00000000..c1cbf3b2 Binary files /dev/null and b/web/escape/audio/voice/male/g3_new.ogg differ diff --git a/web/escape/audio/voice/male/g4_down.m4a b/web/escape/audio/voice/male/g4_down.m4a index ceabb3ea..39d8119a 100644 Binary files a/web/escape/audio/voice/male/g4_down.m4a and b/web/escape/audio/voice/male/g4_down.m4a differ diff --git a/web/escape/audio/voice/male/g4_down.ogg b/web/escape/audio/voice/male/g4_down.ogg index 4e1ff6a5..8ff4bb41 100644 Binary files a/web/escape/audio/voice/male/g4_down.ogg and b/web/escape/audio/voice/male/g4_down.ogg differ diff --git a/web/escape/audio/voice/male/g5_inspect.m4a b/web/escape/audio/voice/male/g5_inspect.m4a index d78cf70e..f6d0b208 100644 Binary files a/web/escape/audio/voice/male/g5_inspect.m4a and b/web/escape/audio/voice/male/g5_inspect.m4a differ diff --git a/web/escape/audio/voice/male/g5_inspect.ogg b/web/escape/audio/voice/male/g5_inspect.ogg index c68e135c..cd3466d8 100644 Binary files a/web/escape/audio/voice/male/g5_inspect.ogg and b/web/escape/audio/voice/male/g5_inspect.ogg differ diff --git a/web/escape/audio/voice/male/g5_key.m4a b/web/escape/audio/voice/male/g5_key.m4a index c01b0a95..7f77c1bb 100644 Binary files a/web/escape/audio/voice/male/g5_key.m4a and b/web/escape/audio/voice/male/g5_key.m4a differ diff --git a/web/escape/audio/voice/male/g5_key.ogg b/web/escape/audio/voice/male/g5_key.ogg index 0be3eb0f..e7aec0ba 100644 Binary files a/web/escape/audio/voice/male/g5_key.ogg and b/web/escape/audio/voice/male/g5_key.ogg differ diff --git a/web/escape/audio/voice/male/g7_goal_new.m4a b/web/escape/audio/voice/male/g7_goal_new.m4a new file mode 100644 index 00000000..5c0e7a7d Binary files /dev/null and b/web/escape/audio/voice/male/g7_goal_new.m4a differ diff --git a/web/escape/audio/voice/male/g7_goal_new.ogg b/web/escape/audio/voice/male/g7_goal_new.ogg new file mode 100644 index 00000000..55fd08c8 Binary files /dev/null and b/web/escape/audio/voice/male/g7_goal_new.ogg differ diff --git a/web/escape/audio/voice/male/g_hint_swipe.m4a b/web/escape/audio/voice/male/g_hint_swipe.m4a new file mode 100644 index 00000000..0d7bf690 Binary files /dev/null and b/web/escape/audio/voice/male/g_hint_swipe.m4a differ diff --git a/web/escape/audio/voice/male/g_hint_swipe.ogg b/web/escape/audio/voice/male/g_hint_swipe.ogg new file mode 100644 index 00000000..d0c979e6 Binary files /dev/null and b/web/escape/audio/voice/male/g_hint_swipe.ogg differ diff --git a/web/escape/audio/voice/male/g_wrong_gesture.m4a b/web/escape/audio/voice/male/g_wrong_gesture.m4a new file mode 100644 index 00000000..f7ef4e73 Binary files /dev/null and b/web/escape/audio/voice/male/g_wrong_gesture.m4a differ diff --git a/web/escape/audio/voice/male/g_wrong_gesture.ogg b/web/escape/audio/voice/male/g_wrong_gesture.ogg new file mode 100644 index 00000000..3761c51f Binary files /dev/null and b/web/escape/audio/voice/male/g_wrong_gesture.ogg differ diff --git a/web/escape/audio/voice/male/g_wrong_tap.m4a b/web/escape/audio/voice/male/g_wrong_tap.m4a new file mode 100644 index 00000000..2c592dd7 Binary files /dev/null and b/web/escape/audio/voice/male/g_wrong_tap.m4a differ diff --git a/web/escape/audio/voice/male/g_wrong_tap.ogg b/web/escape/audio/voice/male/g_wrong_tap.ogg new file mode 100644 index 00000000..968d6837 Binary files /dev/null and b/web/escape/audio/voice/male/g_wrong_tap.ogg differ diff --git a/web/escape/audio/voice/male/gd_box_dir.m4a b/web/escape/audio/voice/male/gd_box_dir.m4a index cef690fd..9b9a1af6 100644 Binary files a/web/escape/audio/voice/male/gd_box_dir.m4a and b/web/escape/audio/voice/male/gd_box_dir.m4a differ diff --git a/web/escape/audio/voice/male/gd_box_dir.ogg b/web/escape/audio/voice/male/gd_box_dir.ogg index f23584be..4d3baefa 100644 Binary files a/web/escape/audio/voice/male/gd_box_dir.ogg and b/web/escape/audio/voice/male/gd_box_dir.ogg differ diff --git a/web/escape/audio/voice/male/gd_cabinet_dir.m4a b/web/escape/audio/voice/male/gd_cabinet_dir.m4a index 1e8801d5..87026e44 100644 Binary files a/web/escape/audio/voice/male/gd_cabinet_dir.m4a and b/web/escape/audio/voice/male/gd_cabinet_dir.m4a differ diff --git a/web/escape/audio/voice/male/gd_cabinet_dir.ogg b/web/escape/audio/voice/male/gd_cabinet_dir.ogg index e9777bae..e65e3a9c 100644 Binary files a/web/escape/audio/voice/male/gd_cabinet_dir.ogg and b/web/escape/audio/voice/male/gd_cabinet_dir.ogg differ diff --git a/web/escape/audio/voice/male/gd_gate_dir.m4a b/web/escape/audio/voice/male/gd_gate_dir.m4a index 7be844f8..d13d6b5e 100644 Binary files a/web/escape/audio/voice/male/gd_gate_dir.m4a and b/web/escape/audio/voice/male/gd_gate_dir.m4a differ diff --git a/web/escape/audio/voice/male/gd_gate_dir.ogg b/web/escape/audio/voice/male/gd_gate_dir.ogg index e44b650d..1e934bd8 100644 Binary files a/web/escape/audio/voice/male/gd_gate_dir.ogg and b/web/escape/audio/voice/male/gd_gate_dir.ogg differ diff --git a/web/escape/audio/voice/male/gd_next_fuse.m4a b/web/escape/audio/voice/male/gd_next_fuse.m4a index a7aa85f8..c62383af 100644 Binary files a/web/escape/audio/voice/male/gd_next_fuse.m4a and b/web/escape/audio/voice/male/gd_next_fuse.m4a differ diff --git a/web/escape/audio/voice/male/gd_next_fuse.ogg b/web/escape/audio/voice/male/gd_next_fuse.ogg index e30c6156..a3021e72 100644 Binary files a/web/escape/audio/voice/male/gd_next_fuse.ogg and b/web/escape/audio/voice/male/gd_next_fuse.ogg differ diff --git a/web/escape/audio/voice/male/gd_room_d.m4a b/web/escape/audio/voice/male/gd_room_d.m4a index 50b9306f..2ea532a6 100644 Binary files a/web/escape/audio/voice/male/gd_room_d.m4a and b/web/escape/audio/voice/male/gd_room_d.m4a differ diff --git a/web/escape/audio/voice/male/gd_room_d.ogg b/web/escape/audio/voice/male/gd_room_d.ogg index 4d30543b..37cf2bb6 100644 Binary files a/web/escape/audio/voice/male/gd_room_d.ogg and b/web/escape/audio/voice/male/gd_room_d.ogg differ diff --git a/web/escape/audio/voice/male/gd_room_l.m4a b/web/escape/audio/voice/male/gd_room_l.m4a index b03cf4d8..81d6d8e1 100644 Binary files a/web/escape/audio/voice/male/gd_room_l.m4a and b/web/escape/audio/voice/male/gd_room_l.m4a differ diff --git a/web/escape/audio/voice/male/gd_room_l.ogg b/web/escape/audio/voice/male/gd_room_l.ogg index c28e30c2..0c17f30c 100644 Binary files a/web/escape/audio/voice/male/gd_room_l.ogg and b/web/escape/audio/voice/male/gd_room_l.ogg differ diff --git a/web/escape/audio/voice/male/gd_room_r.m4a b/web/escape/audio/voice/male/gd_room_r.m4a index f123e8f8..055c0f9c 100644 Binary files a/web/escape/audio/voice/male/gd_room_r.m4a and b/web/escape/audio/voice/male/gd_room_r.m4a differ diff --git a/web/escape/audio/voice/male/gd_room_r.ogg b/web/escape/audio/voice/male/gd_room_r.ogg index 2334df89..db7a83a8 100644 Binary files a/web/escape/audio/voice/male/gd_room_r.ogg and b/web/escape/audio/voice/male/gd_room_r.ogg differ diff --git a/web/escape/audio/voice/male/gd_room_u.m4a b/web/escape/audio/voice/male/gd_room_u.m4a index 7244dbb2..fd718c7b 100644 Binary files a/web/escape/audio/voice/male/gd_room_u.m4a and b/web/escape/audio/voice/male/gd_room_u.m4a differ diff --git a/web/escape/audio/voice/male/gd_room_u.ogg b/web/escape/audio/voice/male/gd_room_u.ogg index 9b7385b1..77fab9db 100644 Binary files a/web/escape/audio/voice/male/gd_room_u.ogg and b/web/escape/audio/voice/male/gd_room_u.ogg differ diff --git a/web/escape/audio/voice/male/guide_blunt_swipe.m4a b/web/escape/audio/voice/male/guide_blunt_swipe.m4a new file mode 100644 index 00000000..2c7e5af7 Binary files /dev/null and b/web/escape/audio/voice/male/guide_blunt_swipe.m4a differ diff --git a/web/escape/audio/voice/male/guide_blunt_swipe.ogg b/web/escape/audio/voice/male/guide_blunt_swipe.ogg new file mode 100644 index 00000000..721dcdaa Binary files /dev/null and b/web/escape/audio/voice/male/guide_blunt_swipe.ogg differ diff --git a/web/escape/audio/voice/male/guide_wrong_down.m4a b/web/escape/audio/voice/male/guide_wrong_down.m4a index d9affc09..284fd6ca 100644 Binary files a/web/escape/audio/voice/male/guide_wrong_down.m4a and b/web/escape/audio/voice/male/guide_wrong_down.m4a differ diff --git a/web/escape/audio/voice/male/guide_wrong_down.ogg b/web/escape/audio/voice/male/guide_wrong_down.ogg index 44f9dcbf..8c2b13f3 100644 Binary files a/web/escape/audio/voice/male/guide_wrong_down.ogg and b/web/escape/audio/voice/male/guide_wrong_down.ogg differ diff --git a/web/escape/audio/voice/male/guide_wrong_left.m4a b/web/escape/audio/voice/male/guide_wrong_left.m4a index 82b12fa1..e23332d6 100644 Binary files a/web/escape/audio/voice/male/guide_wrong_left.m4a and b/web/escape/audio/voice/male/guide_wrong_left.m4a differ diff --git a/web/escape/audio/voice/male/guide_wrong_left.ogg b/web/escape/audio/voice/male/guide_wrong_left.ogg index c8ba89a0..cb8fefe2 100644 Binary files a/web/escape/audio/voice/male/guide_wrong_left.ogg and b/web/escape/audio/voice/male/guide_wrong_left.ogg differ diff --git a/web/escape/audio/voice/male/inspect_box_dead.m4a b/web/escape/audio/voice/male/inspect_box_dead.m4a index e30fdcab..65e5a778 100644 Binary files a/web/escape/audio/voice/male/inspect_box_dead.m4a and b/web/escape/audio/voice/male/inspect_box_dead.m4a differ diff --git a/web/escape/audio/voice/male/inspect_box_dead.ogg b/web/escape/audio/voice/male/inspect_box_dead.ogg index dce7cd15..a068298e 100644 Binary files a/web/escape/audio/voice/male/inspect_box_dead.ogg and b/web/escape/audio/voice/male/inspect_box_dead.ogg differ diff --git a/web/escape/audio/voice/male/inspect_cabinet_locked.m4a b/web/escape/audio/voice/male/inspect_cabinet_locked.m4a index f08e1ca9..b79135b2 100644 Binary files a/web/escape/audio/voice/male/inspect_cabinet_locked.m4a and b/web/escape/audio/voice/male/inspect_cabinet_locked.m4a differ diff --git a/web/escape/audio/voice/male/inspect_cabinet_locked.ogg b/web/escape/audio/voice/male/inspect_cabinet_locked.ogg index 528f7865..5201eecf 100644 Binary files a/web/escape/audio/voice/male/inspect_cabinet_locked.ogg and b/web/escape/audio/voice/male/inspect_cabinet_locked.ogg differ diff --git a/web/escape/audio/voice/male/inspect_gate.m4a b/web/escape/audio/voice/male/inspect_gate.m4a index 0373c176..938303fa 100644 Binary files a/web/escape/audio/voice/male/inspect_gate.m4a and b/web/escape/audio/voice/male/inspect_gate.m4a differ diff --git a/web/escape/audio/voice/male/inspect_gate.ogg b/web/escape/audio/voice/male/inspect_gate.ogg index 2db97401..7c88fbeb 100644 Binary files a/web/escape/audio/voice/male/inspect_gate.ogg and b/web/escape/audio/voice/male/inspect_gate.ogg differ diff --git a/web/escape/audio/voice/male/inspect_gate_ready.m4a b/web/escape/audio/voice/male/inspect_gate_ready.m4a index e91971b8..ccbff53a 100644 Binary files a/web/escape/audio/voice/male/inspect_gate_ready.m4a and b/web/escape/audio/voice/male/inspect_gate_ready.m4a differ diff --git a/web/escape/audio/voice/male/inspect_gate_ready.ogg b/web/escape/audio/voice/male/inspect_gate_ready.ogg index 1526a069..74245486 100644 Binary files a/web/escape/audio/voice/male/inspect_gate_ready.ogg and b/web/escape/audio/voice/male/inspect_gate_ready.ogg differ diff --git a/web/escape/audio/voice/male/inv_generic.m4a b/web/escape/audio/voice/male/inv_generic.m4a new file mode 100644 index 00000000..7d62c38b Binary files /dev/null and b/web/escape/audio/voice/male/inv_generic.m4a differ diff --git a/web/escape/audio/voice/male/inv_generic.ogg b/web/escape/audio/voice/male/inv_generic.ogg new file mode 100644 index 00000000..893f1eae Binary files /dev/null and b/web/escape/audio/voice/male/inv_generic.ogg differ diff --git a/web/escape/audio/voice/male/inv_settle.m4a b/web/escape/audio/voice/male/inv_settle.m4a new file mode 100644 index 00000000..09c9b4f3 Binary files /dev/null and b/web/escape/audio/voice/male/inv_settle.m4a differ diff --git a/web/escape/audio/voice/male/inv_settle.ogg b/web/escape/audio/voice/male/inv_settle.ogg new file mode 100644 index 00000000..b4ae0c32 Binary files /dev/null and b/web/escape/audio/voice/male/inv_settle.ogg differ diff --git a/web/escape/audio/voice/male/key/again.m4a b/web/escape/audio/voice/male/key/again.m4a index ab81eb7b..19f6ffaa 100644 Binary files a/web/escape/audio/voice/male/key/again.m4a and b/web/escape/audio/voice/male/key/again.m4a differ diff --git a/web/escape/audio/voice/male/key/again.ogg b/web/escape/audio/voice/male/key/again.ogg index 60ef67b6..cf402a17 100644 Binary files a/web/escape/audio/voice/male/key/again.ogg and b/web/escape/audio/voice/male/key/again.ogg differ diff --git a/web/escape/audio/voice/male/key/g0_new.m4a b/web/escape/audio/voice/male/key/g0_new.m4a new file mode 100644 index 00000000..c278db93 Binary files /dev/null and b/web/escape/audio/voice/male/key/g0_new.m4a differ diff --git a/web/escape/audio/voice/male/key/g0_new.ogg b/web/escape/audio/voice/male/key/g0_new.ogg new file mode 100644 index 00000000..5287e56f Binary files /dev/null and b/web/escape/audio/voice/male/key/g0_new.ogg differ diff --git a/web/escape/audio/voice/male/key/g0b.m4a b/web/escape/audio/voice/male/key/g0b.m4a new file mode 100644 index 00000000..17517b16 Binary files /dev/null and b/web/escape/audio/voice/male/key/g0b.m4a differ diff --git a/web/escape/audio/voice/male/key/g0b.ogg b/web/escape/audio/voice/male/key/g0b.ogg new file mode 100644 index 00000000..39cfb88a Binary files /dev/null and b/web/escape/audio/voice/male/key/g0b.ogg differ diff --git a/web/escape/audio/voice/male/key/g3_new.m4a b/web/escape/audio/voice/male/key/g3_new.m4a new file mode 100644 index 00000000..ddc7724a Binary files /dev/null and b/web/escape/audio/voice/male/key/g3_new.m4a differ diff --git a/web/escape/audio/voice/male/key/g3_new.ogg b/web/escape/audio/voice/male/key/g3_new.ogg new file mode 100644 index 00000000..b02a9726 Binary files /dev/null and b/web/escape/audio/voice/male/key/g3_new.ogg differ diff --git a/web/escape/audio/voice/male/key/g4_down.m4a b/web/escape/audio/voice/male/key/g4_down.m4a index 3f16b7c0..89bb3850 100644 Binary files a/web/escape/audio/voice/male/key/g4_down.m4a and b/web/escape/audio/voice/male/key/g4_down.m4a differ diff --git a/web/escape/audio/voice/male/key/g4_down.ogg b/web/escape/audio/voice/male/key/g4_down.ogg index e67ead0f..962e2b8b 100644 Binary files a/web/escape/audio/voice/male/key/g4_down.ogg and b/web/escape/audio/voice/male/key/g4_down.ogg differ diff --git a/web/escape/audio/voice/male/key/g5_inspect.m4a b/web/escape/audio/voice/male/key/g5_inspect.m4a index d9cdba15..2c1d05be 100644 Binary files a/web/escape/audio/voice/male/key/g5_inspect.m4a and b/web/escape/audio/voice/male/key/g5_inspect.m4a differ diff --git a/web/escape/audio/voice/male/key/g5_inspect.ogg b/web/escape/audio/voice/male/key/g5_inspect.ogg index 9f508d06..fc36c82d 100644 Binary files a/web/escape/audio/voice/male/key/g5_inspect.ogg and b/web/escape/audio/voice/male/key/g5_inspect.ogg differ diff --git a/web/escape/audio/voice/male/key/g7_goal_new.m4a b/web/escape/audio/voice/male/key/g7_goal_new.m4a new file mode 100644 index 00000000..5e7ea1f2 Binary files /dev/null and b/web/escape/audio/voice/male/key/g7_goal_new.m4a differ diff --git a/web/escape/audio/voice/male/key/g7_goal_new.ogg b/web/escape/audio/voice/male/key/g7_goal_new.ogg new file mode 100644 index 00000000..c7a3f73a Binary files /dev/null and b/web/escape/audio/voice/male/key/g7_goal_new.ogg differ diff --git a/web/escape/audio/voice/male/key/g_hint_swipe.m4a b/web/escape/audio/voice/male/key/g_hint_swipe.m4a new file mode 100644 index 00000000..afc7c432 Binary files /dev/null and b/web/escape/audio/voice/male/key/g_hint_swipe.m4a differ diff --git a/web/escape/audio/voice/male/key/g_hint_swipe.ogg b/web/escape/audio/voice/male/key/g_hint_swipe.ogg new file mode 100644 index 00000000..b3e8bb15 Binary files /dev/null and b/web/escape/audio/voice/male/key/g_hint_swipe.ogg differ diff --git a/web/escape/audio/voice/male/key/g_wrong_tap.m4a b/web/escape/audio/voice/male/key/g_wrong_tap.m4a new file mode 100644 index 00000000..e973ca79 Binary files /dev/null and b/web/escape/audio/voice/male/key/g_wrong_tap.m4a differ diff --git a/web/escape/audio/voice/male/key/g_wrong_tap.ogg b/web/escape/audio/voice/male/key/g_wrong_tap.ogg new file mode 100644 index 00000000..0e58de4c Binary files /dev/null and b/web/escape/audio/voice/male/key/g_wrong_tap.ogg differ diff --git a/web/escape/audio/voice/male/key/guide_blunt_swipe.m4a b/web/escape/audio/voice/male/key/guide_blunt_swipe.m4a new file mode 100644 index 00000000..dac1ff1c Binary files /dev/null and b/web/escape/audio/voice/male/key/guide_blunt_swipe.m4a differ diff --git a/web/escape/audio/voice/male/key/guide_blunt_swipe.ogg b/web/escape/audio/voice/male/key/guide_blunt_swipe.ogg new file mode 100644 index 00000000..bb67bf5a Binary files /dev/null and b/web/escape/audio/voice/male/key/guide_blunt_swipe.ogg differ diff --git a/web/escape/audio/voice/male/key/guide_wrong_down.m4a b/web/escape/audio/voice/male/key/guide_wrong_down.m4a index a0b21931..68f3708e 100644 Binary files a/web/escape/audio/voice/male/key/guide_wrong_down.m4a and b/web/escape/audio/voice/male/key/guide_wrong_down.m4a differ diff --git a/web/escape/audio/voice/male/key/guide_wrong_down.ogg b/web/escape/audio/voice/male/key/guide_wrong_down.ogg index 37e889a7..89e5e117 100644 Binary files a/web/escape/audio/voice/male/key/guide_wrong_down.ogg and b/web/escape/audio/voice/male/key/guide_wrong_down.ogg differ diff --git a/web/escape/audio/voice/male/key/guide_wrong_left.m4a b/web/escape/audio/voice/male/key/guide_wrong_left.m4a index c56367eb..7516144a 100644 Binary files a/web/escape/audio/voice/male/key/guide_wrong_left.m4a and b/web/escape/audio/voice/male/key/guide_wrong_left.m4a differ diff --git a/web/escape/audio/voice/male/key/guide_wrong_left.ogg b/web/escape/audio/voice/male/key/guide_wrong_left.ogg index 7cbc4159..5c7aa597 100644 Binary files a/web/escape/audio/voice/male/key/guide_wrong_left.ogg and b/web/escape/audio/voice/male/key/guide_wrong_left.ogg differ diff --git a/web/escape/audio/voice/male/key/inspect_gate_ready.m4a b/web/escape/audio/voice/male/key/inspect_gate_ready.m4a index 5e1be61c..5de59f20 100644 Binary files a/web/escape/audio/voice/male/key/inspect_gate_ready.m4a and b/web/escape/audio/voice/male/key/inspect_gate_ready.m4a differ diff --git a/web/escape/audio/voice/male/key/inspect_gate_ready.ogg b/web/escape/audio/voice/male/key/inspect_gate_ready.ogg index ba004426..59b6683c 100644 Binary files a/web/escape/audio/voice/male/key/inspect_gate_ready.ogg and b/web/escape/audio/voice/male/key/inspect_gate_ready.ogg differ diff --git a/web/escape/audio/voice/male/key/inv_settle.m4a b/web/escape/audio/voice/male/key/inv_settle.m4a new file mode 100644 index 00000000..3a9a83bb Binary files /dev/null and b/web/escape/audio/voice/male/key/inv_settle.m4a differ diff --git a/web/escape/audio/voice/male/key/inv_settle.ogg b/web/escape/audio/voice/male/key/inv_settle.ogg new file mode 100644 index 00000000..84955f50 Binary files /dev/null and b/web/escape/audio/voice/male/key/inv_settle.ogg differ diff --git a/web/escape/audio/voice/male/key/lp_here.m4a b/web/escape/audio/voice/male/key/lp_here.m4a new file mode 100644 index 00000000..b647b9be Binary files /dev/null and b/web/escape/audio/voice/male/key/lp_here.m4a differ diff --git a/web/escape/audio/voice/male/key/lp_here.ogg b/web/escape/audio/voice/male/key/lp_here.ogg new file mode 100644 index 00000000..92c15a42 Binary files /dev/null and b/web/escape/audio/voice/male/key/lp_here.ogg differ diff --git a/web/escape/audio/voice/male/key/lp_tap.m4a b/web/escape/audio/voice/male/key/lp_tap.m4a new file mode 100644 index 00000000..88efb4d2 Binary files /dev/null and b/web/escape/audio/voice/male/key/lp_tap.m4a differ diff --git a/web/escape/audio/voice/male/key/lp_tap.ogg b/web/escape/audio/voice/male/key/lp_tap.ogg new file mode 100644 index 00000000..6a44d915 Binary files /dev/null and b/web/escape/audio/voice/male/key/lp_tap.ogg differ diff --git a/web/escape/audio/voice/male/key/menu_help.m4a b/web/escape/audio/voice/male/key/menu_help.m4a new file mode 100644 index 00000000..94b2de77 Binary files /dev/null and b/web/escape/audio/voice/male/key/menu_help.m4a differ diff --git a/web/escape/audio/voice/male/key/menu_help.ogg b/web/escape/audio/voice/male/key/menu_help.ogg new file mode 100644 index 00000000..60a029ab Binary files /dev/null and b/web/escape/audio/voice/male/key/menu_help.ogg differ diff --git a/web/escape/audio/voice/male/key/nav_down.m4a b/web/escape/audio/voice/male/key/nav_down.m4a index 2f784968..6c6fecd8 100644 Binary files a/web/escape/audio/voice/male/key/nav_down.m4a and b/web/escape/audio/voice/male/key/nav_down.m4a differ diff --git a/web/escape/audio/voice/male/key/nav_down.ogg b/web/escape/audio/voice/male/key/nav_down.ogg index e33e6b9b..a05acce4 100644 Binary files a/web/escape/audio/voice/male/key/nav_down.ogg and b/web/escape/audio/voice/male/key/nav_down.ogg differ diff --git a/web/escape/audio/voice/male/key/nav_left.m4a b/web/escape/audio/voice/male/key/nav_left.m4a index afe82485..7890a56d 100644 Binary files a/web/escape/audio/voice/male/key/nav_left.m4a and b/web/escape/audio/voice/male/key/nav_left.m4a differ diff --git a/web/escape/audio/voice/male/key/nav_left.ogg b/web/escape/audio/voice/male/key/nav_left.ogg index d7985757..104396d5 100644 Binary files a/web/escape/audio/voice/male/key/nav_left.ogg and b/web/escape/audio/voice/male/key/nav_left.ogg differ diff --git a/web/escape/audio/voice/male/key/nav_right.m4a b/web/escape/audio/voice/male/key/nav_right.m4a index 721ee3ca..cb927997 100644 Binary files a/web/escape/audio/voice/male/key/nav_right.m4a and b/web/escape/audio/voice/male/key/nav_right.m4a differ diff --git a/web/escape/audio/voice/male/key/nav_right.ogg b/web/escape/audio/voice/male/key/nav_right.ogg index 348b82aa..7585a5e8 100644 Binary files a/web/escape/audio/voice/male/key/nav_right.ogg and b/web/escape/audio/voice/male/key/nav_right.ogg differ diff --git a/web/escape/audio/voice/male/key/nav_up.m4a b/web/escape/audio/voice/male/key/nav_up.m4a index 0facb6ae..9b209ca2 100644 Binary files a/web/escape/audio/voice/male/key/nav_up.m4a and b/web/escape/audio/voice/male/key/nav_up.m4a differ diff --git a/web/escape/audio/voice/male/key/nav_up.ogg b/web/escape/audio/voice/male/key/nav_up.ogg index 6f055a07..a8cc4da4 100644 Binary files a/web/escape/audio/voice/male/key/nav_up.ogg and b/web/escape/audio/voice/male/key/nav_up.ogg differ diff --git a/web/escape/audio/voice/male/key/start_room.m4a b/web/escape/audio/voice/male/key/start_room.m4a index 20fc178a..89997ff5 100644 Binary files a/web/escape/audio/voice/male/key/start_room.m4a and b/web/escape/audio/voice/male/key/start_room.m4a differ diff --git a/web/escape/audio/voice/male/key/start_room.ogg b/web/escape/audio/voice/male/key/start_room.ogg index e21c0f58..90936bec 100644 Binary files a/web/escape/audio/voice/male/key/start_room.ogg and b/web/escape/audio/voice/male/key/start_room.ogg differ diff --git a/web/escape/audio/voice/male/key/sys_continue_ask.m4a b/web/escape/audio/voice/male/key/sys_continue_ask.m4a new file mode 100644 index 00000000..5463c7b0 Binary files /dev/null and b/web/escape/audio/voice/male/key/sys_continue_ask.m4a differ diff --git a/web/escape/audio/voice/male/key/sys_continue_ask.ogg b/web/escape/audio/voice/male/key/sys_continue_ask.ogg new file mode 100644 index 00000000..4ffa5d8b Binary files /dev/null and b/web/escape/audio/voice/male/key/sys_continue_ask.ogg differ diff --git a/web/escape/audio/voice/male/key/sys_only_down.m4a b/web/escape/audio/voice/male/key/sys_only_down.m4a new file mode 100644 index 00000000..3db7fca1 Binary files /dev/null and b/web/escape/audio/voice/male/key/sys_only_down.m4a differ diff --git a/web/escape/audio/voice/male/key/sys_only_down.ogg b/web/escape/audio/voice/male/key/sys_only_down.ogg new file mode 100644 index 00000000..0707c4c9 Binary files /dev/null and b/web/escape/audio/voice/male/key/sys_only_down.ogg differ diff --git a/web/escape/audio/voice/male/key/sys_only_left.m4a b/web/escape/audio/voice/male/key/sys_only_left.m4a new file mode 100644 index 00000000..af5675aa Binary files /dev/null and b/web/escape/audio/voice/male/key/sys_only_left.m4a differ diff --git a/web/escape/audio/voice/male/key/sys_only_left.ogg b/web/escape/audio/voice/male/key/sys_only_left.ogg new file mode 100644 index 00000000..36b20236 Binary files /dev/null and b/web/escape/audio/voice/male/key/sys_only_left.ogg differ diff --git a/web/escape/audio/voice/male/key/sys_only_right.m4a b/web/escape/audio/voice/male/key/sys_only_right.m4a new file mode 100644 index 00000000..bfb87a1e Binary files /dev/null and b/web/escape/audio/voice/male/key/sys_only_right.m4a differ diff --git a/web/escape/audio/voice/male/key/sys_only_right.ogg b/web/escape/audio/voice/male/key/sys_only_right.ogg new file mode 100644 index 00000000..bbd769bb Binary files /dev/null and b/web/escape/audio/voice/male/key/sys_only_right.ogg differ diff --git a/web/escape/audio/voice/male/key/sys_only_up.m4a b/web/escape/audio/voice/male/key/sys_only_up.m4a new file mode 100644 index 00000000..5c3a185f Binary files /dev/null and b/web/escape/audio/voice/male/key/sys_only_up.m4a differ diff --git a/web/escape/audio/voice/male/key/sys_only_up.ogg b/web/escape/audio/voice/male/key/sys_only_up.ogg new file mode 100644 index 00000000..77531440 Binary files /dev/null and b/web/escape/audio/voice/male/key/sys_only_up.ogg differ diff --git a/web/escape/audio/voice/male/lp_box.m4a b/web/escape/audio/voice/male/lp_box.m4a new file mode 100644 index 00000000..1ee11695 Binary files /dev/null and b/web/escape/audio/voice/male/lp_box.m4a differ diff --git a/web/escape/audio/voice/male/lp_box.ogg b/web/escape/audio/voice/male/lp_box.ogg new file mode 100644 index 00000000..b4bf85f7 Binary files /dev/null and b/web/escape/audio/voice/male/lp_box.ogg differ diff --git a/web/escape/audio/voice/male/lp_cabinet.m4a b/web/escape/audio/voice/male/lp_cabinet.m4a new file mode 100644 index 00000000..7f6c1d87 Binary files /dev/null and b/web/escape/audio/voice/male/lp_cabinet.m4a differ diff --git a/web/escape/audio/voice/male/lp_cabinet.ogg b/web/escape/audio/voice/male/lp_cabinet.ogg new file mode 100644 index 00000000..00cd890c Binary files /dev/null and b/web/escape/audio/voice/male/lp_cabinet.ogg differ diff --git a/web/escape/audio/voice/male/lp_gate.m4a b/web/escape/audio/voice/male/lp_gate.m4a new file mode 100644 index 00000000..f9b7edb2 Binary files /dev/null and b/web/escape/audio/voice/male/lp_gate.m4a differ diff --git a/web/escape/audio/voice/male/lp_gate.ogg b/web/escape/audio/voice/male/lp_gate.ogg new file mode 100644 index 00000000..0e43021c Binary files /dev/null and b/web/escape/audio/voice/male/lp_gate.ogg differ diff --git a/web/escape/audio/voice/male/lp_here.m4a b/web/escape/audio/voice/male/lp_here.m4a new file mode 100644 index 00000000..a0e78699 Binary files /dev/null and b/web/escape/audio/voice/male/lp_here.m4a differ diff --git a/web/escape/audio/voice/male/lp_here.ogg b/web/escape/audio/voice/male/lp_here.ogg new file mode 100644 index 00000000..3725d999 Binary files /dev/null and b/web/escape/audio/voice/male/lp_here.ogg differ diff --git a/web/escape/audio/voice/male/lp_key.m4a b/web/escape/audio/voice/male/lp_key.m4a new file mode 100644 index 00000000..d3943474 Binary files /dev/null and b/web/escape/audio/voice/male/lp_key.m4a differ diff --git a/web/escape/audio/voice/male/lp_key.ogg b/web/escape/audio/voice/male/lp_key.ogg new file mode 100644 index 00000000..02cb2157 Binary files /dev/null and b/web/escape/audio/voice/male/lp_key.ogg differ diff --git a/web/escape/audio/voice/male/lp_tap.m4a b/web/escape/audio/voice/male/lp_tap.m4a new file mode 100644 index 00000000..f2cea910 Binary files /dev/null and b/web/escape/audio/voice/male/lp_tap.m4a differ diff --git a/web/escape/audio/voice/male/lp_tap.ogg b/web/escape/audio/voice/male/lp_tap.ogg new file mode 100644 index 00000000..a4b6379c Binary files /dev/null and b/web/escape/audio/voice/male/lp_tap.ogg differ diff --git a/web/escape/audio/voice/male/menu_help.m4a b/web/escape/audio/voice/male/menu_help.m4a new file mode 100644 index 00000000..13d1d900 Binary files /dev/null and b/web/escape/audio/voice/male/menu_help.m4a differ diff --git a/web/escape/audio/voice/male/menu_help.ogg b/web/escape/audio/voice/male/menu_help.ogg new file mode 100644 index 00000000..0e78abce Binary files /dev/null and b/web/escape/audio/voice/male/menu_help.ogg differ diff --git a/web/escape/audio/voice/male/menu_item1.m4a b/web/escape/audio/voice/male/menu_item1.m4a new file mode 100644 index 00000000..04e683a3 Binary files /dev/null and b/web/escape/audio/voice/male/menu_item1.m4a differ diff --git a/web/escape/audio/voice/male/menu_item1.ogg b/web/escape/audio/voice/male/menu_item1.ogg new file mode 100644 index 00000000..148c635d Binary files /dev/null and b/web/escape/audio/voice/male/menu_item1.ogg differ diff --git a/web/escape/audio/voice/male/menu_item2.m4a b/web/escape/audio/voice/male/menu_item2.m4a new file mode 100644 index 00000000..1cc3fd1f Binary files /dev/null and b/web/escape/audio/voice/male/menu_item2.m4a differ diff --git a/web/escape/audio/voice/male/menu_item2.ogg b/web/escape/audio/voice/male/menu_item2.ogg new file mode 100644 index 00000000..7666e84c Binary files /dev/null and b/web/escape/audio/voice/male/menu_item2.ogg differ diff --git a/web/escape/audio/voice/male/menu_item3.m4a b/web/escape/audio/voice/male/menu_item3.m4a new file mode 100644 index 00000000..d4d5493b Binary files /dev/null and b/web/escape/audio/voice/male/menu_item3.m4a differ diff --git a/web/escape/audio/voice/male/menu_item3.ogg b/web/escape/audio/voice/male/menu_item3.ogg new file mode 100644 index 00000000..5e963f94 Binary files /dev/null and b/web/escape/audio/voice/male/menu_item3.ogg differ diff --git a/web/escape/audio/voice/male/menu_open.m4a b/web/escape/audio/voice/male/menu_open.m4a index f978c4e6..5dcf913b 100644 Binary files a/web/escape/audio/voice/male/menu_open.m4a and b/web/escape/audio/voice/male/menu_open.m4a differ diff --git a/web/escape/audio/voice/male/menu_open.ogg b/web/escape/audio/voice/male/menu_open.ogg index 8eea57f8..785c814b 100644 Binary files a/web/escape/audio/voice/male/menu_open.ogg and b/web/escape/audio/voice/male/menu_open.ogg differ diff --git a/web/escape/audio/voice/male/nav_down.m4a b/web/escape/audio/voice/male/nav_down.m4a index 207b8ec1..a232819e 100644 Binary files a/web/escape/audio/voice/male/nav_down.m4a and b/web/escape/audio/voice/male/nav_down.m4a differ diff --git a/web/escape/audio/voice/male/nav_down.ogg b/web/escape/audio/voice/male/nav_down.ogg index 4b2306ac..b7d7d213 100644 Binary files a/web/escape/audio/voice/male/nav_down.ogg and b/web/escape/audio/voice/male/nav_down.ogg differ diff --git a/web/escape/audio/voice/male/nav_left.m4a b/web/escape/audio/voice/male/nav_left.m4a index b7ef8b15..9f380003 100644 Binary files a/web/escape/audio/voice/male/nav_left.m4a and b/web/escape/audio/voice/male/nav_left.m4a differ diff --git a/web/escape/audio/voice/male/nav_left.ogg b/web/escape/audio/voice/male/nav_left.ogg index 61bf56a4..a8cccd52 100644 Binary files a/web/escape/audio/voice/male/nav_left.ogg and b/web/escape/audio/voice/male/nav_left.ogg differ diff --git a/web/escape/audio/voice/male/nav_right.m4a b/web/escape/audio/voice/male/nav_right.m4a index b18edf24..b0cac09c 100644 Binary files a/web/escape/audio/voice/male/nav_right.m4a and b/web/escape/audio/voice/male/nav_right.m4a differ diff --git a/web/escape/audio/voice/male/nav_right.ogg b/web/escape/audio/voice/male/nav_right.ogg index f126fabe..15aef861 100644 Binary files a/web/escape/audio/voice/male/nav_right.ogg and b/web/escape/audio/voice/male/nav_right.ogg differ diff --git a/web/escape/audio/voice/male/nav_up.m4a b/web/escape/audio/voice/male/nav_up.m4a index 40fedd38..fc5f3430 100644 Binary files a/web/escape/audio/voice/male/nav_up.m4a and b/web/escape/audio/voice/male/nav_up.m4a differ diff --git a/web/escape/audio/voice/male/nav_up.ogg b/web/escape/audio/voice/male/nav_up.ogg index d80adae8..69142542 100644 Binary files a/web/escape/audio/voice/male/nav_up.ogg and b/web/escape/audio/voice/male/nav_up.ogg differ diff --git a/web/escape/audio/voice/male/replay.m4a b/web/escape/audio/voice/male/replay.m4a index ab74dc5f..549ee88e 100644 Binary files a/web/escape/audio/voice/male/replay.m4a and b/web/escape/audio/voice/male/replay.m4a differ diff --git a/web/escape/audio/voice/male/replay.ogg b/web/escape/audio/voice/male/replay.ogg index 30787521..fb7ad410 100644 Binary files a/web/escape/audio/voice/male/replay.ogg and b/web/escape/audio/voice/male/replay.ogg differ diff --git a/web/escape/audio/voice/male/room_cabinet.m4a b/web/escape/audio/voice/male/room_cabinet.m4a index b7e986dc..66426afa 100644 Binary files a/web/escape/audio/voice/male/room_cabinet.m4a and b/web/escape/audio/voice/male/room_cabinet.m4a differ diff --git a/web/escape/audio/voice/male/room_cabinet.ogg b/web/escape/audio/voice/male/room_cabinet.ogg index d0daee7a..a5bf3cdb 100644 Binary files a/web/escape/audio/voice/male/room_cabinet.ogg and b/web/escape/audio/voice/male/room_cabinet.ogg differ diff --git a/web/escape/audio/voice/male/room_gate.m4a b/web/escape/audio/voice/male/room_gate.m4a index c6f96060..9f14d221 100644 Binary files a/web/escape/audio/voice/male/room_gate.m4a and b/web/escape/audio/voice/male/room_gate.m4a differ diff --git a/web/escape/audio/voice/male/room_gate.ogg b/web/escape/audio/voice/male/room_gate.ogg index c50b0dc6..4dd43a34 100644 Binary files a/web/escape/audio/voice/male/room_gate.ogg and b/web/escape/audio/voice/male/room_gate.ogg differ diff --git a/web/escape/audio/voice/male/room_power.m4a b/web/escape/audio/voice/male/room_power.m4a index b4b3fb5a..db1af162 100644 Binary files a/web/escape/audio/voice/male/room_power.m4a and b/web/escape/audio/voice/male/room_power.m4a differ diff --git a/web/escape/audio/voice/male/room_power.ogg b/web/escape/audio/voice/male/room_power.ogg index e669bb7a..31e12bfd 100644 Binary files a/web/escape/audio/voice/male/room_power.ogg and b/web/escape/audio/voice/male/room_power.ogg differ diff --git a/web/escape/audio/voice/male/room_storage.m4a b/web/escape/audio/voice/male/room_storage.m4a index 3c1fa782..f743681a 100644 Binary files a/web/escape/audio/voice/male/room_storage.m4a and b/web/escape/audio/voice/male/room_storage.m4a differ diff --git a/web/escape/audio/voice/male/room_storage.ogg b/web/escape/audio/voice/male/room_storage.ogg index 3bbe8aac..280db6c5 100644 Binary files a/web/escape/audio/voice/male/room_storage.ogg and b/web/escape/audio/voice/male/room_storage.ogg differ diff --git a/web/escape/audio/voice/male/start_room.m4a b/web/escape/audio/voice/male/start_room.m4a index ddbb24a0..0f2441fb 100644 Binary files a/web/escape/audio/voice/male/start_room.m4a and b/web/escape/audio/voice/male/start_room.m4a differ diff --git a/web/escape/audio/voice/male/start_room.ogg b/web/escape/audio/voice/male/start_room.ogg index 9014b533..0ca0f852 100644 Binary files a/web/escape/audio/voice/male/start_room.ogg and b/web/escape/audio/voice/male/start_room.ogg differ diff --git a/web/escape/audio/voice/male/sys_continue_ask.m4a b/web/escape/audio/voice/male/sys_continue_ask.m4a new file mode 100644 index 00000000..a8f50217 Binary files /dev/null and b/web/escape/audio/voice/male/sys_continue_ask.m4a differ diff --git a/web/escape/audio/voice/male/sys_continue_ask.ogg b/web/escape/audio/voice/male/sys_continue_ask.ogg new file mode 100644 index 00000000..42178fc0 Binary files /dev/null and b/web/escape/audio/voice/male/sys_continue_ask.ogg differ diff --git a/web/escape/audio/voice/male/sys_demo.m4a b/web/escape/audio/voice/male/sys_demo.m4a new file mode 100644 index 00000000..c1e14164 Binary files /dev/null and b/web/escape/audio/voice/male/sys_demo.m4a differ diff --git a/web/escape/audio/voice/male/sys_demo.ogg b/web/escape/audio/voice/male/sys_demo.ogg new file mode 100644 index 00000000..0ed97d6f Binary files /dev/null and b/web/escape/audio/voice/male/sys_demo.ogg differ diff --git a/web/escape/audio/voice/male/sys_now_you.m4a b/web/escape/audio/voice/male/sys_now_you.m4a new file mode 100644 index 00000000..8b94716e Binary files /dev/null and b/web/escape/audio/voice/male/sys_now_you.m4a differ diff --git a/web/escape/audio/voice/male/sys_now_you.ogg b/web/escape/audio/voice/male/sys_now_you.ogg new file mode 100644 index 00000000..cca0e079 Binary files /dev/null and b/web/escape/audio/voice/male/sys_now_you.ogg differ diff --git a/web/escape/audio/voice/male/sys_only_down.m4a b/web/escape/audio/voice/male/sys_only_down.m4a new file mode 100644 index 00000000..4528386b Binary files /dev/null and b/web/escape/audio/voice/male/sys_only_down.m4a differ diff --git a/web/escape/audio/voice/male/sys_only_down.ogg b/web/escape/audio/voice/male/sys_only_down.ogg new file mode 100644 index 00000000..5a0933e9 Binary files /dev/null and b/web/escape/audio/voice/male/sys_only_down.ogg differ diff --git a/web/escape/audio/voice/male/sys_only_left.m4a b/web/escape/audio/voice/male/sys_only_left.m4a new file mode 100644 index 00000000..326a9745 Binary files /dev/null and b/web/escape/audio/voice/male/sys_only_left.m4a differ diff --git a/web/escape/audio/voice/male/sys_only_left.ogg b/web/escape/audio/voice/male/sys_only_left.ogg new file mode 100644 index 00000000..7b10ed36 Binary files /dev/null and b/web/escape/audio/voice/male/sys_only_left.ogg differ diff --git a/web/escape/audio/voice/male/sys_only_right.m4a b/web/escape/audio/voice/male/sys_only_right.m4a new file mode 100644 index 00000000..55398d18 Binary files /dev/null and b/web/escape/audio/voice/male/sys_only_right.m4a differ diff --git a/web/escape/audio/voice/male/sys_only_right.ogg b/web/escape/audio/voice/male/sys_only_right.ogg new file mode 100644 index 00000000..0191c3c2 Binary files /dev/null and b/web/escape/audio/voice/male/sys_only_right.ogg differ diff --git a/web/escape/audio/voice/male/sys_only_up.m4a b/web/escape/audio/voice/male/sys_only_up.m4a new file mode 100644 index 00000000..99b3ea2c Binary files /dev/null and b/web/escape/audio/voice/male/sys_only_up.m4a differ diff --git a/web/escape/audio/voice/male/sys_only_up.ogg b/web/escape/audio/voice/male/sys_only_up.ogg new file mode 100644 index 00000000..d5124bf7 Binary files /dev/null and b/web/escape/audio/voice/male/sys_only_up.ogg differ diff --git a/web/escape/audio/voice/male/sys_say_again.m4a b/web/escape/audio/voice/male/sys_say_again.m4a new file mode 100644 index 00000000..229d4fe3 Binary files /dev/null and b/web/escape/audio/voice/male/sys_say_again.m4a differ diff --git a/web/escape/audio/voice/male/sys_say_again.ogg b/web/escape/audio/voice/male/sys_say_again.ogg new file mode 100644 index 00000000..dcf0b240 Binary files /dev/null and b/web/escape/audio/voice/male/sys_say_again.ogg differ diff --git a/web/escape/audio/voice/male/time_band1.m4a b/web/escape/audio/voice/male/time_band1.m4a index bb64313e..6b0aec1a 100644 Binary files a/web/escape/audio/voice/male/time_band1.m4a and b/web/escape/audio/voice/male/time_band1.m4a differ diff --git a/web/escape/audio/voice/male/time_band1.ogg b/web/escape/audio/voice/male/time_band1.ogg index 1a6f7b67..5ab21337 100644 Binary files a/web/escape/audio/voice/male/time_band1.ogg and b/web/escape/audio/voice/male/time_band1.ogg differ diff --git a/web/escape/audio/voice/male/time_band2.m4a b/web/escape/audio/voice/male/time_band2.m4a index 44e49c2a..16c8c423 100644 Binary files a/web/escape/audio/voice/male/time_band2.m4a and b/web/escape/audio/voice/male/time_band2.m4a differ diff --git a/web/escape/audio/voice/male/time_band2.ogg b/web/escape/audio/voice/male/time_band2.ogg index e9f8ff1b..7db6dc12 100644 Binary files a/web/escape/audio/voice/male/time_band2.ogg and b/web/escape/audio/voice/male/time_band2.ogg differ diff --git a/web/escape/audio/voice/male/time_band3.m4a b/web/escape/audio/voice/male/time_band3.m4a index 8d614bbe..bf3a6e6e 100644 Binary files a/web/escape/audio/voice/male/time_band3.m4a and b/web/escape/audio/voice/male/time_band3.m4a differ diff --git a/web/escape/audio/voice/male/time_band3.ogg b/web/escape/audio/voice/male/time_band3.ogg index dbe55931..cce6c1f5 100644 Binary files a/web/escape/audio/voice/male/time_band3.ogg and b/web/escape/audio/voice/male/time_band3.ogg differ diff --git a/web/escape/audio/voice/male/time_band4.m4a b/web/escape/audio/voice/male/time_band4.m4a index acf75578..12383009 100644 Binary files a/web/escape/audio/voice/male/time_band4.m4a and b/web/escape/audio/voice/male/time_band4.m4a differ diff --git a/web/escape/audio/voice/male/time_band4.ogg b/web/escape/audio/voice/male/time_band4.ogg index e6138936..d14764c2 100644 Binary files a/web/escape/audio/voice/male/time_band4.ogg and b/web/escape/audio/voice/male/time_band4.ogg differ diff --git a/web/escape/audio/voice/male/unlock_cabinet.m4a b/web/escape/audio/voice/male/unlock_cabinet.m4a index eedd0527..0a6a3e22 100644 Binary files a/web/escape/audio/voice/male/unlock_cabinet.m4a and b/web/escape/audio/voice/male/unlock_cabinet.m4a differ diff --git a/web/escape/audio/voice/male/unlock_cabinet.ogg b/web/escape/audio/voice/male/unlock_cabinet.ogg index 4a4a24dc..070dd76c 100644 Binary files a/web/escape/audio/voice/male/unlock_cabinet.ogg and b/web/escape/audio/voice/male/unlock_cabinet.ogg differ diff --git a/web/escape/audio/voice/male/wall.m4a b/web/escape/audio/voice/male/wall.m4a index 6457279b..7c09798b 100644 Binary files a/web/escape/audio/voice/male/wall.m4a and b/web/escape/audio/voice/male/wall.m4a differ diff --git a/web/escape/audio/voice/male/wall.ogg b/web/escape/audio/voice/male/wall.ogg index 7ceddd74..f68ba4bb 100644 Binary files a/web/escape/audio/voice/male/wall.ogg and b/web/escape/audio/voice/male/wall.ogg differ diff --git a/web/escape/audio/voice/male/wall_dir_d.m4a b/web/escape/audio/voice/male/wall_dir_d.m4a index 75ff2e34..ad08129c 100644 Binary files a/web/escape/audio/voice/male/wall_dir_d.m4a and b/web/escape/audio/voice/male/wall_dir_d.m4a differ diff --git a/web/escape/audio/voice/male/wall_dir_d.ogg b/web/escape/audio/voice/male/wall_dir_d.ogg index ee7f878a..a3b99bca 100644 Binary files a/web/escape/audio/voice/male/wall_dir_d.ogg and b/web/escape/audio/voice/male/wall_dir_d.ogg differ diff --git a/web/escape/audio/voice/male/wall_dir_l.m4a b/web/escape/audio/voice/male/wall_dir_l.m4a index 2c5f6f83..b46ff2cc 100644 Binary files a/web/escape/audio/voice/male/wall_dir_l.m4a and b/web/escape/audio/voice/male/wall_dir_l.m4a differ diff --git a/web/escape/audio/voice/male/wall_dir_l.ogg b/web/escape/audio/voice/male/wall_dir_l.ogg index 1def6409..d1a9cb84 100644 Binary files a/web/escape/audio/voice/male/wall_dir_l.ogg and b/web/escape/audio/voice/male/wall_dir_l.ogg differ diff --git a/web/escape/audio/voice/male/wall_dir_r.m4a b/web/escape/audio/voice/male/wall_dir_r.m4a index 1cbe11de..d4d2ad8c 100644 Binary files a/web/escape/audio/voice/male/wall_dir_r.m4a and b/web/escape/audio/voice/male/wall_dir_r.m4a differ diff --git a/web/escape/audio/voice/male/wall_dir_r.ogg b/web/escape/audio/voice/male/wall_dir_r.ogg index 03f0995b..43ce4419 100644 Binary files a/web/escape/audio/voice/male/wall_dir_r.ogg and b/web/escape/audio/voice/male/wall_dir_r.ogg differ diff --git a/web/escape/audio/voice/male/wall_dir_u.m4a b/web/escape/audio/voice/male/wall_dir_u.m4a index 7aea8e30..9f2e0a2e 100644 Binary files a/web/escape/audio/voice/male/wall_dir_u.m4a and b/web/escape/audio/voice/male/wall_dir_u.m4a differ diff --git a/web/escape/audio/voice/male/wall_dir_u.ogg b/web/escape/audio/voice/male/wall_dir_u.ogg index dc5815a7..14f61cc6 100644 Binary files a/web/escape/audio/voice/male/wall_dir_u.ogg and b/web/escape/audio/voice/male/wall_dir_u.ogg differ diff --git a/web/escape/game.js b/web/escape/game.js index e6a8747f..aa391663 100644 --- a/web/escape/game.js +++ b/web/escape/game.js @@ -1,86 +1,66 @@ /** - * 《暗室》Web 版 R2 —— 3x3 黑暗房间网格密室逃脱(按 docs/design/01-escape-redesign.md 实现)。 + * 《暗室》R3 —— 3x3 黑暗房间网格密室逃脱,按 docs/design/26-audio-interaction-playbook.md 全面重构。 * - * 流程:start → REOPEN(二周目询问,可选)→ GUIDE(G0-G7 交互式引导; - * G0 intro 播完 1.2s 无操作自动推进 G1——g1_move 必播;下滑跳过仅限答案 - * 不含滑动的步骤 G0/G3/G5/G6,G1/G2/G4 下滑 = 作答) - * → PLAY(自由探索 + 卡关三级提示计时器)→ ENDING → 单击重开 / 下滑菜单。 + * 交互层(对齐 26 号文档,设计单见 docs/design/26-apply-escape.md): + * - 四向滑动 = 核心玩法(§4.3 裁定:四向是玩法的游戏,帮助换到长按): + * up/down/left/right 永远是移动,不按新手阶梯锁。 + * - 单击 = 检查 + 使用(合并):站在铁柜前拿着钥匙,单击就是开柜;双击=用物品的 + * 旧映射废除(新手期双击按一次单击处理,绑定重要操作必误触)。 + * - 长按 = 「我在哪、我该干嘛」:一句话(目标 + 方位,原则 3)。 + * - PLAY 阶段 = 发言权 ACTION 态(§4.1):环境音就是状态声,常响不停; + * 房间描述 = SPEAKING(音乐压低跨链),播完回 ACTION。 + * - 撞墙 = 失败表「方向错」:闷响 + invalid 回执 + 「那边是墙,换个方向。」+ + * 方位句(归因真话);移动成功 = 脚步声(silent-ok,内容音即回执)。 + * - 卡关接管(§4.5 三级):静默 40s 起、乱点(10s 内 3 次撞墙/无效)即触发—— + * L1 说目标方位句 → L2 「现在只要向左滑」+只放行该方向 → L3 听我做一次 + * (游戏自己走一步)+「现在你来」。进展事件清零重来。 + * - 断点复述(§4.12):每步自动存档;回来问「继续上次 / 重新开始」。 * - * 手势映射(偏离全局默认,引导中讲清): - * - 四向滑动:在相邻房间间移动,撞墙播闷响 + ERROR 震动 + wall 语音(每次都播; - * PLAY 阶段 wall 播完接播陈述式方位句 wall_dir_「左边是墙。」(escape-state - * round2 B1,同向 10s 限频,被打断自然作废);教学腔 guide_blunt_* 仅限引导期) - * - 单击:重播当前房间环境音 + 检查描述(储物间拾钥匙); - * voice 播放中单击 = 跳过本句(audio.skipVoice(),链播继续,12 号文档) - * 并补发检查——环境音即时重播、描述进待播队列等链播播完(不顶句)。 - * 不可跳:引导 G0-G7(引导期单击是答题)、关键获得(found_key/ - * unlock_cabinet/fix_box/escape 结算链)、L3 带路(stuck_*_3 + nav_*) - * - 双击:使用物品(钥匙开柜 / 保险丝修电箱 / 通电后开大门); - * 无可用目标时播报背包/use_fail + L1 级意象暗示(escape-state round2 B4: - * 原直通 L2 明示把答案白送给没卡关的玩家,降级为 L1;答案仍锁在计时器 - * L2/L3 与进展链里) - * - 引导阶段下滑:跳过引导(仅限答案不含滑动的步骤 G0/G3/G5/G6;G1/G2 任意 - * 滑动即答案、G4 的答案就是下滑——这些步骤下滑 = 作答,不可跳过); - * ENDING 下滑:菜单 + * 谜题链(不变):储物间单击找钥匙 → 铁柜房单击开柜得保险丝 → 电箱房单击修电箱 + * → 大门房单击开门逃脱。 * - * 状态 BGM(10 号文档):引导/探索 = bgm/drone;菜单 = bgm/menu(400ms); - * 逃脱结算 = escape 语音链播完淡入 bgm/menu(volume 0.5,1500ms)。 - * - * 卡关检测:按"无操作时长"计时——PLAY 阶段任何有效手势重置计时 - * (round1-live 修复:原按"无进展"计时,玩家活跃操作也每 35~40s 机关枪式 - * 报答案,还三次顶掉进行中的语音)。60s 无操作 L1 暗示(stuck_ask + - * stuck_*_1)、120s L2 明示(stuck_*_2)、180s L3 带路(stuck_*_3 + 按当前 - * 坐标算路链播 nav_*,此后每 60s 无操作重复);进展事件(拾钥匙/开柜/ - * 修电箱/首到关键房间/逃脱)清空已播级别,下段静默期从 L1 重新分级。 - * 提示到点只进待播队列:等当前语音自然播完再播、绝不顶掉进行中的语音; - * 待播期间玩家任何操作即作废该提示(未播级别不计入已播)。 - * escape-state round2 新增平行通道(B3):无进展 150s 且期间 ≥5 次有效手势 - * (活跃但零进展,挂机仍走 60s L1 体系)→ 播不泄题的进度重述 gd_prog_1/2/3 - * (stage=cabinet/box/gate,key 阶段不走本通道),每进展段限 1 次, - * 队列语义与卡关提示同款(不顶句、待播期间有操作即作废)。 - * - * round4 H1(docs/design/14-guide-layer.md「round4 v1.3」):阶段目标句 - * (found_key / gd_next_fuse / fix_box)播完后,按房间图计算目标房间相对当前 - * 房间的真实方位接播方位句;escape-state round2 B2 把三处固定接线点升级为 - * "目标名+方位"一体句(gd_cabinet_dir/gd_box_dir/gd_gate_dir),异常路径方位 - * 与枚举不符时回落 gd_room_l/r/u/d(原句保留)。引导期 g7_goal 不接(引导链 - * 被 playtest 断言冻结);但跳过引导/REOPEN 直进路径补播 g7_goal(round2 B8: - * 任何路径进 PLAY 都至少听过一次目标句)。 - * - * escape-state round2 逻辑修复:B6 引导 busy 窗口缓存首个非跳过分手势、步骤 - * 解锁后按新步骤补判(合法=作答/不合法=答错路径,缓存手势不触发下滑跳过); - * B7 结算链播完前 ENDING 单击不重开(_settleBusy 守卫,链完/30s 兜底解除); - * B5a 空房首访补播 empty_* 一次性听觉锚(不算进展事件)。 - * - * 语音 id 权威清单:docs/audio/round2-voice-ids.md(已删除的 n0–n9 数字拼接链、 - * hint_ 四级提示、intro/tutorial 旧开场不再引用)。震动分工:滑动/点按的识别反馈由 gesture.js 内置,本层只发语义震动 - * (error/success/warning/pickup)。 - * - * 房间布局(x: 0西..2东, y: 0北..2南): - * (0,0) 电箱房·amb_power (1,0) 空房·amb_wind (2,0) 空房·amb_hum - * (0,1) 空房·amb_hum (1,1) 起点·amb_drip (2,1) 铁柜房·amb_cabinet - * (0,2) 储物间·amb_storage (1,2) 空房·amb_wind (2,2) 大门·amb_gate(通电后 amb_gate_live) + * 状态 × 手势矩阵(新手期,完整版见设计单): + * | 状态 | 单击 | 四向滑动 | 长按 | 双击(未解锁) | + * |-----------|-----------------|-------------------|-----------|--------------| + * | 引导答题 | 按步骤答/纠错 | 按步骤答/纠错 | 帮助一句话 | 按单击处理 | + * | 探索(ACTION)| 检查+使用+ok | 移动(脚步)/撞墙(invalid) | 帮助 | 按单击处理 | + * | 描述在播 | 跳过这句+ok | 打断并移动 | 打断,帮助 | 按单击处理 | + * | 结算 | 重新开始+ok | invalid+解释 | 重听结算 | 按单击处理 | + * | 菜单 | 换一项+播报 | 右=确认/左=上一项 | 重听菜单 | 按单击处理 | */ import { Game } from '../engine/game.js'; -import { Guide } from '../engine/guide.js'; +import { Floor } from '../engine/floor.js'; +import { Receipts } from '../engine/receipts.js'; +import { Confusion } from '../engine/confusion.js'; +import { WaitPoint, InvalidHints, markHintSaid, hintSaidRecently } from '../engine/hints.js'; +import { GestureLadder } from '../engine/novice.js'; import { vibrate } from '../engine/haptics.js'; const Phase = { - REOPEN: 'REOPEN', // 二周目:上滑重听引导 / 单击直接开始 - GUIDE: 'GUIDE', // G0-G7 交互式引导 + REOPEN: 'REOPEN', + GUIDE: 'GUIDE', PLAY: 'PLAY', ENDING: 'ENDING', MENU: 'MENU', }; -const G = { UP: 'up', DOWN: 'down', LEFT: 'left', RIGHT: 'right', TAP: 'tap', DOUBLE_TAP: 'doubletap' }; +const G = { UP: 'up', DOWN: 'down', LEFT: 'left', RIGHT: 'right', TAP: 'tap', DOUBLE_TAP: 'doubletap', LONGPRESS: 'longpress' }; +const SWIPES = [G.UP, G.DOWN, G.LEFT, G.RIGHT]; -// 谜题链阶段 → L3 带路目标房间坐标 +// 谜题链阶段 → 带路目标房间坐标 const STAGES = { - key: { target: [0, 2] }, // 找钥匙 → 储物间 - cabinet: { target: [2, 1] }, // 开铁柜 → 铁柜房 - box: { target: [0, 0] }, // 修电箱 → 电箱房 - gate: { target: [2, 2] }, // 开大门 → 大门 + key: { target: [0, 2] }, + cabinet: { target: [2, 1] }, + box: { target: [0, 0] }, + gate: { target: [2, 2] }, +}; + +// 长按「我在哪、我该干嘛」目标句(原则 3:目标 + 一个动作) +const LP_GOAL = { + key: 'voice/lp_key', + cabinet: 'voice/lp_cabinet', + box: 'voice/lp_box', + gate: 'voice/lp_gate', }; // 关键房间:坐标 → { 环境音, 首次到达描述 voice } @@ -91,19 +71,14 @@ const KEY_ROOMS = { '2,2': { amb: 'sfx/amb_gate', room: 'voice/room_gate' }, }; -// 空房环境音轮换(弱化版风声/嗡鸣) +// 空房环境音轮换 const EMPTY_AMB = { '1,0': 'sfx/amb_wind', '0,1': 'sfx/amb_hum', '1,2': 'sfx/amb_wind', '2,0': 'sfx/amb_hum' }; -// 空房首访描述(escape-state round2 B5a:复用现有 empty_*,进入时一次性播; -// 与 _inspect 单击检查映射一致——两间"远处水滴"空房 (1,2)/(2,0) 对称分布, -// 共用 empty_3 也是合法的记忆点) +// 空房首访描述 const EMPTY_FIRST_VOICE = { '1,0': 'voice/empty_1', '0,1': 'voice/empty_2', '1,2': 'voice/empty_3', '2,0': 'voice/empty_3', }; -// B3 无进展进度重述:stage → gd_prog id(key 阶段不走本通道) -const GD_PROG = { cabinet: 'voice/gd_prog_1', box: 'voice/gd_prog_2', gate: 'voice/gd_prog_3' }; - const PREWARM_SFX = [ 'sfx/step', 'sfx/wall', 'sfx/pickup', 'sfx/unlock', 'sfx/door_creak', 'sfx/spark', 'sfx/power_on', 'sfx/gate_open', 'sfx/win_chime', @@ -112,20 +87,47 @@ const PREWARM_SFX = [ ]; const GUIDE_DONE_KEY = 'soundgame.escape.guideDone'; +const SAVE_KEY = 'soundgame.escape.save'; const STATS_KEY = 'soundgame.escape.stats'; -const AUTH_KEY = 'soundgame.auth'; // 大厅登录写入(见 web/index.html) +const AUTH_KEY = 'soundgame.auth'; + +// 静默喂给迷茫检测的节奏:40s 首次、之后每 20s(两次即累计触发接管) +const SILENCE_FIRST_MS = 40000; +const SILENCE_REPEAT_MS = 20000; + +// 无效操作解释(原则 7) +const INVALID_LINES = { + 'locked:longpress': null, // 长按是本游戏核心手势,不会走到这 + 'ending:swipe': 'voice/inv_settle', + 'menu:swipe': null, // 菜单左右滑合法 + 'menu:longpress': 'voice/menu_help', + 'guide:swipe': null, +}; export class EscapeGame extends Game { constructor(audio, root) { super(audio, root); - this.guide = new Guide(audio); // 全局向导层(guide='off' 时全部静默 no-op) - this._session = 0; // 会话令牌:重开/跳过时使旧计时器与语音链回调失效 + this.receipts = new Receipts(audio); + this.floor = new Floor(audio); + this.confusion = new Confusion({ onConfused: (lvl) => this._takeover(lvl) }); + this.invalidHints = new InvalidHints().bind(this.floor); + this.ladder = new GestureLadder(); + this.audio.setRateBoostEnabled(false); // 长按=帮助(核心手势),不触发旁白快进 + this.confusion.silenceMs = SILENCE_FIRST_MS; + + this._session = 0; this._timers = []; - this._stuckTimer = null; + this._silenceTimer = null; + this._restrictDir = null; // 迷茫 L2:只放行的移动方向 + this._lastDescribe = null; // 最近一句房间描述(双击重听用) + this._menuIdx = 0; this._resetState(); this.phase = Phase.REOPEN; } + /** 管道选项(index.html 读):四向滑动 + 长按都是本作核心手势。 */ + get PIPELINE_OPTS() { return { longpressCore: true, swipeCore: true }; } + _resetState() { this.x = 1; this.y = 1; @@ -133,101 +135,85 @@ export class EscapeGame extends Game { this.hasFuse = false; this.cabinetOpen = false; this.powerOn = false; - this.visited = new Set(); // 首到关键房间记录(进展事件 + room_* 描述) - this.emptyVisited = new Set(); // 首到空房记录(B5a 首访描述,不算进展事件) - this._freshEmpty = null; // 首访句刚播过的空房 key(本房间内单击不重复同一句) - this._wallDirAt = {}; // B1 撞墙方位句同向 10s 限频:dir -> 上次播出时刻 - this._settleBusy = false; // B7 结算链守卫:播完前 ENDING 单击不重开 + this.visited = new Set(); + this.emptyVisited = new Set(); + this._freshEmpty = null; + this._wallDirAt = {}; + this._wallExplained = new Set(); // 撞墙归因:每方向每进展段只解释一次(原则 7) + this._describedRoom = new Set(); // 房间检查描述:每房间每进展段只念一次(原则 9) + this._settleBusy = false; this.startElapsedMs = 0; - // 单击跳过:当前 voice 句是否可跳(每次 voice 播放时更新,见 _playVoice) - this._voiceSkippable = false; // 引导状态 - this.guideStep = 0; // 0..7 对应 G0..G7 - this.guideSwipes = 0; // G2 撞墙计数 - this.guideErrs = 0; // 当前步骤连续错误数 - this._guideBusy = false; // 步骤推进语音播放中(忽略非跳过分手势) - this._guidePending = null; // B6:busy 窗口缓存的首个非跳过分手势(仅 1 个、不跨步存活) - // 卡关检测(按"无操作时长"计时,见 _checkStuck) - this._lastActiveAt = Date.now(); - this._stuckShown = new Set(); // 当前进展段内已自动播过的级别 'L1'|'L2'|'L3' - this._lastL3At = 0; - // B3 无进展计时(与无操作计时并存互不复位) - this._lastProgressAt = Date.now(); // 上一次进展事件时刻 - this._gesturesSinceProgress = 0; // 进展段内有效手势数(≥5 才触发 gd_prog) - this._progShown = new Set(); // 已播过 gd_prog 的阶段(每进展段限 1 次) - this._pendingVoice = []; // 待播语音队列:排队等当前语音播完,绝不顶句 - this._playSeq = 0; // voice 播放请求序号(onComplete 内判断是否链播新句) - // 菜单 - this._menuReturn = Phase.ENDING; - this._menuIdx = 0; - this._endingBgmStarted = false; // 结算 pad 是否已起(_escape 幂等守护) + this.guideStep = -1; + this.guideSwipes = 0; + this.guideErrs = 0; + this._endingBgmStarted = false; } // ---------- Game ---------- start() { this.audio.prewarm(PREWARM_SFX); - this.audio.prewarm(['voice/g0_intro', 'voice/g1_move', 'voice/skip_hint', 'voice/reopen_prompt', 'voice/start_room', 'bgm/menu']); this._session++; this._clearTimers(); - this._startStuckTimer(); this.audio.play('bgm/drone'); - this.guide.state('explore'); // 状态签名:探索(与状态 BGM 同点) - const guided = localStorage.getItem(GUIDE_DONE_KEY) === '1'; - if (guided) { - // 二周目:上滑重听引导 / 单击直接开始 + const guided = this._flag(GUIDE_DONE_KEY); + const save = this._loadSave(); + if (guided && save && save.inProgress) { + // 断点复述(§4.12) this.phase = Phase.REOPEN; - // 抉择前提示必须听完;播完给 turn cue(向导层:REOPEN 静默等待的"轮到你") - this._playVoice('voice/reopen_prompt', { - skippable: false, - onComplete: () => this.guide.turn({}), + this.floor.speak(['voice/sys_continue_ask'], { + onDone: () => this.floor.yieldTurn(this._wp({ + id: 'reopen', expected: [G.TAP, G.RIGHT], + full: 'voice/sys_continue_ask', short: 'voice/lp_tap', + silenceMs: [12000, 30000], + onDemo: () => this.floor.chime(), + })), }); + } else if (guided) { + this._enterPlay(true, true); // 二周目:直接开始(start_room + 目标句) } else { this._startGuide(); } } - onGesture(g) { - console.log('[EscapeGame] gesture:', g, 'phase=', this.phase, 'pos=', this.x, this.y); - this.guide.disarm(); // 任何手势复位向导状态(guide=off 时 no-op) - if (this.phase === Phase.PLAY) { - // 卡关提示按"无操作时长"计时:任何有效手势重置计时,并作废待播提示 - this._lastActiveAt = Date.now(); - this._gesturesSinceProgress++; // B3:活跃手势计数(无进展 150s 通道的闸门) - this._dropPendingStuck(); - } - switch (this.phase) { - case Phase.REOPEN: - if (g === G.UP) { this.audio.stopVoice(); this._startGuide(); } - // B8:REOPEN 单击直进路径在 start_room 后接播目标句 g7_goal - else if (g === G.TAP) { this.audio.stopVoice(); this._enterPlay(true, true); } - break; - case Phase.GUIDE: - this._onGuideGesture(g); - break; - case Phase.PLAY: - this._onPlayGesture(g); - break; - case Phase.ENDING: - // B7(escape-state round2):结算链播完前单击不立即重开——time_band/again - // 的完整到达权(live round1 S7:习惯性单击把结算链截断,两句被永久吞掉) - if (g === G.TAP) { if (!this._settleBusy) this._restart(); } - else if (g === G.DOWN) this._openMenu(Phase.ENDING); - break; - case Phase.MENU: - this._onMenuGesture(g); - break; - } - } - stop() { + this._save(true); this._session++; this._clearTimers(); - if (this._stuckTimer) { clearInterval(this._stuckTimer); this._stuckTimer = null; } + this._stopSilenceFeed(); + this.floor.interrupt(); this.audio.stopAll(); } - // ---------- 引导(G0-G7) ---------- + onGesture(g) { + switch (this.phase) { + case Phase.REOPEN: return this._onReopenGesture(g); + case Phase.GUIDE: return this._onGuideGesture(g); + case Phase.PLAY: return this._onPlayGesture(g); + case Phase.ENDING: return this._onEndingGesture(g); + case Phase.MENU: return this._onMenuGesture(g); + } + return 'invalid'; + } + + stateName() { + if (this.phase === Phase.PLAY) return 'play'; + if (this.phase === Phase.ENDING) return 'ending'; + if (this.phase === Phase.MENU) return 'menu'; + if (this.phase === Phase.GUIDE) return 'guide'; + return 'reopen'; + } + + invalidHint(state, g) { + if (state === 'locked') return INVALID_LINES[`locked:${g}`] || 'voice/inv_generic'; + if (g === 'longpress') return INVALID_LINES[`${state}:longpress`] || 'voice/menu_help'; + if (g === 'left' || g === 'right') return INVALID_LINES[`${state}:swipe`] || 'voice/inv_generic'; + if (state === 'ending') return 'voice/inv_settle'; + return 'voice/inv_generic'; + } + + // ---------- 引导(首 3 分钟逐秒脚本,§4.10;设计单有完整表) ---------- _startGuide() { this._session++; @@ -235,413 +221,333 @@ export class EscapeGame extends Game { this._resetState(); this.phase = Phase.GUIDE; this.guideStep = 0; - // round2-live P1 回归修复:intro 自然播完后若玩家没有单击,1.2s 自动推进 - // G1——g1_move(「先活动一下。朝任意方向滑一下…」)必须播出。原实现干等 - // "单击继续"却从无任何一句语音告知该手势(第 1 轮靠解锁单击漏进游戏误推 - // 一步被掩盖,解锁修复后暴露为 intro 后 17~91s 死寂、引导链整体进不去)。 - this._speak(['voice/g0_intro'], () => { - this._timer(() => { - if (this.phase === Phase.GUIDE && this.guideStep === 0 && !this._guideBusy) { - this._advance(1, ['voice/g1_move', 'voice/skip_hint']); - } - }, 1200); - }, { skippable: false }); + this.audio.play('sfx/amb_drip', { volume: 0.6 }); + // 0~3 秒:第一句就让玩家动手(旧 g0_intro 8 秒念完才教学废除) + this._guideSay(['voice/g0_new'], 0); } - /** - * 推进到引导第 step 步:播推进语音,播完才切换步骤号并解锁输入。 - * 语音缺失时 20s 看门狗兜底推进,防卡死。 - */ - _advance(step, ids, onDone) { - this._guideBusy = true; - const s = this._session; - const fromStep = this.guideStep; // 看门狗仅在仍停留在原步骤时兜底,防误触发后续步骤 - const done = () => { - if (s !== this._session) return; - this.guideStep = step; - this.guideErrs = 0; - this.guideSwipes = 0; - this._guideBusy = false; - this.guide.turn({}); // turn cue:引导步骤解锁输入(W2 播完即静默,向导补标记) - if (onDone) onDone(); - // B6(escape-state round2):busy 窗口缓存的手势在步骤解锁后按新步骤补判 - // (onDone 若已离开 GUIDE——G6 链尾进 PLAY——缓存直接作废,不跨阶段存活) - if (this._guidePending !== null) { - const p = this._guidePending; - this._guidePending = null; - if (this.phase === Phase.GUIDE) this._onGuideGesture(p, { fromCache: true }); - } - }; - this._timer(() => { - if (this._guideBusy && this.guideStep === fromStep) done(); - }, 20000); // 看门狗 - // 引导句不可单击跳过(12 号文档:引导句即操作指令) - if (ids && ids.length) this._speak(ids, done, { skippable: false }); - else done(); + /** 播引导链 → 交出话轮等步骤答案。expected = 本步骤接受的答选手势。 */ + _guideSay(ids, step, expected) { + this.guideStep = step; + this.floor.speak(ids, { + onDone: () => this.floor.yieldTurn(this._wp({ + id: `guide${step}`, + expected: expected || SWIPES, + full: null, + short: 'voice/g_hint_swipe', + silenceMs: [6000, 15000], + onDemo: () => this.floor.chime(), + })), + }); } - _onGuideGesture(g, { fromCache = false } = {}) { - // 下滑跳过仅限"答案不含滑动"的步骤(G0/G3/G5/G6):G1/G2 的正确答案是 - // 任意滑动、G4 的答案就是下滑——这些步骤下滑 = 作答,不可跳过(13 号文档 - // 裁定;round2-live:g1_move 明说"朝任意方向滑一下",玩家下滑却被保送 - // 自由探索,教学链就此断裂)。B6:busy 缓存补判的手势不再触发下滑跳过 - // (live round1 S6:答案在 busy 窗口被吞后,玩家紧接着的下滑被当成跳过引导) - if (!fromCache && g === G.DOWN && this.guideStep !== 1 && this.guideStep !== 2 && this.guideStep !== 4) { - this._skipGuide(); - return; - } - if (this._guideBusy) { - // B6:busy 窗口缓存首个非跳过分手势,推进语音播完(_advance done)后按 - // 新步骤补判——合法则直接作答,不合法走答错路径(有纠正语音,可归因)。 - // 仅缓存 1 个、只存活到本步骤解锁,杜绝跨步幽灵输入(00 §5.4:做了手势 - // 3 秒内必须有反馈——原 busy 静默吞手势违反此条,本缓存是合规修复) - if (!fromCache && this._guidePending === null) this._guidePending = g; - return; - } - switch (this.guideStep) { - case 0: // G0:单击继续(intro 播完 1.2s 未操作也会自动推进 G1,见 _startGuide) - if (g === G.TAP) this._advance(1, ['voice/g1_move', 'voice/skip_hint']); // skip_hint 首局插播一次 - // intro 播放中的滑动忽略(不拿"先跟我走"顶掉开场白)——intro 播完自动 - // 接 g1_move;intro 已播完(自动推进前的 1.2s 空档)才温和拦回 - else if (this._isSwipe(g) && !this.audio.isVoicePlaying()) { - this._playVoice('voice/guide_offpath', { skippable: false }); // "先跟我走" - } - break; - case 1: // G1:任意滑动,走一小步 - if (this._isSwipe(g)) { + _onGuideGesture(g) { + // 引导答题:期待手势之外的输入 = 纠错(原则 11:做错不惩罚、当场重教) + const step = this.guideStep; + const isSwipe = SWIPES.includes(g); + switch (step) { + case 0: // 黑暗中先滑一下(任意方向) + if (isSwipe) { this.audio.play('sfx/step', { volume: 0.8 }); - this._advance(2, ['voice/g1_done', 'voice/g2_wall']); - } else this._guideWrong(null); - break; - case 2: // G2:再滑两下碰碰墙(引导期位置固定,滑动即撞墙反馈,每次都播 wall) - if (this._isSwipe(g)) { + this._guideSay(['voice/g0b'], 1); + return 'silent-ok'; // 脚步声即反馈 + } + this._guideWrong('voice/g_wrong_gesture'); + return 'invalid'; + case 1: // 再滑两下试试墙(撞墙也是反馈:教「无效操作长什么样」) + if (isSwipe) { this.audio.play('sfx/wall'); vibrate('error'); this.guideSwipes++; - if (this.guideSwipes >= 2) this._advance(3, ['voice/wall', 'voice/g3_left']); - else this._playVoice('voice/wall', { skippable: false }); - } else this._guideWrong(null); - break; - case 3: // G3:向左滑 → (0,1) + if (this.guideSwipes >= 2) { + this._guideSay(['voice/g3_new'], 2, [G.LEFT]); // wall 刚说过不重播(原则 9) + } else { + this.floor.speak(['voice/wall'], { + onDone: () => { if (this.phase === Phase.GUIDE) this.floor.resumeTurn(); }, + }); + } + return 'invalid'; // 撞墙 = 失败表「方向错」,invalid 回执 + 归因 + } + this._guideWrong('voice/g_wrong_gesture'); + return 'invalid'; + case 2: // 向左滑 → (0,1) if (g === G.LEFT) { this.x = 0; this.y = 1; this.audio.play('sfx/step', { volume: 0.8 }); - this._advance(4, ['voice/g4_down']); // g4_down 兼作 G3 反馈与 G4 指令 - } else if (this._isSwipe(g)) this._guideWrong(G.LEFT); - else this._guideWrong(null); - break; - case 4: // G4:向下滑 → (0,2) 储物间 + this._guideSay(['voice/g4_down'], 3, [G.DOWN]); + return 'silent-ok'; + } + if (isSwipe) this._guideWrong('voice/guide_wrong_left'); + else this._guideWrong('voice/g_wrong_gesture'); + return 'invalid'; + case 3: // 向下滑 → (0,2) 储物间 if (g === G.DOWN) { this.x = 0; this.y = 2; this.audio.play('sfx/step', { volume: 0.8 }); this.visited.add('0,2'); this.audio.play('sfx/amb_storage', { volume: 0.9 }); - this._advance(5, ['voice/g5_inspect']); - } else if (this._isSwipe(g)) this._guideWrong(G.DOWN); - else this._guideWrong(null); - break; - case 5: // G5:单击检查 → 拾得钥匙(谜题链第一环) + this._guideSay(['voice/g5_inspect'], 4, [G.TAP]); + return 'silent-ok'; + } + if (isSwipe) this._guideWrong('voice/guide_wrong_down'); + else this._guideWrong('voice/g_wrong_gesture'); + return 'invalid'; + case 4: // 单击检查 → 拾钥匙(谜题链第一环)→ 自由探索 if (g === G.TAP) { + this.guideStep = 5; // 先落步骤号:链被乱手势打断后重进不重播(幂等) this.hasKey = true; this.audio.play('sfx/pickup'); vibrate('pickup'); - this._advance(6, ['voice/g5_key', 'voice/g6_doubletap']); - } else this._guideWrong(null); - break; - case 6: // G6:双击(用物品/查看背包的手势) - if (g === G.DOUBLE_TAP) { - this._advance(7, ['voice/inv_key', 'voice/g6_done', 'voice/g7_goal'], - () => this._finishGuide()); - } else this._guideWrong(null); - break; - default: // G7:等待 g7_goal 播完(_advance 的 onDone 驱动),手势忽略 - break; + try { localStorage.setItem(GUIDE_DONE_KEY, '1'); } catch (_) { /* 隐私模式 */ } + this.ladder.onGoal(); // 第一个小目标达成 → 手势阶梯阶段 1 + this.floor.speak(['voice/g5_key', 'voice/g7_goal_new'], { + onDone: () => this._enterPlay(false), + }); + return 'silent-ok'; // 拾取音效即反馈 + } + this._guideWrong('voice/g_wrong_tap'); + return 'invalid'; + default: + return 'invalid'; } } - _finishGuide() { - try { localStorage.setItem(GUIDE_DONE_KEY, '1'); } catch (_) { /* 隐私模式 */ } - this._enterPlay(false); - } - - _skipGuide() { - this._session++; // 作废进行中的引导语音链 - this._clearTimers(); - this.audio.stopVoice(); - this._guidePending = null; - try { localStorage.setItem(GUIDE_DONE_KEY, '1'); } catch (_) { /* 隐私模式 */ } - this.guideStep = 7; // 跳过链期间视作 G7 等待:滑动手势全部忽略(防链中误触误答/二次跳过) - // B8(escape-state round2):跳过确认句后、进 PLAY 前补播目标句 g7_goal—— - // 目标陈述原只在引导 G7 链尾出现一次,跳过的玩家(恰是最需要目标的一群) - // 永远听不到(live round1 S4:跳过后目标维度完全空白) - this._speak(['voice/guide_skip'], () => { - this._playVoice('voice/g7_goal', { onComplete: () => this._enterPlay(true) }); - }, { skippable: false }); - // 语音缺失兜底(两句链:覆盖 guide_skip/g7_goal 缺失情形) - this._timer(() => { if (this.phase === Phase.GUIDE) this._enterPlay(true); }, 9000); - } - - /** - * 引导纠错:expected 为期望的滑动方向(guide_wrong_),null 表示手势 - * 类型错(guide_wrong_gesture)。同一步错 3 次给直白重述 - * (guide_blunt_,仅方向步骤有)+ WARNING 震动。 - */ - _guideWrong(expected) { + _guideWrong(voiceId, _g) { this.guideErrs++; - if (this.guideErrs >= 3) { - vibrate('warning'); - this._playVoice(expected ? `voice/guide_blunt_${expected}` : 'voice/guide_wrong_gesture', { skippable: false }); - this.guideErrs = 0; - return; - } vibrate('error'); - this._playVoice(expected ? `voice/guide_wrong_${expected}` : 'voice/guide_wrong_gesture', { skippable: false }); + if (this.guideErrs >= 4) return; // 4 次起只给回执(原则 7),交给迷茫接管 + const id = this.guideErrs >= 3 + ? 'voice/guide_blunt_swipe' // 错 3 次给直白描述 + : (voiceId || 'voice/g_wrong_gesture'); + this.floor.speak([id], { + onDone: () => { if (this.phase === Phase.GUIDE) this.floor.resumeTurn(); }, + }); } - // ---------- 进入 / 重开 PLAY ---------- + // ---------- 进入 / 恢复 / 重开 PLAY ---------- - /** - * @param {boolean} announce 是否播 start_room(跳过引导/二周目直接进入时) - * @param {boolean} withGoal B8:start_room 后接播目标句 g7_goal(REOPEN 直进 - * 路径;跳过引导路径的 g7_goal 已在 _enterPlay 前播过,此处不重复) - */ _enterPlay(announce, withGoal = false) { this._session++; this._clearTimers(); this.phase = Phase.PLAY; - this.startElapsedMs = Date.now(); - this._resetStuck(); + this.startElapsedMs = Date.now() - (this._savedElapsedMs || 0); + this.floor.enterAction(); // 探索态:环境音即状态声(§4.1 ACTION) + this._startSilenceFeed(); + this.confusion.resetForWaitPoint(); if (announce) { - if (withGoal) { - // B8:start_room(可跳)自然播完/被跳过后接播 g7_goal(可跳, - // 二周目老手可单击跳过——教学义务句已随首局完成,此处是状态告知) - this._playVoice('voice/start_room', { onComplete: () => this._playVoice('voice/g7_goal') }); - } else { - this._playVoice('voice/start_room'); - } + const seq = withGoal + ? ['voice/start_room', 'voice/g7_goal_new'] + : ['voice/start_room']; + this._say(seq); } this._playAmbient(); + this._save(true); } _restart() { this._session++; this._clearTimers(); - this.audio.stopVoice(); + this._stopSilenceFeed(); + this.floor.interrupt(); this._resetState(); - this.phase = Phase.PLAY; - this.startElapsedMs = Date.now(); - this._resetStuck(); + this._savedElapsedMs = 0; this.audio.play('bgm/drone'); - this.guide.state('explore'); - this._playVoice('voice/replay'); - this._playAmbient(); + this._enterPlay(false); + this._say(['voice/replay', 'voice/g7_goal_new']); } - // ---------- 探索 ---------- + _onReopenGesture(g) { + const save = this._loadSave(); + switch (g) { + case G.TAP: // 继续上次(§4.12:先播复述句 = 长按帮助句,再交回探索) + if (save) this._restoreState(save); + this.phase = Phase.PLAY; + this._enterPlay(false); + this._longpressHelp(); + return 'ok'; + case G.RIGHT: // 重新开始 + this._resetState(); + this._savedElapsedMs = 0; + this._enterPlay(true, true); + return 'ok'; + case G.LONGPRESS: + this._longpressHelp(); + return 'ok'; + default: + return 'invalid'; + } + } + + _restoreState(save) { + this.x = save.x; this.y = save.y; + this.hasKey = !!save.hasKey; + this.hasFuse = !!save.hasFuse; + this.cabinetOpen = !!save.cabinetOpen; + this.powerOn = !!save.powerOn; + this.visited = new Set(save.visited || []); + this.emptyVisited = new Set(save.emptyVisited || []); + this._savedElapsedMs = save.elapsedMs || 0; + } + + // ---------- 探索(ACTION 态) ---------- _onPlayGesture(g) { + let r; switch (g) { - case G.UP: this._move(0, -1); break; - case G.DOWN: this._move(0, 1); break; - case G.LEFT: this._move(-1, 0); break; - case G.RIGHT: this._move(1, 0); break; - case G.TAP: - // voice 播放中:可跳句单击 = 跳过本句(链播继续)+ 补发检查(round1-live - // 修复:原把整个单击当"跳过键"吃掉,检查动作无声丢失)——环境音即时重播, - // 描述进待播队列等链播播完,不顶句。不可跳句(关键获得/L3 带路)单击 = - // 忽略——若照旧触发检查,描述会顶掉该句。无 voice 时单击 = 原检查语义。 - if (this.audio.isVoicePlaying()) { - if (!this._voiceSkippable) break; - this.audio.skipVoice(); - this._inspect({ queue: true }); - break; - } - this._inspect(); + case G.UP: r = this._move(0, -1); break; + case G.DOWN: r = this._move(0, 1); break; + case G.LEFT: r = this._move(-1, 0); break; + case G.RIGHT: r = this._move(1, 0); break; + case G.TAP: r = this._interact(); break; + case G.LONGPRESS: + this._longpressHelp(); + r = 'ok'; break; - case G.DOUBLE_TAP: this._useItem(); break; + case G.DOUBLE_TAP: { + // 阶段 3 解锁的重听:再说一遍最近一句房间描述 + const id = this._lastDescribe || 'voice/start_room'; + this._say(['voice/sys_say_again', id]); + r = 'ok'; + break; + } + default: + r = 'invalid'; } + // 探索态收尾:手势处理完且没起新链 → 回 ACTION(环境音即状态,§4.1) + if (this.phase === Phase.PLAY && !this.floor.speaking) this.floor.enterAction(); + return r; + } + + /** 长按 = 我在哪、我该干嘛(原则 3:目标 + 方位一个动作,一句话)。 */ + _longpressHelp() { + const stage = this._stage(); + const target = STAGES[stage].target; + const atTarget = target[0] === this.x && target[1] === this.y; + const ids = atTarget + ? [LP_GOAL[stage], 'voice/lp_here'] + : [LP_GOAL[stage], ...this._navVoices(target)]; + this._say(ids); } _move(dx, dy) { + // 迷茫 L2:只放行接管指定的方向(其余手势 invalid 回执,不产生效果) + if (this._restrictDir) { + const dir = dy < 0 ? G.UP : dy > 0 ? G.DOWN : dx < 0 ? G.LEFT : G.RIGHT; + if (dir !== this._restrictDir) return 'invalid'; // pipeline 统一记账+回执 + } const nx = this.x + dx; const ny = this.y + dy; if (nx < 0 || nx > 2 || ny < 0 || ny > 2) { + // 失败表「方向错」:闷响 + invalid 回执;归因句每个方向只解释一次 + // (原则 7:之后只给回执——闷响本身就是内容回执),10s 冷却后可再提醒方位 this.audio.play('sfx/wall'); vibrate('error'); - // 撞墙反馈:先播「那边是墙,换个方向。」,播完接播陈述式方位句 - // wall_dir_「左边是墙。」(escape-state round2 B1:round1-live 回退掉的 - // 是教学腔措辞,方位信息本身保留;复用 14-F2.3 原接线模式——被下一次语音 - // 顶掉则自然作废不迟到误导;同方向 10s 限频 1 次防贴墙连撞连播)。 - // 方位 = 滑动方向(escape 移动坐标系),按真实撞墙方向取句,归因真话。 + this.confusion.recordFail(); const dir = dy < 0 ? 'u' : dy > 0 ? 'd' : dx < 0 ? 'l' : 'r'; - const allowDir = !this._wallDirAt[dir] || Date.now() - this._wallDirAt[dir] >= 10000; - this._playVoice('voice/wall', { - onComplete: allowDir - ? () => { this._wallDirAt[dir] = Date.now(); this._playVoice(`voice/wall_dir_${dir}`); } - : undefined, - }); - return; + if (!this._wallExplained.has(dir)) { + const first = this._wallExplained.size === 0; + this._wallExplained.add(dir); + // 通用句整局一次;之后每个新方向只给方位句(渐短,原则 9) + this._say(first ? ['voice/wall', `voice/wall_dir_${dir}`] : [`voice/wall_dir_${dir}`], { kind: 'feedback' }); + } + return 'invalid'; // 之后只给闷响 + invalid 回执(原则 7) } this.x = nx; this.y = ny; - this.audio.play('sfx/step', { volume: 0.8 }); + this.audio.play('sfx/step', { volume: 0.8 }); // 内容音即回执 this._playAmbient(); - this._freshEmpty = null; // 离开首访句刚播过的空房:单击检查恢复播 empty_* - // 首次到达关键房间:进展事件 + 房间描述 + this._freshEmpty = null; + this._save(); const key = `${nx},${ny}`; if (KEY_ROOMS[key] && !this.visited.has(key)) { this.visited.add(key); this._progressEvent(); - this._playVoice(KEY_ROOMS[key].room); + this._say([KEY_ROOMS[key].room]); } else if (EMPTY_FIRST_VOICE[key] && !this.emptyVisited.has(key)) { - // B5a(escape-state round2):空房首访补播 empty_* 一次性听觉锚 - // (不算进展事件,不清卡关已播级别;重访零语音与关键房间同款) this.emptyVisited.add(key); this._freshEmpty = key; - this._playVoice(EMPTY_FIRST_VOICE[key]); + this._say([EMPTY_FIRST_VOICE[key]]); } + return 'silent-ok'; } - /** 当前房间环境音 id(大门通电后切 amb_gate_live)。 */ - _ambientId() { - const key = `${this.x},${this.y}`; - if (key === '2,2' && this.powerOn) return 'sfx/amb_gate_live'; - if (KEY_ROOMS[key]) return KEY_ROOMS[key].amb; - if (key === '1,1') return 'sfx/amb_drip'; - return EMPTY_AMB[key] || 'sfx/amb_wind'; - } - - /** 进房间自动播环境音(每次都播,配合单击重听)。 */ - _playAmbient() { - this.audio.play(this._ambientId(), { volume: 0.9 }); - } - - /** - * 单击 = 重播当前房间环境音 + 检查描述。 - * queue=true(语音播放中单击跳过后的补发):描述进待播队列等当前语音播完, - * 不顶句;queue=false(无语音时的原检查语义):描述照常 800ms 后直接播。 - */ - _inspect({ queue = false } = {}) { + /** 单击 = 检查 + 使用(先看能否用,再描述)。 */ + _interact() { this._playAmbient(); const s = this._session; - const emit = (id, skippable, onComplete) => { - if (queue) this._enqueueVoice('describe', [id], { skippable, onDone: onComplete }); - else this._playVoice(id, { skippable, onComplete }); - }; - // skippable:found_key 是关键获得(不可跳),其余检查描述可跳 - const describe = (id, skippable = true) => this._timer(() => { - if (s === this._session) emit(id, skippable); + const roomKey = `${this.x},${this.y}` + + ((this.x === 2 && this.y === 1) ? `:c${this.cabinetOpen ? 1 : 0}` : '') + + ((this.x === 0 && this.y === 0) ? `:p${this.powerOn ? 1 : 0}` : '') + + ((this.x === 2 && this.y === 2) ? `:p${this.powerOn ? 1 : 0}` : ''); + const alreadyDescribed = this._describedRoom.has(roomKey); + if (alreadyDescribed) return; // 听过描述的房间:单击 = 重听环境音(渐短档) + const describe = (id, critical = false, onDone) => this._timer(() => { + if (s === this._session) { + this._describedRoom.add(roomKey); + this._say([id], { critical, onDone }); + } }, 800); // 先让环境音进耳朵再播描述 const { x, y } = this; - if (x === 0 && y === 2) { // 储物间 + if (x === 0 && y === 2) { // 储物间:找钥匙(状态一次性,不看 _describedRoom) if (!this.hasKey && !this.cabinetOpen) { this.hasKey = true; this._progressEvent(); this.audio.play('sfx/pickup'); vibrate('pickup'); - // H1(round4)/ B2(escape-state round2):钥匙到手句后接播铁柜房 - // "目标名+方位"一体句 gd_cabinet_dir(下一目标 = cabinet (2,1),固定为右)。 this._timer(() => { if (s !== this._session) return; - emit('voice/found_key', false, - () => this._playGoalDir(STAGES.cabinet.target, 'voice/gd_cabinet_dir', 'voice/gd_room_r')); - }, 800); // 先让环境音进耳朵再播描述 - } else { - describe('voice/empty_2'); - } - } else if (x === 2 && y === 1) { // 铁柜房 - describe(this.cabinetOpen ? 'voice/empty_1' : 'voice/inspect_cabinet_locked'); - } else if (x === 0 && y === 0) { // 电箱房 - describe(this.powerOn ? 'voice/box_fixed' : 'voice/inspect_box_dead'); - } else if (x === 2 && y === 2) { // 大门 - describe(this.powerOn ? 'voice/inspect_gate_ready' : 'voice/inspect_gate'); + this._say(['voice/found_key'], { + onDone: () => this._playGoalDir(STAGES.cabinet.target, 'voice/gd_cabinet_dir', 'voice/gd_room_r'), + }); + }, 800); + } else describe('voice/empty_2'); + } else if (x === 2 && y === 1) { // 铁柜房:单击 = 开柜(拿钥匙时) + if (this.hasKey && !this.cabinetOpen) { + this._unlockCabinet(); + } else describe(this.cabinetOpen ? 'voice/empty_1' : 'voice/inspect_cabinet_locked'); + } else if (x === 0 && y === 0) { // 电箱房:单击 = 修电箱(拿保险丝时) + if (this.hasFuse && !this.powerOn) { + this._fixBox(); + } else describe(this.powerOn ? 'voice/box_fixed' : 'voice/inspect_box_dead'); + } else if (x === 2 && y === 2) { // 大门:单击 = 开门(通电时) + if (this.powerOn) { + this._escape(); + } else describe('voice/inspect_gate'); } else if (x === 1 && y === 1) { describe('voice/start_room'); } else { - // 空房 (1,0)/(0,1)/(1,2)/(2,0):B5a 首访句刚播过的房间内,单击不再重复 - // 同一句(_freshEmpty 离开即清;重访后单击检查语义不变,照常播 empty_*) const key = `${x},${y}`; if (this._freshEmpty !== key) describe(EMPTY_FIRST_VOICE[key] || 'voice/empty_3'); } + return 'silent-ok'; // 环境音/描述即内容反馈 } - _useItem() { - const { x, y } = this; - if (x === 2 && y === 1 && this.hasKey && !this.cabinetOpen) { - // 钥匙开铁柜 -> 得保险丝(钥匙消耗) - this.cabinetOpen = true; - this.hasKey = false; - this.hasFuse = true; - this._progressEvent(); - this.audio.play('sfx/unlock'); - this.audio.play('sfx/door_creak', { volume: 0.7 }); - vibrate('success'); - // G4(round3):拾取反馈链尾接下一目标句「保险丝到手。接下来,去找电箱。」 - // (游戏反馈层全档位,gd_economy 先例——走游戏语音链不走向导通道)——堵上 - // P2-round2 P0「保险丝到手后目标悬空:没有新指令、没有下文」。 - // H1(round4)/ B2(escape-state round2):目标句播完接播"目标名+方位"一体句 - // gd_box_dir(电箱房 (0,0) 固定为左)。 - this._playVoice('voice/unlock_cabinet', { - skippable: false, // 关键获得不可跳 - onComplete: () => this._playVoice('voice/gd_next_fuse', { - onComplete: () => this._playGoalDir(STAGES.box.target, 'voice/gd_box_dir', 'voice/gd_room_l'), - }), - }); - } else if (x === 0 && y === 0 && this.hasFuse && !this.powerOn) { - // 保险丝修电箱 -> 恢复供电(保险丝消耗) - this.powerOn = true; - this.hasFuse = false; - this._progressEvent(); - this.audio.play('sfx/spark'); - this.audio.play('sfx/power_on', { volume: 0.8 }); - vibrate('success'); - // H1(round4)/ B2(escape-state round2):通电目标句后接播"目标名+方位" - // 一体句 gd_gate_dir(大门 (2,2) 固定为右)。 - this._playVoice('voice/fix_box', { - skippable: false, // 关键获得不可跳 - onComplete: () => this._playGoalDir(STAGES.gate.target, 'voice/gd_gate_dir', 'voice/gd_room_r'), - }); - } else if (x === 2 && y === 2 && this.powerOn) { - // 通电后开大门 -> 逃脱 - this._escape(); - } else { - // 兜底:ERROR 震动 + 背包/use_fail + L1 级意象暗示(escape-state round2 B4: - // 原直通 L2 明示把答案白送给没卡关的玩家——live round1 两次实录;降级为 L1 - // 意象句(不点明房间与动作,零泄漏),答案仍锁在计时器 L2/L3 与进展链里。 - // 本进展段已播过 L1 则不重复;播了即记账(沿用 _stuckShown 既有去重)) - vibrate('error'); - const inKeyRoom = `${x},${y}` in KEY_ROOMS; - const ids = [inKeyRoom ? 'voice/use_fail' : this._inventoryVoice()]; - if (!this._stuckShown.has('L1')) { - this._stuckShown.add('L1'); - ids.push(`voice/stuck_${this._stage()}_1`); - } - this._speak(ids); - } + _unlockCabinet() { + this.cabinetOpen = true; + this.hasKey = false; + this.hasFuse = true; + this._progressEvent(); + this.audio.play('sfx/unlock'); + this.audio.play('sfx/door_creak', { volume: 0.7 }); + vibrate('success'); + this._say(['voice/unlock_cabinet', 'voice/gd_next_fuse'], { + onDone: () => this._playGoalDir(STAGES.box.target, 'voice/gd_box_dir', 'voice/gd_room_l'), + }); } - _inventoryVoice() { - if (this.hasKey && this.hasFuse) return 'voice/inv_both'; - if (this.hasKey) return 'voice/inv_key'; - if (this.hasFuse) return 'voice/inv_fuse'; - return 'voice/inv_empty'; + _fixBox() { + this.powerOn = true; + this.hasFuse = false; + this._progressEvent(); + this.audio.play('sfx/spark'); + this.audio.play('sfx/power_on', { volume: 0.8 }); + vibrate('success'); + this._say(['voice/fix_box'], { + onDone: () => this._playGoalDir(STAGES.gate.target, 'voice/gd_gate_dir', 'voice/gd_room_r'), + }); } - /** 当前谜题链阶段:找钥匙 → 开铁柜 → 修电箱 → 开大门。 */ - _stage() { - if (!this.hasKey && !this.cabinetOpen) return 'key'; - if (!this.cabinetOpen) return 'cabinet'; - if (!this.powerOn) return 'box'; - return 'gate'; - } - - /** - * H1(round4):目标房间相对当前房间的方位句(gd_room_l/r/u/d 之一)。 - * 方向语义用本游戏移动坐标系:up/down/left/right 滑动方向即方位 - * (y 0北..2南、x 0西..2东,向上滑 dy=-1)。斜对角只播一句,轴序与 - * _navVoices 一致(先东西后南北);目标在同一房间不播(返回 null)。 - */ _roomDirVoice([tx, ty]) { const dx = tx - this.x; const dy = ty - this.y; @@ -650,87 +556,112 @@ export class EscapeGame extends Game { return dy < 0 ? 'voice/gd_room_u' : 'voice/gd_room_d'; } - /** - * H1(round4)/ B2(escape-state round2):目标句播完后接播目标房间方位句 - * (游戏反馈层全档位,gd_next_fuse 先例)。B2:三处固定接线点传 namedId + - * expectedId,计算方位与枚举一致时播"目标名+方位"一体句;异常路径不符时 - * 回落原 gd_room_*(归因真话,原句保留不删)。 - */ - _playGoalDir(target, namedId = null, expectedId = null) { + _playGoalDir(target, namedId, expectedId) { const dir = this._roomDirVoice(target); if (!dir) return; - this._playVoice(namedId && dir === expectedId ? namedId : dir); + this._say([namedId && dir === expectedId ? namedId : dir]); } - // ---------- 卡关检测与三级提示 ---------- - - _startStuckTimer() { - if (this._stuckTimer) clearInterval(this._stuckTimer); - this._stuckTimer = setInterval(() => this._checkStuck(), 5000); + /** 当前谜题链阶段。 */ + _stage() { + if (!this.hasKey && !this.cabinetOpen) return 'key'; + if (!this.cabinetOpen) return 'cabinet'; + if (!this.powerOn) return 'box'; + return 'gate'; } - _resetStuck() { - this._lastActiveAt = Date.now(); - this._stuckShown = new Set(); - this._lastL3At = 0; - // B3 无进展计时:进入/重开 PLAY 时起算点与闸门一并复位 - this._lastProgressAt = Date.now(); - this._gesturesSinceProgress = 0; - this._progShown = new Set(); + _ambientId() { + const key = `${this.x},${this.y}`; + if (key === '2,2' && this.powerOn) return 'sfx/amb_gate_live'; + if (KEY_ROOMS[key]) return KEY_ROOMS[key].amb; + if (key === '1,1') return 'sfx/amb_drip'; + return EMPTY_AMB[key] || 'sfx/amb_wind'; } - /** 进展事件:清空已播提示级别,下段静默期从 L1 重新分级(计时由手势重置)。 */ - _progressEvent() { - this._stuckShown.clear(); - this._lastL3At = 0; - // B3:无进展计时起算点与活跃手势计数复位(_progShown 按阶段记账,不清) - this._lastProgressAt = Date.now(); - this._gesturesSinceProgress = 0; + _playAmbient() { + this.audio.play(this._ambientId(), { volume: 0.9 }); } - /** - * 卡关轮询(5s):按"无操作时长"分级触发提示(60s 暗示 / 120s 明示 / 180s 带路)。 - * 到点只进待播队列(_enqueueVoice):语音在播则排队等其自然播完,绝不顶句; - * 待播期间玩家任何操作即作废(见 _dropPendingStuck)。 - */ - _checkStuck() { + /** 探索期说话(房间描述/归因/帮助):SPEAKING → 播完回 ACTION。 */ + _say(ids, { critical = false, kind = 'content', onDone } = {}) { + this._lastDescribe = ids[0].startsWith('voice/') ? ids[0] : this._lastDescribe; + this.floor.speak(ids, { + critical, kind, + onDone: () => { + if (this.phase === Phase.PLAY) this.floor.enterAction(); + if (onDone) onDone(); + }, + }); + } + + // ---------- 迷茫接管(§4.5 三级:提示 → 缩小 → 演示) ---------- + + _takeover(level) { if (this.phase !== Phase.PLAY) return; - const idleMs = Date.now() - this._lastActiveAt; - const stage = this._stage(); - if (idleMs >= 180000) { - // L3 带路:首次或每 60s 重复,按当前坐标重算路线(带路语音不可单击跳过) - if (!this._stuckShown.has('L3') || Date.now() - this._lastL3At >= 60000) { - this._stuckShown.add('L3'); - this._lastL3At = Date.now(); - vibrate('warning'); - this._enqueueVoice('stuck', - [`voice/stuck_${stage}_3`, ...this._navVoices(STAGES[stage].target)], - { skippable: false, level: 'L3' }); + if (level === 1) { + // L1:说目标方位(下一步的一个动作;30s 内说过就只给提示音——原则 9) + const target = STAGES[this._stage()].target; + const nav = this._navVoices(target); + if (nav[0] && !hintSaidRecently(nav[0])) { + markHintSaid(nav[0]); + this._say(nav, { kind: 'hint' }); + } else { + this.floor.chime(); } - } else if (idleMs >= 120000 && !this._stuckShown.has('L2')) { - this._stuckShown.add('L2'); - vibrate('warning'); - this._enqueueVoice('stuck', [`voice/stuck_${stage}_2`], { level: 'L2' }); - } else if (idleMs >= 60000 && !this._stuckShown.has('L1')) { - this._stuckShown.add('L1'); - vibrate('warning'); - this._enqueueVoice('stuck', ['voice/stuck_ask', `voice/stuck_${stage}_1`], { level: 'L1' }); - } - // B3(escape-state round2):无进展 150s 活跃计时器(与无操作计时并存互不复位) - // ——活跃但零进展的玩家收到一条不泄题的进度重述 gd_prog_*(完成数由 _stage() - // 反推,归因真话)。闸门:期间 ≥5 次有效手势(=活跃探索;挂机交给 60s L1 - // 体系);key 阶段不走本通道(L1/L2/L3 已覆盖"找不到钥匙",避免双计时器 - // 对同一阶段轰炸);每进展段限 1 次(触发即记账,待播被操作作废也不重发); - // 队列语义与卡关提示同款(绝不顶句,待播期间有操作即作废),warning 震同款。 - if (stage !== 'key' && !this._progShown.has(stage) - && Date.now() - this._lastProgressAt >= 150000 && this._gesturesSinceProgress >= 5) { - this._progShown.add(stage); - vibrate('warning'); - this._enqueueVoice('stuck', [GD_PROG[stage]], { level: 'PROG' }); + } else if (level === 2) { + // L2:只放行第一个导航方向 + const nav = this._navVoices(STAGES[this._stage()].target); + const dirVoice = nav[0]; + this._restrictDir = dirVoice === 'voice/nav_left' ? G.LEFT + : dirVoice === 'voice/nav_right' ? G.RIGHT + : dirVoice === 'voice/nav_up' ? G.UP : G.DOWN; + const ONLY = { up: 'voice/sys_only_up', down: 'voice/sys_only_down', + left: 'voice/sys_only_left', right: 'voice/sys_only_right' }; + const onlyId = ONLY[this._restrictDir]; + markHintSaid(onlyId); + this._say([onlyId], { kind: 'hint' }); + } else if (level === 3) { + // L3:听我做一次 —— 游戏自己朝目标走一步,再交回 + this._restrictDir = null; + this._say(['voice/sys_demo'], { + kind: 'hint', + onDone: () => { + if (this.phase !== Phase.PLAY) return; + const [tx, ty] = STAGES[this._stage()].target; + const dx = Math.sign(tx - this.x); + const dy = dx === 0 ? Math.sign(ty - this.y) : 0; + this.x += dx; + this.y += dy; + this.audio.play('sfx/step', { volume: 0.8 }); + this._playAmbient(); + this._say(['voice/sys_now_you'], { kind: 'hint' }); + }, + }); } } - /** 从当前坐标到目标房间的导航语音(每轴一句,最多两句)。 */ + /** 静默喂迷茫检测:40s 首次、之后每 20s(有操作即重排)。 */ + _startSilenceFeed() { + this._stopSilenceFeed(); + const s = this._session; + const tick = () => { + if (s !== this._session || this.phase !== Phase.PLAY) return; + this.confusion.recordSilence(); + this._silenceTimer = setTimeout(tick, SILENCE_REPEAT_MS); + }; + this._silenceTimer = setTimeout(tick, SILENCE_FIRST_MS); + } + + _stopSilenceFeed() { + if (this._silenceTimer) { clearTimeout(this._silenceTimer); this._silenceTimer = null; } + } + + /** 进展事件:迷茫计数清零、限制解除,下段从 L1 重新分级。 */ + _progressEvent() { + this.confusion.resetForWaitPoint(); + this._restrictDir = null; + } + _navVoices([tx, ty]) { const ids = []; const dx = tx - this.x; @@ -740,90 +671,125 @@ export class EscapeGame extends Game { return ids; } - // ---------- 菜单 ---------- + // ---------- 菜单 / 结算 ---------- - _openMenu(returnPhase) { - this._menuReturn = returnPhase; - this._menuIdx = 0; + _onEndingGesture(g) { + switch (g) { + case G.TAP: + if (!this._settleBusy) this._restart(); + return 'ok'; + case G.DOWN: + this._openMenu(); + return 'ok'; + case G.LONGPRESS: { + // 重听结算(结果一句) + this._say(['voice/again']); + return 'ok'; + } + default: + return 'invalid'; + } + } + + _openMenu() { this.phase = Phase.MENU; - // 状态 BGM:菜单 = bgm/menu(10 号文档,400ms crossfade;与结算 pad 同音量 0.5, - // 同一首引擎不重启,关菜单回 ENDING 时无缝保持) + this._menuIdx = 0; + this._stopSilenceFeed(); + this.floor.interrupt(); this.audio.play('bgm/menu', { volume: 0.5, fadeMs: 400 }); - this.guide.state('menu'); - this._speak(['voice/menu_open', 'voice/menu_restart'], null, { skippable: false }); + this.floor.speak(['voice/menu_open', 'voice/menu_item1'], { + onDone: () => this.floor.yieldTurn(this._wp({ + id: 'menu', expected: [G.TAP, G.LEFT, G.RIGHT], + full: 'voice/menu_help', short: 'voice/menu_help', + silenceMs: [20000, 45000], + onDemo: () => this.floor.chime(), + })), + }); } _onMenuGesture(g) { - const items = ['voice/menu_restart', 'voice/menu_home', 'voice/menu_back']; - if (g === G.TAP) { - this._menuIdx = (this._menuIdx + 1) % items.length; - this._playVoice(items[this._menuIdx], { skippable: false }); - } else if (g === G.UP) { - this._playVoice('voice/help_ops', { skippable: false }); // 操作说明 - } else if (g === G.DOWN) { - this._closeMenu(); // 下滑 = 关闭菜单(13 号文档 §7.5 菜单约定) - } else if (g === G.DOUBLE_TAP) { - const sel = this._menuIdx; - if (sel === 0) { this.phase = this._menuReturn; this._restart(); } // 重开:_restart 内切回 bgm/drone - else if (sel === 1) this._goHome(); - else this._closeMenu(); // "返回" + const items = ['voice/menu_item1', 'voice/menu_item2', 'voice/menu_item3']; + const announce = () => { + this.floor.speak([items[this._menuIdx]], { + onDone: () => { if (this.floor.waitPoint) this.floor.resumeTurn(); }, + }); + }; + switch (g) { + case G.TAP: + this._menuIdx = (this._menuIdx + 1) % items.length; + announce(); + return 'hold'; + case G.LEFT: + this._menuIdx = (this._menuIdx + items.length - 1) % items.length; + announce(); + return 'hold'; + case G.RIGHT: { + const sel = this._menuIdx; + if (sel === 0) this._restart(); + else if (sel === 1) { this.stop(); window.location.href = '../'; } + else this._closeMenu(); + return 'ok'; + } + case G.LONGPRESS: + this.floor.speak(['voice/menu_help'], { + onDone: () => { if (this.floor.waitPoint) this.floor.resumeTurn(); }, + }); + return 'hold'; + default: + return 'invalid'; } } - /** 关闭菜单回到来源状态,并按来源恢复状态 BGM(ENDING 保持 menu pad)。 */ _closeMenu() { - this.phase = this._menuReturn; - if (this._menuReturn === Phase.ENDING) { - // 返回结算:结算态 BGM 即 bgm/menu(同 id 引擎不重启,无缝保持) - this.audio.play('bgm/menu', { volume: 0.5 }); - } else { - this.audio.play('bgm/drone'); // 关闭菜单切回主题曲 - this.guide.state('explore'); - } + this.phase = Phase.ENDING; + this.audio.play('bgm/menu', { volume: 0.5 }); + this.floor.yieldTurn(this._settleWp()); // 回结算等待,不留安静态(§4.1) } - _goHome() { - this.stop(); - window.location.href = '../'; + _settleWp() { + return this._wp({ + id: 'settle', expected: [G.TAP], + full: 'voice/again', short: 'voice/lp_tap', + silenceMs: [20000, 45000], + onDemo: () => this.floor.chime(), + }); } - // ---------- 结局 ---------- - _escape() { this.phase = Phase.ENDING; this._progressEvent(); + this._stopSilenceFeed(); vibrate('success'); this.audio.stopBgm(2500); - this.guide.state('settle'); // 状态签名:结算(逃脱) this._endingBgmStarted = false; - // B7(escape-state round2):结算链守卫——播完前 ENDING 单击不立即重开 - // (_startEndingBgm 处解除:链自然播完或 30s 资产缺失兜底,两条路径汇合) this._settleBusy = true; const totalSec = Math.floor((Date.now() - this.startElapsedMs) / 1000); this._saveEscape(totalSec); - // 结算语音链(关键获得,不可跳);播完淡入 menu pad(10 号文档:volume 0.5,1500ms) - this._speak(['voice/escape', this._timeBandVoice(totalSec), 'voice/again'], - () => this._startEndingBgm(), { skippable: false }); - // 开门/胜利音效后移 2.6s(round2-live P2 修复:原与 escape 人声同帧对撞, - // gate_open 2.8s 轰鸣恰好盖住台词头句「你扳动开关。电机轰鸣,」,三次 live - // 开头全丢;后移后人声头部无遮挡,轰鸣落在「电机轰鸣,铁门缓缓升起」附近) + this._save(false); + const got = this.ladder.onGameDone(); + const tail = ['voice/again']; + if (got) tail.push(this.ladder.teachLine(got)); + // §4.6:critical 句必须短于 3 秒——只把「单击再来一次」承诺段标 critical + this.floor.speak(['voice/escape', this._timeBandVoice(totalSec)], { + onDone: () => this.floor.speak(tail, { + critical: true, + onDone: () => { this._startEndingBgm(); this.floor.yieldTurn(this._settleWp()); }, + }), + }); this._timer(() => { this.audio.play('sfx/gate_open'); this.audio.play('sfx/win_chime', { volume: 0.8 }); }, 2600); - // 语音资产缺失时链不推进,30s 兜底起结算 pad(escape+time_band+again 正常 ~10s 内播完) - this._timer(() => this._startEndingBgm(), 30000); + this._timer(() => this._startEndingBgm(), 30000); // 资产缺失兜底 } - /** 逃脱结算 BGM:drone 淡出后淡入 bgm/menu(幂等,仅在仍是结算态时生效)。 */ _startEndingBgm() { if (this._endingBgmStarted || this.phase !== Phase.ENDING) return; - this._settleBusy = false; // B7:结算链播完(或 30s 兜底)后解除单击守卫 + this._settleBusy = false; this._endingBgmStarted = true; this.audio.play('bgm/menu', { volume: 0.5, fadeMs: 1500 }); } - /** 用时播报:4 段整句(time_band1..4),替代数字拼接。 */ _timeBandVoice(sec) { if (sec < 120) return 'voice/time_band1'; if (sec < 300) return 'voice/time_band2'; @@ -831,7 +797,33 @@ export class EscapeGame extends Game { return 'voice/time_band4'; } - // ---------- 进度存档 ---------- + // ---------- 存档 ---------- + + _flag(key) { + try { return localStorage.getItem(key) === '1'; } catch (_) { return false; } + } + + _save(inProgress) { + try { + localStorage.setItem(SAVE_KEY, JSON.stringify({ + inProgress: !!inProgress && this.phase === Phase.PLAY, + x: this.x, y: this.y, + hasKey: this.hasKey, hasFuse: this.hasFuse, + cabinetOpen: this.cabinetOpen, powerOn: this.powerOn, + visited: [...this.visited], emptyVisited: [...this.emptyVisited], + elapsedMs: this.startElapsedMs ? Date.now() - this.startElapsedMs : (this._savedElapsedMs || 0), + })); + } catch (_) { /* 隐私模式 */ } + } + + _loadSave() { + try { + const raw = localStorage.getItem(SAVE_KEY); + return raw ? JSON.parse(raw) : null; + } catch (_) { + return null; + } + } _loadStats() { try { @@ -841,7 +833,6 @@ export class EscapeGame extends Game { } } - /** 逃脱成功:本地统计 + (已登录时)同步服务器。 */ _saveEscape(totalSec) { const stats = this._loadStats(); stats.escapes += 1; @@ -859,7 +850,6 @@ export class EscapeGame extends Game { }); } - /** PUT /api/progress(未登录/服务器不可达静默跳过)。 */ _putProgress(data) { try { const auth = JSON.parse(localStorage.getItem(AUTH_KEY)); @@ -877,73 +867,8 @@ export class EscapeGame extends Game { // ---------- 工具 ---------- - _isSwipe(g) { - return g === G.UP || g === G.DOWN || g === G.LEFT || g === G.RIGHT; - } - - /** - * 对白包装:登记本句是否允许单击跳过(12 号文档裁定表,默认可跳)。 - * 本句自然播完(含被单击跳过)且没有链播新句时,补播待播队列里的语音。 - */ - _playVoice(id, { skippable = true, onComplete } = {}) { - this._playSeq++; - this._voiceSkippable = skippable; - this.audio.play(id, { - onComplete: () => { - const seq = this._playSeq; - if (onComplete) onComplete(); - if (seq === this._playSeq && !this.audio.isVoicePlaying()) this._pumpPending(); - }, - }); - } - - /** - * 待播语音队列:排队等当前语音自然播完,绝不顶掉进行中的语音(round1-live - * 修复:卡关提示/补发检查原直接开播,三次顶掉进行中的语音)。 - * kind='stuck'(卡关提示,带 level):待播期间玩家任何操作即作废; - * kind='describe'(跳过语音后补发的检查描述):连点时只补最新一次。 - */ - _enqueueVoice(kind, ids, { skippable = true, onDone = null, level = null } = {}) { - if (kind === 'describe') { - this._pendingVoice = this._pendingVoice.filter((p) => p.kind !== 'describe'); - } - this._pendingVoice.push({ kind, level, ids, skippable, onDone, s: this._session }); - this._pumpPending(); - } - - /** 当前无语音在播时播出队首待播句(仅限 PLAY 阶段;跨会话条目直接作废)。 */ - _pumpPending() { - if (this.phase !== Phase.PLAY || this.audio.isVoicePlaying()) return; - while (this._pendingVoice.length) { - const item = this._pendingVoice.shift(); - if (item.s !== this._session) continue; - this._speak(item.ids, item.onDone, { skippable: item.skippable }); - return; - } - } - - /** 玩家有操作了:作废待播的卡关提示;未播级别不计入已播,下段静默期可重新触发。 */ - _dropPendingStuck() { - this._pendingVoice = this._pendingVoice.filter((p) => { - if (p.kind !== 'stuck') return true; - this._stuckShown.delete(p.level); - if (p.level === 'L3') this._lastL3At = 0; - return false; - }); - } - - /** 对白排队依次播放(onComplete 链式触发;被打断即作废整条链)。 */ - _speak(ids, onDone, { skippable = true } = {}) { - if (ids.length === 0) { if (onDone) onDone(); return; } - const s = this._session; - this._playVoice(ids[0], { - skippable, - onComplete: () => { - if (s !== this._session) return; - if (ids.length > 1) this._speak(ids.slice(1), onDone, { skippable }); - else if (onDone) onDone(); - }, - }); + _wp(conf) { + return new WaitPoint(conf, { floor: this.floor, confusion: this.confusion }); } _timer(fn, ms) { diff --git a/web/escape/index.html b/web/escape/index.html index 2eafe386..5e97b7b5 100644 --- a/web/escape/index.html +++ b/web/escape/index.html @@ -16,6 +16,7 @@