diff --git a/docs/design/01-escape-redesign.md b/docs/design/01-escape-redesign.md
index fd5c3dc8..09277d77 100644
--- a/docs/design/01-escape-redesign.md
+++ b/docs/design/01-escape-redesign.md
@@ -103,3 +103,46 @@ start → GUIDE(可下滑跳过)→ PLAY(自由探索 + 三级提示计时
```
PLAY 阶段手势:四向滑=移动 / 单击=重播环境音+检查 / 双击=使用物品(无目标→背包+L2 明示)。ENDING:单击=重开,下滑=菜单(回大厅)。
+
+---
+
+## 修订记录(追加不改旧文,编号自 R1 起)
+
+### R1 · escape-state round2 状态引导改进(2026-09-15,方案见 25 号文档 §4.1)
+
+按 25 号文档 §4.3 实施清单 F1-F9 落地(F6/B5b 按方案"可分期"留档):
+
+1. **撞墙报方位(B1/F1)**:§2.1 G2 表格的撞墙反馈在 PLAY 阶段升级为
+ `wall` 播完接播陈述式方位句 `wall_dir_
`「左边是墙。」等 4 句
+ (同向 10s 限频、被打断作废)。这是对 round1-live"回退教学腔方位句"的
+ 部分再修订——回退掉的是措辞,方位信息以世界内陈述句回归。
+2. **目标句带目标名(B2/F2)**:三处进展链尾方位句由「右面的房间。」升级为
+ "目标名+方位"一体句(`gd_cabinet_dir`/`gd_box_dir`/`gd_gate_dir`),
+ 原 `gd_room_*` 保留作异常路径回落。
+3. **无进展计时器回收(B3/F3)**:§3.1"以上一次进展事件起算"的原始意图以
+ **平行通道**部分回收——无进展 150s 且期间 ≥5 次有效手势(活跃迷路)播
+ 不泄题的进度重述 `gd_prog_1/2/3`(每进展段限 1 次);现行"无操作 60/120/
+ 180s"三级提示骨架不动(round1-live 修订继续有效)。
+4. **§3.2 末条修订(B4/F4)**:「主动求提示保留:先播背包,再播 L2 级明示」
+ 修订为「先播背包/use_fail,再播 **L1 级意象暗示**」——live 两次实录没卡关
+ 的玩家被白送 L2 答案(泄漏 > 帮助),L2/L3 仍锁在计时器里(修订留痕于此,
+ 25 号文档 §4.1 B4 有完整证据链)。
+5. **空房首访描述(B5a/F5)**:§4 房间声音去重补一条——空房首访一次性补播
+ 现有 `empty_*`(不算进展事件;B5b 含方位特征的改词重录留后续轮)。
+6. **引导 busy 手势缓存(B6/F7)**:§2.1 补——步骤推进语音播放中(busy)
+ 的首个非跳过分手势被缓存,解锁后按新步骤补判(合法=作答,不合法=答错
+ 路径;缓存手势不触发下滑跳过),符合 00 文档 §5.4"做了手势 3 秒内必须有
+ 反馈"。
+7. **结算链单击守卫(B7/F8)**:§7 流程图 ENDING 补——结算链播完前单击不
+ 立即重开(`_settleBusy`,链完/30s 兜底解除),保障用时播报与操作指引的
+ 完整到达权。
+8. **目标句全局可达(B8/F9)**:§2.1"跳过引导直达 PLAY"修订——跳过确认句
+ 后与 REOPEN 直进路径都补播一次 `g7_goal`(任何路径进 PLAY 都至少听过
+ 一次目标句;二周目可单击跳过)。
+
+新断言登记(13 号文档不动):escape playtest 场景 G-N(B1/B2/B3/B4/B5a/
+B6/B7/B8 各场景,含边界:B3 进展复位、B4 降级后 L2 仍可达、B6 缓存按序
+补判、B7 结算链完整到达)共 +85 条至 235 全绿;guide.test ⑩c 换线 +
+⑩d B6 full 档共 132 全绿;逐条勾账与既有断言变更登记见 25 号文档
+「阶段四 · 实施勾账」。新语音 10 句台词表见 01-escape-voice-ids.md
+「escape-state round2 新增主表句」。
diff --git a/docs/design/01-escape-voice-ids.md b/docs/design/01-escape-voice-ids.md
index 7e900301..9b2f0219 100644
--- a/docs/design/01-escape-voice-ids.md
+++ b/docs/design/01-escape-voice-ids.md
@@ -53,3 +53,26 @@ web/engine/gesture.js inputMode())时,引擎 voice 候选链前置加载
| `again` | 按空格,再来一次。按下方向键,打开菜单。 |
| `help_ops` | 方向键,在房间之间移动。按空格,检查房间,或者重听房间的声音。按回车,使用物品,或者查看背包。 |
| `skip_hint` | 按空格可跳过旁白。 |
+
+---
+
+## escape-state round2 新增主表句(10 句,25 号文档 §4.1 方案 B1/B2/B3)
+
+> 状态引导改进方案([25-escape-map-flow.md](25-escape-map-flow.md) §4.1 B1/B2/B3、§4.3
+> 实施清单 F1/F2/F3)的新增语音,台词与 25 号文档草案**逐字一致**。全部不含操作词,
+> 无键盘变体(变体只覆盖含操作词的句子,id 集合 ⊆ 主表的既有约定不变)。
+> 管线:`gen_voice_dual.py --game escape --only ` → `--check` → `--convert` →
+> `check_all_assets.py` 终检。
+
+| id | 台词全文 | 方案 |
+|---|---|---|
+| `wall_dir_l` | 左边是墙。 | B1 撞墙方位(陈述式) |
+| `wall_dir_r` | 右边是墙。 | B1 |
+| `wall_dir_u` | 上边是墙。 | B1 |
+| `wall_dir_d` | 下边是墙。 | B1 |
+| `gd_cabinet_dir` | 铁柜,在右面的房间。 | B2 目标名+方位一体句(found_key 链尾) |
+| `gd_box_dir` | 电箱,在左面的房间。 | B2(gd_next_fuse 链尾) |
+| `gd_gate_dir` | 大门,在右面的房间。 | B2(fix_box 链尾) |
+| `gd_prog_1` | 钥匙已经到手。找到能用它的锁。 | B3 无进展进度重述(stage=cabinet) |
+| `gd_prog_2` | 铁柜已经打开。电还没有通。 | B3(stage=box) |
+| `gd_prog_3` | 电已经通了。去找那扇大门。 | B3(stage=gate) |
diff --git a/docs/design/25-escape-map-flow.md b/docs/design/25-escape-map-flow.md
new file mode 100644
index 00000000..78e8ecfd
--- /dev/null
+++ b/docs/design/25-escape-map-flow.md
@@ -0,0 +1,645 @@
+# 25 · 《暗室》地图与玩家流程文档 + 状态引导改进方案
+
+> **as-built 声明**:本文档为「以代码为准」的现行实现文档化,权威来源是
+> `web/escape/game.js`(837 行,R2 重设计 + round1~round4 历次 live 修复),
+> 与 01 号设计文档(01-escape-redesign.md)对账,**凡不一致处以代码为准**,
+> 差异全录于 §1.5。语音 id 全集与全文以 `tools/gen_voice_dual.py` 的
+> `ESCAPE_LINES`(80 句)为准。
+>
+> 阶段三「状态引导改进方案」的取证材料在 `research/escape-state/round1/`
+> (mock 转写 + live 真机一局 + P1/P3 人设评审)。方案只产出设计,未实施。
+>
+> 轮次:escape-state round1(2026-09-14)。靶问题(用户真机反馈):
+> **玩家不知道自己的状态**——不知道自己在哪、不知道目标、不知道进度。
+
+---
+
+# 阶段一 · as-built 文档化
+
+## 1. 地图章
+
+### 1.1 房间总图(3x3,坐标 x 0西→2东,y 0北→2南)
+
+```
+ x=0(西) x=1 x=2(东)
+y=0 ┌───────────┬───────────┬───────────┐
+(北) │ (0,0) │ (1,0) │ (2,0) │
+ │ 电箱房 │ 空房·风 │ 空房·嗡鸣 │
+ │ amb_power │ amb_wind │ amb_hum │
+ ├───────────┼───────────┼───────────┤
+y=1 │ (0,1) │ (1,1) │ (2,1) │
+ │ 空房·嗡鸣 │ 起点 │ 铁柜房 │
+ │ amb_hum │ amb_drip │ amb_cabinet│
+ ├───────────┼───────────┼───────────┤
+y=2 │ (0,2) │ (1,2) │ (2,2) │
+(南) │ 储物间 │ 空房·风 │ 大门 │
+ │ amb_storage│ amb_wind │ amb_gate │
+ └───────────┴───────────┴───────────┘
+ (通电后 amb_gate → amb_gate_live)
+```
+
+- **连通关系**:全部相邻房间四向连通——`_move()` 只做 0..2 边界检查
+ (game.js:394-417),内部 12 条边全部可走。**全图无锁门、无单向通道**,
+ 唯一"锁"是动作门(双击使用物品的前置条件),不是空间门。
+- **撞墙边界**:仅外圈 12 段墙。撞墙 = 目标坐标出界 → `sfx/wall` 闷响 +
+ `error` 震 + `voice/wall`「那边是墙,换个方向。」(每次撞都播,
+ game.js:397-404)。
+- **起点**:(1,1) 正中央,检查语音 `start_room`「你站在设施中央的房间里」
+ ——"中央"是全图**唯一一次**给出的绝对方位锚点(见 §3 盲区 S1)。
+
+### 1.2 每房间资产与点位表
+
+| 房间 | 坐标 | 环境音 id | 进入描述(仅首到) | 单击检查语音(状态相关) | 双击使用 |
+|---|---|---|---|---|---|
+| 电箱房 | (0,0) | `sfx/amb_power` | `room_power`「这个房间的墙上,有一只电箱。」 | 未修 `inspect_box_dead` / 已修 `box_fixed` | hasFuse → `fix_box`(修电) |
+| 空房 | (1,0) | `sfx/amb_wind` | 无 | `empty_1`「空空荡荡,只有你自己的回声。」 | 兜底 |
+| 空房 | (2,0) | `sfx/amb_hum` | 无 | `empty_3`「除了远处的水滴声,这里什么都没有。」 | 兜底 |
+| 空房 | (0,1) | `sfx/amb_hum` | 无 | `empty_2`「摸到的只有冰冷粗糙的墙壁。」 | 兜底 |
+| 起点 | (1,1) | `sfx/amb_drip` | 无 | `start_room`(中央房间+操作总览) | 兜底 |
+| 铁柜房 | (2,1) | `sfx/amb_cabinet` | `room_cabinet`「这个房间里,立着一个铁柜。」 | 未开 `inspect_cabinet_locked` / 已开 `empty_1` | hasKey → `unlock_cabinet`(得保险丝) |
+| 储物间 | (0,2) | `sfx/amb_storage` | `room_storage`「货架上堆满杂物,角落里传来水滴声。」 | 有钥匙且柜未开 → **拾取钥匙** `found_key`;否则 `empty_2` | 兜底 |
+| 空房 | (1,2) | `sfx/amb_wind` | 无 | `empty_3` | 兜底 |
+| 大门 | (2,2) | `sfx/amb_gate`(通电后 `amb_gate_live`) | `room_gate`「面前,是一扇沉重的铁门。」 | 未通电 `inspect_gate` / 已通电 `inspect_gate_ready` | powerOn → 逃脱结算 |
+
+要点:
+- **环境音 6 类铺 9 房**:4 间空房只有 2 种音(wind×2、hum×2)且成对角分布
+ ((1,0)/(1,2) 风、(0,1)/(2,0) 嗡鸣)——**空房之间声学不可区分**(盲区 S5)。
+- **首到描述只播一次**(`visited` 集合,game.js:112,412-416);重访关键房间
+ 零语音,只换环境音。
+- **进房间自动播环境音**(每次移动都播,game.js:429-431),单击检查也先重播
+ 环境音再播描述(800ms 间隔让环境音先进耳朵,game.js:446-448)。
+- 大门通电后环境音切换 `amb_gate_live` 是**唯一**一条"谜题状态改变可听觉感知"
+ 的环境通道(01 号文档 §4.4 的设计意图,代码兑现)。
+
+### 1.3 解谜链依赖图
+
+```
+[储物间 (0,2) 单击] ──得钥匙──▶ [铁柜房 (2,1) 双击] ──得保险丝──▶
+[电箱房 (0,0) 双击] ──通电──▶ [大门 (2,2) 双击] ──▶ 逃脱结算
+ hasKey=true cabinetOpen=true powerOn=true
+ hasFuse=true
+```
+
+- 链上 4 个动作,每个都是"在对的房间双击",前置 = 上一环的物品/状态。
+- 物品即进度:`hasKey`/`hasFuse` 为真即代表链推进到对应环;`cabinetOpen`/
+ `powerOn` 为真代表该环完成。`_stage()`(game.js:538-543)按此回退链判定
+ 当前阶段:`key → cabinet → box → gate`。
+- **最短通关路径**(11 次移动 + 1 单击 + 3 双击,约 2-3 分钟):
+ (1,1)→左(0,1)→下(0,2)【单击拾钥匙】→右(1,2)→右(2,2)→上(2,1)【双击开柜】
+ →左(1,1)→左(0,1)→上(0,0)【双击修电箱】→右(1,0)→右(2,0)→下(2,2)【双击逃脱】。
+ 注意这条最短路径**恰好绕过所有空房**,且铁柜房/电箱房的方位句
+ (H1)在这条路径上分别给出"右面的房间"(储物间→铁柜房)与
+ "左面的房间"(铁柜房→电箱房)。
+
+### 1.4 房间 → 谜题 → 状态映射表
+
+| 阶段 `_stage()` | 判定条件 | 目标房间(L3 带路 STAGES) | L1 暗示 | L2 明示 | 进展事件(清已播级别) |
+|---|---|---|---|---|---|
+| `key` | `!hasKey && !cabinetOpen` | (0,2) 储物间 | `stuck_key_1` 铁锈味意象 | `stuck_key_2` 西南角储物间单击 | 拾钥匙、**首到储物间** |
+| `cabinet` | `hasKey` | (2,1) 铁柜房 | `stuck_cabinet_1` 钥匙在你身上 | `stuck_cabinet_2` 东边铁柜房双击 | 开柜(得保险丝) |
+| `box` | `cabinetOpen && !powerOn` | (0,0) 电箱房 | `stuck_box_1` 电流声意象 | `stuck_box_2` 西北角电箱房双击 | 修电箱 |
+| `gate` | `powerOn` | (2,2) 大门 | `stuck_gate_1` 电已通 | `stuck_gate_2` 东南角大门双击 | 逃脱 |
+
+- 进展事件调 `_progressEvent()`:清空 `_stuckShown`,下段静默期从 L1 重新分级
+ (game.js:578-582)。
+- 代码把**首到储物间**也算进展事件(KEY_ROOMS 含 (0,2)),01 号文档 §3.1
+ 只列了铁柜房/电箱房/大门——差异见 §1.5-③。
+
+### 1.5 代码 vs 设计文档差异清单(以代码为准)
+
+| # | 01 号设计文档 | 现行代码 | 性质 |
+|---|---|---|---|
+| ① | §3.1/§3.2 卡关计时"以上一次**进展事件**起算"(无进展 60/120/180s) | 按"**无操作时长**"计时:PLAY 阶段任何有效手势重置计时(game.js:26-31,160-164,589-612) | round1-live 修复(原机关枪式报答案+顶语音),设计意图被反向修订 |
+| ② | §2.1 G0「单击继续」+ CONFIRM 震 | intro 播完 **1.2s 无操作自动推进 G1**(game.js:205-211);G0 单击仍有效 | round2-live 修复(intro 后死寂 17~91s) |
+| ③ | §3.1 进展事件列"首到铁柜房/电箱房/大门" | 首到**储物间**同样计进展(KEY_ROOMS 四房间统一处理) | 代码超集 |
+| ④ | §2.1「引导期间任何步骤可下滑跳过」 | 仅 G0/G3/G5/G6 可下滑跳过;G1/G2/G4 下滑=作答(game.js:239-245) | 13 号文档裁定,round2-live 修复教学链断裂 |
+| ⑤ | §3.2「双击无目标兜底:先播背包,再播 L2」 | 在**关键房间**内播 `use_fail` 而非背包;非关键房才播背包(game.js:519-527) | 代码细化 |
+| ⑥ | 14 号文档 v1 裁定 5/§2:「连续撞墙 ≥2 次 → full 档卡关三级阈值提前一级」 | **未落地**:game.js 无任何撞墙连击/阈值提前逻辑(已核实无 wallStreak 类状态) | 文档先行、代码缺失,以代码为准(=无此功能) |
+| ⑦ | §2.1 G2 反馈含"未撞墙:脚步 + SWIPE_OK"分支 | 引导期位置固定,**滑动即撞墙反馈**(每滑必 wall 闷响+error 震+wall 语音),无"未撞墙"分支(game.js:264-272) | 设计草案分支不存在 |
+| ⑧ | §2.1 G6 反馈「兜底播报背包 + CONFIRM 震」 | G6 双击答对播 `inv_key`+`g6_done`+`g7_goal` 链(无单独 confirm 震,靠引擎双击内置 confirm) | 等价 |
+| ⑨ | (01 号文档无 REOPEN 相) | 存在 REOPEN 二周目阶段:上滑重听引导/单击直接开始(game.js:54-55,143-151) | 01 号文档 §2.1 末段提到过一次,代码为完整实现 |
+| ⑩ | §3.2 L1 附"语音提示前播'需要帮助吗?'" | `stuck_ask` 仅 L1 前插播,L2/L3 无(game.js:607-611) | 一致(此处列出防误扩) |
+| ⑪ | 01 号文档 §4.2「环境音扩到 6 种」 | 实际 8 个环境音资产 6 类(含 `amb_gate_live` 通电变体) | 一致(超集) |
+
+另注:14 号文档 round2 F2.3「撞墙接播 guide_blunt_ 方位句」曾被落地、
+后被 round1-live **回退**为只播 `voice/wall` 极简句(game.js:400-403 注释),
+教学腔方位句仅限引导期 G3 纠错路径——这条历史是阶段三 B1 方案的前车之鉴
+(方位信息要给,但措辞不能再走教学腔)。
+
+---
+
+## 2. 玩家流程章
+
+### 2.1 全局状态机
+
+```
+ ┌────────────── 首次开局 ──────────────┐
+ ▼ │
+ start ─▶ REOPEN(二周目才有:上滑重听引导/单击直接开始)
+ │ 首局自动
+ ▼
+ GUIDE(脚本化教学,玩家手势推进,G0→G7)
+ │ G6 双击答对 → g7_goal 链播完 _finishGuide
+ │ G0/G3/G5/G6 下滑跳过 → guide_skip → PLAY
+ ▼
+ PLAY(自由探索 + 卡关三级提示计时器;谜题链阶段 key→cabinet→box→gate)
+ │ 大门双击(powerOn)→ _escape()
+ ▼
+ ENDING(结算链 escape→time_band→again;单击重开 / 下滑菜单)
+ │ 下滑
+ ▼
+ MENU(模态:单击循环选项 / 双击执行 / 上滑帮助 / 下滑关闭)
+```
+
+- **会话令牌** `_session`:重开/跳过/回大厅时递增,使旧计时器与语音链回调
+ 全部失效(game.js:98,826-831)。
+- **语音纪律基础设施**:待播队列 `_pendingVoice`(kind=`stuck` 卡关提示 /
+ `describe` 补发检查),只在当前语音自然播完后播出,**绝不顶句**;待播期间
+ 玩家任何操作即作废卡关提示(未播级别不计入已播,game.js:125,783-810)。
+- 单击跳句(12 号文档):可跳句播放中单击 = `skipVoice()` 跳过本句 + 补发检查
+ (环境音即时重播、描述进待播队列);不可跳句(引导 G0-G7、关键获得链、
+ L3 带路)单击 = 忽略(game.js:377-388,445)。
+
+### 2.2 引导子状态 G0-G7(GUIDE 阶段)
+
+| 步 | 语音(全文见 §3 账本) | 等待手势 | 答对反馈 | 答错(3 次给 blunt+warning 震) |
+|---|---|---|---|---|
+| G0 | `g0_intro`(剧情) | 单击;**1.2s 无操作自动推进** | g1_move+skip_hint | intro 播放中滑动=忽略;空档滑动=`guide_offpath`「先跟我走。」 |
+| G1 | `g1_move`「朝任意方向滑一下」 | 任意滑动(下滑也算作答,不可跳过) | step + `g1_done` + `g2_wall` | `guide_wrong_gesture` |
+| G2 | `g2_wall`「再滑两下,试试碰碰墙」 | 滑动×2(每滑必播 wall 闷响+error 震+`wall`) | 满 2 次 → `wall`+`g3_left` | 非滑动 = wrong_gesture |
+| G3 | `g3_left`「向左滑。」 | 左滑(→(0,1)) | step + `g4_down` | 其他滑动 = `guide_wrong_left` |
+| G4 | `g4_down`「再向下滑。」 | 下滑(→(0,2) 储物间) | step + `amb_storage` + `g5_inspect` | 其他滑动 = `guide_wrong_down` |
+| G5 | `g5_inspect`「单击检查」 | 单击 → **拾钥匙** | pickup + `g5_key` | 非单击 = wrong_gesture |
+| G6 | `g6_doubletap`「双击试试」 | 双击 | `inv_key`+`g6_done`+`g7_goal` | 非双击 = wrong_gesture |
+| G7 | `g7_goal`「找到能用这把钥匙打开的东西…」 | (无,链播完自动进 PLAY) | — | 手势忽略 |
+
+- 引导期位置是脚本写死的:(1,1)→(0,1)→(0,2),与真实地图一致;G2 的"撞墙"
+ 是伪撞墙(位置并未移动)。
+- `_guideBusy` 推进语音播放中**忽略一切非跳过分手势**——这是阶段四 live 局
+ 抓到的事故点(盲区 S6):G3 答案在 busy 窗口内被吞,玩家紧接着的下滑被
+ 当成"跳过引导"。
+
+### 2.3 手势语义总表(逐阶段)
+
+| 手势 | REOPEN | GUIDE | PLAY | ENDING | MENU |
+|---|---|---|---|---|---|
+| 上滑 | 重听引导 | 下滑跳过仅限 G0/G3/G5/G6,**上滑无跳过语义**;其余步上滑=答错/忽略 | **移动(dy=-1)** | 忽略 | 播 `help_ops` 操作说明 |
+| 下滑 | 忽略 | G0/G3/G5/G6=跳过引导;G1/G2/G4=作答 | **移动(dy=+1)** | 打开菜单 | 关闭菜单 |
+| 左/右滑 | 忽略 | 按步骤判定(G3 左滑=答案) | **移动(dx=∓1)** | 忽略 | 忽略 |
+| 单击 | 直接开始(stopVoice+进 PLAY) | 按步骤判定(G0/G5=推进/拾取) | voice 播放中=跳句+补检查;无 voice=检查 | **重开一局** | 循环切换菜单项(播选项名) |
+| 双击 | 忽略 | 按步骤判定(G6=答案) | 使用物品:有目标=解谜动作;无目标=**兜底**(背包或 use_fail + `stuck__2` L2 明示) | 忽略 | 执行当前项 |
+| 长按 | 引擎:旁白快进(后松开) | 同左 | 同左 | 同左 | 同左 |
+
+- escape 的手势映射**全面偏离全局默认**(四向滑=移动、双击=使用而非重听),
+ 00 号文档 §5.3 豁免表已备案;偏离内容在引导 G1/G2/G5/G6 逐一手把手教过。
+- **PLAY 阶段没有菜单、没有上滑帮助**(00 §5.3:帮助入口 = 双击兜底 +
+ 卡关计时器)——"主动查状态"在 escape 里只有双击兜底一个口子,而它直通
+ L2 明示(盲区 S4)。
+
+### 2.4 卡关三级提示(PLAY 阶段自动触发)
+
+| 级别 | 触发(现行代码:无操作时长) | 内容 | 附带 | 重复规则 |
+|---|---|---|---|---|
+| L1 暗示 | 60s 无任何手势 | `stuck_ask`「需要帮助吗?」+ `stuck__1` 意象句 | warning 震 | 每进展段 1 次 |
+| L2 明示 | 120s | `stuck__2` 直接点明房间+动作 | warning 震 | 每进展段 1 次 |
+| L3 带路 | 180s | `stuck__3` + `nav_*` 按当前坐标算路(每轴一句,最多两句,先东西后南北) | warning 震 | 此后每 60s 无操作重复,按当时坐标重算 |
+
+- **队列与作废**:提示只进待播队列,等当前语音播完;待播期间玩家任何操作
+ 即作废(`_dropPendingStuck`,game.js:803-810)。→ 活跃玩家可能连 L1 都
+ 听不到,但它本来就不该听到(计时按无操作)。
+- **双击兜底 = 另一条 L2 通道**(game.js:519-527):在关键房间播 use_fail、
+ 在非关键房播背包,**都接 `stuck__2`**——玩家主动探状态会被直接
+ 喂答案(阶段四 live 两次实录,见盲区 S4)。
+- 14 号文档裁定的"连续撞墙 ≥2 次阈值提前一级"未落地(§1.5-⑥)。
+
+### 2.5 关键路径与分支
+
+**主路径(引导完整走)**:REOPEN(跳过)→ G0 intro → G1 滑一步 → G2 撞墙×2
+→ G3 左滑 → G4 下滑进储物间 → G5 单击拾钥匙 → G6 双击 → G7 目标陈述 →
+PLAY(已持钥匙,阶段=cabinet)→ 自由探索开柜→修电箱→开门。
+引导结束瞬间玩家已知:移动方式、撞墙反馈、检查、双击、背包里有钥匙、
+下一步目标 = 找锁。**这是全游戏信息最充裕的时刻。**
+
+**分支与边界路径**:
+1. **跳过引导**(G0/G3/G5/G6 下滑):`guide_skip`「跳过引导,直接开始自由
+ 探索。」→ 直接 PLAY,**不持钥匙、没听过 g7_goal**——开局只有
+ `start_room` 一句操作总览,目标维度完全空白(盲区 S4,live 实录)。
+2. **二周目** REOPEN:上滑=重听引导;单击=直接开始(播 start_room)。
+3. **撞墙**:外圈 12 段;反馈 wall 闷响+error 震+`wall` 极简句。无方位
+ 信息(教学腔方位句已被 round1-live 回退,§1.5 注)。
+4. **双击兜底**:非关键房 = 背包句 + L2;关键房 = use_fail + L2。
+ (live 实录:柜已开后再双击铁柜房,听到 use_fail + `stuck_box_2`——
+ 玩家并没有卡关,答案句白送。)
+5. **空房**:进入只播环境音(与邻房可能同音),单击才播 empty_*。
+6. **重访关键房间**:零语音(首到描述只一次)。
+7. **结算**:escape 链(不可跳)播完淡入 bgm/menu;单击=立即重开
+ (`_restart` 不管结算链是否播完——live 实录结算链被单击截断,
+ time_band/again 未播,见盲区 S7);下滑=菜单(重开/回大厅/返回)。
+8. **菜单**:暂停一切(escape 无局内计时器,影响仅限语音队列)。
+
+### 2.6 各阶段玩家可获得的信息渠道
+
+| 阶段 | 语音 | earcon/音效 | 震动 | 环境音 | BGM |
+|---|---|---|---|---|---|
+| REOPEN | reopen_prompt | turn cue | —(等抉择) | — | bgm/drone |
+| GUIDE | 逐句指令+纠错(不可跳) | step/wall/pickup | error(答错)/warning(3 连错)/pickup(拾钥匙)/引擎内置 swipe/confirm | amb_storage(G4 到储物间) | bgm/drone |
+| PLAY | 首到房间描述/检查句/进展链(found_key→gd_room_*)/卡关三级/双击兜底 | step/wall/pickup/unlock/door_creak/spark/power_on/gate_open/win_chime | error(撞墙/无效使用)/success(开柜修电/逃脱)/pickup(拾钥匙)/warning(卡关提示)/引擎 swipe | 每房进来自动播,单击可重听 | bgm/drone |
+| ENDING | escape+time_band+again(不可跳) | gate_open(后移 2.6s)+win_chime | success | — | drone 淡出→bgm/menu 淡入 |
+| MENU | menu_open+选项名/help_ops | — | confirm(双击执行) | — | bgm/menu |
+
+信息渠道的四division 铁律(14 号裁定 7):语音说"你要做什么"、earcon 说
+"哪类事/哪个状态"、BGM 说"宏观状态"、震动说"刚才那下对不对"。escape 的
+earcon 仅向导层 8 签名(explore/menu/settle/turn)+ 内容音效;**游戏层没有
+任何自研 earcon**——方位/进度类信息目前没有 earcon 通道可用(新增需走
+14 号文档的引擎词表修订,或改用语音/环境音通道)。
+
+---
+
+## 3. 信息账本(语音 id 全集三类归档 + 盲区清单)
+
+### 3.1 语音 id 全集(80 句,按「状态告知 / 操作反馈 / 叙事·教学」三类)
+
+**A 类 · 状态告知**(回答"我在哪/我有什么/进展如何/系统处于什么状态"):
+
+| id | 全文(摘要) | 覆盖的状态维度 |
+|---|---|---|
+| `start_room` | 你站在设施中央的房间里。滑动移动,单击检查,双击使用物品。 | 绝对方位(唯一一次)+ 操作总览 |
+| `room_storage` / `room_cabinet` / `room_power` / `room_gate` | 储物间/铁柜/电箱/大门 首到描述 | 房间身份(仅首到) |
+| `empty_1/2/3` | 空空荡荡/冰冷墙壁/远处水滴 | 空房身份(仅单击检查时) |
+| `inspect_cabinet_locked` / `inspect_box_dead` / `box_fixed` / `inspect_gate` / `inspect_gate_ready` | 各互动物状态描述 | 谜题对象状态 |
+| `inv_empty/key/fuse/both` | 背包四档 | 物品持有 |
+| `wall` | 那边是墙,换个方向。 | 边界负信息(无方位) |
+| `stuck_ask` + `stuck__1` ×4 | 需要帮助吗?+ 意象暗示 | 目标(L1 级,意象) |
+| `stuck__2` ×4 | 点明房间+动作 | 目标(L2 级,明示) |
+| `stuck__3` ×4 + `nav_udlr` ×4 | 带路 | 目标+路线(L3 级) |
+| `gd_next_fuse` | 保险丝到手。接下来,去找电箱。 | 目标(进展事件当下) |
+| `gd_room_l/r/u/d` | 左/右/上/下面的房间。 | 目标相对方位(无目标名) |
+| `time_band1-4` | 用时四档整句 | 结算用时 |
+| `use_fail` | 这里没有用得上东西的地方。 | 无效使用(状态否定) |
+| `menu_open/restart/home/back` + `help_ops` + `reopen_prompt` | 菜单与帮助 | 系统状态 |
+| `box_fixed`(重复列出处归入检查) | — | — |
+
+**B 类 · 操作反馈**(回答"刚才那下对不对/成了没"):
+
+| id | 全文(摘要) | 触发 |
+|---|---|---|
+| `g1_done` / `g5_key` / `g6_done` | 引导答对确认 | 引导 G1/G5/G6 |
+| `found_key` | 摸到钥匙(长句,不可跳) | 拾取 |
+| `unlock_cabinet` | 开柜得保险丝(不可跳) | 解谜成功 1 |
+| `fix_box` | 装保险丝通电(不可跳) | 解谜成功 2 |
+| `escape` | 逃脱结算(不可跳) | 通关 |
+| `replay` | 黑暗再次合拢(重开) | 重开 |
+| `again` | 单击再来一次(结算链尾) | 结算 |
+| `use_fail`(兼 A 类) | 无效使用 | 双击无目标 |
+| `guide_wrong_up/down/left/right/gesture` ×5 | 不是这个方向/操作 | 引导答错 |
+| `guide_blunt_up/down/left/right` ×4 | 直白重述(3 连错) | 引导连续答错 |
+| `guide_offpath` | 先跟我走。 | 引导期越轨滑动 |
+
+**C 类 · 叙事·教学**(铺氛围/教手势,不承担状态):
+
+| id | 全文(摘要) |
+|---|---|
+| `g0_intro` | 黑暗。你在废弃的地下设施里醒来…… |
+| `g1_move` / `g2_wall` / `g3_left` / `g4_down` / `g5_inspect` / `g6_doubletap` / `g7_goal` | 引导指令六步 + 目标陈述 |
+| `skip_hint` / `guide_skip` | 跳过旁白提示 / 跳过确认 |
+| `reopen_prompt`(兼 A) | 二周目抉择 |
+
+### 3.2 状态维度 × 信息渠道矩阵(空白格 = 盲区)
+
+| 状态维度 | 语音 | 环境音 | 震动 | earcon/BGM | 结论 |
+|---|---|---|---|---|---|
+| 绝对方位(我在图上的哪格) | 仅开局 `start_room` 一句"中央";L2 角落词 | 无 | 无 | 无 | **盲区 S1** |
+| 相对方位(目标在我哪边) | 仅进展链当下(gd_room)+ L3 带路 | 无 | 无 | 无 | 半盲区(时机太窄) |
+| 撞墙方位(哪面不通) | 无(教学腔已被回退) | 无 | 无(撞墙=error 通式) | 无 | **盲区 S1b** |
+| 已探索地图(我来过这吗) | 无(首到描述不重复,重访零语音) | 无 | 无 | 无 | **盲区 S2** |
+| 空房身份(这屋是哪间) | 仅单击检查时 empty_*,且文本无方位 | 2 种音铺 4 房,不可区分 | 无 | 无 | **盲区 S5** |
+| 谜题链全局进度(几环完成/还剩几环) | 无主动渠道;inv_* 只报物品 | 仅大门通电有 amb_gate_live | 无 | 无 | **盲区 S3** |
+| 当前目标(L0 轻量重述) | 仅引导尾 g7_goal(跳过即永久丢失)+ 进展链当下一句 | 无 | 无 | 无 | **盲区 S4** |
+| 主动查询入口 | 双击兜底 = 直通 L2 明示(泄漏) | — | — | — | **盲区 S4b(机制错配)** |
+| 物品持有 | inv_* ✓(双击兜底时) | 无 | 拾取时 pickup ✓ | 无 | 基本覆盖 |
+| 互动物状态(锁/电/门) | 检查语音 ✓ | 大门通电 ✓ | 解谜 success ✓ | 无 | 覆盖(需玩家记得检查) |
+| 卡关计时器状态 | 无(提示到点才出现) | — | — | — | 设计内,不动 |
+| 引导 busy 窗口(我手势算数吗) | 无 | 无 | 仅引擎 [10] | 无 | **盲区 S6**(事故级) |
+| 结算完整性(我通关用时/重开确认) | 结算链可被单击无条件截断 | — | — | — | **盲区 S7** |
+
+> S1/S2/S3/S4/S5 是用户真机反馈"不知道自己在哪/目标/进度"的精确落点;
+> S6/S7 是 live 局新抓到的机制级盲区。S1b(撞墙无方位)是 P1 评审的死线项。
+
+---
+
+# 阶段三 · 状态引导改进方案(escape-state round1)
+
+## 4.0 取证摘要
+
+- **mock 转写**(`research/escape-state/round1/transcript-mock.md`):引导完整
+ 路径(G0-G7 全覆盖),证明"引导结束时信息最充裕";此后唯一信息增量来自
+ 进展链(mock 行 107-108、126:gd_next_fuse + 方位句)。
+- **live 真机一局**(`research/escape-state/round1/live-transcript.md`,女声响
+ full 档,约 14 分钟,打到通关并重开):52 个语音段,55 条判定行
+ (OK 35 / SFX_BGM_ONLY 18 / TRUNCATED 2;两条 TRUNCATED 均有玩家手势
+ 归因,非资产缺陷)。
+ 关键事件:引导期 busy 吞手势(行 30-35)、误触下滑跳过引导(行 61-74)、
+ 双击兜底两次直通 L2 明示(行 161-178、230-243)、结算链被单击截断
+ (行 253-275)、L1/L2/L3 三级按时触发且 L3 每 60s 按坐标重算重播
+ (行 75-86、288-294、295-346)。
+- **人设评审**:P1 视障(`p1-blind.md`)+ P3 白领(`p3-office.md`),双席对
+ 盲区清单达成一致。
+
+设计约束(本方案全部条目的共同红线):
+- **00 号宪法**:整句预生成、禁运行时拼接;数字信息分档整句;震动语义按
+ 00 §2.2 既有 10 震型、不新增语义震;手势映射零改动(上滑仍是移动)。
+- **14 号向导层**:三档语义(full=earcon+语音+震 / standard=耳标+震 / off=
+ 全静);新 earcon 不进本方案(引擎 8 签名词表修订成本过高,方位与进度信息
+ 一律走语音通道,游戏反馈层全档位,与 gd_next_fuse/gd_room 先例同级)。
+- **归因真话律**:进度句报的完成数必须等于真实状态机(hasKey/cabinetOpen/
+ powerOn 派生),不许多报少报。
+- **难度不降低**:所有新语音只报"状态与方向",不报"做法";L2/L3 的到达不因
+ 新通道提前或变频繁。
+- **节奏不破**:单局 15-30 分钟的宏观节奏由谜题链决定,新播报全部挂在
+ "进展事件 / 首次 / 限频"上,不给活跃玩家增加语音负担。
+
+## 4.1 方案条目
+
+### B1 · 撞墙报方位(陈述式,非教学腔)
+
+- **问题(证据)**:撞墙反馈只有 `wall`「那边是墙,换个方向。」——无方位。
+ live 行 40-55 连撞两次地图零增量;P1「每次错都学不到东西」、P3「盲操的
+ 试错成本全是沉没成本」。14 号 F2.3 曾用 `guide_blunt_*`「手指贴着屏幕,向
+ X 推一下」补方位,被 round1-live 以"教学腔中后期出戏"回退——回退掉的是
+ 措辞,方位信息本身没有错杀的理由。
+- **目标状态**:玩家每次撞墙都能建立一个"此方向不通"的地图标记,措辞是
+ 世界内陈述句而非操作教学。
+- **方案**:新增 4 句整句(游戏反馈层全档位,撞墙既有链路 `sfx/wall`+error
+ 震+`wall` 播完后接播,被下一次移动打断则自然作废——复用 F2.3 原接线模式):
+ - `wall_dir_l`「左边是墙。」 / `wall_dir_r`「右边是墙。」 /
+ `wall_dir_u`「上边是墙。」 / `wall_dir_d`「下边是墙。」
+ - 触发:PLAY 阶段撞墙、`voice/wall` 播完后、同方向 10s 内限频 1 次
+ (防贴墙连撞连播)。**仅报方位事实,不说"换个方向/推一下"**——后半句
+ 现有 `wall` 已覆盖。
+ - 方位 = 滑动方向(escape 移动坐标系),按真实撞墙方向取句,归因真话。
+ - 震动:沿用 error,不新增。
+- **宪法条款**:00 §2.2(既有 error 震型);00 §3.2(整句预生成);14 裁定 7
+ (语音报事实、耳标报类别——本句是纯语音通道);14 F2.3 先例(接线模式)。
+- **拥挤评估**:撞墙在 3x3 图中的理论上限是外圈 12 段;限频后一局新增语音
+ ≤6-8 句短句(各 ≤5 字),对 15-30 分钟单局节奏影响可忽略。风险:贴墙探索
+ 型玩家会听到成串方位句——10s 同向限频 + 移动即作废已把上限压住。
+
+### B2 · 目标方位句并入目标名(H1 的措辞升级)
+
+- **问题(证据)**:H1 方位句只有方向词无目标名:「右面的房间。」(live 行
+ 112-116、156-160、201-208)。P3:嘈杂环境 1.3s 短句被吃掉后只剩"……的
+ 房间"(行 206-208 ASR sim 0.75 实证);P1:"右面是哪?我赌不起"。三处
+ 触发点(found_key / gd_next_fuse / fix_box)在谜题链正常路径上位置固定,
+ 方位可枚举。
+- **目标状态**:进展链尾玩家听到"目标名 + 方位"一体句,单句即可行动。
+- **方案**:新增 3 句整句替换三处 `gd_room_*` 接播(接线点不变,全档位):
+ - found_key 链尾 → `gd_cabinet_dir`「铁柜,在右面的房间。」(储物间 (0,2)
+ → 铁柜房 (2,1),轴序先东西,dx=+2 固定为右)
+ - unlock_cabinet/gd_next_fuse 链尾 → `gd_box_dir`「电箱,在左面的房间。」
+ (铁柜房 (2,1) → 电箱房 (0,0),dx=−2 固定为左)
+ - fix_box 链尾 → `gd_gate_dir`「大门,在右面的房间。」(电箱房 (0,0) →
+ 大门 (2,2),dx=+2 固定为右)
+ - 原 `gd_room_l/r/u/d` 4 句保留不删(guide.test.mjs 的 H1 方向映射断言与
+ playtest 断言引用它们,删除触碰断言零改动红线);新句并行接线,仅替换
+ 三处触发点。方位若在异常路径上计算结果与枚举不符(理论上不存在——三处
+ 触发点坐标固定),兜底回落 `gd_room_*`。
+- **宪法条款**:00 §3.2(整句);归因真话(方位按房间图枚举核对过:三处
+ 触发点→目标均同排/跨排但轴序先东西,方向唯一);14 H1 裁定(接线模式
+ 原样,只改词)。
+- **拥挤评估**:3 处替换、0 处新增播出次数;句长 +4 字。对节奏零影响。
+ 附带收益:修了 P3 的"关键信息听不清"——信息密度翻倍,重听价值也翻倍。
+
+### B3 · 无进展计时器 + L0 进度重述(活跃迷路通道)
+
+- **问题(证据)**:卡关计时按"无操作"(§1.5-①,round1-live 修订),**活跃
+ 但无进展的玩家永远不会收到任何状态输入**——live 局我在解谜链外探路时
+ (行 87-94、179-220 各段 transit)游戏只给我脚步和环境音。01 号文档原始
+ 设计是"无进展起算",代码把"无进展"改成"无操作"后,"活跃迷路"这个状态
+ 从提示体系里消失了。用户反馈"不知道进度"正是这个空洞。
+- **目标状态**:玩家长时间有操作但零进展时,收到一条**不泄题**的进度重述,
+ 知道自己已完成几环、目标仍是什么,而不是答案。
+- **方案**:
+ - 新增独立计时:以 `_progressEvent()` 起算的"无进展时长"(与现有无操作
+ 计时器并存,互不复位)。触发点:无进展 ≥150s 且期间有 ≥5 次有效手势
+ (=活跃探索,排除挂机——挂机交给现有 60s L1 体系)→ 每进展段限 1 次。
+ - 播报内容(整句分档,不拼接,归因真话——完成数由 `_stage()` 反推):
+ - `gd_prog_1`「钥匙已经到手。找到能用它的锁。」(stage=cabinet)
+ - `gd_prog_2`「铁柜已经打开。电还没有通。」(stage=box)
+ - `gd_prog_3`「电已经通了。去找那扇大门。」(stage=gate)
+ - key 阶段无进展 150s 不走本通道(现有 L1/L2/L3 已覆盖"找不到钥匙",
+ 避免双计时器对同一阶段轰炸)。
+ - 队列语义沿用 `_pendingVoice`(绝不顶句,待播期间有操作即作废);warning
+ 震与 L1-L3 同款。
+- **宪法条款**:00 §3.2(整句分档);归因真话律;14 裁定 5(escape 卡关
+ 骨架保留,本通道是**新增平行通道**,不动既有 60/120/180 任何一级);01 号
+ 文档 §3.1 原始意图的部分回收。
+- **拥挤评估**:触发频率 = 每进展段最多 1 次 × 最多 3 段(cabinet/box/gate)
+ = 全局限 3 句;且要求 150s 活跃零进展,正常通关(2-3 分钟/环节)永远触发
+ 不到。只补"该响没响"的洞,不给顺畅玩家加噪。
+
+### B4 · 双击兜底降级:背包 + L1 意象,不再直通 L2
+
+- **问题(证据)**:双击无目标 = 玩家**主动查询**的唯一入口,现状直通
+ L2 明示:live 行 161-178(柜已开后再双击铁柜房 → use_fail + `stuck_box_2`
+ 把电箱房和动作全报出来)、行 230-243(同理 `stuck_gate_2`)。P1 定为 P0
+ "谜题被白送"——没卡关的人收到卡关 120s 才该有的答案,既是泄漏也稀释 L2
+ 的稀缺性(难度变相降低)。
+- **目标状态**:主动查询返回"背包 + 当前目标的 L1 级暗示",答案仍锁在
+ 计时器 L2/L3 与进展链里。
+- **方案**:`_useItem()` 兜底链第二句由 `voice/stuck__2` 改为
+ `voice/stuck__1`(资产现成,4 句意象暗示:铁锈味/钥匙在身上/
+ 电流声/电已通——全部不点明房间与动作,零新资产)。`use_fail` 与背包句
+ 不动。已在 `_stuckShown` 里记过 L1 的进展段不重复播(沿用既有去重)。
+- **宪法条款**:00 §5.3(escape 帮助入口语义不变,只是档位归位);01 号
+ §3.2「主动求提示保留:先播背包,再播 L2」——本条是对 01 的**有意修订**
+ (泄漏 > 帮助,证据双实录),在本文档留痕;归因真话(L1 句无答案可泄漏)。
+- **拥挤评估**:播出次数不变(一次兜底两句);内容变轻。L2 触发总量下降
+ (对非卡关玩家),L3 不受影响。对卡关玩家:60s L1 → 120s L2 → 180s L3
+ 的阶梯完整保留,难度不降。
+
+### B5 · 空房首访描述(复用现有资产)+ 方位线索句(新词,二选一落地)
+
+- **问题(证据)**:进入空房零语音(live 行 87-94、179-186、209-220),
+ 4 间空房 2 种环境音不可区分(amb_hum 行 90/182;amb_wind 行 120/212)。
+ 玩家对 5/9 的地图面积永远建不起模型——用户反馈"不知道自己在哪"的大头。
+- **目标状态**:每间空房至少有一个可建立心智地图的听觉锚(首访一次性,
+ 之后可单击复核)。
+- **方案**(两档,按实施预算二选一,建议 a 先行):
+ - **a(零新资产)**:空房**首访**时补播现有 `empty_1/2/3`(现只在单击
+ 检查时播;进入时播、单击时不再重复首访句)。三个 id 恰好够分给
+ (1,0)/(0,1)/(1,2)+(2,0)…4 房 3 句,需把 (1,2)/(2,0) 拆开:建议
+ (1,0)→empty_1、(0,1)→empty_2、(1,2)→empty_3、(2,0)→复用 empty_3 或
+ 保持沉默(两间"远处水滴"空房对称分布也是合法的记忆点)。
+ - **b(3-4 句新词,推荐终态)**:empty_* 改词重录为**含固定方位特征**的
+ 句子(房间特征是固定的,方位词不随玩家状态变,归因真话):
+ `empty_1`→「北面墙缝透进一丝风。这里什么都没有。」((1,0) 北外墙)、
+ `empty_2`→「回声从东面的墙折回来。这里空空荡荡。」((0,1) 东侧内墙——
+ 内墙方位特征需与地图核对后定稿)、`empty_3a/3b` 同理。每句给一个方向
+ 锚 + 一个可分辨特征,四间空房从此声学可区分。
+ - 触发:进入空房首访(visited 集合扩到空房坐标);震动/earcon 不动。
+- **宪法条款**:00 §3.2(整句);归因真话(方位特征写死前按房间图逐间核对);
+ 14 裁定 7(描述句走语音通道)。
+- **拥挤评估**:每房一生一次,全局 +≤4 句;单击检查路径不变。对节奏零影响。
+ 风险:empty_* 改词会动既有资产文本(playtest 断言若引用文本需同步登记)——
+ 方案 a 完全绕开此风险。
+
+### B6 · 引导 busy 窗口手势缓存(逻辑修复)
+
+- **问题(证据)**:live 行 30-35:`g2_wall` 播放中(37.60-43.28)我滑了一下
+ (41.57),只有引擎 [10] 轻震——墙没响、G2 计数没走,**静默吞手势**。若玩家
+ 信了这一下、只再补一滑,G2 永远不满足"滑两下",引导卡死(虽有 20s 看门狗
+ 兜底推进,但那是防资产缺失的,不是交互语义)。P1 特别检查项"失败可否归因"
+ 判半不过。
+- **目标状态**:busy 窗口内的首个手势在窗口结束后被补判(或明确拒绝并给
+ 反馈)。
+- **方案**:`_guideBusy` 期间缓存首个非跳过分手势,推进语音播完(done())
+ 后立即按新步骤判定:若缓存手势对新步骤合法则直接作答(玩家体验为"我的
+ 输入没丢");不合法则按新步骤的答错路径处理(有纠正语音,可归因)。缓存
+ 仅 1 个、只存活到本步骤解锁,杜绝跨步幽灵输入。
+- **宪法条款**:00 §5.4「做了一个手势 3 秒内必须有反馈」(当前 busy 吞手势
+ 违反此条,本条是合规修复);14 裁定 5(引导链结构不动)。
+- **拥挤评估**:零资产、零新增语音;只改引导期手势时序。引导链被 playtest
+ 断言冻结的部分(finishVoices(3)→PLAY 等)不受影响——缓存只在 busy 窗口
+ 内生效,脚本化玩家的手势本就在 turn cue 后送达。
+
+### B7 · 结算链单击守卫(逻辑修复)
+
+- **问题(证据)**:live 行 253-275:`inspect_gate_ready` 被随后的逃脱双击
+ 截断(行 256-257,属正常链式替换);更严重的是我的习惯性单击(369.22)在
+ 结算链 escape 播到 10.7/15.3s 时直接触发重开——`time_band1`(用时)与
+ `again` 两句被永久吞掉(行 263-275)。P3 定为 P0:"结算是我唯一确认打完了/
+ 打得如何的窗口,一碰就碎"。
+- **目标状态**:结算链播完前,ENDING 单击不立即重开。
+- **方案**:`_escape()` 起设 `_settleBusy=true`(结算链 onDone/30s 兜底处
+ 复位);ENDING 单击在 `_settleBusy` 期间忽略(或在 full 档播一句既有
+ `again` 前句?—— 不新增语音,直接忽略+引擎 confirm 震已有)。`again` 句
+ 本身已含操作指引,链播完后再单击重开的原语义不变。30s 资产缺失兜底保留
+ (防静默死锁)。
+- **宪法条款**:00 §5.4(单击 3s 内反馈——忽略路径有引擎 confirm 震兜底,
+ 合规);00 §3.2(结算整句分档的完整到达权);14 裁定 4(不顶句、不丢句)。
+- **拥挤评估**:零资产;只影响结算后前 ~15s 的重开时序。结算链播完 → 单击
+ 重开 → replay 的既有闭环在转写里完整存在(mock 行 144-149),本条只是
+ 把闭环的起点从"escape 第一句"推迟到"again 播完"。
+
+### B8 · 跳过引导后补播目标句(零资产)
+
+- **问题(证据)**:`guide_skip` 路径(live 行 61-74)与 REOPEN 单击直进
+ 路径都只播 `start_room`(手势总览)——**目标陈述 g7_goal 只在引导 G7 链尾
+ 出现一次**,跳过的玩家(恰是最需要目标的一群,P1/P3 共识)永远听不到
+ "找到能用这把钥匙打开的东西"。且跳过后阶段=key、身上无物品,60s 内只有
+ 意象级 L1(行 75-86)。
+- **目标状态**:任何路径进入 PLAY 时,玩家都至少听过一次目标句。
+- **方案**:`guide_skip` 确认句后、`_enterPlay` 前补播 `g7_goal`(资产现成,
+ 链式 `_speak`);REOPEN 单击直进路径同样在 `start_room` 后接 `g7_goal`。
+ 二周目老手可单击跳过该句(skippable 默认 true,符合 12 号文档——教学
+ 义务句已随首局完成,此处是状态告知不是教学)。
+- **宪法条款**:00 §3.2;14 H1 注(g7_goal 不接方位句的红线仅冻结于**引导
+ 链内**——本方案接在跳过确认链后,不触碰被冻结的引导链断言);01 号 §2.1
+ 设计意图(G7 目标陈述的全局可达)。
+- **拥挤评估**:两句短链、每会话最多一次;二周目玩家 1 次单击即可跳过。
+ 对节奏影响 ≤5s,且只发生在"信息最空白"的路径上。
+
+### B9 · L2 台词顺带修订(不推荐本轮做,留档)
+
+live 行 288-294 可见 L2/L3 句仍用"西南角/西北角/东南角"绝对方位词——对
+从未建立坐标系的玩家,绝对角落词不如相对方向可用(L3 带路已经改用相对
+滑动方向)。但 L2 明示句的改词会动 12 句资产与全部相关断言,且 L3 已兜底
+带路,**收益/成本比低,列为后续轮候选**,不在本轮实施清单内。
+
+## 4.2 盲区 → 方案对照表
+
+| 盲区 | 方案 | 新语音资产 | 逻辑改动 | 证据 |
+|---|---|---|---|---|
+| S1/S1b 方位(撞墙/平时) | B1 | +4 句 | 撞墙链路接播+限频 | live 行 40-55;P1/P3 P0 |
+| 相对方位半盲区(无目标名) | B2 | +3 句 | 三处接线点换 id | live 行 112-116/156-160/201-208 |
+| S3 全局进度无渠道 | B3 | +3 句 | 新计时器+触发 | 用户反馈;live transit 段 |
+| S4/S4b 目标重述/查询泄漏 | B4 + B8 | 0(复用) | 兜底换 id;跳过链补播 | live 行 61-74/161-178/230-243 |
+| S5 空房不可区分 | B5a/b | 0 或 +3~4 句 | 首访描述接线 | live 行 90/182/120/212 |
+| S6 busy 吞手势 | B6 | 0 | 缓存补判 | live 行 30-35 |
+| S7 结算链截断 | B7 | 0 | 守卫 | live 行 253-275 |
+
+## 4.3 实施清单(F 类编号,**仅登记,未实施**)
+
+| # | 内容 | 资产管线步骤 | 断言登记建议 |
+|---|---|---|---|
+| F1(B1) | 撞墙方位句 `wall_dir_l/r/u/d` ×4,撞墙链路接播 + 同向 10s 限频 | `tools/gen_voice_dual.py --game escape --only wall_dir_l,wall_dir_r,wall_dir_u,wall_dir_d` → `--convert` → `check_all_assets.py` → `extract_voice_text.py` 刷新 | escape playtest:撞墙后方位句到达/限频生效/移动打断作废;guide.test 不受影(PLAY 期行为) |
+| F2(B2) | `gd_cabinet_dir` / `gd_box_dir` / `gd_gate_dir` ×3,替换三处 gd_room_* 接播(原句保留) | 同 F1 管线,3 句 | playtest:三处进展链尾新句到达且方位正确;原 H1 断言(gd_room_* 映射表)保留不动 |
+| F3(B3) | `gd_prog_1/2/3` ×3 + 无进展 150s 活跃计时器(≥5 手势闸门、每段限 1 次、队列语义) | 同 F1 管线,3 句 | playtest(guide=off):计时器不误触(挂机不触、进展后清零、限 1 次);guide.test:队列不顶句、作废语义 |
+| F4(B4) | 双击兜底第二句 stuck_*_2 → stuck_*_1 | **零资产** | playtest:兜底链内容变更需逐条同步(现有断言若期望 stuck_*_2 必改);L2 计时器触发路径断言不动 |
+| F5(B5a) | 空房首访补播 empty_*(零资产先行版) | 零资产 | playtest:首访到达/重访不重复/单击检查语义不变 |
+| F6(B5b,可分期) | empty_* 改词带方位特征 ×3~4 | 同 F1 管线;**改词前逐间核对房间图方位特征(归因真话)** | 文本断言同步;双耳一致性 |
+| F7(B6) | 引导 busy 手势缓存补判 | 零资产 | guide.test:busy 窗口手势缓存→解锁后补判/答错路径;既有引导推进断言零改动红线 |
+| F8(B7) | 结算链单击守卫 `_settleBusy` | 零资产 | playtest:结算链完整播完(time_band/again 到达)后才可重开;30s 兜底仍生效 |
+| F9(B8) | 跳过引导/REOPEN 直进补播 g7_goal | 零资产 | playtest:两条路径 start_room 后 g7_goal 到达;二周目单击可跳过 |
+
+统一收尾(每轮 F 实施完必做):`check_all_assets.py` 全绿 → 6 playtest
+(guide=off)断言对账(escape 现有 103 条,F4/F5/F7/F9 会改其中若干,
+须逐条登记变更原因)→ `guide.test.mjs` 增补 → live 复跑一局验证新句在
+真实混音中可闻(B2/B3 短句尤其要做 ASR 层判定)。
+
+---
+
+# 阶段四 · 实施勾账(escape-state round2,2026-09-15)
+
+> §4.3 清单已从"仅登记,未实施"转为**已实施**(除 F6 按方案"可分期"留档,
+> 见实施裁定 1)。验证档案:`research/escape-state/round2/`(mock 转写 +
+> live 真机一局 38 段 OK 37 / SFX_BGM_ONLY 19 / 零截断,S1-S7 逐条核对)。
+> 新断言全部登记在 `web/escape/test/playtest.mjs`(103 → 235 条全绿)与
+> `web/harness/guide.test.mjs`(120 → 132 条全绿);既有断言变更 3 处,
+> 逐条登记于下表"断言变更"列。
+
+| # | 落地 | 断言登记(场景.条数) | 断言变更 | live 证据(round2/live-transcript.md 行号) |
+|---|---|---|---|---|
+| F1(B1) | ✅ `wall_dir_l/r/u/d` ×4 双性别已生成落盘;`_move` 撞墙链路 `wall` 播完接播、同向 10s 限频、被打断作废(game.js `_move`) | playtest G×13(引导期不播/接播顺序/打断作废/限频与恢复)+ 资产校验 F 段扩 wall_dir 族 | 无 | 行 141-143 wall_dir_u、行 166-168 wall_dir_l、199.5s 限频 |
+| F2(B2) | ✅ `gd_cabinet_dir/gd_box_dir/gd_gate_dir` ×3 替换三处接播;`_playGoalDir(target, namedId, expectedId)` 异常路径回落 `gd_room_*`(原句保留) | playtest H×12(三处到达+顺序+不再播 gd_room_*+回落+同房间不播);guide.test ⑩c 换线 + 回落断言 | guide.test ⑩c 三处接线断言由 gd_room_* 改为 gd_*_dir(映射表单元断言保留不动) | 行 231-233 gd_box_dir、行 306-312 gd_gate_dir |
+| F3(B3) | ✅ `gd_prog_1/2/3` ×3 + `_checkStuck` 平行通道(150s 无进展 ∧ ≥5 手势 ∧ stage≠key,每段限 1 次,队列/作废语义同卡关提示) | playtest I×11(key 不走/闸门/触发/限 1 次/进展复位/box 段再触/入队不顶句/作废计入不重发) | 无 | 行 265-268 gd_prog_2(活跃 154s 触发,期间 L1-L3 零误触) |
+| F4(B4) | ✅ `_useItem` 兜底第二句 `stuck_*_2`→`stuck_*_1`,L1 播了即记账沿用 `_stuckShown` 去重 | playtest J×8(背包+L1/不直通 L2/记账/再兜底不重复/60s 不重复 L1/120s L2 仍到达) | playtest 场景 A 兜底断言 stuck_key_2→stuck_key_1(+不播 _2 反向断言) | 行 180-182 use_fail+stuck_cabinet_1 |
+| F5(B5a) | ✅ 空房首访补播 `empty_*`((1,0)→1/(0,1)→2/(1,2)→3/(2,0)→3 复用),`_freshEmpty` 实现"本房间内单击不重复首访句";不算进展事件 | playtest K×9(首访到达/非进展事件/房内单击不重复/重访零语音/重访单击照常/四房映射) | 无(单击检查语义不变) | 行 115-118/189-192/199-202 三间首访 |
+| F6(B5b) | ⏸ **留档**(实施裁定 1) | — | — | — |
+| F7(B6) | ✅ `_guidePending` 缓存首个非跳过分手势,`_advance` done() 解锁后按新步骤补判(fromCache 绕过下滑跳过检查);跳过分手势不缓存;G6 链尾离 GUIDE 缓存作废 | playtest L×15(不当场判定/补判作答/答错路径/不触发跳过/仅 1 个/跳过分手势不缓存/引导链完整);guide.test ⑩d×7(full 档同款) | 无(既有引导推进断言零改动) | live-events 82.3s 缓存→87.9s 补判 |
+| F8(B7) | ✅ `_settleBusy` 守卫:`_escape` 挂载,`_startEndingBgm`(链完/30s 兜底汇合点)解除;ENDING 单击守卫期忽略 | playtest M×10(守卫期不重开/链完整 time_band+again/解除后重开/again 缺失 30s 兜底) | 无 | 行 336-347 习惯性单击被守卫挡下,time_band3+again 完整到达 |
+| F9(B8) | ✅ `_skipGuide` 链 guide_skip→g7_goal→_enterPlay;REOPEN 单击 `_enterPlay(true, true)` start_room→g7_goal;`_skipGuide` 期间 guideStep=7(裁定 4) | playtest N×6(直进顺序/可单击跳过)+ A2/B 场景跳过链更新(g7_goal 到达、顺序、两句后再进 PLAY) | playtest A2/B 跳过断言由"一句后 PLAY"改为"guide_skip→g7_goal 两句后 PLAY" | g7_goal OK sim=1(行 149.5s) |
+
+统一收尾执行记录:`check_all_assets.py` 全项目 15 游戏全绿(escape voice 各 90
+id 双性别一致);escape playtest 235/0、guide.test 132/0、其余 14 款 playtest
+回归全绿、engine-voice 19/0 与 engine-input 72/0 无连带;live 复跑一局
+(端口 8145,打完杀净)S1-S7 每个时刻现在都有落点(round2/README.md 对照表)。
+
+## 实施裁定记录(编号自本轮起,接续追加不改旧文)
+
+1. **F6(B5b)分期留档**:B5b 草案台词未定稿——`empty_2` 东侧内墙方位特征
+ 原文标注"需与地图核对后定稿"、`empty_3a/3b` 无台词草案,无法按"台词与
+ 草案逐字一致"的管线铁律生成;且 B5 原文即"建议 a 先行"、F6 标注"可分期"。
+ 本轮只落地 F5(B5a 零资产版),B5b 连同 B9(L2 角落词改词)一并列为
+ 后续轮候选。
+2. **B4 去重双向化**:方案只规定"已在 `_stuckShown` 记过 L1 的进展段不重复
+ 播";实施把兜底播出 L1 也**记账**(播了即加入 `_stuckShown`),否则 60s
+ 计时器会把玩家刚听过的同一句意象暗示再播一遍(去重只做单向等于没做)。
+ 附带效果:L1 整级(含 `stuck_ask`)跳过,L2/L3 阶梯与触发时点不变
+ (playtest J 冻结)。
+3. **B3 作废计入已播**:待播 `gd_prog` 在排队期间被玩家操作作废后仍计入
+ "每进展段限 1 次"(触发即记账)。方案"每进展段限 1 次"若按播出记账,
+ 5s 轮询会在条件持续成立时立即重发,形成骚扰循环;按触发记账与该条
+ "全局限 3 句"的拥挤评估一致(playtest I⑧ 冻结)。
+4. **B8 跳过链期间 guideStep=7**:`_skipGuide` 的两句链(guide_skip→g7_goal)
+ 播放期间相位仍是 GUIDE,原 guideStep 下单击/滑动会被当成引导作答(既有
+ 竞态:skip 链中单击可误推 G1)。落地把 guideStep 置 7(G7 等待语义:
+ 手势全忽略),链中输入 robustness 与 G7 自然链尾对齐;语音缺失兜底计时
+ 5s→9s(覆盖两句链)。
+5. **B5a 两个二选一**:(2,0) 选"复用 empty_3"(方案原文认可"两间'远处水滴'
+ 空房对称分布也是合法的记忆点");"单击时不再重复首访句"以 `_freshEmpty`
+ 实现——仅首访句刚播过的那个房间内抑制同句,离开即恢复单击播句,
+ 重访后单击检查语义完全不变(playtest K 冻结)。
+6. **B6 缓存手势绕过下滑跳过**:方案"不合法则按新步骤的答错路径处理"
+ 意味着缓存的下滑在可跳过步骤上也不再触发 `_skipGuide`(fromCache 绕过
+ skip 检查)——这正是 live round1 S6 事故(答案被吞后下滑被当成跳过)的
+ 修复点;实时注入的下滑跳过语义不变(playtest L⑤/guide.test ⑩d 冻结)。
+7. **B1 限频记账点**:同向 10s 限频按**方位句实际开播时刻**记账;仅调度
+ (wall 接播注册)未播出即被打断的不占额度(playtest G③"被作废的方向
+ 补撞后正常接播"冻结)。
+
+## 盲区清单核销(§3.2 → round2)
+
+S1/S1b(B1)、相对方位半盲区(B2)、S2(B5a)、S3(B3)、S4/S4b(B4+B8)、
+S5(B5a,B5b 留档)、S6(B6)、S7(B7)——live round2 逐条核对证据见
+`research/escape-state/round2/README.md`,每个盲区时刻现在都有落点。
diff --git a/research/escape-state/round1/live-events.jsonl b/research/escape-state/round1/live-events.jsonl
new file mode 100644
index 00000000..790db220
--- /dev/null
+++ b/research/escape-state/round1/live-events.jsonl
@@ -0,0 +1,271 @@
+{"ev":"probe_ready","t":50.099999994039536,"ta":null}
+{"ev":"gesture","type":"tap","x":195,"y":422,"t":30065,"ta":null}
+{"ev":"recstart","mime":"audio/webm;codecs=opus","t":30170.90000000596,"ta":0}
+{"ev":"fetch","url":"audio/sfx/step.ogg","status":200,"ok":true,"ms":14,"t":30185.29999998212,"ta":0.014}
+{"ev":"fetch","url":"audio/sfx/wall.ogg","status":200,"ok":true,"ms":14,"t":30185.40000000596,"ta":0.015}
+{"ev":"fetch","url":"audio/sfx/pickup.ogg","status":200,"ok":true,"ms":14,"t":30185.5,"ta":0.015}
+{"ev":"fetch","url":"audio/sfx/unlock.ogg","status":200,"ok":true,"ms":14,"t":30185.5,"ta":0.015}
+{"ev":"fetch","url":"audio/sfx/door_creak.ogg","status":200,"ok":true,"ms":14,"t":30185.5,"ta":0.015}
+{"ev":"fetch","url":"audio/sfx/spark.ogg","status":200,"ok":true,"ms":14,"t":30185.5,"ta":0.015}
+{"ev":"fetch","url":"audio/sfx/power_on.ogg","status":200,"ok":true,"ms":14,"t":30185.69999998808,"ta":0.015}
+{"ev":"fetch","url":"audio/sfx/gate_open.ogg","status":200,"ok":true,"ms":15,"t":30186.40000000596,"ta":0.016}
+{"ev":"fetch","url":"audio/sfx/win_chime.ogg","status":200,"ok":true,"ms":16,"t":30187.79999998212,"ta":0.017}
+{"ev":"fetch","url":"audio/sfx/amb_storage.ogg","status":200,"ok":true,"ms":18,"t":30189.69999998808,"ta":0.019}
+{"ev":"fetch","url":"audio/sfx/amb_cabinet.ogg","status":200,"ok":true,"ms":18,"t":30190.40000000596,"ta":0.02}
+{"ev":"fetch","url":"audio/sfx/amb_power.ogg","status":200,"ok":true,"ms":20,"t":30192.40000000596,"ta":0.022}
+{"ev":"fetch","url":"audio/sfx/amb_gate.ogg","status":200,"ok":true,"ms":22,"t":30194,"ta":0.023}
+{"ev":"fetch","url":"audio/sfx/amb_gate_live.ogg","status":200,"ok":true,"ms":28,"t":30200.19999998808,"ta":0.029}
+{"ev":"fetch","url":"audio/sfx/amb_drip.ogg","status":200,"ok":true,"ms":28,"t":30200.29999998212,"ta":0.029}
+{"ev":"fetch","url":"audio/sfx/amb_wind.ogg","status":200,"ok":true,"ms":29,"t":30201.19999998808,"ta":0.03}
+{"ev":"fetch","url":"audio/sfx/amb_hum.ogg","status":200,"ok":true,"ms":29,"t":30201.29999998212,"ta":0.03}
+{"ev":"fetch","url":"audio/voice/female/g0_intro.ogg","status":200,"ok":true,"ms":30,"t":30202.59999999404,"ta":0.032}
+{"ev":"fetch","url":"audio/voice/female/skip_hint.ogg","status":200,"ok":true,"ms":33,"t":30205.29999998212,"ta":0.034}
+{"ev":"fetch","url":"audio/voice/female/g1_move.ogg","status":200,"ok":true,"ms":34,"t":30206.69999998808,"ta":0.036}
+{"ev":"fetch","url":"audio/voice/female/reopen_prompt.ogg","status":200,"ok":true,"ms":35,"t":30207.90000000596,"ta":0.037}
+{"ev":"fetch","url":"audio/voice/female/start_room.ogg","status":200,"ok":true,"ms":36,"t":30208.5,"ta":0.038}
+{"ev":"fetch","url":"audio/bgm/menu.ogg","status":200,"ok":true,"ms":36,"t":30209,"ta":0.038}
+{"ev":"fetch","url":"audio/bgm/drone.ogg","status":200,"ok":true,"ms":37,"t":30209.69999998808,"ta":0.039}
+{"ev":"vstart","url":"audio/voice/female/g0_intro.ogg","t":30259.19999998808,"dur":6.64,"ta":0.088}
+{"ev":"start","t":30453.69999998808,"sid":1,"url":"audio/bgm/drone.ogg","layer":"bgm","dur":117,"rate":1,"loop":true,"when":0,"ta":0.283}
+{"ev":"vend","url":"audio/voice/female/g0_intro.ogg","t":37066.59999999404,"reason":"kill","ta":6.896}
+{"ev":"vstart","url":"audio/voice/female/g1_move.ogg","t":38267.09999999404,"dur":4.56,"ta":8.096}
+{"ev":"vend","url":"audio/voice/female/g1_move.ogg","t":42847.59999999404,"reason":"kill","ta":12.677}
+{"ev":"vstart","url":"audio/voice/female/skip_hint.ogg","t":42847.90000000596,"dur":1.6,"ta":12.677}
+{"ev":"vend","url":"audio/voice/female/skip_hint.ogg","t":44449.5,"reason":"kill","ta":14.279}
+{"ev":"gesture","type":"swipeup","x":195,"y":422,"t":64687.79999998212,"ta":34.517}
+{"ev":"vibrate","pattern":"[10]","t":65048.29999998212,"ta":34.877}
+{"ev":"start","t":65049.09999999404,"sid":2,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":34.878}
+{"ev":"fetch","url":"audio/voice/female/g1_done.ogg","status":200,"ok":true,"ms":3,"t":65051.40000000596,"ta":34.881}
+{"ev":"vstart","url":"audio/voice/female/g1_done.ogg","t":65055.59999999404,"dur":2.72,"ta":34.885}
+{"ev":"vend","url":"audio/voice/female/g1_done.ogg","t":67760.29999998212,"reason":"kill","ta":37.589}
+{"ev":"fetch","url":"audio/voice/female/g2_wall.ogg","status":200,"ok":true,"ms":3,"t":67763.09999999404,"ta":37.592}
+{"ev":"vstart","url":"audio/voice/female/g2_wall.ogg","t":67768.69999998808,"dur":5.68,"ta":37.598}
+{"ev":"gesture","type":"swipeleft","x":195,"y":422,"t":71744.59999999404,"ta":41.574}
+{"ev":"vibrate","pattern":"[10]","t":72097.79999998212,"ta":41.927}
+{"ev":"vend","url":"audio/voice/female/g2_wall.ogg","t":73448.79999998212,"reason":"kill","ta":43.278}
+{"ev":"gesture","type":"swipeleft","x":195,"y":422,"t":76509.09999999404,"ta":46.338}
+{"ev":"vibrate","pattern":"[10]","t":76864.19999998808,"ta":46.693}
+{"ev":"vibrate","pattern":"[60,40,60]","t":76864.40000000596,"ta":46.694}
+{"ev":"start","t":76864.69999998808,"sid":3,"url":"audio/sfx/wall.ogg","layer":"sfx","dur":0.44709750566893425,"rate":1,"loop":false,"when":0,"ta":46.694}
+{"ev":"fetch","url":"audio/voice/female/wall.ogg","status":200,"ok":true,"ms":2,"t":76866.79999998212,"ta":46.696}
+{"ev":"vstart","url":"audio/voice/female/wall.ogg","t":76870.29999998212,"dur":2.08,"ta":46.699}
+{"ev":"vend","url":"audio/voice/female/wall.ogg","t":79207.09999999404,"reason":"kill","ta":49.036}
+{"ev":"gesture","type":"swipeleft","x":195,"y":422,"t":83826.69999998808,"ta":53.656}
+{"ev":"vibrate","pattern":"[10]","t":84180.59999999404,"ta":54.01}
+{"ev":"vibrate","pattern":"[60,40,60]","t":84180.79999998212,"ta":54.01}
+{"ev":"start","t":84180.90000000596,"sid":4,"url":"audio/sfx/wall.ogg","layer":"sfx","dur":0.44709750566893425,"rate":1,"loop":false,"when":0,"ta":54.01}
+{"ev":"vstart","url":"audio/voice/female/wall.ogg","t":84181,"dur":2.08,"ta":54.01}
+{"ev":"vend","url":"audio/voice/female/wall.ogg","t":86520.40000000596,"reason":"kill","ta":56.35}
+{"ev":"fetch","url":"audio/voice/female/g3_left.ogg","status":200,"ok":true,"ms":3,"t":86523.29999998212,"ta":56.352}
+{"ev":"vstart","url":"audio/voice/female/g3_left.ogg","t":86526.79999998212,"dur":2.96,"ta":56.356}
+{"ev":"vend","url":"audio/voice/female/g3_left.ogg","t":89515.59999999404,"reason":"kill","ta":59.345}
+{"ev":"gesture","type":"swipedown","x":195,"y":422,"t":91087.40000000596,"ta":60.917}
+{"ev":"vibrate","pattern":"[10]","t":91447,"ta":61.276}
+{"ev":"fetch","url":"audio/voice/female/guide_skip.ogg","status":200,"ok":true,"ms":2,"t":91449.69999998808,"ta":61.279}
+{"ev":"vstart","url":"audio/voice/female/guide_skip.ogg","t":91453.79999998212,"dur":2.88,"ta":61.283}
+{"ev":"vend","url":"audio/voice/female/guide_skip.ogg","t":94437.79999998212,"reason":"kill","ta":64.267}
+{"ev":"vstart","url":"audio/voice/female/start_room.ogg","t":94438.09999999404,"dur":7.2,"ta":64.267}
+{"ev":"start","t":94438.90000000596,"sid":5,"url":"audio/sfx/amb_drip.ogg","layer":"sfx","dur":5,"rate":1,"loop":false,"when":0,"ta":64.268}
+{"ev":"vend","url":"audio/voice/female/start_room.ogg","t":101983.69999998808,"reason":"kill","ta":71.813}
+{"ev":"vibrate","pattern":"[30,50,30,50,30]","t":155172.90000000596,"ta":125.002}
+{"ev":"fetch","url":"audio/voice/female/stuck_ask.ogg","status":200,"ok":true,"ms":3,"t":155176.09999999404,"ta":125.005}
+{"ev":"vstart","url":"audio/voice/female/stuck_ask.ogg","t":155179.29999998212,"dur":1.28,"ta":125.008}
+{"ev":"vend","url":"audio/voice/female/stuck_ask.ogg","t":156778.09999999404,"reason":"kill","ta":126.607}
+{"ev":"fetch","url":"audio/voice/female/stuck_key_1.ogg","status":200,"ok":true,"ms":2,"t":156780.59999999404,"ta":126.61}
+{"ev":"vstart","url":"audio/voice/female/stuck_key_1.ogg","t":156784.29999998212,"dur":2.88,"ta":126.613}
+{"ev":"vend","url":"audio/voice/female/stuck_key_1.ogg","t":159773.19999998808,"reason":"kill","ta":129.602}
+{"ev":"gesture","type":"swipeleft","x":195,"y":422,"t":161038.69999998808,"ta":130.868}
+{"ev":"vibrate","pattern":"[10]","t":161394.19999998808,"ta":131.223}
+{"ev":"start","t":161394.5,"sid":6,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":131.224}
+{"ev":"start","t":161394.59999999404,"sid":7,"url":"audio/sfx/amb_hum.ogg","layer":"sfx","dur":5,"rate":1,"loop":false,"when":0,"ta":131.224}
+{"ev":"gesture","type":"swipedown","x":195,"y":422,"t":167817.79999998212,"ta":137.647}
+{"ev":"vibrate","pattern":"[10]","t":168177.69999998808,"ta":138.007}
+{"ev":"start","t":168178.29999998212,"sid":8,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":138.007}
+{"ev":"start","t":168178.29999998212,"sid":9,"url":"audio/sfx/amb_storage.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":138.007}
+{"ev":"fetch","url":"audio/voice/female/room_storage.ogg","status":200,"ok":true,"ms":3,"t":168180.79999998212,"ta":138.01}
+{"ev":"vstart","url":"audio/voice/female/room_storage.ogg","t":168186.90000000596,"dur":5.36,"ta":138.016}
+{"ev":"vend","url":"audio/voice/female/room_storage.ogg","t":173866.79999998212,"reason":"kill","ta":143.696}
+{"ev":"gesture","type":"tap","x":195,"y":422,"t":175088.19999998808,"ta":144.917}
+{"ev":"vibrate","pattern":"[20]","t":175453.29999998212,"ta":145.282}
+{"ev":"vibrate","pattern":"[15]","t":175453.59999999404,"ta":145.283}
+{"ev":"start","t":175453.69999998808,"sid":10,"url":"audio/sfx/amb_storage.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":145.283}
+{"ev":"start","t":175453.69999998808,"sid":11,"url":"audio/sfx/pickup.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":0,"ta":145.283}
+{"ev":"fetch","url":"audio/voice/female/found_key.ogg","status":200,"ok":true,"ms":3,"t":176256.69999998808,"ta":146.086}
+{"ev":"vstart","url":"audio/voice/female/found_key.ogg","t":176266.69999998808,"dur":9.96,"ta":146.096}
+{"ev":"vend","url":"audio/voice/female/found_key.ogg","t":186404.29999998212,"reason":"kill","ta":156.233}
+{"ev":"fetch","url":"audio/voice/female/gd_room_r.ogg","status":200,"ok":true,"ms":3,"t":186407.40000000596,"ta":156.237}
+{"ev":"vstart","url":"audio/voice/female/gd_room_r.ogg","t":186410.29999998212,"dur":1.28,"ta":156.239}
+{"ev":"vend","url":"audio/voice/female/gd_room_r.ogg","t":188006.40000000596,"reason":"kill","ta":157.836}
+{"ev":"gesture","type":"swiperight","x":195,"y":422,"t":205702,"ta":175.531}
+{"ev":"vibrate","pattern":"[10]","t":206059.29999998212,"ta":175.888}
+{"ev":"start","t":206059.59999999404,"sid":12,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":175.889}
+{"ev":"start","t":206059.59999999404,"sid":13,"url":"audio/sfx/amb_wind.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":175.889}
+{"ev":"gesture","type":"swiperight","x":195,"y":422,"t":209469.5,"ta":179.299}
+{"ev":"vibrate","pattern":"[10]","t":209825.5,"ta":179.655}
+{"ev":"start","t":209825.90000000596,"sid":14,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":179.655}
+{"ev":"start","t":209825.90000000596,"sid":15,"url":"audio/sfx/amb_gate.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":179.655}
+{"ev":"fetch","url":"audio/voice/female/room_gate.ogg","status":200,"ok":true,"ms":2,"t":209828,"ta":179.657}
+{"ev":"vstart","url":"audio/voice/female/room_gate.ogg","t":209833,"dur":3.52,"ta":179.662}
+{"ev":"gesture","type":"swipeup","x":195,"y":422,"t":213236.40000000596,"ta":183.066}
+{"ev":"vibrate","pattern":"[10]","t":213592.19999998808,"ta":183.421}
+{"ev":"vend","url":"audio/voice/female/room_gate.ogg","t":213592.40000000596,"reason":"kill","ta":183.422}
+{"ev":"start","t":213592.59999999404,"sid":16,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":183.422}
+{"ev":"start","t":213592.59999999404,"sid":17,"url":"audio/sfx/amb_cabinet.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":183.422}
+{"ev":"fetch","url":"audio/voice/female/room_cabinet.ogg","status":200,"ok":true,"ms":3,"t":213595.19999998808,"ta":183.424}
+{"ev":"vstart","url":"audio/voice/female/room_cabinet.ogg","t":213599.19999998808,"dur":2.64,"ta":183.428}
+{"ev":"vend","url":"audio/voice/female/room_cabinet.ogg","t":216309.29999998212,"reason":"kill","ta":186.138}
+{"ev":"gesture","type":"doubletap","x":195,"y":422,"t":222734.5,"ta":192.564}
+{"ev":"vibrate","pattern":"[20]","t":222982.59999999404,"ta":192.812}
+{"ev":"vibrate","pattern":"[100]","t":222982.90000000596,"ta":192.812}
+{"ev":"start","t":222983.29999998212,"sid":18,"url":"audio/sfx/unlock.ogg","layer":"sfx","dur":0.49777777777777776,"rate":1,"loop":false,"when":0,"ta":192.812}
+{"ev":"start","t":222983.29999998212,"sid":19,"url":"audio/sfx/door_creak.ogg","layer":"sfx","dur":1.3999773242630384,"rate":1,"loop":false,"when":0,"ta":192.812}
+{"ev":"fetch","url":"audio/voice/female/unlock_cabinet.ogg","status":200,"ok":true,"ms":3,"t":222985.59999999404,"ta":192.815}
+{"ev":"vstart","url":"audio/voice/female/unlock_cabinet.ogg","t":222997.40000000596,"dur":11.48,"ta":192.827}
+{"ev":"vend","url":"audio/voice/female/unlock_cabinet.ogg","t":234628.09999999404,"reason":"kill","ta":204.457}
+{"ev":"fetch","url":"audio/voice/female/gd_next_fuse.ogg","status":200,"ok":true,"ms":3,"t":234630.79999998212,"ta":204.46}
+{"ev":"vstart","url":"audio/voice/female/gd_next_fuse.ogg","t":234635.5,"dur":3.28,"ta":204.465}
+{"ev":"vend","url":"audio/voice/female/gd_next_fuse.ogg","t":238087.59999999404,"reason":"kill","ta":207.917}
+{"ev":"fetch","url":"audio/voice/female/gd_room_l.ogg","status":200,"ok":true,"ms":3,"t":238090.40000000596,"ta":207.92}
+{"ev":"vstart","url":"audio/voice/female/gd_room_l.ogg","t":238093.5,"dur":1.68,"ta":207.923}
+{"ev":"vend","url":"audio/voice/female/gd_room_l.ogg","t":239968.19999998808,"reason":"kill","ta":209.797}
+{"ev":"gesture","type":"doubletap","x":195,"y":422,"t":252754.09999999404,"ta":222.583}
+{"ev":"vibrate","pattern":"[20]","t":253003,"ta":222.832}
+{"ev":"vibrate","pattern":"[60,40,60]","t":253003.09999999404,"ta":222.832}
+{"ev":"fetch","url":"audio/voice/female/use_fail.ogg","status":200,"ok":true,"ms":3,"t":253005.69999998808,"ta":222.835}
+{"ev":"vstart","url":"audio/voice/female/use_fail.ogg","t":253013.29999998212,"dur":7.36,"ta":222.842}
+{"ev":"vend","url":"audio/voice/female/use_fail.ogg","t":260562.69999998808,"reason":"kill","ta":230.392}
+{"ev":"fetch","url":"audio/voice/female/stuck_box_2.ogg","status":200,"ok":true,"ms":3,"t":260565.59999999404,"ta":230.395}
+{"ev":"vstart","url":"audio/voice/female/stuck_box_2.ogg","t":260571.29999998212,"dur":4.88,"ta":230.4}
+{"ev":"gesture","type":"swipeleft","x":195,"y":422,"t":263135.90000000596,"ta":232.965}
+{"ev":"vibrate","pattern":"[10]","t":263489.90000000596,"ta":233.319}
+{"ev":"start","t":263490.09999999404,"sid":20,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":233.319}
+{"ev":"start","t":263490.09999999404,"sid":21,"url":"audio/sfx/amb_drip.ogg","layer":"sfx","dur":5,"rate":1,"loop":false,"when":0,"ta":233.319}
+{"ev":"vend","url":"audio/voice/female/stuck_box_2.ogg","t":265508.09999999404,"reason":"kill","ta":235.337}
+{"ev":"gesture","type":"swipeleft","x":195,"y":422,"t":266899.09999999404,"ta":236.728}
+{"ev":"vibrate","pattern":"[10]","t":267256.7999999821,"ta":237.086}
+{"ev":"start","t":267257.09999999404,"sid":22,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":237.086}
+{"ev":"start","t":267257.09999999404,"sid":23,"url":"audio/sfx/amb_hum.ogg","layer":"sfx","dur":5,"rate":1,"loop":false,"when":0,"ta":237.086}
+{"ev":"gesture","type":"swipeup","x":195,"y":422,"t":270667.09999999404,"ta":240.496}
+{"ev":"vibrate","pattern":"[10]","t":271023.40000000596,"ta":240.853}
+{"ev":"start","t":271023.7999999821,"sid":24,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":240.853}
+{"ev":"start","t":271023.7999999821,"sid":25,"url":"audio/sfx/amb_power.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":240.853}
+{"ev":"fetch","url":"audio/voice/female/room_power.ogg","status":200,"ok":true,"ms":2,"t":271026,"ta":240.855}
+{"ev":"vstart","url":"audio/voice/female/room_power.ogg","t":271030.2999999821,"dur":2.64,"ta":240.859}
+{"ev":"vend","url":"audio/voice/female/room_power.ogg","t":273750.5,"reason":"kill","ta":243.58}
+{"ev":"gesture","type":"doubletap","x":195,"y":422,"t":297793,"ta":267.622}
+{"ev":"vibrate","pattern":"[20]","t":298041.1999999881,"ta":267.87}
+{"ev":"vibrate","pattern":"[100]","t":298041.40000000596,"ta":267.871}
+{"ev":"start","t":298041.6999999881,"sid":26,"url":"audio/sfx/spark.ogg","layer":"sfx","dur":0.8970975056689342,"rate":1,"loop":false,"when":0,"ta":267.871}
+{"ev":"start","t":298041.6999999881,"sid":27,"url":"audio/sfx/power_on.ogg","layer":"sfx","dur":2.2,"rate":1,"loop":false,"when":0,"ta":267.871}
+{"ev":"fetch","url":"audio/voice/female/fix_box.ogg","status":200,"ok":true,"ms":2,"t":298043.90000000596,"ta":267.873}
+{"ev":"vstart","url":"audio/voice/female/fix_box.ogg","t":298051.7999999821,"dur":7.52,"ta":267.881}
+{"ev":"vend","url":"audio/voice/female/fix_box.ogg","t":305605.40000000596,"reason":"kill","ta":275.435}
+{"ev":"vstart","url":"audio/voice/female/gd_room_r.ogg","t":305605.59999999404,"dur":1.28,"ta":275.435}
+{"ev":"vend","url":"audio/voice/female/gd_room_r.ogg","t":307207.59999999404,"reason":"kill","ta":277.037}
+{"ev":"gesture","type":"swiperight","x":195,"y":422,"t":308359.2999999821,"ta":278.188}
+{"ev":"vibrate","pattern":"[10]","t":308721.90000000596,"ta":278.551}
+{"ev":"start","t":308722.09999999404,"sid":28,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":278.551}
+{"ev":"start","t":308722.09999999404,"sid":29,"url":"audio/sfx/amb_wind.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":278.551}
+{"ev":"gesture","type":"swiperight","x":195,"y":422,"t":312131.6999999881,"ta":281.961}
+{"ev":"vibrate","pattern":"[10]","t":312488,"ta":282.317}
+{"ev":"start","t":312488.2999999821,"sid":30,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":282.317}
+{"ev":"start","t":312488.2999999821,"sid":31,"url":"audio/sfx/amb_hum.ogg","layer":"sfx","dur":5,"rate":1,"loop":false,"when":0,"ta":282.317}
+{"ev":"gesture","type":"swipedown","x":195,"y":422,"t":315896.1999999881,"ta":285.725}
+{"ev":"vibrate","pattern":"[10]","t":316254.90000000596,"ta":286.084}
+{"ev":"start","t":316255.1999999881,"sid":32,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":286.084}
+{"ev":"start","t":316255.1999999881,"sid":33,"url":"audio/sfx/amb_cabinet.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":286.084}
+{"ev":"gesture","type":"tap","x":195,"y":422,"t":325678.09999999404,"ta":295.507}
+{"ev":"vibrate","pattern":"[20]","t":326040.59999999404,"ta":295.87}
+{"ev":"start","t":326041,"sid":34,"url":"audio/sfx/amb_cabinet.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":295.87}
+{"ev":"fetch","url":"audio/voice/female/empty_1.ogg","status":200,"ok":true,"ms":3,"t":326844.5,"ta":296.674}
+{"ev":"vstart","url":"audio/voice/female/empty_1.ogg","t":326851.2999999821,"dur":5.2,"ta":296.68}
+{"ev":"vend","url":"audio/voice/female/empty_1.ogg","t":332074,"reason":"kill","ta":301.903}
+{"ev":"gesture","type":"doubletap","x":195,"y":422,"t":335912.40000000596,"ta":305.742}
+{"ev":"vibrate","pattern":"[20]","t":336160,"ta":305.989}
+{"ev":"vibrate","pattern":"[60,40,60]","t":336160.1999999881,"ta":305.989}
+{"ev":"vstart","url":"audio/voice/female/use_fail.ogg","t":336160.2999999821,"dur":7.36,"ta":305.989}
+{"ev":"vend","url":"audio/voice/female/use_fail.ogg","t":343706.1999999881,"reason":"kill","ta":313.535}
+{"ev":"fetch","url":"audio/voice/female/stuck_gate_2.ogg","status":200,"ok":true,"ms":3,"t":343708.90000000596,"ta":313.538}
+{"ev":"vstart","url":"audio/voice/female/stuck_gate_2.ogg","t":343715.2999999821,"dur":5.44,"ta":313.544}
+{"ev":"vend","url":"audio/voice/female/stuck_gate_2.ogg","t":349394.6999999881,"reason":"kill","ta":319.224}
+{"ev":"gesture","type":"swipedown","x":195,"y":422,"t":369477.2999999821,"ta":339.306}
+{"ev":"vibrate","pattern":"[10]","t":369835.6999999881,"ta":339.665}
+{"ev":"start","t":369836,"sid":35,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":339.665}
+{"ev":"start","t":369836,"sid":36,"url":"audio/sfx/amb_gate_live.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":339.665}
+{"ev":"gesture","type":"tap","x":195,"y":422,"t":379260.40000000596,"ta":349.09}
+{"ev":"vibrate","pattern":"[20]","t":379623.90000000596,"ta":349.453}
+{"ev":"start","t":379624.2999999821,"sid":37,"url":"audio/sfx/amb_gate_live.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":349.453}
+{"ev":"fetch","url":"audio/voice/female/inspect_gate_ready.ogg","status":200,"ok":true,"ms":3,"t":380427.1999999881,"ta":350.256}
+{"ev":"vstart","url":"audio/voice/female/inspect_gate_ready.ogg","t":380436,"dur":8.72,"ta":350.265}
+{"ev":"gesture","type":"doubletap","x":195,"y":422,"t":388748.40000000596,"ta":358.578}
+{"ev":"vibrate","pattern":"[20]","t":388997.1999999881,"ta":358.826}
+{"ev":"vibrate","pattern":"[100]","t":388997.5,"ta":358.827}
+{"ev":"vend","url":"audio/voice/female/inspect_gate_ready.ogg","t":388998.09999999404,"reason":"kill","ta":358.827}
+{"ev":"fetch","url":"audio/voice/female/escape.ogg","status":200,"ok":true,"ms":3,"t":389000.59999999404,"ta":358.83}
+{"ev":"vstart","url":"audio/voice/female/escape.ogg","t":389013.90000000596,"dur":15.32,"ta":358.843}
+{"ev":"stop","t":391557.7999999821,"sid":1,"when":0,"ta":361.387}
+{"ev":"start","t":391599.09999999404,"sid":38,"url":"audio/sfx/gate_open.ogg","layer":"sfx","dur":2.7999773242630384,"rate":1,"loop":false,"when":0,"ta":361.428}
+{"ev":"start","t":391599.09999999404,"sid":39,"url":"audio/sfx/win_chime.ogg","layer":"sfx","dur":1.6,"rate":1,"loop":false,"when":0,"ta":361.428}
+{"ev":"gesture","type":"tap","x":195,"y":422,"t":399386.7999999821,"ta":369.216}
+{"ev":"vibrate","pattern":"[20]","t":399751,"ta":369.58}
+{"ev":"vend","url":"audio/voice/female/escape.ogg","t":399751.2999999821,"reason":"kill","ta":369.58}
+{"ev":"start","t":399751.59999999404,"sid":40,"url":"audio/bgm/drone.ogg","layer":"bgm","dur":117,"rate":1,"loop":true,"when":0,"ta":369.581}
+{"ev":"start","t":399751.59999999404,"sid":41,"url":"audio/sfx/amb_drip.ogg","layer":"sfx","dur":5,"rate":1,"loop":false,"when":0,"ta":369.581}
+{"ev":"fetch","url":"audio/voice/female/replay.ogg","status":200,"ok":true,"ms":3,"t":399754.1999999881,"ta":369.583}
+{"ev":"vstart","url":"audio/voice/female/replay.ogg","t":399762.90000000596,"dur":8.24,"ta":369.592}
+{"ev":"vend","url":"audio/voice/female/replay.ogg","t":407950.6999999881,"reason":"kill","ta":377.78}
+{"ev":"vibrate","pattern":"[30,50,30,50,30]","t":460172.90000000596,"ta":430.002}
+{"ev":"vstart","url":"audio/voice/female/stuck_ask.ogg","t":460173.1999999881,"dur":1.28,"ta":430.002}
+{"ev":"vend","url":"audio/voice/female/stuck_ask.ogg","t":461769.7999999821,"reason":"kill","ta":431.599}
+{"ev":"vstart","url":"audio/voice/female/stuck_key_1.ogg","t":461769.7999999821,"dur":2.88,"ta":431.599}
+{"ev":"vend","url":"audio/voice/female/stuck_key_1.ogg","t":464764.90000000596,"reason":"kill","ta":434.594}
+{"ev":"vibrate","pattern":"[30,50,30,50,30]","t":520173,"ta":490.002}
+{"ev":"fetch","url":"audio/voice/female/stuck_key_2.ogg","status":200,"ok":true,"ms":3,"t":520176.09999999404,"ta":490.005}
+{"ev":"vstart","url":"audio/voice/female/stuck_key_2.ogg","t":520182.5,"dur":4.48,"ta":490.012}
+{"ev":"vend","url":"audio/voice/female/stuck_key_2.ogg","t":524760.099999994,"reason":"kill","ta":494.589}
+{"ev":"vibrate","pattern":"[30,50,30,50,30]","t":580172.900000006,"ta":550.002}
+{"ev":"fetch","url":"audio/voice/female/stuck_key_3.ogg","status":200,"ok":true,"ms":3,"t":580175.7999999821,"ta":550.005}
+{"ev":"vstart","url":"audio/voice/female/stuck_key_3.ogg","t":580180.099999994,"dur":3.12,"ta":550.009}
+{"ev":"vend","url":"audio/voice/female/stuck_key_3.ogg","t":583641,"reason":"kill","ta":553.47}
+{"ev":"fetch","url":"audio/voice/female/nav_left.ogg","status":200,"ok":true,"ms":3,"t":583643.6999999881,"ta":553.473}
+{"ev":"vstart","url":"audio/voice/female/nav_left.ogg","t":583646.7999999821,"dur":1.28,"ta":553.476}
+{"ev":"vend","url":"audio/voice/female/nav_left.ogg","t":585243.099999994,"reason":"kill","ta":555.072}
+{"ev":"fetch","url":"audio/voice/female/nav_down.ogg","status":200,"ok":true,"ms":3,"t":585245.6999999881,"ta":555.075}
+{"ev":"vstart","url":"audio/voice/female/nav_down.ogg","t":585248.599999994,"dur":0.9570975056689343,"ta":555.078}
+{"ev":"vend","url":"audio/voice/female/nav_down.ogg","t":586380.7999999821,"reason":"kill","ta":556.21}
+{"ev":"vibrate","pattern":"[30,50,30,50,30]","t":640172.900000006,"ta":610.002}
+{"ev":"vstart","url":"audio/voice/female/stuck_key_3.ogg","t":640173.099999994,"dur":3.12,"ta":610.002}
+{"ev":"vend","url":"audio/voice/female/stuck_key_3.ogg","t":643636.5,"reason":"kill","ta":613.466}
+{"ev":"vstart","url":"audio/voice/female/nav_left.ogg","t":643636.599999994,"dur":1.28,"ta":613.466}
+{"ev":"vend","url":"audio/voice/female/nav_left.ogg","t":645238.6999999881,"reason":"kill","ta":615.068}
+{"ev":"vstart","url":"audio/voice/female/nav_down.ogg","t":645238.6999999881,"dur":0.9570975056689343,"ta":615.068}
+{"ev":"vend","url":"audio/voice/female/nav_down.ogg","t":646376,"reason":"kill","ta":616.205}
+{"ev":"vibrate","pattern":"[30,50,30,50,30]","t":700173,"ta":670.002}
+{"ev":"vstart","url":"audio/voice/female/stuck_key_3.ogg","t":700173.099999994,"dur":3.12,"ta":670.002}
+{"ev":"vend","url":"audio/voice/female/stuck_key_3.ogg","t":703631.6999999881,"reason":"kill","ta":673.461}
+{"ev":"vstart","url":"audio/voice/female/nav_left.ogg","t":703631.7999999821,"dur":1.28,"ta":673.461}
+{"ev":"vend","url":"audio/voice/female/nav_left.ogg","t":705233.6999999881,"reason":"kill","ta":675.063}
+{"ev":"vstart","url":"audio/voice/female/nav_down.ogg","t":705233.7999999821,"dur":0.9570975056689343,"ta":675.063}
+{"ev":"vend","url":"audio/voice/female/nav_down.ogg","t":706371.2999999821,"reason":"kill","ta":676.2}
+{"ev":"vibrate","pattern":"[30,50,30,50,30]","t":760173,"ta":730.002}
+{"ev":"vstart","url":"audio/voice/female/stuck_key_3.ogg","t":760173.099999994,"dur":3.12,"ta":730.002}
+{"ev":"vend","url":"audio/voice/female/stuck_key_3.ogg","t":763627,"reason":"kill","ta":733.456}
+{"ev":"vstart","url":"audio/voice/female/nav_left.ogg","t":763627,"dur":1.28,"ta":733.456}
+{"ev":"vend","url":"audio/voice/female/nav_left.ogg","t":765228.900000006,"reason":"kill","ta":735.058}
+{"ev":"vstart","url":"audio/voice/female/nav_down.ogg","t":765229,"dur":0.9570975056689343,"ta":735.058}
+{"ev":"vend","url":"audio/voice/female/nav_down.ogg","t":766366.6999999881,"reason":"kill","ta":736.196}
+{"ev":"vibrate","pattern":"[30,50,30,50,30]","t":820172.900000006,"ta":790.002}
+{"ev":"vstart","url":"audio/voice/female/stuck_key_3.ogg","t":820173.1999999881,"dur":3.12,"ta":790.002}
+{"ev":"vend","url":"audio/voice/female/stuck_key_3.ogg","t":823622.2999999821,"reason":"kill","ta":793.451}
+{"ev":"vstart","url":"audio/voice/female/nav_left.ogg","t":823622.400000006,"dur":1.28,"ta":793.452}
+{"ev":"vend","url":"audio/voice/female/nav_left.ogg","t":825224.5,"reason":"kill","ta":795.054}
+{"ev":"vstart","url":"audio/voice/female/nav_down.ogg","t":825224.5,"dur":0.9570975056689343,"ta":795.054}
+{"ev":"vend","url":"audio/voice/female/nav_down.ogg","t":826362,"reason":"kill","ta":796.191}
+{"ev":"vibrate","pattern":"[30,50,30,50,30]","t":880173.099999994,"ta":850.002}
+{"ev":"vstart","url":"audio/voice/female/stuck_key_3.ogg","t":880173.2999999821,"dur":3.12,"ta":850.002}
+{"ev":"vend","url":"audio/voice/female/stuck_key_3.ogg","t":883617.6999999881,"reason":"kill","ta":853.447}
+{"ev":"vstart","url":"audio/voice/female/nav_left.ogg","t":883617.7999999821,"dur":1.28,"ta":853.447}
+{"ev":"vend","url":"audio/voice/female/nav_left.ogg","t":885219.900000006,"reason":"kill","ta":855.049}
+{"ev":"vstart","url":"audio/voice/female/nav_down.ogg","t":885220,"dur":0.9570975056689343,"ta":855.049}
+{"ev":"vend","url":"audio/voice/female/nav_down.ogg","t":886357.2999999821,"reason":"kill","ta":856.186}
+{"ev":"recstop","t":918408.5,"ta":888.238}
diff --git a/research/escape-state/round1/live-transcript.md b/research/escape-state/round1/live-transcript.md
new file mode 100644
index 00000000..544d55b2
--- /dev/null
+++ b/research/escape-state/round1/live-transcript.md
@@ -0,0 +1,346 @@
+# escape 实时会话 transcript
+
+- `[ 0.00s]` REC 开始 (audio/webm;codecs=opus)
+- `[ 0.09s]` VOICE ▶ g0_intro (dur 6.64s)
+- `[ 0.28s]` BGM ♪ drone (loop)
+- `[ 6.90s]` VOICE ■ g0_intro (kill)
+- `[ 6.90s]` VERDICT g0_intro: **OK** sim=1
+ - 期望: 黑暗。你在废弃的地下设施里醒来,什么都想不起来。你必须找到出路。
+ - 听到: 黑暗,你在废弃的地下设施里醒来,什么都想不起来,你必须找到出路。
+- `[ 8.10s]` VOICE ▶ g1_move (dur 4.56s)
+- `[ 12.68s]` VOICE ■ g1_move (kill)
+- `[ 12.68s]` VOICE ▶ skip_hint (dur 1.6s)
+- `[ 12.68s]` VERDICT g1_move: **OK** sim=0.9697
+ - 期望: 先活动一下。朝任意方向滑一下,走一小步。
+ - 听到: 先活动一下,朝任意方向滑一下,走一小。
+- `[ 14.28s]` VOICE ■ skip_hint (kill)
+- `[ 14.28s]` VERDICT skip_hint: **OK** sim=0.7143
+ - 期望: 单击可跳过旁白。
+ - 听到: 单机可跳过旁本。
+- `[ 34.52s]` **GESTURE** swipeup @(195,422)
+- `[ 34.88s]` VIBRATE [10]
+- `[ 34.88s]` SFX ♪ step
+- `[ 34.88s]` VOICE ▶ g1_done (dur 2.72s)
+- `[ 34.88s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 37.59s]` VOICE ■ g1_done (kill)
+- `[ 37.59s]` VERDICT g1_done: **OK** sim=0.9412
+ - 期望: 很好,你迈出了第一步。
+ - 听到: 很好,你迈出了第一。
+- `[ 37.60s]` VOICE ▶ g2_wall (dur 5.68s)
+- `[ 41.57s]` **GESTURE** swipeleft @(195,422)
+- `[ 41.93s]` VIBRATE [10]
+- `[ 43.28s]` VOICE ■ g2_wall (kill)
+- `[ 43.28s]` VERDICT g2_wall: **OK** sim=0.9268
+ - 期望: 这个房间不大,四面都是墙。再滑两下,试试碰碰墙。
+ - 听到: 一、这个房间不大,四面都是墙,再划两下试试碰碰墙。
+- `[ 46.34s]` **GESTURE** swipeleft @(195,422)
+- `[ 46.69s]` VIBRATE [10]
+- `[ 46.69s]` VIBRATE [60,40,60]
+- `[ 46.69s]` SFX ♪ wall
+- `[ 46.70s]` VOICE ▶ wall (dur 2.08s)
+- `[ 46.70s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 49.04s]` VOICE ■ wall (kill)
+- `[ 49.04s]` VERDICT wall: **OK** sim=1
+ - 期望: 那边是墙,换个方向。
+ - 听到: 那边是墙,换个方向。
+- `[ 53.66s]` **GESTURE** swipeleft @(195,422)
+- `[ 54.01s]` VIBRATE [10]
+- `[ 54.01s]` VIBRATE [60,40,60]
+- `[ 54.01s]` SFX ♪ wall
+- `[ 54.01s]` VOICE ▶ wall (dur 2.08s)
+- `[ 54.01s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 56.35s]` VOICE ■ wall (kill)
+- `[ 56.35s]` VERDICT wall: **OK** sim=1
+ - 期望: 那边是墙,换个方向。
+ - 听到: 那边是墙,换个方向。
+- `[ 56.36s]` VOICE ▶ g3_left (dur 2.96s)
+- `[ 59.34s]` VOICE ■ g3_left (kill)
+- `[ 59.34s]` VERDICT g3_left: **OK** sim=0.9
+ - 期望: 跟着我指的路走。向左滑。
+ - 听到: 跟着我指的路走向左画。
+- `[ 60.92s]` **GESTURE** swipedown @(195,422)
+- `[ 61.28s]` VIBRATE [10]
+- `[ 61.28s]` VOICE ▶ guide_skip (dur 2.88s)
+- `[ 61.28s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 64.27s]` VOICE ■ guide_skip (kill)
+- `[ 64.27s]` VOICE ▶ start_room (dur 7.2s)
+- `[ 64.27s]` VERDICT guide_skip: **OK** sim=1
+ - 期望: 跳过引导,直接开始自由探索。
+ - 听到: 跳过引导,直接开始自由探索。
+- `[ 64.27s]` SFX ♪ amb_drip
+- `[ 71.81s]` VOICE ■ start_room (kill)
+- `[ 71.81s]` VERDICT start_room: **OK** sim=1
+ - 期望: 你站在设施中央的房间里。滑动移动,单击检查,双击使用物品。
+ - 听到: 你站在设施中央的房间里滑动移动,单击检查,双击使用物品。
+- `[ 125.00s]` VIBRATE [30,50,30,50,30]
+- `[ 125.01s]` VOICE ▶ stuck_ask (dur 1.28s)
+- `[ 125.01s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 126.61s]` VOICE ■ stuck_ask (kill)
+- `[ 126.61s]` VERDICT stuck_ask: **OK** sim=0.9091
+ - 期望: 需要帮助吗?
+ - 听到: 需要我帮助吗?
+- `[ 126.61s]` VOICE ▶ stuck_key_1 (dur 2.88s)
+- `[ 129.60s]` VOICE ■ stuck_key_1 (kill)
+- `[ 129.60s]` VERDICT stuck_key_1: **OK** sim=0.9231
+ - 期望: 铁锈味最重的地方,值得仔细看看。
+ - 听到: 铁锈味最重的地方值得仔细。
+- `[ 130.87s]` **GESTURE** swipeleft @(195,422)
+- `[ 131.22s]` VIBRATE [10]
+- `[ 131.22s]` SFX ♪ step
+- `[ 131.22s]` SFX ♪ amb_hum
+- `[ 137.65s]` **GESTURE** swipedown @(195,422)
+- `[ 138.01s]` VIBRATE [10]
+- `[ 138.01s]` SFX ♪ step
+- `[ 138.01s]` SFX ♪ amb_storage
+- `[ 138.02s]` VOICE ▶ room_storage (dur 5.36s)
+- `[ 138.02s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 143.70s]` VOICE ■ room_storage (kill)
+- `[ 143.70s]` VERDICT room_storage: **OK** sim=1
+ - 期望: 一间储物间。货架上堆满杂物,角落里传来水滴声。
+ - 听到: 一间储物间货架上堆满杂物,角落里传来水滴声。
+- `[ 144.92s]` **GESTURE** tap @(195,422)
+- `[ 145.28s]` VIBRATE [20]
+- `[ 145.28s]` VIBRATE [15]
+- `[ 145.28s]` SFX ♪ amb_storage
+- `[ 145.28s]` SFX ♪ pickup
+- `[ 146.10s]` VOICE ▶ found_key (dur 9.96s)
+- `[ 146.10s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 156.23s]` VOICE ■ found_key (kill)
+- `[ 156.23s]` VERDICT found_key: **OK** sim=1
+ - 期望: 你蹲下身在储物架的角落里摸索,指尖碰到一个冰凉的金属物件——是一把钥匙!钥匙到手了。
+ - 听到: 你蹲下身,在储物架的角落里摸索,指尖碰到一个冰凉的金属物件,是一把钥匙,钥匙到手了。
+- `[ 156.24s]` VOICE ▶ gd_room_r (dur 1.28s)
+- `[ 157.84s]` VOICE ■ gd_room_r (kill)
+- `[ 157.84s]` VERDICT gd_room_r: **OK** sim=1
+ - 期望: 右面的房间。
+ - 听到: 右面的房间。
+- `[ 175.53s]` **GESTURE** swiperight @(195,422)
+- `[ 175.89s]` VIBRATE [10]
+- `[ 175.89s]` SFX ♪ step
+- `[ 175.89s]` SFX ♪ amb_wind
+- `[ 179.30s]` **GESTURE** swiperight @(195,422)
+- `[ 179.66s]` VIBRATE [10]
+- `[ 179.66s]` SFX ♪ step
+- `[ 179.66s]` SFX ♪ amb_gate
+- `[ 179.66s]` VOICE ▶ room_gate (dur 3.52s)
+- `[ 179.66s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 183.07s]` **GESTURE** swipeup @(195,422)
+- `[ 183.42s]` VIBRATE [10]
+- `[ 183.42s]` VOICE ■ room_gate (kill)
+- `[ 183.42s]` SFX ♪ step
+- `[ 183.42s]` SFX ♪ amb_cabinet
+- `[ 183.42s]` VERDICT room_gate: **OK** sim=1
+ - 期望: 面前,是一扇沉重的铁门。
+ - 听到: 面前是一扇沉重的铁门。
+- `[ 183.43s]` VOICE ▶ room_cabinet (dur 2.64s)
+- `[ 186.14s]` VOICE ■ room_cabinet (kill)
+- `[ 186.14s]` VERDICT room_cabinet: **OK** sim=1
+ - 期望: 这个房间里,立着一个铁柜。
+ - 听到: 这个房间里立着一个铁柜。
+- `[ 192.56s]` **GESTURE** doubletap @(195,422)
+- `[ 192.81s]` VIBRATE [20]
+- `[ 192.81s]` VIBRATE [100]
+- `[ 192.81s]` SFX ♪ unlock
+- `[ 192.81s]` SFX ♪ door_creak
+- `[ 192.83s]` VOICE ▶ unlock_cabinet (dur 11.48s)
+- `[ 192.83s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 204.46s]` VOICE ■ unlock_cabinet (kill)
+- `[ 204.46s]` VERDICT unlock_cabinet: **OK** sim=0.9714
+ - 期望: 钥匙插进锁孔,缓缓转动。咔哒一声,柜门开了。柜子深处,躺着一枚保险丝。保险丝到手了。
+ - 听到: 钥匙插进锁孔,缓缓转动,咔嗒一声,柜门开了,柜子深处躺着一枚保险丝,保险丝到手了。
+- `[ 204.47s]` VOICE ▶ gd_next_fuse (dur 3.28s)
+- `[ 207.92s]` VOICE ■ gd_next_fuse (kill)
+- `[ 207.92s]` VERDICT gd_next_fuse: **OK** sim=1
+ - 期望: 保险丝到手。接下来,去找电箱。
+ - 听到: 保险丝到手,接下来去找电箱。
+- `[ 207.92s]` VOICE ▶ gd_room_l (dur 1.68s)
+- `[ 209.80s]` VOICE ■ gd_room_l (kill)
+- `[ 209.80s]` VERDICT gd_room_l: **OK** sim=1
+ - 期望: 左面的房间。
+ - 听到: 左面的房间。
+- `[ 222.58s]` **GESTURE** doubletap @(195,422)
+- `[ 222.83s]` VIBRATE [20]
+- `[ 222.83s]` VIBRATE [60,40,60]
+- `[ 222.84s]` VOICE ▶ use_fail (dur 7.36s)
+- `[ 222.84s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 230.39s]` VOICE ■ use_fail (kill)
+- `[ 230.39s]` VERDICT use_fail: **OK** sim=0.9655
+ - 期望: 这里没有用得上东西的地方。换个房间试试,或者先单击检查一下周围。
+ - 听到: 这里没有用得上东西的地方,换个房间试试,或者先单机检查一下周围。
+- `[ 230.40s]` VOICE ▶ stuck_box_2 (dur 4.88s)
+- `[ 232.97s]` **GESTURE** swipeleft @(195,422)
+- `[ 233.32s]` VIBRATE [10]
+- `[ 233.32s]` SFX ♪ step
+- `[ 233.32s]` SFX ♪ amb_drip
+- `[ 235.34s]` VOICE ■ stuck_box_2 (kill)
+- `[ 235.34s]` VERDICT stuck_box_2: **OK** sim=1
+ - 期望: 去西北角的电箱房,双击,把保险丝装进电箱。
+ - 听到: 去西北角的电箱房,双击把保险丝装进电箱。
+- `[ 236.73s]` **GESTURE** swipeleft @(195,422)
+- `[ 237.09s]` VIBRATE [10]
+- `[ 237.09s]` SFX ♪ step
+- `[ 237.09s]` SFX ♪ amb_hum
+- `[ 240.50s]` **GESTURE** swipeup @(195,422)
+- `[ 240.85s]` VIBRATE [10]
+- `[ 240.85s]` SFX ♪ step
+- `[ 240.85s]` SFX ♪ amb_power
+- `[ 240.86s]` VOICE ▶ room_power (dur 2.64s)
+- `[ 240.86s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 243.58s]` VOICE ■ room_power (kill)
+- `[ 243.58s]` VERDICT room_power: **OK** sim=1
+ - 期望: 这个房间的墙上,有一只电箱。
+ - 听到: 这个房间的墙上有一只电箱。
+- `[ 267.62s]` **GESTURE** doubletap @(195,422)
+- `[ 267.87s]` VIBRATE [20]
+- `[ 267.87s]` VIBRATE [100]
+- `[ 267.87s]` SFX ♪ spark
+- `[ 267.87s]` SFX ♪ power_on
+- `[ 267.88s]` VOICE ▶ fix_box (dur 7.52s)
+- `[ 267.88s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 275.44s]` VOICE ■ fix_box (kill)
+- `[ 275.44s]` VOICE ▶ gd_room_r (dur 1.28s)
+- `[ 275.44s]` VERDICT fix_box: **OK** sim=0.9492
+ - 期望: 你把保险丝用力按进卡座。火花一闪,嗡鸣声响起——头顶的线路恢复供电了!
+ - 听到: 你把保险丝用力按静卡座,火花一闪,嗡鸣声响起,头顶的线路恢复供电。
+- `[ 277.04s]` VOICE ■ gd_room_r (kill)
+- `[ 277.04s]` VERDICT gd_room_r: **OK** sim=0.75
+ - 期望: 右面的房间。
+ - 听到: 右面的。
+- `[ 278.19s]` **GESTURE** swiperight @(195,422)
+- `[ 278.55s]` VIBRATE [10]
+- `[ 278.55s]` SFX ♪ step
+- `[ 278.55s]` SFX ♪ amb_wind
+- `[ 281.96s]` **GESTURE** swiperight @(195,422)
+- `[ 282.32s]` VIBRATE [10]
+- `[ 282.32s]` SFX ♪ step
+- `[ 282.32s]` SFX ♪ amb_hum
+- `[ 285.73s]` **GESTURE** swipedown @(195,422)
+- `[ 286.08s]` VIBRATE [10]
+- `[ 286.08s]` SFX ♪ step
+- `[ 286.08s]` SFX ♪ amb_cabinet
+- `[ 295.51s]` **GESTURE** tap @(195,422)
+- `[ 295.87s]` VIBRATE [20]
+- `[ 295.87s]` SFX ♪ amb_cabinet
+- `[ 296.68s]` VOICE ▶ empty_1 (dur 5.2s)
+- `[ 296.68s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 301.90s]` VOICE ■ empty_1 (kill)
+- `[ 301.90s]` VERDICT empty_1: **OK** sim=1
+ - 期望: 房间里空空荡荡,只有你自己的回声。
+ - 听到: 房间里空空荡荡,只有你自己的回声。
+- `[ 305.74s]` **GESTURE** doubletap @(195,422)
+- `[ 305.99s]` VIBRATE [20]
+- `[ 305.99s]` VIBRATE [60,40,60]
+- `[ 305.99s]` VOICE ▶ use_fail (dur 7.36s)
+- `[ 305.99s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 313.54s]` VOICE ■ use_fail (kill)
+- `[ 313.54s]` VERDICT use_fail: **OK** sim=0.9655
+ - 期望: 这里没有用得上东西的地方。换个房间试试,或者先单击检查一下周围。
+ - 听到: 这里没有用得上东西的地方,换个房间试试,或者先单机检查一下周围。
+- `[ 313.54s]` VOICE ▶ stuck_gate_2 (dur 5.44s)
+- `[ 319.22s]` VOICE ■ stuck_gate_2 (kill)
+- `[ 319.22s]` VERDICT stuck_gate_2: **OK** sim=1
+ - 期望: 去东南角的大门,双击,打开它,离开这里。
+ - 听到: 去东南角的大门,双击打开它,离开这里。
+- `[ 339.31s]` **GESTURE** swipedown @(195,422)
+- `[ 339.67s]` VIBRATE [10]
+- `[ 339.67s]` SFX ♪ step
+- `[ 339.67s]` SFX ♪ amb_gate_live
+- `[ 349.09s]` **GESTURE** tap @(195,422)
+- `[ 349.45s]` VIBRATE [20]
+- `[ 349.45s]` SFX ♪ amb_gate_live
+- `[ 350.26s]` VOICE ▶ inspect_gate_ready (dur 8.72s)
+- `[ 350.26s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 358.58s]` **GESTURE** doubletap @(195,422)
+- `[ 358.83s]` VIBRATE [20]
+- `[ 358.83s]` VIBRATE [100]
+- `[ 358.83s]` VOICE ■ inspect_gate_ready (kill)
+- `[ 358.83s]` VERDICT inspect_gate_ready: **TRUNCATED** sim=0.9667
+ - 期望: 电子锁的指示灯亮了,发出微弱的绿光。大门已经通电。双击,打开它,离开这里。
+ - 听到: 电子锁的指示灯亮了,发出微弱的绿光,大门已经通电,双击打开它,离开。
+- `[ 358.84s]` VOICE ▶ escape (dur 15.32s)
+- `[ 361.43s]` SFX ♪ gate_open
+- `[ 361.43s]` SFX ♪ win_chime
+- `[ 369.22s]` **GESTURE** tap @(195,422)
+- `[ 369.58s]` VIBRATE [20]
+- `[ 369.58s]` VOICE ■ escape (kill)
+- `[ 369.58s]` VERDICT escape: **TRUNCATED** sim=0.7711
+ - 期望: 你扳动开关。电机轰鸣,沉重的铁门缓缓升起,刺眼的光线涌了进来。你眯起眼睛,向前迈出了第一步。自由了。你逃出来了。
+ - 听到: 开你搬动开关,电击轰,沉重的铁门缓缓升起,刺眼的光线涌了进来,你眯起眼睛向前迈。
+- `[ 369.58s]` BGM ♪ drone (loop)
+- `[ 369.58s]` SFX ♪ amb_drip
+- `[ 369.59s]` VOICE ▶ replay (dur 8.24s)
+- `[ 377.78s]` VOICE ■ replay (kill)
+- `[ 377.78s]` VERDICT replay: **OK** sim=0.9545
+ - 期望: 黑暗再次合拢。你回到了中央的房间。这一次,你会更快。
+ - 听到: 前黑暗再次合拢,你回到了中央的房间,这一次你会更。
+- `[ 430.00s]` VIBRATE [30,50,30,50,30]
+- `[ 430.00s]` VOICE ▶ stuck_ask (dur 1.28s)
+- `[ 430.00s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 431.60s]` VOICE ■ stuck_ask (kill)
+- `[ 431.60s]` VOICE ▶ stuck_key_1 (dur 2.88s)
+- `[ 431.60s]` VERDICT stuck_ask: **OK** sim=0.9091
+ - 期望: 需要帮助吗?
+ - 听到: 需要我帮助吗?
+- `[ 434.59s]` VOICE ■ stuck_key_1 (kill)
+- `[ 434.59s]` VERDICT stuck_key_1: **OK** sim=0.9286
+ - 期望: 铁锈味最重的地方,值得仔细看看。
+ - 听到: 铁修味最重的地方,值得仔细看看。
+- `[ 490.00s]` VIBRATE [30,50,30,50,30]
+- `[ 490.01s]` VOICE ▶ stuck_key_2 (dur 4.48s)
+- `[ 490.01s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 494.59s]` VOICE ■ stuck_key_2 (kill)
+- `[ 494.59s]` VERDICT stuck_key_2: **OK** sim=1
+ - 期望: 去西南角的储物间,单击检查,找到钥匙。
+ - 听到: 去西南角的储物间单击检查,找到钥匙。
+- `[ 550.00s]` VIBRATE [30,50,30,50,30]
+- `[ 550.01s]` VOICE ▶ stuck_key_3 (dur 3.12s)
+- `[ 550.01s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 553.47s]` VOICE ■ stuck_key_3 (kill)
+- `[ 553.47s]` VERDICT stuck_key_3: **OK** sim=1
+ - 期望: 跟我说的方向走,去西南角的储物间。
+ - 听到: 跟我说的方向走,去西南角的储物间。
+- `[ 553.48s]` VOICE ▶ nav_left (dur 1.28s)
+- `[ 555.07s]` VOICE ■ nav_left (kill)
+- `[ 555.07s]` VERDICT nav_left: **OK** sim=1
+ - 期望: 向左滑。
+ - 听到: 向左滑。
+- `[ 555.08s]` VOICE ▶ nav_down (dur 0.9570975056689343s)
+- `[ 556.21s]` VOICE ■ nav_down (kill)
+- `[ 556.21s]` VERDICT nav_down: **OK** sim=1
+ - 期望: 向下滑。
+ - 听到: 向下滑。
+- `[ 610.00s]` VIBRATE [30,50,30,50,30]
+- `[ 610.00s]` VOICE ▶ stuck_key_3 (dur 3.12s)
+- `[ 613.47s]` VOICE ■ stuck_key_3 (kill)
+- `[ 613.47s]` VOICE ▶ nav_left (dur 1.28s)
+- `[ 615.07s]` VOICE ■ nav_left (kill)
+- `[ 615.07s]` VOICE ▶ nav_down (dur 0.9570975056689343s)
+- `[ 616.21s]` VOICE ■ nav_down (kill)
+- `[ 670.00s]` VIBRATE [30,50,30,50,30]
+- `[ 670.00s]` VOICE ▶ stuck_key_3 (dur 3.12s)
+- `[ 673.46s]` VOICE ■ stuck_key_3 (kill)
+- `[ 673.46s]` VOICE ▶ nav_left (dur 1.28s)
+- `[ 675.06s]` VOICE ■ nav_left (kill)
+- `[ 675.06s]` VOICE ▶ nav_down (dur 0.9570975056689343s)
+- `[ 676.20s]` VOICE ■ nav_down (kill)
+- `[ 730.00s]` VIBRATE [30,50,30,50,30]
+- `[ 730.00s]` VOICE ▶ stuck_key_3 (dur 3.12s)
+- `[ 733.46s]` VOICE ■ stuck_key_3 (kill)
+- `[ 733.46s]` VOICE ▶ nav_left (dur 1.28s)
+- `[ 735.06s]` VOICE ■ nav_left (kill)
+- `[ 735.06s]` VOICE ▶ nav_down (dur 0.9570975056689343s)
+- `[ 736.20s]` VOICE ■ nav_down (kill)
+- `[ 790.00s]` VIBRATE [30,50,30,50,30]
+- `[ 790.00s]` VOICE ▶ stuck_key_3 (dur 3.12s)
+- `[ 793.45s]` VOICE ■ stuck_key_3 (kill)
+- `[ 793.45s]` VOICE ▶ nav_left (dur 1.28s)
+- `[ 795.05s]` VOICE ■ nav_left (kill)
+- `[ 795.05s]` VOICE ▶ nav_down (dur 0.9570975056689343s)
+- `[ 796.19s]` VOICE ■ nav_down (kill)
+- `[ 850.00s]` VIBRATE [30,50,30,50,30]
+- `[ 850.00s]` VOICE ▶ stuck_key_3 (dur 3.12s)
+- `[ 853.45s]` VOICE ■ stuck_key_3 (kill)
+- `[ 853.45s]` VOICE ▶ nav_left (dur 1.28s)
+- `[ 855.05s]` VOICE ■ nav_left (kill)
+- `[ 855.05s]` VOICE ▶ nav_down (dur 0.9570975056689343s)
+- `[ 856.19s]` VOICE ■ nav_down (kill)
diff --git a/research/escape-state/round1/p1-blind.md b/research/escape-state/round1/p1-blind.md
new file mode 100644
index 00000000..0adb23f7
--- /dev/null
+++ b/research/escape-state/round1/p1-blind.md
@@ -0,0 +1,64 @@
+# P1 视障资深玩家评审:《暗室》(escape-state 取证轮 R1)
+
+> 评审依据:`live-transcript.md`(真机 live 一局,SenseVoice 转写,行号即该文件行)+
+> `transcript-mock.md`(mock 新手局转写)。本轮只聚焦一个问题:**哪些时刻我
+> 不知道自己在哪 / 该干嘛 / 进展到哪**。我读的是转写,按"如果是我戴着耳机在
+> 75 档语速下听到这些"代入;ASR 近失(如"单机"→"单击")不计入缺陷。
+
+### 暗室
+- 状态感知:2/5 —— 引导期每秒都有指令带路,状态满格;一进自由探索就开始
+ 失聪。**①撞墙只有"那边是墙",没有方位**(live 行 40-55):我在 (0,0) 连撞
+ 两次,听到的句子一字不差,但我的地图上一个标记都没增加——我不知道刚才是
+ 哪面墙,只知道"此路不通"。**②空房声学 identical**:(0,1) 是 amb_hum(行 90),
+ 后来 (2,0) 也是 amb_hum(行 182);(1,2) amb_wind(行 120),(1,0) 也是
+ amb_wind(行 212)。四间空房两种音,我对着转写都对不出哪间是哪间,耳朵更
+ 不可能。**③重访已探索房间零语音**:我重返铁柜房时(行 220、223)只听到
+ 环境音,"这个房间里立着一个铁柜"是首到专属——我作为记图全靠耳朵的人,
+ 第二次经过没有任何"我来过这"的锚。
+- 下一步明确性:3/5 —— 进展事件当下很明确:found_key 后接"右面的房间。"
+ (行 112-116),unlock 后"左面的房间。"(行 156-160),修电箱后"右面的房间。"
+ (行 201-208)——链条本身做得好。但有两个洞:**①方向句没有目标名**:
+ "右面的房间。"——右面是哪?我下一秒要干什么?我手里钥匙对应的是铁柜,
+ 句子不说,得我赌。**②引导期手势被静默吞掉**(行 30-35):g2_wall 还没播完
+ 我滑了一下,只收到引擎一声 [10] 轻震,墙没响、计数没走——我这一滑到底算数
+ 吗?教科书式的无声失败。如果我信了这一下、只再补一滑,就永远卡在 G2。
+- 愿意继续玩:是 —— 解谜链手感对(双击使用、震动脉络清楚),但我要先能
+ 建出自己的地图才谈得上"探索",现在建图材料不够。
+- P0 问题(不知道自己在哪/该干嘛/进展到哪的具体时刻):
+ 1. **跳过引导后目标全丢**(行 61-74):swipedown 被当成跳过(G3 下滑=跳过
+ 是设计,但我是在引导语音刚说完"向左滑"之后、还没作答时滑错的),然后
+ `guide_skip` + `start_room` 两句了事——**"找到能用钥匙打开的东西"这句
+ 目标陈述我一辈子没听到**(g7_goal 只存在于引导链尾)。开局 64s 起我站
+ 在 (1,1),知道三个手势,不知道要干什么。
+ 2. **主动查状态被直接喂答案**(行 161-178、230-243):我双击铁柜房(柜已开、
+ 手里是保险丝),只是想知道"我现在该干嘛",游戏播 use_fail 之后直接上
+ `stuck_box_2`「去西北角的电箱房,双击,把保险丝装进电箱」——L2 明示是
+ 给卡关 120 秒的人的,我没卡关,谜题被白送。这是提示泄漏,也是唯一一个
+ "主动查询"入口被做成答案分发口。
+ 3. **地图进度无账本**:全 9 房走了 7 房,没有任何一句告诉我"钥匙✓、铁柜✓、
+ 电✓、还差大门"——进展播报只在进展事件当下播一次(found_key 链等),
+ 中途无法回查(escape 连上滑状态键都没有,00 §5.3 豁免表)。
+- 建议(按对"我愿意玩下去"的影响排序):
+ 1. 给 escape 一个 L0 级进度重述通道(不泄题:报"已完成几环/还差几环"),
+ 取代双击兜底直通 L2 的现状(见 25 号文档 B3/B5)。
+ 2. 撞墙报方位但用陈述句(「左边是墙。」),别再走"手指贴着屏幕推"教学腔
+ (round1-live 回退原因我同意,但方位本身是该给的,见 B1)。
+ 3. 目标方位句并入目标名(「铁柜,在右面的房间。」),并给四间空房各自
+ 可分辨的声学签名或首访方位线索句(见 B4/B6)。
+
+#### 听觉惯例符合度(逐条核对特别检查项)
+- 语音可打断:过。单击跳句机制干净,关键获得链(found_key/unlock/fix_box)
+ 不可跳有事件层兜住(行 106-116 链完整)。
+- 语音可重听:过。单击检查=重播环境音+描述,空房也可重听。
+- 等待有无声:**半不过**。行 72 的 3s 静默是设计内的"辨方向",可以;但自由
+ 探索期最长的静默段(行 87-94 我在空房之间移动)只有脚步声和环境音——
+ 这不是"死寂",是"零信息",比死寂更糟:我一直在动,游戏一直说话(环境音),
+ 可我什么也学不到。
+- 失败可否归因:半过。撞墙可归因(有闷响+error 震+短句),但**被吞手势不可
+ 归因**(行 30-35)——引导 busy 窗口是静默 return,震都没有第二声。
+- 菜单列表导航:未验证(结算后我直接重开,没进菜单)。
+- status key:**缺席**。PLAY 期上滑=移动,全游戏无任何时刻查询"我在哪/我
+ 有什么/还剩几环"——这是我这个人设最依赖的锚,本作没有。双击兜底本可补位,
+ 现状是答案泄漏口(见 P0-2)。
+- 提示永不逐字重复:过。L3 带路每 60s 重算重播但坐标若没变内容相同属合理
+ (行 295-346 从 (1,1) 重复带路,玩家没动当然重复同一路线)。
diff --git a/research/escape-state/round1/p3-office.md b/research/escape-state/round1/p3-office.md
new file mode 100644
index 00000000..f22203fe
--- /dev/null
+++ b/research/escape-state/round1/p3-office.md
@@ -0,0 +1,51 @@
+# P3 时间紧张的白领评审:《暗室》(escape-state 取证轮 R1)
+
+> 评审依据:`live-transcript.md`(真机 live 一局)+ `transcript-mock.md`(mock
+> 新手局)。本轮只聚焦一个问题:**哪些时刻我不知道自己在哪 / 该干嘛 / 进展
+> 到哪**。我代入的场景是地铁单程 25 分钟、站立单手持机、随时可能被打断——
+> 被打断后能不能一句话接上,是我的生死线。
+
+### 暗室
+- 状态感知:2/5 —— 我被 put off 的第一个时刻是**空房 transit**:行 87-94
+ 我从起点滑到储物间,中间经过 (0,1) 只有脚步+嗡鸣,两行事件、零句话;
+ 行 179-186、209-220 同理((0,1)→(0,0)、(1,0)→(2,0)→(2,1) 全是静默移动)。
+ 地铁里听不清环境音细节,这几段对我来说就是"盲滑"。**撞了墙也不知道方位**
+ (行 40-55):"那边是墙"告诉我此路不通,不告诉我我刚才想去哪——单手持机
+ 滑错方向太常见了,每次错都学不到东西。
+- 下一步明确性:3/5 —— 好消息是**进展链很会带路**:拿到钥匙接"右面的房间。"
+ (行 112-116)、开柜接"左面的房间。"(行 156-160)、修电箱接"右面的房间。"
+ (行 201-208),只要我在解谜链上,永远不迷路。坏消息有二:**①方向句无
+ 目标名**:地铁里"右面的房间。"(1.3s 短句)极易被车厢噪音盖住前半句,
+ 我只听到"……的房间",不知道去找什么(行 206-208 的 ASR sim 只有 0.75,
+ 正是短句被环境吃掉的实证)。**②被打断恢复无锚**:行 253-275 我双击大门
+ 之后结算链 escape 刚播 10 秒,我一个习惯性单击(想确认是不是要操作)直接
+ 重开——time_band 和 again 都没播,我连自己通关用时多少都不知道就进下一局
+ 了。25 分钟通勤被打断 3 次就会这样 3 次。
+- 愿意继续玩:否 —— 单局 6 分钟节奏其实合适,但我每次被打断回来都要重新
+ 摸一遍地图,"一句话接上"做不到。
+- P0 问题(不知道自己在哪/该干嘛/进展到哪的具体时刻):
+ 1. **结算链无保护**(行 253-275):ENDING 单击无条件重开,结算语音播一半
+ 就没了(行 266 TRUNCATED sim 0.77)——我作为"随时被打断"的人,结算
+ 是我唯一确认"我打完了/打得如何"的窗口,这个窗口一碰就碎。
+ 2. **目标句短且无名**(行 112-116):嘈杂环境下 1.3s 的"右面的房间。"承载
+ 不了关键导航信息;该把目标名并进整句("铁柜在右面的房间"信息量翻倍、
+ 时长只多 1 秒)。
+ 3. **跳过引导=目标全丢**(行 61-74):误触下滑跳过之后只有 start_room 的
+ 手势总览,没有任何目标提示;我这种没耐心的玩家正是最容易跳过引导的人,
+ 跳过之后 60 秒(行 75-86)才等到第一句 L1 暗示。开局一分钟是纯摸黑。
+- 建议(按对"我愿意玩下去"的影响排序):
+ 1. 结算链播完前单击重开加守卫(或单击=跳过下一不可跳句的确认):我要听完
+ 用时播报(见 25 号文档 B8)。
+ 2. 目标方位句并入目标名并重录(「铁柜,在右面的房间。」,见 B4)。
+ 3. 跳过引导的确认句后补播 g7_goal 目标句(资产现成,见 B9);撞墙给陈述式
+ 方位句(见 B1),让我每次失误都变成地图信息。
+
+#### 通勤场景符合度(逐条核对特别检查项)
+- 3 分钟内有没有一次完整乐趣:有——mock 局 35s 就到电箱房、2 分钟内通关的
+ 关键路径存在(mock 行 144-149),链式解谜本身爽点密。
+- 被打断后能不能一句话接上:**不能**(见 P0-1/P0-3),这是唯一硬伤。
+- 会不会太吵:不会,语音密度低、无高频刺耳件(转写无声频谱数据,此项不评)。
+- 操作能不能单手盲操:能,四向滑+单击+双击单手成立;但盲操的代价是撞墙
+ 频繁,而撞墙不给方位(行 40-55),盲操的试错成本全是沉没成本。
+- 关键信息听不清怎么办:**无解**——没有重听上一句的通道(单击只重播环境音),
+ 短方位句被噪音吃掉就永久丢失,直到下一次进展事件(可能就是 60-180s 后)。
diff --git a/research/escape-state/round1/transcript-mock.md b/research/escape-state/round1/transcript-mock.md
new file mode 100644
index 00000000..75572399
--- /dev/null
+++ b/research/escape-state/round1/transcript-mock.md
@@ -0,0 +1,151 @@
+# 试玩转写:《escape》
+
+> 本文件是「玩家感知日志」:一位新手玩家的整局体验转写。语音列出了玩家听到的每句话全文,
+> 音效/BGM/震动按发生时间列出。「静默」段 = 玩家什么也听不到。手势是剧本注入的(模拟真人手部动作),
+> 非最优操作:包含犹豫、误操作与乱试。语音时长按语速估算(±20%),仅供节奏参考。
+t+ 0.0s (玩家点开游戏,轻触屏幕开始)
+t+ 0.0s ♫ BGM 切换 → bgm/drone
+t+ 0.0s ♪ 向导音:explore(探索/安全)
+t+ 0.0s ▶ 语音:「黑暗。你在废弃的地下设施里醒来,什么都想不起来。你必须找到出路。」
+t+ 0.0s 【状态】黑屏,只有引子的声音,先听完 引导第 1/8 步,等我做手势(语音播放中,语速1)
+t+ 0.0s 【状态】引导第 1/8 步 引导第 1/8 步,等我做手势(语音播放中,语速1)
+
+#### 【玩家动作】照引导单击了一下
+
+t+ 0.0s ▶ 语音:「先活动一下。朝任意方向滑一下,走一小步。」
+t+ 2.1s ▶ 语音:「单击可跳过旁白。」
+t+ 4.2s ♪ 向导音:turn(轮到你)
+t+ 4.2s 【状态】引导第 2/8 步 引导第 2/8 步,等我做手势(BGM=bgm/drone,语速1)
+
+#### 【玩家动作】随便滑了一步,有脚步声
+
+t+ 4.2s ♪ 音效 sfx/step
+t+ 4.2s ▶ 语音:「很好,你迈出了第一步。」
+t+ 6.4s ▶ 语音:「这个房间不大,四面都是墙。再滑两下,试试碰碰墙。」
+t+ 8.5s ♪ 向导音:turn(轮到你)
+t+ 8.5s 【状态】引导第 3/8 步 引导第 3/8 步,等我做手势(BGM=bgm/drone,语速1)
+t+ 8.5s ♪ 音效 sfx/wall
+t+ 8.5s ▶ 语音:「那边是墙,换个方向。」
+t+ 8.5s 〰 震动:error(做错了:两连短震)
+
+#### 【玩家动作】又滑一步,又撞墙了——墙的方位听出来了
+
+t+ 10.6s ♪ 音效 sfx/wall
+t+ 10.6s ▶ 语音:「那边是墙,换个方向。」
+t+ 10.6s 〰 震动:error(做错了:两连短震)
+t+ 12.7s ▶ 语音:「跟着我指的路走。向左滑。」
+t+ 14.8s ♪ 向导音:turn(轮到你)
+t+ 14.8s 【状态】引导第 4/8 步 引导第 4/8 步,等我做手势(BGM=bgm/drone,语速1)
+
+#### 【玩家动作】这回往左,走通了一步
+
+t+ 14.8s ♪ 音效 sfx/step
+t+ 14.8s ▶ 语音:「对。再向下滑。」
+t+ 17.0s ♪ 向导音:turn(轮到你)
+t+ 17.0s 【状态】引导第 5/8 步 引导第 5/8 步,等我做手势(BGM=bgm/drone,语速1)
+
+#### 【玩家动作】往下滑,进了储物间
+
+t+ 17.0s ♪ 音效 sfx/step
+t+ 17.0s ♪ 音效 sfx/amb_storage
+t+ 17.0s ▶ 语音:「听到水声了吗?单击一下,仔细检查这个房间。」
+t+ 19.1s ♪ 向导音:turn(轮到你)
+t+ 19.1s 【状态】引导第 6/8 步 引导第 6/8 步,等我做手势(BGM=bgm/drone,语速1)
+
+#### 【玩家动作】单击摸一遍房间,摸到钥匙了
+
+t+ 19.1s ♪ 音效 sfx/pickup
+t+ 19.1s ▶ 语音:「你摸到了一把钥匙!」
+t+ 19.1s 〰 震动:pickup(获得)
+t+ 21.2s ▶ 语音:「钥匙能开锁。双击,是把物品用出去的用法。现在,双击试试。」
+t+ 23.3s ♪ 向导音:turn(轮到你)
+t+ 23.3s 【状态】引导第 7/8 步 引导第 7/8 步,等我做手势(BGM=bgm/drone,语速1)
+
+#### 【玩家动作】双击看了看背包
+
+t+ 23.3s ▶ 语音:「你身上带着一把钥匙。」
+t+ 25.4s ▶ 语音:「对,就是这样。接下来,靠你自己了。」
+t+ 27.6s ▶ 语音:「找到能用这把钥匙打开的东西。需要帮助时,在原地双击。」
+t+ 29.7s ♪ 向导音:turn(轮到你)
+t+ 29.7s ♪ 音效 sfx/amb_storage
+t+ 29.7s 【状态】引导完毕,正式开始。我在储物间 探索中,我在 (0,2)。四向滑挪步,单击摸一遍房间,双击用物品(BGM=bgm/drone,语速1)
+t+ 29.7s ……… 静默 3s(辨了辨方向) ………
+t+ 29.7s ♪ 音效 sfx/step
+t+ 29.7s ♪ 音效 sfx/amb_hum
+t+ 35.0s ♪ 音效 sfx/step
+t+ 35.0s ♪ 音效 sfx/amb_power
+t+ 35.0s ▶ 语音:「这个房间的墙上,有一只电箱。」
+t+ 37.1s ♪ 音效 sfx/wall
+t+ 37.1s ▶ 语音:「那边是墙,换个方向。」
+t+ 37.1s 〰 震动:error(做错了:两连短震)
+
+#### 【玩家动作】北边是墙,撞了一下——「那边是墙」后面还跟了句方向(向上推的是北墙)
+
+t+ 39.2s ♪ 音效 sfx/wall
+t+ 39.2s ▶ 语音:「那边是墙,换个方向。」
+t+ 39.2s 〰 震动:error(做错了:两连短震)
+
+#### 【玩家动作】又撞了一下——听到游戏主动来问我了
+
+t+ 50.0s ♪ 音效 sfx/step
+t+ 50.0s ♪ 音效 sfx/amb_wind
+
+#### 【玩家动作】往东走,风声换了个方向
+
+t+ 55.0s ♪ 音效 sfx/step
+t+ 55.0s ♪ 音效 sfx/amb_hum
+t+ 60.0s ♪ 音效 sfx/step
+t+ 60.0s ♪ 音效 sfx/amb_cabinet
+t+ 60.0s ▶ 语音:「这个房间里,立着一个铁柜。」
+
+#### 【玩家动作】到了铁柜房,柜子锁着
+
+t+ 62.1s ♪ 音效 sfx/unlock
+t+ 62.1s ♪ 音效 sfx/door_creak
+t+ 62.1s ▶ 语音:「钥匙插进锁孔,缓缓转动。咔哒一声,柜门开了。柜子深处,躺着一枚保险丝。保险丝到手了。」
+t+ 62.1s 〰 震动:success(成功长震)
+t+ 64.2s ▶ 向导语音:「保险丝到手。接下来,去找电箱。」
+t+ 66.4s ▶ 向导语音:「左面的房间。」
+t+ 68.5s 【状态】柜子开了。「保险丝到手。接下来,去找电箱。」后面还跟了一句「左面的房间。」——这回知道该往哪边摸了 探索中,我在 (2,1)。四向滑挪步,单击摸一遍房间,双击用物品(BGM=bgm/drone,语速1)
+t+ 68.5s ♪ 音效 sfx/step
+t+ 68.5s ♪ 音效 sfx/amb_drip
+
+#### 【玩家动作】照方位句往左(西)摸
+
+t+ 75.0s ♪ 音效 sfx/step
+t+ 75.0s ♪ 音效 sfx/amb_hum
+t+ 80.0s ♪ 音效 sfx/step
+t+ 80.0s ♪ 音效 sfx/amb_power
+
+#### 【玩家动作】电流声越来越清晰——电箱房到了
+
+t+ 85.0s ♪ 音效 sfx/spark
+t+ 85.0s ♪ 音效 sfx/power_on
+t+ 85.0s ▶ 语音:「你把保险丝用力按进卡座。火花一闪,嗡鸣声响起——头顶的线路恢复供电了!」
+t+ 85.0s 〰 震动:success(成功长震)
+t+ 87.1s ▶ 向导语音:「右面的房间。」
+t+ 95.0s 【状态】电箱修好了。目标句后面照样跟着方位——「右面的房间。」大门在东边 探索中,我在 (0,0)。四向滑挪步,单击摸一遍房间,双击用物品(BGM=bgm/drone,语速1)
+t+ 95.0s ♪ 音效 sfx/step
+t+ 95.0s ♪ 音效 sfx/amb_wind
+
+#### 【玩家动作】照方位句往右(东)摸
+
+t+ 100.0s ♪ 音效 sfx/step
+t+ 100.0s ♪ 音效 sfx/amb_hum
+t+ 105.0s ♪ 音效 sfx/step
+t+ 105.0s ♪ 音效 sfx/amb_cabinet
+t+ 110.0s ♪ 音效 sfx/step
+t+ 110.0s ♪ 音效 sfx/amb_gate_live
+t+ 110.0s ▶ 语音:「面前,是一扇沉重的铁门。」
+
+#### 【玩家动作】脚步声停下来——面前这间就是大门
+
+t+ 112.1s ♪ 向导音:settle(结算)
+t+ 112.1s ▶ 语音:「你扳动开关。电机轰鸣,沉重的铁门缓缓升起,刺眼的光线涌了进来。你眯起眼睛,向前迈出了第一步。自由了。你逃出来了。」
+t+ 112.1s 〰 震动:success(成功长震)
+t+ 114.2s ▶ 语音:「你逃出来了,用时不到两分钟。了不起!」
+t+ 114.7s ♪ 音效 sfx/gate_open
+t+ 114.7s ♪ 音效 sfx/win_chime
+t+ 116.4s ▶ 语音:「单击屏幕,再来一次。下滑,打开菜单。」
+t+ 118.5s ♫ BGM 切换 → bgm/menu(淡入 1500ms)
+t+ 118.5s 【状态】钥匙插进锁孔,门开了——结算播报来了,这局是完整的赢 逃脱了,结算播报中。播完单击再来,下滑开菜单(BGM=bgm/menu,语速1)
diff --git a/research/escape-state/round2/README.md b/research/escape-state/round2/README.md
new file mode 100644
index 00000000..8569cfdc
--- /dev/null
+++ b/research/escape-state/round2/README.md
@@ -0,0 +1,44 @@
+# escape-state round2 · 状态引导改进方案(25 号文档 B1-B8)修复验证档案
+
+> 轮次:escape-state round2(2026-09-14/15)。对象:25 号文档 §4.3 实施清单
+> F1-F9(F6/B5b 按方案"可分期"留档,见 25 号文档实施裁定 1)。
+> 档案格式对齐 round1:mock 转写 + live 真机一局 + 事件/判定原件。
+> round1 的 P1/P3 人设评审不重做(方案条目即由双席评审产出,本轮验证落地效果)。
+
+## 产物
+
+| 文件 | 内容 |
+|---|---|
+| `transcript-mock.md` | mock 转写(`transcribe.mjs escape`,sessions/escape.mjs 已按新行为微调):新手完整一局,覆盖 B1 撞墙方位句+限频、B5a 空房首访、B2 一体句 ×2、结算链完整到达 |
+| `live-transcript.md` / `live-events.jsonl` / `verdicts.json` / `mix.webm` | live 真机一局(control.mjs 端口 8145,女声响 full 档,约 10.4 分钟,引导→探索→通关→重开),认真新手人设 |
+| `README.md` | 本文件(盲区清单 S1-S7 逐条核对) |
+
+## live 一局总览
+
+- 38 个语音段,56 条判定行:**OK 37 / SFX_BGM_ONLY 19 / TRUNCATED 0 / MISMATCH 0**
+ (round1 为 OK 35 / SFX_BGM_ONLY 18 / TRUNCATED 2——两条截断均有手势归因;
+ 本轮零截断,含 B7 守卫挡下的那一次习惯性单击)。
+- 新语音 10 句在真实混音中全部到达且 ASR sim≥0.93:wall_dir_u/l、empty_2/1/3(首访)、
+ stuck_cabinet_1(B4 降级)、gd_box_dir/gd_gate_dir(B2 一体句)、gd_prog_2(B3)。
+
+## 盲区清单 S1-S7 逐条核对(25 号文档 §3.2)
+
+| 盲区 | 修复 | live 证据(行号 = live-transcript.md) | 结论 |
+|---|---|---|---|
+| S1/S1b 方位(撞墙无方位) | B1 wall_dir_* | 行 141-143:wall 后接播「上边是墙。」OK sim=1;行 166-168:「左边是墙。」OK;同向 10s 限频生效(199.5s 第二次撞北墙只有 wall 无 wall_dir_u) | ✅ 每次撞墙建立"此方向不通"地图标记 |
+| 相对方位半盲区(无目标名) | B2 gd_*_dir 一体句 | 行 231-233:「电箱,在左面的房间。」OK sim=1;行 306-312:「大门,在右面的房间。」OK sim=1 | ✅ 单句即可行动,不再是"……的房间" |
+| S2 已探索地图 | B5a 空房首访描述 | 行 115-118 (0,1) empty_2、行 189-192 (1,0) empty_1、行 199-202 (2,0) empty_3,均首访一次性 OK | ✅ 空房进入即有听觉锚 |
+| S3 全局进度无渠道 | B3 gd_prog_* | 行 265-268:开柜后活跃探路 ~154s(10 次手势零进展、避开 (2,2) 防进展重置)→ gd_prog_2「铁柜已经打开。电还没有通。」OK sim=1;期间 L1/L2/L3 零误触 | ✅ 活跃迷路通道补洞,不泄题 |
+| S4/S4b 目标重述/查询泄漏 | B4 + B8 | B4:行 180-182 双击兜底播 use_fail + stuck_cabinet_1(L1 意象,OK 0.93),**不再直通 stuck_*_2**;B8:g7_goal 在引导链尾 OK sim=1(跳过/REOPEN 路径补播由 playtest A2/B/N 断言覆盖) | ✅ 主动查询不再白送答案 |
+| S5 空房不可区分 | B5a(B5b 留档) | 三间首访空房文本各异(回声/墙壁/水滴),live 中与环境音叠加可分辨 | ✅(B5b 方位特征改词留后续轮) |
+| S6 busy 吞手势 | B6 缓存补判 | 行 79-87(live-events.jsonl):82.3s swipeleft 落在 g2_wall busy 窗口(82.2-87.9)只有 [10] 震——87.9s 解锁瞬间补判:error 震 + wall 语音 + G2 计数 1(对照 round1 行 30-35 同场景静默吞手势) | ✅ "我的输入没丢",可归因 |
+| S7 结算链截断 | B7 _settleBusy | 行 336-347:587.46s 习惯性单击落在 escape 播到 4.4/15.3s——只发引擎 confirm 震,链继续;time_band3(行 346)、again 完整到达 OK;链播完后单击正常重开(replay 起播) | ✅ 结算完整到达权 + 重开闭环 |
+
+## 附带观察
+
+- escape 长句 live 实测 15.32s(dur 探针值),结算链全长 ~24s(escape+time_band3+again),
+ B7 守卫窗口即此区间;30s 资产缺失兜底由 playtest 场景 M 第二条路径冻结。
+- time_band3「用时不到十分钟」与本局 ~9.7 分钟一致(归因真话)。
+- female/wall_dir_r 为 3.04s 慢速 take(同句 male 1.04s,ratio 2.92 未触 3x 离群线),
+ QC:ASR 双性别内容均「右边是墙。」逐字正确,慢速贴合"沉稳平静"instruct,留档备查;
+ 本局 female 档未触发该句(撞的是北/西墙),male 档或未限频路径如有听感问题再重录。
diff --git a/research/escape-state/round2/live-events.jsonl b/research/escape-state/round2/live-events.jsonl
new file mode 100644
index 00000000..bc5b01fc
--- /dev/null
+++ b/research/escape-state/round2/live-events.jsonl
@@ -0,0 +1,289 @@
+{"ev":"probe_ready","t":61.10000002384186,"ta":null}
+{"ev":"gesture","type":"tap","x":195,"y":422,"t":2065.4000000059605,"ta":-5.104}
+{"ev":"recstart","mime":"audio/webm;codecs=opus","t":7169.100000023842,"ta":0}
+{"ev":"fetch","url":"audio/sfx/step.ogg","status":200,"ok":true,"ms":15,"t":7184.100000023842,"ta":0.015}
+{"ev":"fetch","url":"audio/sfx/wall.ogg","status":200,"ok":true,"ms":15,"t":7184.200000017881,"ta":0.015}
+{"ev":"fetch","url":"audio/sfx/pickup.ogg","status":200,"ok":true,"ms":15,"t":7184.300000011921,"ta":0.015}
+{"ev":"fetch","url":"audio/sfx/unlock.ogg","status":200,"ok":true,"ms":15,"t":7184.300000011921,"ta":0.015}
+{"ev":"fetch","url":"audio/sfx/door_creak.ogg","status":200,"ok":true,"ms":15,"t":7184.300000011921,"ta":0.015}
+{"ev":"fetch","url":"audio/sfx/spark.ogg","status":200,"ok":true,"ms":14,"t":7184.300000011921,"ta":0.015}
+{"ev":"fetch","url":"audio/sfx/power_on.ogg","status":200,"ok":true,"ms":14,"t":7184.4000000059605,"ta":0.015}
+{"ev":"fetch","url":"audio/sfx/gate_open.ogg","status":200,"ok":true,"ms":16,"t":7186.100000023842,"ta":0.017}
+{"ev":"fetch","url":"audio/sfx/win_chime.ogg","status":200,"ok":true,"ms":16,"t":7186.300000011921,"ta":0.017}
+{"ev":"fetch","url":"audio/sfx/amb_storage.ogg","status":200,"ok":true,"ms":18,"t":7187.600000023842,"ta":0.019}
+{"ev":"fetch","url":"audio/sfx/amb_cabinet.ogg","status":200,"ok":true,"ms":18,"t":7188.5,"ta":0.019}
+{"ev":"fetch","url":"audio/sfx/amb_power.ogg","status":200,"ok":true,"ms":19,"t":7189.4000000059605,"ta":0.02}
+{"ev":"fetch","url":"audio/sfx/amb_gate.ogg","status":200,"ok":true,"ms":25,"t":7195.700000017881,"ta":0.027}
+{"ev":"fetch","url":"audio/sfx/amb_gate_live.ogg","status":200,"ok":true,"ms":25,"t":7195.700000017881,"ta":0.027}
+{"ev":"fetch","url":"audio/sfx/amb_drip.ogg","status":200,"ok":true,"ms":27,"t":7196.800000011921,"ta":0.028}
+{"ev":"fetch","url":"audio/sfx/amb_wind.ogg","status":200,"ok":true,"ms":27,"t":7197.600000023842,"ta":0.029}
+{"ev":"fetch","url":"audio/sfx/amb_hum.ogg","status":200,"ok":true,"ms":28,"t":7198.700000017881,"ta":0.03}
+{"ev":"fetch","url":"audio/voice/female/g0_intro.ogg","status":200,"ok":true,"ms":30,"t":7200.100000023842,"ta":0.031}
+{"ev":"fetch","url":"audio/voice/female/g1_move.ogg","status":200,"ok":true,"ms":34,"t":7204.5,"ta":0.035}
+{"ev":"fetch","url":"audio/voice/female/skip_hint.ogg","status":200,"ok":true,"ms":36,"t":7206.600000023842,"ta":0.038}
+{"ev":"fetch","url":"audio/voice/female/reopen_prompt.ogg","status":200,"ok":true,"ms":38,"t":7208.4000000059605,"ta":0.039}
+{"ev":"fetch","url":"audio/voice/female/start_room.ogg","status":200,"ok":true,"ms":38,"t":7209,"ta":0.04}
+{"ev":"fetch","url":"audio/bgm/menu.ogg","status":200,"ok":true,"ms":38,"t":7209.100000023842,"ta":0.04}
+{"ev":"fetch","url":"audio/bgm/drone.ogg","status":200,"ok":true,"ms":39,"t":7209.800000011921,"ta":0.041}
+{"ev":"vstart","url":"audio/voice/female/g0_intro.ogg","t":7245.700000017881,"dur":6.64,"ta":0.077}
+{"ev":"start","t":7439.800000011921,"sid":1,"url":"audio/bgm/drone.ogg","layer":"bgm","dur":117,"rate":1,"loop":true,"when":0,"ta":0.271}
+{"ev":"vend","url":"audio/voice/female/g0_intro.ogg","t":14100.40000000596,"reason":"kill","ta":6.931}
+{"ev":"vstart","url":"audio/voice/female/g1_move.ogg","t":15300.90000000596,"dur":4.56,"ta":8.132}
+{"ev":"vend","url":"audio/voice/female/g1_move.ogg","t":19910.20000001788,"reason":"kill","ta":12.741}
+{"ev":"vstart","url":"audio/voice/female/skip_hint.ogg","t":19910.30000001192,"dur":1.6,"ta":12.741}
+{"ev":"vend","url":"audio/voice/female/skip_hint.ogg","t":21530.100000023842,"reason":"kill","ta":14.361}
+{"ev":"gesture","type":"swipeup","x":195,"y":422,"t":86221.70000001788,"ta":79.053}
+{"ev":"vibrate","pattern":"[10]","t":86578.60000002384,"ta":79.41}
+{"ev":"start","t":86579.60000002384,"sid":2,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":79.411}
+{"ev":"fetch","url":"audio/voice/female/g1_done.ogg","status":200,"ok":true,"ms":3,"t":86582,"ta":79.413}
+{"ev":"vstart","url":"audio/voice/female/g1_done.ogg","t":86586.30000001192,"dur":2.72,"ta":79.417}
+{"ev":"vend","url":"audio/voice/female/g1_done.ogg","t":89320.20000001788,"reason":"kill","ta":82.151}
+{"ev":"fetch","url":"audio/voice/female/g2_wall.ogg","status":200,"ok":true,"ms":3,"t":89322.80000001192,"ta":82.154}
+{"ev":"vstart","url":"audio/voice/female/g2_wall.ogg","t":89328.30000001192,"dur":5.68,"ta":82.159}
+{"ev":"gesture","type":"swipeleft","x":195,"y":422,"t":89490,"ta":82.321}
+{"ev":"vibrate","pattern":"[10]","t":89844.70000001788,"ta":82.676}
+{"ev":"vend","url":"audio/voice/female/g2_wall.ogg","t":95050.10000002384,"reason":"kill","ta":87.881}
+{"ev":"vibrate","pattern":"[60,40,60]","t":95050.30000001192,"ta":87.881}
+{"ev":"start","t":95050.70000001788,"sid":3,"url":"audio/sfx/wall.ogg","layer":"sfx","dur":0.44709750566893425,"rate":1,"loop":false,"when":0,"ta":87.882}
+{"ev":"fetch","url":"audio/voice/female/wall.ogg","status":200,"ok":true,"ms":2,"t":95052.70000001788,"ta":87.884}
+{"ev":"vstart","url":"audio/voice/female/wall.ogg","t":95056.40000000596,"dur":2.08,"ta":87.887}
+{"ev":"vend","url":"audio/voice/female/wall.ogg","t":97410.10000002384,"reason":"kill","ta":90.241}
+{"ev":"gesture","type":"swiperight","x":195,"y":422,"t":116300.20000001788,"ta":109.131}
+{"ev":"vibrate","pattern":"[10]","t":116660.10000002384,"ta":109.491}
+{"ev":"vibrate","pattern":"[60,40,60]","t":116660.20000001788,"ta":109.491}
+{"ev":"start","t":116660.40000000596,"sid":4,"url":"audio/sfx/wall.ogg","layer":"sfx","dur":0.44709750566893425,"rate":1,"loop":false,"when":0,"ta":109.491}
+{"ev":"vstart","url":"audio/voice/female/wall.ogg","t":116660.5,"dur":2.08,"ta":109.491}
+{"ev":"vend","url":"audio/voice/female/wall.ogg","t":119030.30000001192,"reason":"kill","ta":111.861}
+{"ev":"fetch","url":"audio/voice/female/g3_left.ogg","status":200,"ok":true,"ms":2,"t":119032.80000001192,"ta":111.864}
+{"ev":"vstart","url":"audio/voice/female/g3_left.ogg","t":119037.10000002384,"dur":2.96,"ta":111.868}
+{"ev":"vend","url":"audio/voice/female/g3_left.ogg","t":122050.10000002384,"reason":"kill","ta":114.881}
+{"ev":"gesture","type":"swipeleft","x":195,"y":422,"t":124070.30000001192,"ta":116.901}
+{"ev":"vibrate","pattern":"[10]","t":124426.70000001788,"ta":117.258}
+{"ev":"start","t":124427.20000001788,"sid":5,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":117.258}
+{"ev":"fetch","url":"audio/voice/female/g4_down.ogg","status":200,"ok":true,"ms":2,"t":124429.20000001788,"ta":117.26}
+{"ev":"vstart","url":"audio/voice/female/g4_down.ogg","t":124432.80000001192,"dur":1.84,"ta":117.264}
+{"ev":"vend","url":"audio/voice/female/g4_down.ogg","t":126340.20000001788,"reason":"kill","ta":119.171}
+{"ev":"gesture","type":"swipedown","x":195,"y":422,"t":129835.90000000596,"ta":122.667}
+{"ev":"vibrate","pattern":"[10]","t":130192.90000000596,"ta":123.024}
+{"ev":"start","t":130193.30000001192,"sid":6,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":123.024}
+{"ev":"start","t":130193.40000000596,"sid":7,"url":"audio/sfx/amb_storage.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":123.024}
+{"ev":"fetch","url":"audio/voice/female/g5_inspect.ogg","status":200,"ok":true,"ms":2,"t":130195.5,"ta":123.026}
+{"ev":"vstart","url":"audio/voice/female/g5_inspect.ogg","t":130200.90000000596,"dur":3.92,"ta":123.032}
+{"ev":"vend","url":"audio/voice/female/g5_inspect.ogg","t":134340.10000002384,"reason":"kill","ta":127.171}
+{"ev":"gesture","type":"tap","x":195,"y":422,"t":137604.80000001192,"ta":130.436}
+{"ev":"vibrate","pattern":"[20]","t":137970.20000001788,"ta":130.801}
+{"ev":"vibrate","pattern":"[15]","t":137970.40000000596,"ta":130.801}
+{"ev":"start","t":137970.70000001788,"sid":8,"url":"audio/sfx/pickup.ogg","layer":"sfx","dur":0.3470748299319728,"rate":1,"loop":false,"when":0,"ta":130.802}
+{"ev":"fetch","url":"audio/voice/female/g5_key.ogg","status":200,"ok":true,"ms":3,"t":137973.10000002384,"ta":130.804}
+{"ev":"vstart","url":"audio/voice/female/g5_key.ogg","t":137976.80000001192,"dur":1.52,"ta":130.808}
+{"ev":"vend","url":"audio/voice/female/g5_key.ogg","t":139590.20000001788,"reason":"kill","ta":132.421}
+{"ev":"fetch","url":"audio/voice/female/g6_doubletap.ogg","status":200,"ok":true,"ms":2,"t":139592.60000002384,"ta":132.424}
+{"ev":"vstart","url":"audio/voice/female/g6_doubletap.ogg","t":139602.40000000596,"dur":10.64,"ta":132.433}
+{"ev":"gesture","type":"doubletap","x":195,"y":422,"t":148080.20000001788,"ta":140.911}
+{"ev":"vibrate","pattern":"[20]","t":148329.60000002384,"ta":141.161}
+{"ev":"vend","url":"audio/voice/female/g6_doubletap.ogg","t":150560.10000002384,"reason":"kill","ta":143.391}
+{"ev":"fetch","url":"audio/voice/female/inv_key.ogg","status":200,"ok":true,"ms":3,"t":150563,"ta":143.394}
+{"ev":"vstart","url":"audio/voice/female/inv_key.ogg","t":150566.70000001788,"dur":1.68,"ta":143.398}
+{"ev":"vend","url":"audio/voice/female/inv_key.ogg","t":152460.10000002384,"reason":"kill","ta":145.291}
+{"ev":"fetch","url":"audio/voice/female/g6_done.ogg","status":200,"ok":true,"ms":3,"t":152462.70000001788,"ta":145.294}
+{"ev":"vstart","url":"audio/voice/female/g6_done.ogg","t":152468,"dur":3.92,"ta":145.299}
+{"ev":"vend","url":"audio/voice/female/g6_done.ogg","t":156620.20000001788,"reason":"kill","ta":149.451}
+{"ev":"fetch","url":"audio/voice/female/g7_goal.ogg","status":200,"ok":true,"ms":3,"t":156622.80000001192,"ta":149.454}
+{"ev":"vstart","url":"audio/voice/female/g7_goal.ogg","t":156629.40000000596,"dur":6.88,"ta":149.46}
+{"ev":"vend","url":"audio/voice/female/g7_goal.ogg","t":163760.30000001192,"reason":"kill","ta":156.591}
+{"ev":"start","t":163760.70000001788,"sid":9,"url":"audio/sfx/amb_storage.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":156.592}
+{"ev":"gesture","type":"swipeup","x":195,"y":422,"t":185967.10000002384,"ta":178.798}
+{"ev":"vibrate","pattern":"[10]","t":186324.30000001192,"ta":179.155}
+{"ev":"start","t":186324.90000000596,"sid":10,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":179.156}
+{"ev":"start","t":186324.90000000596,"sid":11,"url":"audio/sfx/amb_hum.ogg","layer":"sfx","dur":5,"rate":1,"loop":false,"when":0,"ta":179.156}
+{"ev":"fetch","url":"audio/voice/female/empty_2.ogg","status":200,"ok":true,"ms":3,"t":186327.30000001192,"ta":179.158}
+{"ev":"vstart","url":"audio/voice/female/empty_2.ogg","t":186333.30000001192,"dur":4.88,"ta":179.164}
+{"ev":"vend","url":"audio/voice/female/empty_2.ogg","t":191320.30000001192,"reason":"kill","ta":184.151}
+{"ev":"gesture","type":"swipeup","x":195,"y":422,"t":192735.70000001788,"ta":185.567}
+{"ev":"vibrate","pattern":"[10]","t":193090.80000001192,"ta":185.922}
+{"ev":"start","t":193091.20000001788,"sid":12,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":185.922}
+{"ev":"start","t":193091.20000001788,"sid":13,"url":"audio/sfx/amb_power.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":185.922}
+{"ev":"fetch","url":"audio/voice/female/room_power.ogg","status":200,"ok":true,"ms":3,"t":193093.70000001788,"ta":185.925}
+{"ev":"vstart","url":"audio/voice/female/room_power.ogg","t":193097.70000001788,"dur":2.64,"ta":185.929}
+{"ev":"vend","url":"audio/voice/female/room_power.ogg","t":195840,"reason":"kill","ta":188.671}
+{"ev":"gesture","type":"swipeup","x":195,"y":422,"t":199501.40000000596,"ta":192.332}
+{"ev":"vibrate","pattern":"[10]","t":199856.80000001192,"ta":192.688}
+{"ev":"vibrate","pattern":"[60,40,60]","t":199856.90000000596,"ta":192.688}
+{"ev":"start","t":199857,"sid":14,"url":"audio/sfx/wall.ogg","layer":"sfx","dur":0.44709750566893425,"rate":1,"loop":false,"when":0,"ta":192.688}
+{"ev":"vstart","url":"audio/voice/female/wall.ogg","t":199857,"dur":2.08,"ta":192.688}
+{"ev":"vend","url":"audio/voice/female/wall.ogg","t":202220.20000001788,"reason":"kill","ta":195.051}
+{"ev":"fetch","url":"audio/voice/female/wall_dir_u.ogg","status":200,"ok":true,"ms":3,"t":202223.60000002384,"ta":195.055}
+{"ev":"vstart","url":"audio/voice/female/wall_dir_u.ogg","t":202227.10000002384,"dur":1.36,"ta":195.058}
+{"ev":"vend","url":"audio/voice/female/wall_dir_u.ogg","t":203840.10000002384,"reason":"kill","ta":196.671}
+{"ev":"gesture","type":"swipeup","x":195,"y":422,"t":206268,"ta":199.099}
+{"ev":"vibrate","pattern":"[10]","t":206623.40000000596,"ta":199.454}
+{"ev":"vibrate","pattern":"[60,40,60]","t":206623.70000001788,"ta":199.455}
+{"ev":"start","t":206623.80000001192,"sid":15,"url":"audio/sfx/wall.ogg","layer":"sfx","dur":0.44709750566893425,"rate":1,"loop":false,"when":0,"ta":199.455}
+{"ev":"vstart","url":"audio/voice/female/wall.ogg","t":206623.90000000596,"dur":2.08,"ta":199.455}
+{"ev":"vend","url":"audio/voice/female/wall.ogg","t":208990.20000001788,"reason":"kill","ta":201.821}
+{"ev":"gesture","type":"swipeleft","x":195,"y":422,"t":211033.30000001192,"ta":203.864}
+{"ev":"vibrate","pattern":"[10]","t":211390,"ta":204.221}
+{"ev":"vibrate","pattern":"[60,40,60]","t":211390.10000002384,"ta":204.221}
+{"ev":"start","t":211390.30000001192,"sid":16,"url":"audio/sfx/wall.ogg","layer":"sfx","dur":0.44709750566893425,"rate":1,"loop":false,"when":0,"ta":204.221}
+{"ev":"vstart","url":"audio/voice/female/wall.ogg","t":211390.30000001192,"dur":2.08,"ta":204.221}
+{"ev":"vend","url":"audio/voice/female/wall.ogg","t":213760.10000002384,"reason":"kill","ta":206.591}
+{"ev":"fetch","url":"audio/voice/female/wall_dir_l.ogg","status":200,"ok":true,"ms":3,"t":213762.70000001788,"ta":206.594}
+{"ev":"vstart","url":"audio/voice/female/wall_dir_l.ogg","t":213766.10000002384,"dur":1.04,"ta":206.597}
+{"ev":"vend","url":"audio/voice/female/wall_dir_l.ogg","t":214920.10000002384,"reason":"kill","ta":207.751}
+{"ev":"gesture","type":"doubletap","x":195,"y":422,"t":217799.90000000596,"ta":210.631}
+{"ev":"vibrate","pattern":"[20]","t":218046.80000001192,"ta":210.878}
+{"ev":"vibrate","pattern":"[60,40,60]","t":218047.20000001788,"ta":210.878}
+{"ev":"fetch","url":"audio/voice/female/use_fail.ogg","status":200,"ok":true,"ms":3,"t":218049.80000001192,"ta":210.881}
+{"ev":"vstart","url":"audio/voice/female/use_fail.ogg","t":218057.40000000596,"dur":7.36,"ta":210.888}
+{"ev":"vend","url":"audio/voice/female/use_fail.ogg","t":225650.10000002384,"reason":"kill","ta":218.481}
+{"ev":"fetch","url":"audio/voice/female/stuck_cabinet_1.ogg","status":200,"ok":true,"ms":3,"t":225653.20000001788,"ta":218.484}
+{"ev":"vstart","url":"audio/voice/female/stuck_cabinet_1.ogg","t":225659,"dur":3.84,"ta":218.49}
+{"ev":"vend","url":"audio/voice/female/stuck_cabinet_1.ogg","t":229790.20000001788,"reason":"kill","ta":222.621}
+{"ev":"gesture","type":"swiperight","x":195,"y":422,"t":263046.3000000119,"ta":255.877}
+{"ev":"vibrate","pattern":"[10]","t":263404.2000000179,"ta":256.235}
+{"ev":"start","t":263404.60000002384,"sid":17,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":256.236}
+{"ev":"start","t":263404.60000002384,"sid":18,"url":"audio/sfx/amb_wind.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":256.236}
+{"ev":"fetch","url":"audio/voice/female/empty_1.ogg","status":200,"ok":true,"ms":3,"t":263407.2000000179,"ta":256.238}
+{"ev":"vstart","url":"audio/voice/female/empty_1.ogg","t":263413.60000002384,"dur":5.2,"ta":256.245}
+{"ev":"vend","url":"audio/voice/female/empty_1.ogg","t":268670.10000002384,"reason":"kill","ta":261.501}
+{"ev":"gesture","type":"swiperight","x":195,"y":422,"t":269814.3000000119,"ta":262.645}
+{"ev":"vibrate","pattern":"[10]","t":270170.5,"ta":263.001}
+{"ev":"start","t":270171,"sid":19,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":263.002}
+{"ev":"start","t":270171,"sid":20,"url":"audio/sfx/amb_hum.ogg","layer":"sfx","dur":5,"rate":1,"loop":false,"when":0,"ta":263.002}
+{"ev":"fetch","url":"audio/voice/female/empty_3.ogg","status":200,"ok":true,"ms":3,"t":270173.3000000119,"ta":263.004}
+{"ev":"vstart","url":"audio/voice/female/empty_3.ogg","t":270178.60000002384,"dur":3.68,"ta":263.01}
+{"ev":"vend","url":"audio/voice/female/empty_3.ogg","t":274030.2000000179,"reason":"kill","ta":266.861}
+{"ev":"gesture","type":"swipedown","x":195,"y":422,"t":276579.5,"ta":269.41}
+{"ev":"vibrate","pattern":"[10]","t":276937.5,"ta":269.768}
+{"ev":"start","t":276937.90000000596,"sid":21,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":269.769}
+{"ev":"start","t":276937.90000000596,"sid":22,"url":"audio/sfx/amb_cabinet.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":269.769}
+{"ev":"fetch","url":"audio/voice/female/room_cabinet.ogg","status":200,"ok":true,"ms":3,"t":276940.40000000596,"ta":269.771}
+{"ev":"vstart","url":"audio/voice/female/room_cabinet.ogg","t":276944.7000000179,"dur":2.64,"ta":269.776}
+{"ev":"vend","url":"audio/voice/female/room_cabinet.ogg","t":279690.10000002384,"reason":"kill","ta":272.521}
+{"ev":"gesture","type":"doubletap","x":195,"y":422,"t":283348,"ta":276.179}
+{"ev":"vibrate","pattern":"[20]","t":283593.90000000596,"ta":276.425}
+{"ev":"vibrate","pattern":"[100]","t":283594.10000002384,"ta":276.425}
+{"ev":"start","t":283594.3000000119,"sid":23,"url":"audio/sfx/unlock.ogg","layer":"sfx","dur":0.49777777777777776,"rate":1,"loop":false,"when":0,"ta":276.425}
+{"ev":"start","t":283594.3000000119,"sid":24,"url":"audio/sfx/door_creak.ogg","layer":"sfx","dur":1.3999773242630384,"rate":1,"loop":false,"when":0,"ta":276.425}
+{"ev":"fetch","url":"audio/voice/female/unlock_cabinet.ogg","status":200,"ok":true,"ms":2,"t":283596.60000002384,"ta":276.428}
+{"ev":"vstart","url":"audio/voice/female/unlock_cabinet.ogg","t":283607,"dur":11.48,"ta":276.438}
+{"ev":"vend","url":"audio/voice/female/unlock_cabinet.ogg","t":295320.10000002384,"reason":"kill","ta":288.151}
+{"ev":"fetch","url":"audio/voice/female/gd_next_fuse.ogg","status":200,"ok":true,"ms":3,"t":295323.10000002384,"ta":288.154}
+{"ev":"vstart","url":"audio/voice/female/gd_next_fuse.ogg","t":295328.10000002384,"dur":3.28,"ta":288.159}
+{"ev":"vend","url":"audio/voice/female/gd_next_fuse.ogg","t":298800.40000000596,"reason":"kill","ta":291.631}
+{"ev":"fetch","url":"audio/voice/female/gd_box_dir.ogg","status":200,"ok":true,"ms":3,"t":298803.5,"ta":291.634}
+{"ev":"vstart","url":"audio/voice/female/gd_box_dir.ogg","t":298807.3000000119,"dur":2.16,"ta":291.638}
+{"ev":"vend","url":"audio/voice/female/gd_box_dir.ogg","t":301180.10000002384,"reason":"kill","ta":294.011}
+{"ev":"gesture","type":"swipeleft","x":195,"y":422,"t":317593.90000000596,"ta":310.425}
+{"ev":"vibrate","pattern":"[10]","t":317952.10000002384,"ta":310.783}
+{"ev":"start","t":317952.3000000119,"sid":25,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":310.783}
+{"ev":"start","t":317952.3000000119,"sid":26,"url":"audio/sfx/amb_drip.ogg","layer":"sfx","dur":5,"rate":1,"loop":false,"when":0,"ta":310.783}
+{"ev":"gesture","type":"swipeleft","x":195,"y":422,"t":334362.90000000596,"ta":327.194}
+{"ev":"vibrate","pattern":"[10]","t":334718.3000000119,"ta":327.549}
+{"ev":"start","t":334718.60000002384,"sid":27,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":327.55}
+{"ev":"start","t":334718.7000000179,"sid":28,"url":"audio/sfx/amb_hum.ogg","layer":"sfx","dur":5,"rate":1,"loop":false,"when":0,"ta":327.55}
+{"ev":"gesture","type":"swiperight","x":195,"y":422,"t":351128.5,"ta":343.959}
+{"ev":"vibrate","pattern":"[10]","t":351484.2000000179,"ta":344.315}
+{"ev":"start","t":351484.40000000596,"sid":29,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":344.315}
+{"ev":"start","t":351484.5,"sid":30,"url":"audio/sfx/amb_drip.ogg","layer":"sfx","dur":5,"rate":1,"loop":false,"when":0,"ta":344.315}
+{"ev":"gesture","type":"swiperight","x":195,"y":422,"t":367894.40000000596,"ta":360.725}
+{"ev":"vibrate","pattern":"[10]","t":368250.40000000596,"ta":361.081}
+{"ev":"start","t":368250.7000000179,"sid":31,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":361.082}
+{"ev":"start","t":368250.7000000179,"sid":32,"url":"audio/sfx/amb_cabinet.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":361.082}
+{"ev":"gesture","type":"swipeup","x":195,"y":422,"t":384660.10000002384,"ta":377.491}
+{"ev":"vibrate","pattern":"[10]","t":385016,"ta":377.847}
+{"ev":"start","t":385016.2000000179,"sid":33,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":377.847}
+{"ev":"start","t":385016.2000000179,"sid":34,"url":"audio/sfx/amb_hum.ogg","layer":"sfx","dur":5,"rate":1,"loop":false,"when":0,"ta":377.847}
+{"ev":"gesture","type":"swipedown","x":195,"y":422,"t":416051.60000002384,"ta":408.883}
+{"ev":"vibrate","pattern":"[10]","t":416414.8000000119,"ta":409.246}
+{"ev":"start","t":416415.10000002384,"sid":35,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":409.246}
+{"ev":"start","t":416415.10000002384,"sid":36,"url":"audio/sfx/amb_cabinet.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":409.246}
+{"ev":"gesture","type":"swipeleft","x":195,"y":422,"t":432824.8000000119,"ta":425.656}
+{"ev":"vibrate","pattern":"[10]","t":433180.7000000179,"ta":426.012}
+{"ev":"start","t":433181,"sid":37,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":426.012}
+{"ev":"start","t":433181,"sid":38,"url":"audio/sfx/amb_drip.ogg","layer":"sfx","dur":5,"rate":1,"loop":false,"when":0,"ta":426.012}
+{"ev":"vibrate","pattern":"[30,50,30,50,30]","t":437170.90000000596,"ta":430.002}
+{"ev":"fetch","url":"audio/voice/female/gd_prog_2.ogg","status":200,"ok":true,"ms":3,"t":437173.90000000596,"ta":430.005}
+{"ev":"vstart","url":"audio/voice/female/gd_prog_2.ogg","t":437178.10000002384,"dur":2.56,"ta":430.009}
+{"ev":"vend","url":"audio/voice/female/gd_prog_2.ogg","t":439920.2000000179,"reason":"kill","ta":432.751}
+{"ev":"gesture","type":"swipeleft","x":195,"y":422,"t":449589.90000000596,"ta":442.421}
+{"ev":"vibrate","pattern":"[10]","t":449947,"ta":442.778}
+{"ev":"start","t":449947.3000000119,"sid":39,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":442.778}
+{"ev":"start","t":449947.40000000596,"sid":40,"url":"audio/sfx/amb_hum.ogg","layer":"sfx","dur":5,"rate":1,"loop":false,"when":0,"ta":442.778}
+{"ev":"gesture","type":"swiperight","x":195,"y":422,"t":466357.10000002384,"ta":459.188}
+{"ev":"vibrate","pattern":"[10]","t":466713.2000000179,"ta":459.544}
+{"ev":"start","t":466713.3000000119,"sid":41,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":459.544}
+{"ev":"start","t":466713.5,"sid":42,"url":"audio/sfx/amb_drip.ogg","layer":"sfx","dur":5,"rate":1,"loop":false,"when":0,"ta":459.544}
+{"ev":"gesture","type":"swiperight","x":195,"y":422,"t":483123.3000000119,"ta":475.954}
+{"ev":"vibrate","pattern":"[10]","t":483478.8000000119,"ta":476.31}
+{"ev":"start","t":483479.10000002384,"sid":43,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":476.31}
+{"ev":"start","t":483479.10000002384,"sid":44,"url":"audio/sfx/amb_cabinet.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":476.31}
+{"ev":"gesture","type":"swipeleft","x":195,"y":422,"t":534004.3000000119,"ta":526.835}
+{"ev":"vibrate","pattern":"[10]","t":534360.1000000238,"ta":527.191}
+{"ev":"start","t":534360.3000000119,"sid":45,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":527.191}
+{"ev":"start","t":534360.400000006,"sid":46,"url":"audio/sfx/amb_drip.ogg","layer":"sfx","dur":5,"rate":1,"loop":false,"when":0,"ta":527.191}
+{"ev":"gesture","type":"swipeleft","x":195,"y":422,"t":538768.8000000119,"ta":531.6}
+{"ev":"vibrate","pattern":"[10]","t":539126.8000000119,"ta":531.958}
+{"ev":"start","t":539127.1000000238,"sid":47,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":531.958}
+{"ev":"start","t":539127.1000000238,"sid":48,"url":"audio/sfx/amb_hum.ogg","layer":"sfx","dur":5,"rate":1,"loop":false,"when":0,"ta":531.958}
+{"ev":"gesture","type":"swipeup","x":195,"y":422,"t":543536.400000006,"ta":536.367}
+{"ev":"vibrate","pattern":"[10]","t":543893.6000000238,"ta":536.725}
+{"ev":"start","t":543893.900000006,"sid":49,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":536.725}
+{"ev":"start","t":543893.900000006,"sid":50,"url":"audio/sfx/amb_power.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":536.725}
+{"ev":"gesture","type":"doubletap","x":195,"y":422,"t":548303.3000000119,"ta":541.134}
+{"ev":"vibrate","pattern":"[20]","t":548551.1000000238,"ta":541.382}
+{"ev":"vibrate","pattern":"[100]","t":548551.400000006,"ta":541.382}
+{"ev":"start","t":548551.7000000179,"sid":51,"url":"audio/sfx/spark.ogg","layer":"sfx","dur":0.8970975056689342,"rate":1,"loop":false,"when":0,"ta":541.383}
+{"ev":"start","t":548551.7000000179,"sid":52,"url":"audio/sfx/power_on.ogg","layer":"sfx","dur":2.2,"rate":1,"loop":false,"when":0,"ta":541.383}
+{"ev":"fetch","url":"audio/voice/female/fix_box.ogg","status":200,"ok":true,"ms":3,"t":548554.3000000119,"ta":541.385}
+{"ev":"vstart","url":"audio/voice/female/fix_box.ogg","t":548562.400000006,"dur":7.52,"ta":541.393}
+{"ev":"vend","url":"audio/voice/female/fix_box.ogg","t":556150.1000000238,"reason":"kill","ta":548.981}
+{"ev":"fetch","url":"audio/voice/female/gd_gate_dir.ogg","status":200,"ok":true,"ms":3,"t":556153.3000000119,"ta":548.984}
+{"ev":"vstart","url":"audio/voice/female/gd_gate_dir.ogg","t":556157.3000000119,"dur":2.32,"ta":548.988}
+{"ev":"gesture","type":"swiperight","x":195,"y":422,"t":557962.3000000119,"ta":550.793}
+{"ev":"vibrate","pattern":"[10]","t":558326.1000000238,"ta":551.157}
+{"ev":"start","t":558326.400000006,"sid":53,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":551.157}
+{"ev":"start","t":558326.400000006,"sid":54,"url":"audio/sfx/amb_wind.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":551.157}
+{"ev":"vend","url":"audio/voice/female/gd_gate_dir.ogg","t":558530,"reason":"kill","ta":551.361}
+{"ev":"gesture","type":"swiperight","x":195,"y":422,"t":562736.3000000119,"ta":555.567}
+{"ev":"vibrate","pattern":"[10]","t":563092.3000000119,"ta":555.923}
+{"ev":"start","t":563092.5,"sid":55,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":555.923}
+{"ev":"start","t":563092.5,"sid":56,"url":"audio/sfx/amb_hum.ogg","layer":"sfx","dur":5,"rate":1,"loop":false,"when":0,"ta":555.923}
+{"ev":"gesture","type":"swipedown","x":195,"y":422,"t":567502.3000000119,"ta":560.333}
+{"ev":"vibrate","pattern":"[10]","t":567859.3000000119,"ta":560.69}
+{"ev":"start","t":567859.5,"sid":57,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":560.69}
+{"ev":"start","t":567859.5,"sid":58,"url":"audio/sfx/amb_cabinet.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":560.69}
+{"ev":"gesture","type":"swipedown","x":195,"y":422,"t":572269.900000006,"ta":565.101}
+{"ev":"vibrate","pattern":"[10]","t":572625.3000000119,"ta":565.456}
+{"ev":"start","t":572625.8000000119,"sid":59,"url":"audio/sfx/step.ogg","layer":"sfx","dur":0.27709750566893426,"rate":1,"loop":false,"when":0,"ta":565.457}
+{"ev":"start","t":572625.8000000119,"sid":60,"url":"audio/sfx/amb_gate_live.ogg","layer":"sfx","dur":6,"rate":1,"loop":false,"when":0,"ta":565.457}
+{"ev":"fetch","url":"audio/voice/female/room_gate.ogg","status":200,"ok":true,"ms":3,"t":572628.3000000119,"ta":565.459}
+{"ev":"vstart","url":"audio/voice/female/room_gate.ogg","t":572633.400000006,"dur":3.52,"ta":565.464}
+{"ev":"vend","url":"audio/voice/female/room_gate.ogg","t":576480.2000000179,"reason":"kill","ta":569.311}
+{"ev":"gesture","type":"doubletap","x":195,"y":422,"t":589967.5,"ta":582.798}
+{"ev":"vibrate","pattern":"[20]","t":590216.2000000179,"ta":583.047}
+{"ev":"vibrate","pattern":"[100]","t":590216.5,"ta":583.047}
+{"ev":"fetch","url":"audio/voice/female/escape.ogg","status":200,"ok":true,"ms":3,"t":590219.5,"ta":583.05}
+{"ev":"vstart","url":"audio/voice/female/escape.ogg","t":590232.7000000179,"dur":15.32,"ta":583.064}
+{"ev":"stop","t":592776.8000000119,"sid":1,"when":0,"ta":585.608}
+{"ev":"start","t":592817.400000006,"sid":61,"url":"audio/sfx/gate_open.ogg","layer":"sfx","dur":2.7999773242630384,"rate":1,"loop":false,"when":0,"ta":585.648}
+{"ev":"start","t":592817.400000006,"sid":62,"url":"audio/sfx/win_chime.ogg","layer":"sfx","dur":1.6,"rate":1,"loop":false,"when":0,"ta":585.648}
+{"ev":"gesture","type":"tap","x":195,"y":422,"t":594627.5,"ta":587.458}
+{"ev":"vibrate","pattern":"[20]","t":594990.8000000119,"ta":587.822}
+{"ev":"vend","url":"audio/voice/female/escape.ogg","t":605660.1000000238,"reason":"kill","ta":598.491}
+{"ev":"fetch","url":"audio/voice/female/time_band3.ogg","status":200,"ok":true,"ms":3,"t":605663.2000000179,"ta":598.494}
+{"ev":"vstart","url":"audio/voice/female/time_band3.ogg","t":605667.8000000119,"dur":3.2,"ta":598.499}
+{"ev":"vend","url":"audio/voice/female/time_band3.ogg","t":609140.2000000179,"reason":"kill","ta":601.971}
+{"ev":"fetch","url":"audio/voice/female/again.ogg","status":200,"ok":true,"ms":2,"t":609142.6000000238,"ta":601.974}
+{"ev":"vstart","url":"audio/voice/female/again.ogg","t":609148.400000006,"dur":4.96,"ta":601.979}
+{"ev":"vend","url":"audio/voice/female/again.ogg","t":614410.1000000238,"reason":"kill","ta":607.241}
+{"ev":"start","t":614410.400000006,"sid":63,"url":"audio/bgm/menu.ogg","layer":"bgm","dur":90,"rate":1,"loop":true,"when":0,"ta":607.241}
+{"ev":"gesture","type":"tap","x":195,"y":422,"t":630374.6000000238,"ta":623.206}
+{"ev":"vibrate","pattern":"[20]","t":630739.2000000179,"ta":623.57}
+{"ev":"start","t":630740,"sid":64,"url":"audio/bgm/drone.ogg","layer":"bgm","dur":117,"rate":1,"loop":true,"when":0,"ta":623.571}
+{"ev":"start","t":630740.1000000238,"sid":65,"url":"audio/sfx/amb_drip.ogg","layer":"sfx","dur":5,"rate":1,"loop":false,"when":0,"ta":623.571}
+{"ev":"fetch","url":"audio/voice/female/replay.ogg","status":200,"ok":true,"ms":3,"t":630742.5,"ta":623.573}
+{"ev":"vstart","url":"audio/voice/female/replay.ogg","t":630751.2000000179,"dur":8.24,"ta":623.582}
+{"ev":"stop","t":631600.2000000179,"sid":63,"when":0,"ta":624.431}
+{"ev":"vend","url":"audio/voice/female/replay.ogg","t":639000.2000000179,"reason":"kill","ta":631.831}
+{"ev":"recstop","t":646344.7000000179,"ta":639.176}
diff --git a/research/escape-state/round2/live-transcript.md b/research/escape-state/round2/live-transcript.md
new file mode 100644
index 00000000..c5e60181
--- /dev/null
+++ b/research/escape-state/round2/live-transcript.md
@@ -0,0 +1,362 @@
+# escape 实时会话 transcript
+
+- `[ -5.10s]` **GESTURE** tap @(195,422)
+- `[ 0.00s]` REC 开始 (audio/webm;codecs=opus)
+- `[ 0.08s]` VOICE ▶ g0_intro (dur 6.64s)
+- `[ 0.27s]` BGM ♪ drone (loop)
+- `[ 6.93s]` VOICE ■ g0_intro (kill)
+- `[ 6.93s]` VERDICT g0_intro: **OK** sim=1
+ - 期望: 黑暗。你在废弃的地下设施里醒来,什么都想不起来。你必须找到出路。
+ - 听到: 黑暗,你在废弃的地下设施里醒来,什么都想不起来,你必须找到出路。
+- `[ 8.13s]` VOICE ▶ g1_move (dur 4.56s)
+- `[ 12.74s]` VOICE ■ g1_move (kill)
+- `[ 12.74s]` VOICE ▶ skip_hint (dur 1.6s)
+- `[ 12.74s]` VERDICT g1_move: **OK** sim=0.9697
+ - 期望: 先活动一下。朝任意方向滑一下,走一小步。
+ - 听到: 先活动一下,朝任意方向滑一下,走一小。
+- `[ 14.36s]` VOICE ■ skip_hint (kill)
+- `[ 14.36s]` VERDICT skip_hint: **OK** sim=0.8571
+ - 期望: 单击可跳过旁白。
+ - 听到: 单击可跳过旁板。
+- `[ 79.05s]` **GESTURE** swipeup @(195,422)
+- `[ 79.41s]` VIBRATE [10]
+- `[ 79.41s]` SFX ♪ step
+- `[ 79.42s]` VOICE ▶ g1_done (dur 2.72s)
+- `[ 79.42s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 82.15s]` VOICE ■ g1_done (kill)
+- `[ 82.15s]` VERDICT g1_done: **OK** sim=0.9412
+ - 期望: 很好,你迈出了第一步。
+ - 听到: 很好,你迈出了第一。
+- `[ 82.16s]` VOICE ▶ g2_wall (dur 5.68s)
+- `[ 82.32s]` **GESTURE** swipeleft @(195,422)
+- `[ 82.68s]` VIBRATE [10]
+- `[ 87.88s]` VOICE ■ g2_wall (kill)
+- `[ 87.88s]` VIBRATE [60,40,60]
+- `[ 87.88s]` VERDICT g2_wall: **OK** sim=1
+ - 期望: 这个房间不大,四面都是墙。再滑两下,试试碰碰墙。
+ - 听到: 这个房间不大,四面都是墙,再滑两下试试碰碰墙。
+- `[ 87.88s]` SFX ♪ wall
+- `[ 87.89s]` VOICE ▶ wall (dur 2.08s)
+- `[ 90.24s]` VOICE ■ wall (kill)
+- `[ 90.24s]` VERDICT wall: **OK** sim=0.9412
+ - 期望: 那边是墙,换个方向。
+ - 听到: 墙那边是墙,换个方向。
+- `[ 109.13s]` **GESTURE** swiperight @(195,422)
+- `[ 109.49s]` VIBRATE [10]
+- `[ 109.49s]` VIBRATE [60,40,60]
+- `[ 109.49s]` SFX ♪ wall
+- `[ 109.49s]` VOICE ▶ wall (dur 2.08s)
+- `[ 109.49s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 111.86s]` VOICE ■ wall (kill)
+- `[ 111.86s]` VERDICT wall: **OK** sim=1
+ - 期望: 那边是墙,换个方向。
+ - 听到: 那边是墙,换个方向。
+- `[ 111.87s]` VOICE ▶ g3_left (dur 2.96s)
+- `[ 114.88s]` VOICE ■ g3_left (kill)
+- `[ 114.88s]` VERDICT g3_left: **OK** sim=0.9
+ - 期望: 跟着我指的路走。向左滑。
+ - 听到: 跟着我指的路走,向左画。
+- `[ 116.90s]` **GESTURE** swipeleft @(195,422)
+- `[ 117.26s]` VIBRATE [10]
+- `[ 117.26s]` SFX ♪ step
+- `[ 117.26s]` VOICE ▶ g4_down (dur 1.84s)
+- `[ 117.26s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 119.17s]` VOICE ■ g4_down (kill)
+- `[ 119.17s]` VERDICT g4_down: **OK** sim=1
+ - 期望: 对。再向下滑。
+ - 听到: 对,再向下滑。
+- `[ 122.67s]` **GESTURE** swipedown @(195,422)
+- `[ 123.02s]` VIBRATE [10]
+- `[ 123.02s]` SFX ♪ step
+- `[ 123.02s]` SFX ♪ amb_storage
+- `[ 123.03s]` VOICE ▶ g5_inspect (dur 3.92s)
+- `[ 123.03s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 127.17s]` VOICE ■ g5_inspect (kill)
+- `[ 127.17s]` VERDICT g5_inspect: **OK** sim=1
+ - 期望: 听到水声了吗?单击一下,仔细检查这个房间。
+ - 听到: 听到水声了吗?单击一下,仔细检查这个房间。
+- `[ 130.44s]` **GESTURE** tap @(195,422)
+- `[ 130.80s]` VIBRATE [20]
+- `[ 130.80s]` VIBRATE [15]
+- `[ 130.80s]` SFX ♪ pickup
+- `[ 130.81s]` VOICE ▶ g5_key (dur 1.52s)
+- `[ 130.81s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 132.42s]` VOICE ■ g5_key (kill)
+- `[ 132.42s]` VERDICT g5_key: **OK** sim=1
+ - 期望: 你摸到了一把钥匙!
+ - 听到: 你摸到了一把钥匙。
+- `[ 132.43s]` VOICE ▶ g6_doubletap (dur 10.64s)
+- `[ 140.91s]` **GESTURE** doubletap @(195,422)
+- `[ 141.16s]` VIBRATE [20]
+- `[ 143.39s]` VOICE ■ g6_doubletap (kill)
+- `[ 143.39s]` VERDICT g6_doubletap: **OK** sim=0.9787
+ - 期望: 钥匙能开锁。双击,是把物品用出去的用法。现在,双击试试。
+ - 听到: 匙钥匙能开锁,双击是把物品用出去的用法。现在双击试试。
+- `[ 143.40s]` VOICE ▶ inv_key (dur 1.68s)
+- `[ 145.29s]` VOICE ■ inv_key (kill)
+- `[ 145.29s]` VERDICT inv_key: **OK** sim=1
+ - 期望: 你身上带着一把钥匙。
+ - 听到: 你身上带着一把钥匙。
+- `[ 145.30s]` VOICE ▶ g6_done (dur 3.92s)
+- `[ 149.45s]` VOICE ■ g6_done (kill)
+- `[ 149.45s]` VERDICT g6_done: **OK** sim=1
+ - 期望: 对,就是这样。接下来,靠你自己了。
+ - 听到: 对,就是这样,接下来靠你自己了。
+- `[ 149.46s]` VOICE ▶ g7_goal (dur 6.88s)
+- `[ 156.59s]` VOICE ■ g7_goal (kill)
+- `[ 156.59s]` VERDICT g7_goal: **OK** sim=1
+ - 期望: 找到能用这把钥匙打开的东西。需要帮助时,在原地双击。
+ - 听到: 找到能用这把钥匙打开的东西,需要帮助时在原地双击。
+- `[ 156.59s]` SFX ♪ amb_storage
+- `[ 178.80s]` **GESTURE** swipeup @(195,422)
+- `[ 179.16s]` VIBRATE [10]
+- `[ 179.16s]` SFX ♪ step
+- `[ 179.16s]` SFX ♪ amb_hum
+- `[ 179.16s]` VOICE ▶ empty_2 (dur 4.88s)
+- `[ 179.16s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 184.15s]` VOICE ■ empty_2 (kill)
+- `[ 184.15s]` VERDICT empty_2: **OK** sim=1
+ - 期望: 你伸出手,摸到的只有冰冷粗糙的墙壁。
+ - 听到: 你伸出手摸到的只有冰冷粗糙的墙壁。
+- `[ 185.57s]` **GESTURE** swipeup @(195,422)
+- `[ 185.92s]` VIBRATE [10]
+- `[ 185.92s]` SFX ♪ step
+- `[ 185.92s]` SFX ♪ amb_power
+- `[ 185.93s]` VOICE ▶ room_power (dur 2.64s)
+- `[ 185.93s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 188.67s]` VOICE ■ room_power (kill)
+- `[ 188.67s]` VERDICT room_power: **OK** sim=1
+ - 期望: 这个房间的墙上,有一只电箱。
+ - 听到: 这个房间的墙上有一只电箱。
+- `[ 192.33s]` **GESTURE** swipeup @(195,422)
+- `[ 192.69s]` VIBRATE [10]
+- `[ 192.69s]` VIBRATE [60,40,60]
+- `[ 192.69s]` SFX ♪ wall
+- `[ 192.69s]` VOICE ▶ wall (dur 2.08s)
+- `[ 192.69s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 195.05s]` VOICE ■ wall (kill)
+- `[ 195.05s]` VERDICT wall: **OK** sim=1
+ - 期望: 那边是墙,换个方向。
+ - 听到: 那边是墙,换个方向。
+- `[ 195.06s]` VOICE ▶ wall_dir_u (dur 1.36s)
+- `[ 196.67s]` VOICE ■ wall_dir_u (kill)
+- `[ 196.67s]` VERDICT wall_dir_u: **OK** sim=1
+ - 期望: 上边是墙。
+ - 听到: 上边是墙。
+- `[ 199.10s]` **GESTURE** swipeup @(195,422)
+- `[ 199.45s]` VIBRATE [10]
+- `[ 199.46s]` VIBRATE [60,40,60]
+- `[ 199.46s]` SFX ♪ wall
+- `[ 199.46s]` VOICE ▶ wall (dur 2.08s)
+- `[ 199.46s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 201.82s]` VOICE ■ wall (kill)
+- `[ 201.82s]` VERDICT wall: **OK** sim=1
+ - 期望: 那边是墙,换个方向。
+ - 听到: 那边是墙,换个方向。
+- `[ 203.86s]` **GESTURE** swipeleft @(195,422)
+- `[ 204.22s]` VIBRATE [10]
+- `[ 204.22s]` VIBRATE [60,40,60]
+- `[ 204.22s]` SFX ♪ wall
+- `[ 204.22s]` VOICE ▶ wall (dur 2.08s)
+- `[ 204.22s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 206.59s]` VOICE ■ wall (kill)
+- `[ 206.59s]` VERDICT wall: **OK** sim=1
+ - 期望: 那边是墙,换个方向。
+ - 听到: 那边是墙,换个方向。
+- `[ 206.60s]` VOICE ▶ wall_dir_l (dur 1.04s)
+- `[ 207.75s]` VOICE ■ wall_dir_l (kill)
+- `[ 207.75s]` VERDICT wall_dir_l: **OK** sim=1
+ - 期望: 左边是墙。
+ - 听到: 左边是墙。
+- `[ 210.63s]` **GESTURE** doubletap @(195,422)
+- `[ 210.88s]` VIBRATE [20]
+- `[ 210.88s]` VIBRATE [60,40,60]
+- `[ 210.89s]` VOICE ▶ use_fail (dur 7.36s)
+- `[ 210.89s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 218.48s]` VOICE ■ use_fail (kill)
+- `[ 218.48s]` VERDICT use_fail: **OK** sim=0.9655
+ - 期望: 这里没有用得上东西的地方。换个房间试试,或者先单击检查一下周围。
+ - 听到: 这里没有用得上东西的地方,换个房间试试,或者先单机检查一下周围。
+- `[ 218.49s]` VOICE ▶ stuck_cabinet_1 (dur 3.84s)
+- `[ 222.62s]` VOICE ■ stuck_cabinet_1 (kill)
+- `[ 222.62s]` VERDICT stuck_cabinet_1: **OK** sim=0.9333
+ - 期望: 钥匙就在你身上。它能打开某一把锁。
+ - 听到: 钥匙就在你身上,他能打开某一把锁。
+- `[ 255.88s]` **GESTURE** swiperight @(195,422)
+- `[ 256.24s]` VIBRATE [10]
+- `[ 256.24s]` SFX ♪ step
+- `[ 256.24s]` SFX ♪ amb_wind
+- `[ 256.25s]` VOICE ▶ empty_1 (dur 5.2s)
+- `[ 256.25s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 261.50s]` VOICE ■ empty_1 (kill)
+- `[ 261.50s]` VERDICT empty_1: **OK** sim=1
+ - 期望: 房间里空空荡荡,只有你自己的回声。
+ - 听到: 房间里空空荡荡,只有你自己的回声。
+- `[ 262.64s]` **GESTURE** swiperight @(195,422)
+- `[ 263.00s]` VIBRATE [10]
+- `[ 263.00s]` SFX ♪ step
+- `[ 263.00s]` SFX ♪ amb_hum
+- `[ 263.01s]` VOICE ▶ empty_3 (dur 3.68s)
+- `[ 263.01s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 266.86s]` VOICE ■ empty_3 (kill)
+- `[ 266.86s]` VERDICT empty_3: **OK** sim=1
+ - 期望: 除了远处的水滴声,这里什么都没有。
+ - 听到: 除了远处的水滴声,这里什么都没有。
+- `[ 269.41s]` **GESTURE** swipedown @(195,422)
+- `[ 269.77s]` VIBRATE [10]
+- `[ 269.77s]` SFX ♪ step
+- `[ 269.77s]` SFX ♪ amb_cabinet
+- `[ 269.78s]` VOICE ▶ room_cabinet (dur 2.64s)
+- `[ 269.78s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 272.52s]` VOICE ■ room_cabinet (kill)
+- `[ 272.52s]` VERDICT room_cabinet: **OK** sim=1
+ - 期望: 这个房间里,立着一个铁柜。
+ - 听到: 这个房间里立着一个铁柜。
+- `[ 276.18s]` **GESTURE** doubletap @(195,422)
+- `[ 276.43s]` VIBRATE [20]
+- `[ 276.43s]` VIBRATE [100]
+- `[ 276.43s]` SFX ♪ unlock
+- `[ 276.43s]` SFX ♪ door_creak
+- `[ 276.44s]` VOICE ▶ unlock_cabinet (dur 11.48s)
+- `[ 276.44s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 288.15s]` VOICE ■ unlock_cabinet (kill)
+- `[ 288.15s]` VERDICT unlock_cabinet: **OK** sim=0.9714
+ - 期望: 钥匙插进锁孔,缓缓转动。咔哒一声,柜门开了。柜子深处,躺着一枚保险丝。保险丝到手了。
+ - 听到: 钥匙插进锁孔,缓缓转动,咔嗒一声,柜门开了,柜子深处躺着一枚保险丝,保险丝到手了。
+- `[ 288.16s]` VOICE ▶ gd_next_fuse (dur 3.28s)
+- `[ 291.63s]` VOICE ■ gd_next_fuse (kill)
+- `[ 291.63s]` VERDICT gd_next_fuse: **OK** sim=1
+ - 期望: 保险丝到手。接下来,去找电箱。
+ - 听到: 保险丝到手,接下来去找电箱。
+- `[ 291.64s]` VOICE ▶ gd_box_dir (dur 2.16s)
+- `[ 294.01s]` VOICE ■ gd_box_dir (kill)
+- `[ 294.01s]` VERDICT gd_box_dir: **OK** sim=1
+ - 期望: 电箱,在左面的房间。
+ - 听到: 电箱在左面的房间。
+- `[ 310.43s]` **GESTURE** swipeleft @(195,422)
+- `[ 310.78s]` VIBRATE [10]
+- `[ 310.78s]` SFX ♪ step
+- `[ 310.78s]` SFX ♪ amb_drip
+- `[ 327.19s]` **GESTURE** swipeleft @(195,422)
+- `[ 327.55s]` VIBRATE [10]
+- `[ 327.55s]` SFX ♪ step
+- `[ 327.55s]` SFX ♪ amb_hum
+- `[ 343.96s]` **GESTURE** swiperight @(195,422)
+- `[ 344.31s]` VIBRATE [10]
+- `[ 344.31s]` SFX ♪ step
+- `[ 344.31s]` SFX ♪ amb_drip
+- `[ 360.73s]` **GESTURE** swiperight @(195,422)
+- `[ 361.08s]` VIBRATE [10]
+- `[ 361.08s]` SFX ♪ step
+- `[ 361.08s]` SFX ♪ amb_cabinet
+- `[ 377.49s]` **GESTURE** swipeup @(195,422)
+- `[ 377.85s]` VIBRATE [10]
+- `[ 377.85s]` SFX ♪ step
+- `[ 377.85s]` SFX ♪ amb_hum
+- `[ 408.88s]` **GESTURE** swipedown @(195,422)
+- `[ 409.25s]` VIBRATE [10]
+- `[ 409.25s]` SFX ♪ step
+- `[ 409.25s]` SFX ♪ amb_cabinet
+- `[ 425.66s]` **GESTURE** swipeleft @(195,422)
+- `[ 426.01s]` VIBRATE [10]
+- `[ 426.01s]` SFX ♪ step
+- `[ 426.01s]` SFX ♪ amb_drip
+- `[ 430.00s]` VIBRATE [30,50,30,50,30]
+- `[ 430.01s]` VOICE ▶ gd_prog_2 (dur 2.56s)
+- `[ 430.01s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 432.75s]` VOICE ■ gd_prog_2 (kill)
+- `[ 432.75s]` VERDICT gd_prog_2: **OK** sim=1
+ - 期望: 铁柜已经打开。电还没有通。
+ - 听到: 铁柜已经打开,电还没有通。
+- `[ 442.42s]` **GESTURE** swipeleft @(195,422)
+- `[ 442.78s]` VIBRATE [10]
+- `[ 442.78s]` SFX ♪ step
+- `[ 442.78s]` SFX ♪ amb_hum
+- `[ 459.19s]` **GESTURE** swiperight @(195,422)
+- `[ 459.54s]` VIBRATE [10]
+- `[ 459.54s]` SFX ♪ step
+- `[ 459.54s]` SFX ♪ amb_drip
+- `[ 475.95s]` **GESTURE** swiperight @(195,422)
+- `[ 476.31s]` VIBRATE [10]
+- `[ 476.31s]` SFX ♪ step
+- `[ 476.31s]` SFX ♪ amb_cabinet
+- `[ 526.84s]` **GESTURE** swipeleft @(195,422)
+- `[ 527.19s]` VIBRATE [10]
+- `[ 527.19s]` SFX ♪ step
+- `[ 527.19s]` SFX ♪ amb_drip
+- `[ 531.60s]` **GESTURE** swipeleft @(195,422)
+- `[ 531.96s]` VIBRATE [10]
+- `[ 531.96s]` SFX ♪ step
+- `[ 531.96s]` SFX ♪ amb_hum
+- `[ 536.37s]` **GESTURE** swipeup @(195,422)
+- `[ 536.73s]` VIBRATE [10]
+- `[ 536.73s]` SFX ♪ step
+- `[ 536.73s]` SFX ♪ amb_power
+- `[ 541.13s]` **GESTURE** doubletap @(195,422)
+- `[ 541.38s]` VIBRATE [20]
+- `[ 541.38s]` VIBRATE [100]
+- `[ 541.38s]` SFX ♪ spark
+- `[ 541.38s]` SFX ♪ power_on
+- `[ 541.39s]` VOICE ▶ fix_box (dur 7.52s)
+- `[ 541.39s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 548.98s]` VOICE ■ fix_box (kill)
+- `[ 548.98s]` VERDICT fix_box: **OK** sim=0.9153
+ - 期望: 你把保险丝用力按进卡座。火花一闪,嗡鸣声响起——头顶的线路恢复供电了!
+ - 听到: 你把保险丝用力安静卡座,火花一闪,嗡鸣声响起,头顶的线路恢复供电。
+- `[ 548.99s]` VOICE ▶ gd_gate_dir (dur 2.32s)
+- `[ 550.79s]` **GESTURE** swiperight @(195,422)
+- `[ 551.16s]` VIBRATE [10]
+- `[ 551.16s]` SFX ♪ step
+- `[ 551.16s]` SFX ♪ amb_wind
+- `[ 551.36s]` VOICE ■ gd_gate_dir (kill)
+- `[ 551.36s]` VERDICT gd_gate_dir: **OK** sim=1
+ - 期望: 大门,在右面的房间。
+ - 听到: 大门在右面的房间。
+- `[ 555.57s]` **GESTURE** swiperight @(195,422)
+- `[ 555.92s]` VIBRATE [10]
+- `[ 555.92s]` SFX ♪ step
+- `[ 555.92s]` SFX ♪ amb_hum
+- `[ 560.33s]` **GESTURE** swipedown @(195,422)
+- `[ 560.69s]` VIBRATE [10]
+- `[ 560.69s]` SFX ♪ step
+- `[ 560.69s]` SFX ♪ amb_cabinet
+- `[ 565.10s]` **GESTURE** swipedown @(195,422)
+- `[ 565.46s]` VIBRATE [10]
+- `[ 565.46s]` SFX ♪ step
+- `[ 565.46s]` SFX ♪ amb_gate_live
+- `[ 565.46s]` VOICE ▶ room_gate (dur 3.52s)
+- `[ 565.46s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 569.31s]` VOICE ■ room_gate (kill)
+- `[ 569.31s]` VERDICT room_gate: **OK** sim=1
+ - 期望: 面前,是一扇沉重的铁门。
+ - 听到: 面前是一扇沉重的铁门。
+- `[ 582.80s]` **GESTURE** doubletap @(195,422)
+- `[ 583.05s]` VIBRATE [20]
+- `[ 583.05s]` VIBRATE [100]
+- `[ 583.06s]` VOICE ▶ escape (dur 15.32s)
+- `[ 583.06s]` VERDICT (无语音区间): **SFX_BGM_ONLY**
+- `[ 585.65s]` SFX ♪ gate_open
+- `[ 585.65s]` SFX ♪ win_chime
+- `[ 587.46s]` **GESTURE** tap @(195,422)
+- `[ 587.82s]` VIBRATE [20]
+- `[ 598.49s]` VOICE ■ escape (kill)
+- `[ 598.49s]` VERDICT escape: **OK** sim=0.9053
+ - 期望: 你扳动开关。电机轰鸣,沉重的铁门缓缓升起,刺眼的光线涌了进来。你眯起眼睛,向前迈出了第一步。自由了。你逃出来了。
+ - 听到: 你搬动开关,电击轰,沉重的体能缓缓升起,刺眼的光线涌了进来,你眯起眼睛向前迈出了第一步,自由了,你逃出来了。
+- `[ 598.50s]` VOICE ▶ time_band3 (dur 3.2s)
+- `[ 601.97s]` VOICE ■ time_band3 (kill)
+- `[ 601.97s]` VERDICT time_band3: **OK** sim=0.88
+ - 期望: 你逃出来了,用时不到十分钟。
+ - 听到: 你逃出来了,用时不到10分钟。
+- `[ 601.98s]` VOICE ▶ again (dur 4.96s)
+- `[ 607.24s]` VOICE ■ again (kill)
+- `[ 607.24s]` BGM ♪ menu (loop)
+- `[ 607.24s]` VERDICT again: **OK** sim=1
+ - 期望: 单击屏幕,再来一次。下滑,打开菜单。
+ - 听到: 单击屏幕再来一次下滑打开菜单。
+- `[ 623.21s]` **GESTURE** tap @(195,422)
+- `[ 623.57s]` VIBRATE [20]
+- `[ 623.57s]` BGM ♪ drone (loop)
+- `[ 623.57s]` SFX ♪ amb_drip
+- `[ 623.58s]` VOICE ▶ replay (dur 8.24s)
+- `[ 631.83s]` VOICE ■ replay (kill)
diff --git a/research/escape-state/round2/mix.webm b/research/escape-state/round2/mix.webm
new file mode 100644
index 00000000..7e27c75c
Binary files /dev/null and b/research/escape-state/round2/mix.webm differ
diff --git a/research/escape-state/round2/transcript-mock.md b/research/escape-state/round2/transcript-mock.md
new file mode 100644
index 00000000..22e9fe3f
--- /dev/null
+++ b/research/escape-state/round2/transcript-mock.md
@@ -0,0 +1,164 @@
+# 试玩转写:《escape》
+
+> 本文件是「玩家感知日志」:一位新手玩家的整局体验转写。语音列出了玩家听到的每句话全文,
+> 音效/BGM/震动按发生时间列出。「静默」段 = 玩家什么也听不到。手势是剧本注入的(模拟真人手部动作),
+> 非最优操作:包含犹豫、误操作与乱试。语音时长按语速估算(±20%),仅供节奏参考。
+t+ 0.0s (玩家点开游戏,轻触屏幕开始)
+t+ 0.0s ♫ BGM 切换 → bgm/drone
+t+ 0.0s ♪ 向导音:explore(探索/安全)
+t+ 0.0s ▶ 语音:「黑暗。你在废弃的地下设施里醒来,什么都想不起来。你必须找到出路。」
+t+ 0.0s 【状态】黑屏,只有引子的声音,先听完 引导第 1/8 步,等我做手势(语音播放中,语速1)
+t+ 0.0s 【状态】引导第 1/8 步 引导第 1/8 步,等我做手势(语音播放中,语速1)
+
+#### 【玩家动作】照引导单击了一下
+
+t+ 0.0s ▶ 语音:「先活动一下。朝任意方向滑一下,走一小步。」
+t+ 2.1s ▶ 语音:「单击可跳过旁白。」
+t+ 4.2s ♪ 向导音:turn(轮到你)
+t+ 4.2s 【状态】引导第 2/8 步 引导第 2/8 步,等我做手势(BGM=bgm/drone,语速1)
+
+#### 【玩家动作】随便滑了一步,有脚步声
+
+t+ 4.2s ♪ 音效 sfx/step
+t+ 4.2s ▶ 语音:「很好,你迈出了第一步。」
+t+ 6.4s ▶ 语音:「这个房间不大,四面都是墙。再滑两下,试试碰碰墙。」
+t+ 8.5s ♪ 向导音:turn(轮到你)
+t+ 8.5s 【状态】引导第 3/8 步 引导第 3/8 步,等我做手势(BGM=bgm/drone,语速1)
+t+ 8.5s ♪ 音效 sfx/wall
+t+ 8.5s ▶ 语音:「那边是墙,换个方向。」
+t+ 8.5s 〰 震动:error(做错了:两连短震)
+
+#### 【玩家动作】又滑一步,又撞墙了——墙的方位听出来了
+
+t+ 10.6s ♪ 音效 sfx/wall
+t+ 10.6s ▶ 语音:「那边是墙,换个方向。」
+t+ 10.6s 〰 震动:error(做错了:两连短震)
+t+ 12.7s ▶ 语音:「跟着我指的路走。向左滑。」
+t+ 14.8s ♪ 向导音:turn(轮到你)
+t+ 14.8s 【状态】引导第 4/8 步 引导第 4/8 步,等我做手势(BGM=bgm/drone,语速1)
+
+#### 【玩家动作】这回往左,走通了一步
+
+t+ 14.8s ♪ 音效 sfx/step
+t+ 14.8s ▶ 语音:「对。再向下滑。」
+t+ 17.0s ♪ 向导音:turn(轮到你)
+t+ 17.0s 【状态】引导第 5/8 步 引导第 5/8 步,等我做手势(BGM=bgm/drone,语速1)
+
+#### 【玩家动作】往下滑,进了储物间
+
+t+ 17.0s ♪ 音效 sfx/step
+t+ 17.0s ♪ 音效 sfx/amb_storage
+t+ 17.0s ▶ 语音:「听到水声了吗?单击一下,仔细检查这个房间。」
+t+ 19.1s ♪ 向导音:turn(轮到你)
+t+ 19.1s 【状态】引导第 6/8 步 引导第 6/8 步,等我做手势(BGM=bgm/drone,语速1)
+
+#### 【玩家动作】单击摸一遍房间,摸到钥匙了
+
+t+ 19.1s ♪ 音效 sfx/pickup
+t+ 19.1s ▶ 语音:「你摸到了一把钥匙!」
+t+ 19.1s 〰 震动:pickup(获得)
+t+ 21.2s ▶ 语音:「钥匙能开锁。双击,是把物品用出去的用法。现在,双击试试。」
+t+ 23.3s ♪ 向导音:turn(轮到你)
+t+ 23.3s 【状态】引导第 7/8 步 引导第 7/8 步,等我做手势(BGM=bgm/drone,语速1)
+
+#### 【玩家动作】双击看了看背包
+
+t+ 23.3s ▶ 语音:「你身上带着一把钥匙。」
+t+ 25.4s ▶ 语音:「对,就是这样。接下来,靠你自己了。」
+t+ 27.6s ▶ 语音:「找到能用这把钥匙打开的东西。需要帮助时,在原地双击。」
+t+ 29.7s ♪ 向导音:turn(轮到你)
+t+ 29.7s ♪ 音效 sfx/amb_storage
+t+ 29.7s 【状态】引导完毕,正式开始。我在储物间 探索中,我在 (0,2)。四向滑挪步,单击摸一遍房间,双击用物品(BGM=bgm/drone,语速1)
+t+ 29.7s ……… 静默 3s(辨了辨方向) ………
+t+ 29.7s ♪ 音效 sfx/step
+t+ 29.7s ♪ 音效 sfx/amb_hum
+t+ 29.7s ▶ 语音:「你伸出手,摸到的只有冰冷粗糙的墙壁。」
+
+#### 【玩家动作】路过一间空房,它自己开口了:摸到的只有冰冷粗糙的墙壁
+
+t+ 31.8s ♪ 音效 sfx/step
+t+ 31.8s ♪ 音效 sfx/amb_power
+t+ 31.8s ▶ 语音:「这个房间的墙上,有一只电箱。」
+
+#### 【玩家动作】电流声——电箱房到了
+
+t+ 33.9s ♪ 音效 sfx/wall
+t+ 33.9s ▶ 语音:「那边是墙,换个方向。」
+t+ 33.9s 〰 震动:error(做错了:两连短震)
+t+ 36.0s ▶ 语音:「上边是墙。」
+
+#### 【玩家动作】北边是墙,撞了一下——「那边是墙」后面跟了句「上边是墙。」,这面不通记住了
+
+t+ 36.0s ♪ 音效 sfx/wall
+t+ 36.0s ▶ 语音:「那边是墙,换个方向。」
+t+ 36.0s 〰 震动:error(做错了:两连短震)
+
+#### 【玩家动作】又撞了一下——同方向 10 秒内不重复报方位,游戏不啰嗦
+
+t+ 45.0s ♪ 音效 sfx/step
+t+ 45.0s ♪ 音效 sfx/amb_wind
+t+ 45.0s ▶ 语音:「房间里空空荡荡,只有你自己的回声。」
+
+#### 【玩家动作】往东走,一间空房自报家门:空空荡荡只有回声
+
+t+ 47.1s ♪ 音效 sfx/step
+t+ 47.1s ♪ 音效 sfx/amb_hum
+t+ 47.1s ▶ 语音:「除了远处的水滴声,这里什么都没有。」
+
+#### 【玩家动作】又一间空房:除了远处的水滴声什么都没有
+
+t+ 49.2s ♪ 音效 sfx/step
+t+ 49.2s ♪ 音效 sfx/amb_cabinet
+t+ 49.2s ▶ 语音:「这个房间里,立着一个铁柜。」
+
+#### 【玩家动作】到了铁柜房,柜子锁着
+
+t+ 51.4s ♪ 音效 sfx/unlock
+t+ 51.4s ♪ 音效 sfx/door_creak
+t+ 51.4s ▶ 语音:「钥匙插进锁孔,缓缓转动。咔哒一声,柜门开了。柜子深处,躺着一枚保险丝。保险丝到手了。」
+t+ 51.4s 〰 震动:success(成功长震)
+t+ 53.5s ▶ 向导语音:「保险丝到手。接下来,去找电箱。」
+t+ 55.6s ▶ 向导语音:「电箱,在左面的房间。」
+t+ 57.7s 【状态】柜子开了。「保险丝到手。接下来,去找电箱。」后面跟的是「电箱,在左面的房间。」——目标名和方位一句话说全了 探索中,我在 (2,1)。四向滑挪步,单击摸一遍房间,双击用物品(BGM=bgm/drone,语速1)
+t+ 57.7s ♪ 音效 sfx/step
+t+ 57.7s ♪ 音效 sfx/amb_drip
+
+#### 【玩家动作】照方位句往左(西)摸
+
+t+ 60.0s ♪ 音效 sfx/step
+t+ 60.0s ♪ 音效 sfx/amb_hum
+t+ 65.0s ♪ 音效 sfx/step
+t+ 65.0s ♪ 音效 sfx/amb_power
+
+#### 【玩家动作】电流声越来越清晰——电箱房到了
+
+t+ 70.0s ♪ 音效 sfx/spark
+t+ 70.0s ♪ 音效 sfx/power_on
+t+ 70.0s ▶ 语音:「你把保险丝用力按进卡座。火花一闪,嗡鸣声响起——头顶的线路恢复供电了!」
+t+ 70.0s 〰 震动:success(成功长震)
+t+ 72.1s ▶ 向导语音:「大门,在右面的房间。」
+t+ 80.0s 【状态】电箱修好了。目标句后面照样是目标名+方位——「大门,在右面的房间。」 探索中,我在 (0,0)。四向滑挪步,单击摸一遍房间,双击用物品(BGM=bgm/drone,语速1)
+t+ 80.0s ♪ 音效 sfx/step
+t+ 80.0s ♪ 音效 sfx/amb_wind
+
+#### 【玩家动作】照方位句往右(东)摸
+
+t+ 85.0s ♪ 音效 sfx/step
+t+ 85.0s ♪ 音效 sfx/amb_hum
+t+ 90.0s ♪ 音效 sfx/step
+t+ 90.0s ♪ 音效 sfx/amb_cabinet
+t+ 95.0s ♪ 音效 sfx/step
+t+ 95.0s ♪ 音效 sfx/amb_gate_live
+t+ 95.0s ▶ 语音:「面前,是一扇沉重的铁门。」
+
+#### 【玩家动作】脚步声停下来——面前这间就是大门
+
+t+ 97.1s ♪ 向导音:settle(结算)
+t+ 97.1s ▶ 语音:「你扳动开关。电机轰鸣,沉重的铁门缓缓升起,刺眼的光线涌了进来。你眯起眼睛,向前迈出了第一步。自由了。你逃出来了。」
+t+ 97.1s 〰 震动:success(成功长震)
+t+ 99.2s ▶ 语音:「你逃出来了,用时不到两分钟。了不起!」
+t+ 99.7s ♪ 音效 sfx/gate_open
+t+ 99.7s ♪ 音效 sfx/win_chime
+t+ 101.4s ▶ 语音:「单击屏幕,再来一次。下滑,打开菜单。」
+t+ 103.5s ♫ BGM 切换 → bgm/menu(淡入 1500ms)
+t+ 103.5s 【状态】钥匙插进锁孔,门开了——结算播报来了,这局是完整的赢 逃脱了,结算播报中。播完单击再来,下滑开菜单(BGM=bgm/menu,语速1)
diff --git a/research/escape-state/round2/verdicts.json b/research/escape-state/round2/verdicts.json
new file mode 100644
index 00000000..e5b42269
--- /dev/null
+++ b/research/escape-state/round2/verdicts.json
@@ -0,0 +1,1012 @@
+{
+ "game": "escape",
+ "settings": {
+ "voice": "female",
+ "guide": "full",
+ "speed": 1,
+ "bgm": 1
+ },
+ "outdir": "/home/xsl/AutoVideo/SoundGame/web/harness/live/out/escape-20260914162023",
+ "segs": [
+ {
+ "idx": 0,
+ "voiceId": "g0_intro",
+ "t0": 0.077,
+ "t1": 6.931,
+ "dur": 6.64,
+ "expEnd": 6.717,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "黑暗,你在废弃的地下设施里醒来,什么都想不起来,你必须找到出路。",
+ "similarity": 1
+ },
+ {
+ "idx": 1,
+ "voiceId": "g1_move",
+ "t0": 8.132,
+ "t1": 12.741,
+ "dur": 4.56,
+ "expEnd": 12.692,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "先活动一下,朝任意方向滑一下,走一小。",
+ "similarity": 0.9697
+ },
+ {
+ "idx": 2,
+ "voiceId": "skip_hint",
+ "t0": 12.741,
+ "t1": 14.361,
+ "dur": 1.6,
+ "expEnd": 14.341,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "单击可跳过旁板。",
+ "similarity": 0.8571
+ },
+ {
+ "idx": 3,
+ "voiceId": "g1_done",
+ "t0": 79.417,
+ "t1": 82.151,
+ "dur": 2.72,
+ "expEnd": 82.137,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "很好,你迈出了第一。",
+ "similarity": 0.9412
+ },
+ {
+ "idx": 4,
+ "voiceId": "g2_wall",
+ "t0": 82.159,
+ "t1": 87.881,
+ "dur": 5.68,
+ "expEnd": 87.839,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "这个房间不大,四面都是墙,再滑两下试试碰碰墙。",
+ "similarity": 1
+ },
+ {
+ "idx": 5,
+ "voiceId": "wall",
+ "t0": 87.887,
+ "t1": 90.241,
+ "dur": 2.08,
+ "expEnd": 89.967,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "墙那边是墙,换个方向。",
+ "similarity": 0.9412
+ },
+ {
+ "idx": 6,
+ "voiceId": "wall",
+ "t0": 109.491,
+ "t1": 111.861,
+ "dur": 2.08,
+ "expEnd": 111.571,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "那边是墙,换个方向。",
+ "similarity": 1
+ },
+ {
+ "idx": 7,
+ "voiceId": "g3_left",
+ "t0": 111.868,
+ "t1": 114.881,
+ "dur": 2.96,
+ "expEnd": 114.828,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "跟着我指的路走,向左画。",
+ "similarity": 0.9
+ },
+ {
+ "idx": 8,
+ "voiceId": "g4_down",
+ "t0": 117.264,
+ "t1": 119.171,
+ "dur": 1.84,
+ "expEnd": 119.104,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "对,再向下滑。",
+ "similarity": 1
+ },
+ {
+ "idx": 9,
+ "voiceId": "g5_inspect",
+ "t0": 123.032,
+ "t1": 127.171,
+ "dur": 3.92,
+ "expEnd": 126.952,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "听到水声了吗?单击一下,仔细检查这个房间。",
+ "similarity": 1
+ },
+ {
+ "idx": 10,
+ "voiceId": "g5_key",
+ "t0": 130.808,
+ "t1": 132.421,
+ "dur": 1.52,
+ "expEnd": 132.328,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "你摸到了一把钥匙。",
+ "similarity": 1
+ },
+ {
+ "idx": 11,
+ "voiceId": "g6_doubletap",
+ "t0": 132.433,
+ "t1": 143.391,
+ "dur": 10.64,
+ "expEnd": 143.073,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "匙钥匙能开锁,双击是把物品用出去的用法。现在双击试试。",
+ "similarity": 0.9787
+ },
+ {
+ "idx": 12,
+ "voiceId": "inv_key",
+ "t0": 143.398,
+ "t1": 145.291,
+ "dur": 1.68,
+ "expEnd": 145.078,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "你身上带着一把钥匙。",
+ "similarity": 1
+ },
+ {
+ "idx": 13,
+ "voiceId": "g6_done",
+ "t0": 145.299,
+ "t1": 149.451,
+ "dur": 3.92,
+ "expEnd": 149.219,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "对,就是这样,接下来靠你自己了。",
+ "similarity": 1
+ },
+ {
+ "idx": 14,
+ "voiceId": "g7_goal",
+ "t0": 149.46,
+ "t1": 156.591,
+ "dur": 6.88,
+ "expEnd": 156.34,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "找到能用这把钥匙打开的东西,需要帮助时在原地双击。",
+ "similarity": 1
+ },
+ {
+ "idx": 15,
+ "voiceId": "empty_2",
+ "t0": 179.164,
+ "t1": 184.151,
+ "dur": 4.88,
+ "expEnd": 184.044,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "你伸出手摸到的只有冰冷粗糙的墙壁。",
+ "similarity": 1
+ },
+ {
+ "idx": 16,
+ "voiceId": "room_power",
+ "t0": 185.929,
+ "t1": 188.671,
+ "dur": 2.64,
+ "expEnd": 188.569,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "这个房间的墙上有一只电箱。",
+ "similarity": 1
+ },
+ {
+ "idx": 17,
+ "voiceId": "wall",
+ "t0": 192.688,
+ "t1": 195.051,
+ "dur": 2.08,
+ "expEnd": 194.768,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "那边是墙,换个方向。",
+ "similarity": 1
+ },
+ {
+ "idx": 18,
+ "voiceId": "wall_dir_u",
+ "t0": 195.058,
+ "t1": 196.671,
+ "dur": 1.36,
+ "expEnd": 196.418,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "上边是墙。",
+ "similarity": 1
+ },
+ {
+ "idx": 19,
+ "voiceId": "wall",
+ "t0": 199.455,
+ "t1": 201.821,
+ "dur": 2.08,
+ "expEnd": 201.535,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "那边是墙,换个方向。",
+ "similarity": 1
+ },
+ {
+ "idx": 20,
+ "voiceId": "wall",
+ "t0": 204.221,
+ "t1": 206.591,
+ "dur": 2.08,
+ "expEnd": 206.301,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "那边是墙,换个方向。",
+ "similarity": 1
+ },
+ {
+ "idx": 21,
+ "voiceId": "wall_dir_l",
+ "t0": 206.597,
+ "t1": 207.751,
+ "dur": 1.04,
+ "expEnd": 207.637,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "左边是墙。",
+ "similarity": 1
+ },
+ {
+ "idx": 22,
+ "voiceId": "use_fail",
+ "t0": 210.888,
+ "t1": 218.481,
+ "dur": 7.36,
+ "expEnd": 218.248,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "这里没有用得上东西的地方,换个房间试试,或者先单机检查一下周围。",
+ "similarity": 0.9655
+ },
+ {
+ "idx": 23,
+ "voiceId": "stuck_cabinet_1",
+ "t0": 218.49,
+ "t1": 222.621,
+ "dur": 3.84,
+ "expEnd": 222.33,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "钥匙就在你身上,他能打开某一把锁。",
+ "similarity": 0.9333
+ },
+ {
+ "idx": 24,
+ "voiceId": "empty_1",
+ "t0": 256.245,
+ "t1": 261.501,
+ "dur": 5.2,
+ "expEnd": 261.445,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "房间里空空荡荡,只有你自己的回声。",
+ "similarity": 1
+ },
+ {
+ "idx": 25,
+ "voiceId": "empty_3",
+ "t0": 263.01,
+ "t1": 266.861,
+ "dur": 3.68,
+ "expEnd": 266.69,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "除了远处的水滴声,这里什么都没有。",
+ "similarity": 1
+ },
+ {
+ "idx": 26,
+ "voiceId": "room_cabinet",
+ "t0": 269.776,
+ "t1": 272.521,
+ "dur": 2.64,
+ "expEnd": 272.416,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "这个房间里立着一个铁柜。",
+ "similarity": 1
+ },
+ {
+ "idx": 27,
+ "voiceId": "unlock_cabinet",
+ "t0": 276.438,
+ "t1": 288.151,
+ "dur": 11.48,
+ "expEnd": 287.918,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "钥匙插进锁孔,缓缓转动,咔嗒一声,柜门开了,柜子深处躺着一枚保险丝,保险丝到手了。",
+ "similarity": 0.9714
+ },
+ {
+ "idx": 28,
+ "voiceId": "gd_next_fuse",
+ "t0": 288.159,
+ "t1": 291.631,
+ "dur": 3.28,
+ "expEnd": 291.439,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "保险丝到手,接下来去找电箱。",
+ "similarity": 1
+ },
+ {
+ "idx": 29,
+ "voiceId": "gd_box_dir",
+ "t0": 291.638,
+ "t1": 294.011,
+ "dur": 2.16,
+ "expEnd": 293.798,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "电箱在左面的房间。",
+ "similarity": 1
+ },
+ {
+ "idx": 30,
+ "voiceId": "gd_prog_2",
+ "t0": 430.009,
+ "t1": 432.751,
+ "dur": 2.56,
+ "expEnd": 432.569,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "铁柜已经打开,电还没有通。",
+ "similarity": 1
+ },
+ {
+ "idx": 31,
+ "voiceId": "fix_box",
+ "t0": 541.393,
+ "t1": 548.981,
+ "dur": 7.52,
+ "expEnd": 548.913,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "你把保险丝用力安静卡座,火花一闪,嗡鸣声响起,头顶的线路恢复供电。",
+ "similarity": 0.9153
+ },
+ {
+ "idx": 32,
+ "voiceId": "gd_gate_dir",
+ "t0": 548.988,
+ "t1": 551.361,
+ "dur": 2.32,
+ "expEnd": 551.308,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "大门在右面的房间。",
+ "similarity": 1
+ },
+ {
+ "idx": 33,
+ "voiceId": "room_gate",
+ "t0": 565.464,
+ "t1": 569.311,
+ "dur": 3.52,
+ "expEnd": 568.984,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "面前是一扇沉重的铁门。",
+ "similarity": 1
+ },
+ {
+ "idx": 34,
+ "voiceId": "escape",
+ "t0": 583.064,
+ "t1": 598.491,
+ "dur": 15.32,
+ "expEnd": 598.384,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "你搬动开关,电击轰,沉重的体能缓缓升起,刺眼的光线涌了进来,你眯起眼睛向前迈出了第一步,自由了,你逃出来了。",
+ "similarity": 0.9053
+ },
+ {
+ "idx": 35,
+ "voiceId": "time_band3",
+ "t0": 598.499,
+ "t1": 601.971,
+ "dur": 3.2,
+ "expEnd": 601.699,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "你逃出来了,用时不到10分钟。",
+ "similarity": 0.88
+ },
+ {
+ "idx": 36,
+ "voiceId": "again",
+ "t0": 601.979,
+ "t1": 607.241,
+ "dur": 4.96,
+ "expEnd": 606.939,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": "OK",
+ "heard": "单击屏幕再来一次下滑打开菜单。",
+ "similarity": 1
+ },
+ {
+ "idx": 37,
+ "voiceId": "replay",
+ "t0": 623.582,
+ "t1": 631.831,
+ "dur": 8.24,
+ "expEnd": 631.822,
+ "truncated": false,
+ "reason": "kill",
+ "verdict": null,
+ "heard": null,
+ "similarity": null
+ }
+ ],
+ "rows": [
+ {
+ "t0": 0.077,
+ "t1": 6.931,
+ "voiceId": "g0_intro",
+ "expected": "黑暗。你在废弃的地下设施里醒来,什么都想不起来。你必须找到出路。",
+ "heard": "黑暗,你在废弃的地下设施里醒来,什么都想不起来,你必须找到出路。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 8.132,
+ "t1": 12.741,
+ "voiceId": "g1_move",
+ "expected": "先活动一下。朝任意方向滑一下,走一小步。",
+ "heard": "先活动一下,朝任意方向滑一下,走一小。",
+ "similarity": 0.9697,
+ "verdict": "OK"
+ },
+ {
+ "t0": 12.741,
+ "t1": 14.361,
+ "voiceId": "skip_hint",
+ "expected": "单击可跳过旁白。",
+ "heard": "单击可跳过旁板。",
+ "similarity": 0.8571,
+ "verdict": "OK"
+ },
+ {
+ "t0": 79.417,
+ "t1": 82.151,
+ "voiceId": "g1_done",
+ "expected": "很好,你迈出了第一步。",
+ "heard": "很好,你迈出了第一。",
+ "similarity": 0.9412,
+ "verdict": "OK"
+ },
+ {
+ "t0": 82.159,
+ "t1": 87.881,
+ "voiceId": "g2_wall",
+ "expected": "这个房间不大,四面都是墙。再滑两下,试试碰碰墙。",
+ "heard": "这个房间不大,四面都是墙,再滑两下试试碰碰墙。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 87.887,
+ "t1": 90.241,
+ "voiceId": "wall",
+ "expected": "那边是墙,换个方向。",
+ "heard": "墙那边是墙,换个方向。",
+ "similarity": 0.9412,
+ "verdict": "OK"
+ },
+ {
+ "t0": 109.491,
+ "t1": 111.861,
+ "voiceId": "wall",
+ "expected": "那边是墙,换个方向。",
+ "heard": "那边是墙,换个方向。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 111.868,
+ "t1": 114.881,
+ "voiceId": "g3_left",
+ "expected": "跟着我指的路走。向左滑。",
+ "heard": "跟着我指的路走,向左画。",
+ "similarity": 0.9,
+ "verdict": "OK"
+ },
+ {
+ "t0": 117.264,
+ "t1": 119.171,
+ "voiceId": "g4_down",
+ "expected": "对。再向下滑。",
+ "heard": "对,再向下滑。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 123.032,
+ "t1": 127.171,
+ "voiceId": "g5_inspect",
+ "expected": "听到水声了吗?单击一下,仔细检查这个房间。",
+ "heard": "听到水声了吗?单击一下,仔细检查这个房间。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 130.808,
+ "t1": 132.421,
+ "voiceId": "g5_key",
+ "expected": "你摸到了一把钥匙!",
+ "heard": "你摸到了一把钥匙。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 132.433,
+ "t1": 143.391,
+ "voiceId": "g6_doubletap",
+ "expected": "钥匙能开锁。双击,是把物品用出去的用法。现在,双击试试。",
+ "heard": "匙钥匙能开锁,双击是把物品用出去的用法。现在双击试试。",
+ "similarity": 0.9787,
+ "verdict": "OK"
+ },
+ {
+ "t0": 143.398,
+ "t1": 145.291,
+ "voiceId": "inv_key",
+ "expected": "你身上带着一把钥匙。",
+ "heard": "你身上带着一把钥匙。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 145.299,
+ "t1": 149.451,
+ "voiceId": "g6_done",
+ "expected": "对,就是这样。接下来,靠你自己了。",
+ "heard": "对,就是这样,接下来靠你自己了。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 14.361,
+ "t1": 79.417,
+ "voiceId": null,
+ "expected": null,
+ "heard": "",
+ "similarity": null,
+ "verdict": "SFX_BGM_ONLY"
+ },
+ {
+ "t0": 90.241,
+ "t1": 109.491,
+ "voiceId": null,
+ "expected": null,
+ "heard": "",
+ "similarity": null,
+ "verdict": "SFX_BGM_ONLY"
+ },
+ {
+ "t0": 114.881,
+ "t1": 117.264,
+ "voiceId": null,
+ "expected": null,
+ "heard": "",
+ "similarity": null,
+ "verdict": "SFX_BGM_ONLY"
+ },
+ {
+ "t0": 119.171,
+ "t1": 123.032,
+ "voiceId": null,
+ "expected": null,
+ "heard": "",
+ "similarity": null,
+ "verdict": "SFX_BGM_ONLY"
+ },
+ {
+ "t0": 127.171,
+ "t1": 130.808,
+ "voiceId": null,
+ "expected": null,
+ "heard": "",
+ "similarity": null,
+ "verdict": "SFX_BGM_ONLY"
+ },
+ {
+ "t0": 149.46,
+ "t1": 156.591,
+ "voiceId": "g7_goal",
+ "expected": "找到能用这把钥匙打开的东西。需要帮助时,在原地双击。",
+ "heard": "找到能用这把钥匙打开的东西,需要帮助时在原地双击。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 179.164,
+ "t1": 184.151,
+ "voiceId": "empty_2",
+ "expected": "你伸出手,摸到的只有冰冷粗糙的墙壁。",
+ "heard": "你伸出手摸到的只有冰冷粗糙的墙壁。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 185.929,
+ "t1": 188.671,
+ "voiceId": "room_power",
+ "expected": "这个房间的墙上,有一只电箱。",
+ "heard": "这个房间的墙上有一只电箱。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 192.688,
+ "t1": 195.051,
+ "voiceId": "wall",
+ "expected": "那边是墙,换个方向。",
+ "heard": "那边是墙,换个方向。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 195.058,
+ "t1": 196.671,
+ "voiceId": "wall_dir_u",
+ "expected": "上边是墙。",
+ "heard": "上边是墙。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 199.455,
+ "t1": 201.821,
+ "voiceId": "wall",
+ "expected": "那边是墙,换个方向。",
+ "heard": "那边是墙,换个方向。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 204.221,
+ "t1": 206.591,
+ "voiceId": "wall",
+ "expected": "那边是墙,换个方向。",
+ "heard": "那边是墙,换个方向。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 206.597,
+ "t1": 207.751,
+ "voiceId": "wall_dir_l",
+ "expected": "左边是墙。",
+ "heard": "左边是墙。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 210.888,
+ "t1": 218.481,
+ "voiceId": "use_fail",
+ "expected": "这里没有用得上东西的地方。换个房间试试,或者先单击检查一下周围。",
+ "heard": "这里没有用得上东西的地方,换个房间试试,或者先单机检查一下周围。",
+ "similarity": 0.9655,
+ "verdict": "OK"
+ },
+ {
+ "t0": 156.591,
+ "t1": 179.164,
+ "voiceId": null,
+ "expected": null,
+ "heard": "",
+ "similarity": null,
+ "verdict": "SFX_BGM_ONLY"
+ },
+ {
+ "t0": 184.151,
+ "t1": 185.929,
+ "voiceId": null,
+ "expected": null,
+ "heard": "",
+ "similarity": null,
+ "verdict": "SFX_BGM_ONLY"
+ },
+ {
+ "t0": 188.671,
+ "t1": 192.688,
+ "voiceId": null,
+ "expected": null,
+ "heard": "",
+ "similarity": null,
+ "verdict": "SFX_BGM_ONLY"
+ },
+ {
+ "t0": 196.671,
+ "t1": 199.455,
+ "voiceId": null,
+ "expected": null,
+ "heard": "",
+ "similarity": null,
+ "verdict": "SFX_BGM_ONLY"
+ },
+ {
+ "t0": 201.821,
+ "t1": 204.221,
+ "voiceId": null,
+ "expected": null,
+ "heard": "",
+ "similarity": null,
+ "verdict": "SFX_BGM_ONLY"
+ },
+ {
+ "t0": 207.751,
+ "t1": 210.888,
+ "voiceId": null,
+ "expected": null,
+ "heard": "",
+ "similarity": null,
+ "verdict": "SFX_BGM_ONLY"
+ },
+ {
+ "t0": 218.49,
+ "t1": 222.621,
+ "voiceId": "stuck_cabinet_1",
+ "expected": "钥匙就在你身上。它能打开某一把锁。",
+ "heard": "钥匙就在你身上,他能打开某一把锁。",
+ "similarity": 0.9333,
+ "verdict": "OK"
+ },
+ {
+ "t0": 256.245,
+ "t1": 261.501,
+ "voiceId": "empty_1",
+ "expected": "房间里空空荡荡,只有你自己的回声。",
+ "heard": "房间里空空荡荡,只有你自己的回声。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 263.01,
+ "t1": 266.861,
+ "voiceId": "empty_3",
+ "expected": "除了远处的水滴声,这里什么都没有。",
+ "heard": "除了远处的水滴声,这里什么都没有。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 269.776,
+ "t1": 272.521,
+ "voiceId": "room_cabinet",
+ "expected": "这个房间里,立着一个铁柜。",
+ "heard": "这个房间里立着一个铁柜。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 276.438,
+ "t1": 288.151,
+ "voiceId": "unlock_cabinet",
+ "expected": "钥匙插进锁孔,缓缓转动。咔哒一声,柜门开了。柜子深处,躺着一枚保险丝。保险丝到手了。",
+ "heard": "钥匙插进锁孔,缓缓转动,咔嗒一声,柜门开了,柜子深处躺着一枚保险丝,保险丝到手了。",
+ "similarity": 0.9714,
+ "verdict": "OK"
+ },
+ {
+ "t0": 222.621,
+ "t1": 256.245,
+ "voiceId": null,
+ "expected": null,
+ "heard": "",
+ "similarity": null,
+ "verdict": "SFX_BGM_ONLY"
+ },
+ {
+ "t0": 261.501,
+ "t1": 263.01,
+ "voiceId": null,
+ "expected": null,
+ "heard": "",
+ "similarity": null,
+ "verdict": "SFX_BGM_ONLY"
+ },
+ {
+ "t0": 266.861,
+ "t1": 269.776,
+ "voiceId": null,
+ "expected": null,
+ "heard": "",
+ "similarity": null,
+ "verdict": "SFX_BGM_ONLY"
+ },
+ {
+ "t0": 272.521,
+ "t1": 276.438,
+ "voiceId": null,
+ "expected": null,
+ "heard": "",
+ "similarity": null,
+ "verdict": "SFX_BGM_ONLY"
+ },
+ {
+ "t0": 288.159,
+ "t1": 291.631,
+ "voiceId": "gd_next_fuse",
+ "expected": "保险丝到手。接下来,去找电箱。",
+ "heard": "保险丝到手,接下来去找电箱。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 291.638,
+ "t1": 294.011,
+ "voiceId": "gd_box_dir",
+ "expected": "电箱,在左面的房间。",
+ "heard": "电箱在左面的房间。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 430.009,
+ "t1": 432.751,
+ "voiceId": "gd_prog_2",
+ "expected": "铁柜已经打开。电还没有通。",
+ "heard": "铁柜已经打开,电还没有通。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 294.011,
+ "t1": 430.009,
+ "voiceId": null,
+ "expected": null,
+ "heard": "",
+ "similarity": null,
+ "verdict": "SFX_BGM_ONLY"
+ },
+ {
+ "t0": 541.393,
+ "t1": 548.981,
+ "voiceId": "fix_box",
+ "expected": "你把保险丝用力按进卡座。火花一闪,嗡鸣声响起——头顶的线路恢复供电了!",
+ "heard": "你把保险丝用力安静卡座,火花一闪,嗡鸣声响起,头顶的线路恢复供电。",
+ "similarity": 0.9153,
+ "verdict": "OK"
+ },
+ {
+ "t0": 548.988,
+ "t1": 551.361,
+ "voiceId": "gd_gate_dir",
+ "expected": "大门,在右面的房间。",
+ "heard": "大门在右面的房间。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 565.464,
+ "t1": 569.311,
+ "voiceId": "room_gate",
+ "expected": "面前,是一扇沉重的铁门。",
+ "heard": "面前是一扇沉重的铁门。",
+ "similarity": 1,
+ "verdict": "OK"
+ },
+ {
+ "t0": 432.751,
+ "t1": 541.393,
+ "voiceId": null,
+ "expected": null,
+ "heard": "",
+ "similarity": null,
+ "verdict": "SFX_BGM_ONLY"
+ },
+ {
+ "t0": 551.361,
+ "t1": 565.464,
+ "voiceId": null,
+ "expected": null,
+ "heard": "",
+ "similarity": null,
+ "verdict": "SFX_BGM_ONLY"
+ },
+ {
+ "t0": 583.064,
+ "t1": 598.491,
+ "voiceId": "escape",
+ "expected": "你扳动开关。电机轰鸣,沉重的铁门缓缓升起,刺眼的光线涌了进来。你眯起眼睛,向前迈出了第一步。自由了。你逃出来了。",
+ "heard": "你搬动开关,电击轰,沉重的体能缓缓升起,刺眼的光线涌了进来,你眯起眼睛向前迈出了第一步,自由了,你逃出来了。",
+ "similarity": 0.9053,
+ "verdict": "OK"
+ },
+ {
+ "t0": 569.311,
+ "t1": 583.064,
+ "voiceId": null,
+ "expected": null,
+ "heard": "",
+ "similarity": null,
+ "verdict": "SFX_BGM_ONLY"
+ },
+ {
+ "t0": 598.499,
+ "t1": 601.971,
+ "voiceId": "time_band3",
+ "expected": "你逃出来了,用时不到十分钟。",
+ "heard": "你逃出来了,用时不到10分钟。",
+ "similarity": 0.88,
+ "verdict": "OK"
+ },
+ {
+ "t0": 601.979,
+ "t1": 607.241,
+ "voiceId": "again",
+ "expected": "单击屏幕,再来一次。下滑,打开菜单。",
+ "heard": "单击屏幕再来一次下滑打开菜单。",
+ "similarity": 1,
+ "verdict": "OK"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tools/gen_voice_dual.py b/tools/gen_voice_dual.py
index b7769608..f8a2c78f 100644
--- a/tools/gen_voice_dual.py
+++ b/tools/gen_voice_dual.py
@@ -237,6 +237,20 @@ ESCAPE_LINES = [
("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, "电已经通了。去找那扇大门。"),
]
diff --git a/web/escape/audio/voice/female/gd_box_dir.m4a b/web/escape/audio/voice/female/gd_box_dir.m4a
new file mode 100644
index 00000000..559a1d7e
Binary files /dev/null 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
new file mode 100644
index 00000000..7579c597
Binary files /dev/null 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
new file mode 100644
index 00000000..6365ee1c
Binary files /dev/null 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
new file mode 100644
index 00000000..f68da7bd
Binary files /dev/null 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
new file mode 100644
index 00000000..82f3caff
Binary files /dev/null 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
new file mode 100644
index 00000000..8b094d6c
Binary files /dev/null and b/web/escape/audio/voice/female/gd_gate_dir.ogg differ
diff --git a/web/escape/audio/voice/female/gd_prog_1.m4a b/web/escape/audio/voice/female/gd_prog_1.m4a
new file mode 100644
index 00000000..beed374a
Binary files /dev/null and b/web/escape/audio/voice/female/gd_prog_1.m4a differ
diff --git a/web/escape/audio/voice/female/gd_prog_1.ogg b/web/escape/audio/voice/female/gd_prog_1.ogg
new file mode 100644
index 00000000..411fa107
Binary files /dev/null and b/web/escape/audio/voice/female/gd_prog_1.ogg differ
diff --git a/web/escape/audio/voice/female/gd_prog_2.m4a b/web/escape/audio/voice/female/gd_prog_2.m4a
new file mode 100644
index 00000000..c2c49cb1
Binary files /dev/null and b/web/escape/audio/voice/female/gd_prog_2.m4a differ
diff --git a/web/escape/audio/voice/female/gd_prog_2.ogg b/web/escape/audio/voice/female/gd_prog_2.ogg
new file mode 100644
index 00000000..33c29fff
Binary files /dev/null and b/web/escape/audio/voice/female/gd_prog_2.ogg differ
diff --git a/web/escape/audio/voice/female/gd_prog_3.m4a b/web/escape/audio/voice/female/gd_prog_3.m4a
new file mode 100644
index 00000000..61acb6e3
Binary files /dev/null and b/web/escape/audio/voice/female/gd_prog_3.m4a differ
diff --git a/web/escape/audio/voice/female/gd_prog_3.ogg b/web/escape/audio/voice/female/gd_prog_3.ogg
new file mode 100644
index 00000000..27535f86
Binary files /dev/null and b/web/escape/audio/voice/female/gd_prog_3.ogg differ
diff --git a/web/escape/audio/voice/female/wall_dir_d.m4a b/web/escape/audio/voice/female/wall_dir_d.m4a
new file mode 100644
index 00000000..b6196f09
Binary files /dev/null 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
new file mode 100644
index 00000000..9013daf8
Binary files /dev/null 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
new file mode 100644
index 00000000..29be1d18
Binary files /dev/null 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
new file mode 100644
index 00000000..a8147763
Binary files /dev/null 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
new file mode 100644
index 00000000..141e305f
Binary files /dev/null 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
new file mode 100644
index 00000000..121386e5
Binary files /dev/null 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
new file mode 100644
index 00000000..19669261
Binary files /dev/null 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
new file mode 100644
index 00000000..7fe8bbc1
Binary files /dev/null and b/web/escape/audio/voice/female/wall_dir_u.ogg differ
diff --git a/web/escape/audio/voice/male/gd_box_dir.m4a b/web/escape/audio/voice/male/gd_box_dir.m4a
new file mode 100644
index 00000000..cef690fd
Binary files /dev/null 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
new file mode 100644
index 00000000..f23584be
Binary files /dev/null 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
new file mode 100644
index 00000000..1e8801d5
Binary files /dev/null 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
new file mode 100644
index 00000000..e9777bae
Binary files /dev/null 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
new file mode 100644
index 00000000..7be844f8
Binary files /dev/null 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
new file mode 100644
index 00000000..e44b650d
Binary files /dev/null and b/web/escape/audio/voice/male/gd_gate_dir.ogg differ
diff --git a/web/escape/audio/voice/male/gd_prog_1.m4a b/web/escape/audio/voice/male/gd_prog_1.m4a
new file mode 100644
index 00000000..432a595a
Binary files /dev/null and b/web/escape/audio/voice/male/gd_prog_1.m4a differ
diff --git a/web/escape/audio/voice/male/gd_prog_1.ogg b/web/escape/audio/voice/male/gd_prog_1.ogg
new file mode 100644
index 00000000..8250439b
Binary files /dev/null and b/web/escape/audio/voice/male/gd_prog_1.ogg differ
diff --git a/web/escape/audio/voice/male/gd_prog_2.m4a b/web/escape/audio/voice/male/gd_prog_2.m4a
new file mode 100644
index 00000000..278ff11d
Binary files /dev/null and b/web/escape/audio/voice/male/gd_prog_2.m4a differ
diff --git a/web/escape/audio/voice/male/gd_prog_2.ogg b/web/escape/audio/voice/male/gd_prog_2.ogg
new file mode 100644
index 00000000..691ea1f1
Binary files /dev/null and b/web/escape/audio/voice/male/gd_prog_2.ogg differ
diff --git a/web/escape/audio/voice/male/gd_prog_3.m4a b/web/escape/audio/voice/male/gd_prog_3.m4a
new file mode 100644
index 00000000..ad04365f
Binary files /dev/null and b/web/escape/audio/voice/male/gd_prog_3.m4a differ
diff --git a/web/escape/audio/voice/male/gd_prog_3.ogg b/web/escape/audio/voice/male/gd_prog_3.ogg
new file mode 100644
index 00000000..0d7c7bc3
Binary files /dev/null and b/web/escape/audio/voice/male/gd_prog_3.ogg differ
diff --git a/web/escape/audio/voice/male/wall_dir_d.m4a b/web/escape/audio/voice/male/wall_dir_d.m4a
new file mode 100644
index 00000000..75ff2e34
Binary files /dev/null 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
new file mode 100644
index 00000000..ee7f878a
Binary files /dev/null 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
new file mode 100644
index 00000000..2c5f6f83
Binary files /dev/null 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
new file mode 100644
index 00000000..1def6409
Binary files /dev/null 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
new file mode 100644
index 00000000..1cbe11de
Binary files /dev/null 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
new file mode 100644
index 00000000..03f0995b
Binary files /dev/null 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
new file mode 100644
index 00000000..7aea8e30
Binary files /dev/null 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
new file mode 100644
index 00000000..dc5815a7
Binary files /dev/null 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 405905f6..e6a8747f 100644
--- a/web/escape/game.js
+++ b/web/escape/game.js
@@ -8,14 +8,17 @@
*
* 手势映射(偏离全局默认,引导中讲清):
* - 四向滑动:在相邻房间间移动,撞墙播闷响 + ERROR 震动 + wall 语音(每次都播;
- * PLAY 阶段只播「那边是墙」,教学腔方位句 guide_blunt_* 仅限引导期)
+ * 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_*)
* - 双击:使用物品(钥匙开柜 / 保险丝修电箱 / 通电后开大门);
- * 无可用目标时播报背包 + L2 级明示提示
+ * 无可用目标时播报背包/use_fail + L1 级意象暗示(escape-state round2 B4:
+ * 原直通 L2 明示把答案白送给没卡关的玩家,降级为 L1;答案仍锁在计时器
+ * L2/L3 与进展链里)
* - 引导阶段下滑:跳过引导(仅限答案不含滑动的步骤 G0/G3/G5/G6;G1/G2 任意
* 滑动即答案、G4 的答案就是下滑——这些步骤下滑 = 作答,不可跳过);
* ENDING 下滑:菜单
@@ -31,12 +34,23 @@
* 修电箱/首到关键房间/逃脱)清空已播级别,下段静默期从 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)播完后,按房间图计算目标房间相对当前
- * 房间的真实方位,接播 gd_room_l/r/u/d 之一(游戏反馈层全档位;目标在同一
- * 房间不播;方向 = escape 移动坐标系,up/down/left/right 滑动方向即方位)。
- * 引导期 g7_goal 不接(引导本身逐句带路,且引导链被 playtest 断言冻结)。
+ * 房间的真实方位接播方位句;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 内置,本层只发语义震动
@@ -80,6 +94,16 @@ const KEY_ROOMS = {
// 空房环境音轮换(弱化版风声/嗡鸣)
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',
@@ -110,6 +134,10 @@ export class EscapeGame extends Game {
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.startElapsedMs = 0;
// 单击跳过:当前 voice 句是否可跳(每次 voice 播放时更新,见 _playVoice)
this._voiceSkippable = false;
@@ -118,10 +146,15 @@ export class EscapeGame extends Game {
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 内判断是否链播新句)
// 菜单
@@ -160,12 +193,14 @@ export class EscapeGame extends Game {
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(); }
- else if (g === G.TAP) { this.audio.stopVoice(); this._enterPlay(true); }
+ // 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);
@@ -174,7 +209,9 @@ export class EscapeGame extends Game {
this._onPlayGesture(g);
break;
case Phase.ENDING:
- if (g === G.TAP) this._restart();
+ // 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:
@@ -227,6 +264,13 @@ export class EscapeGame extends Game {
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();
@@ -236,16 +280,24 @@ export class EscapeGame extends Game {
else done();
}
- _onGuideGesture(g) {
+ _onGuideGesture(g, { fromCache = false } = {}) {
// 下滑跳过仅限"答案不含滑动"的步骤(G0/G3/G5/G6):G1/G2 的正确答案是
// 任意滑动、G4 的答案就是下滑——这些步骤下滑 = 作答,不可跳过(13 号文档
// 裁定;round2-live:g1_move 明说"朝任意方向滑一下",玩家下滑却被保送
- // 自由探索,教学链就此断裂)
- if (g === G.DOWN && this.guideStep !== 1 && this.guideStep !== 2 && this.guideStep !== 4) {
+ // 自由探索,教学链就此断裂)。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) 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 首局插播一次
@@ -316,10 +368,17 @@ export class EscapeGame extends Game {
this._session++; // 作废进行中的引导语音链
this._clearTimers();
this.audio.stopVoice();
+ this._guidePending = null;
try { localStorage.setItem(GUIDE_DONE_KEY, '1'); } catch (_) { /* 隐私模式 */ }
- this._speak(['voice/guide_skip'], () => this._enterPlay(true), { skippable: false });
- // 语音缺失兜底
- this._timer(() => { if (this.phase === Phase.GUIDE) this._enterPlay(true); }, 5000);
+ 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);
}
/**
@@ -341,14 +400,26 @@ export class EscapeGame extends Game {
// ---------- 进入 / 重开 PLAY ----------
- /** @param {boolean} announce 是否播 start_room(跳过引导/二周目直接进入时) */
- _enterPlay(announce) {
+ /**
+ * @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();
- if (announce) this._playVoice('voice/start_room');
+ 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');
+ }
+ }
this._playAmbient();
}
@@ -397,22 +468,37 @@ export class EscapeGame extends Game {
if (nx < 0 || nx > 2 || ny < 0 || ny > 2) {
this.audio.play('sfx/wall');
vibrate('error');
- // 撞墙极简反馈:只播「那边是墙,换个方向。」(round1-live 修复:原接播
- // guide_blunt_「手指贴着屏幕,向 X 推一下」教学腔方位句,中后期出戏;
- // 教学腔仅限引导期 G2/G3 纠错路径,见 _onGuideGesture/_guideWrong)
- this._playVoice('voice/wall');
+ // 撞墙反馈:先播「那边是墙,换个方向。」,播完接播陈述式方位句
+ // wall_dir_「左边是墙。」(escape-state round2 B1:round1-live 回退掉的
+ // 是教学腔措辞,方位信息本身保留;复用 14-F2.3 原接线模式——被下一次语音
+ // 顶掉则自然作废不迟到误导;同方向 10s 限频 1 次防贴墙连撞连播)。
+ // 方位 = 滑动方向(escape 移动坐标系),按真实撞墙方向取句,归因真话。
+ 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;
}
this.x = nx;
this.y = ny;
this.audio.play('sfx/step', { volume: 0.8 });
this._playAmbient();
+ this._freshEmpty = null; // 离开首访句刚播过的空房:单击检查恢复播 empty_*
// 首次到达关键房间:进展事件 + 房间描述
const key = `${nx},${ny}`;
if (KEY_ROOMS[key] && !this.visited.has(key)) {
this.visited.add(key);
this._progressEvent();
this._playVoice(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]);
}
}
@@ -454,10 +540,12 @@ export class EscapeGame extends Game {
this._progressEvent();
this.audio.play('sfx/pickup');
vibrate('pickup');
- // H1(round4):钥匙到手句后接播铁柜房方位(下一目标 = cabinet (2,1))。
+ // 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));
+ emit('voice/found_key', false,
+ () => this._playGoalDir(STAGES.cabinet.target, 'voice/gd_cabinet_dir', 'voice/gd_room_r'));
}, 800); // 先让环境音进耳朵再播描述
} else {
describe('voice/empty_2');
@@ -470,12 +558,11 @@ export class EscapeGame extends Game {
describe(this.powerOn ? 'voice/inspect_gate_ready' : 'voice/inspect_gate');
} else if (x === 1 && y === 1) {
describe('voice/start_room');
- } else if (x === 1 && y === 0) {
- describe('voice/empty_1');
- } else if (x === 0 && y === 1) {
- describe('voice/empty_2');
} else {
- describe('voice/empty_3'); // (1,2)、(2,0)
+ // 空房 (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');
}
}
@@ -493,11 +580,12 @@ export class EscapeGame extends Game {
// G4(round3):拾取反馈链尾接下一目标句「保险丝到手。接下来,去找电箱。」
// (游戏反馈层全档位,gd_economy 先例——走游戏语音链不走向导通道)——堵上
// P2-round2 P0「保险丝到手后目标悬空:没有新指令、没有下文」。
- // H1(round4):目标句播完接播目标房间方位句(P3 唯一拒因:目标无方位)。
+ // 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),
+ onComplete: () => this._playGoalDir(STAGES.box.target, 'voice/gd_box_dir', 'voice/gd_room_l'),
}),
});
} else if (x === 0 && y === 0 && this.hasFuse && !this.powerOn) {
@@ -508,22 +596,28 @@ export class EscapeGame extends Game {
this.audio.play('sfx/spark');
this.audio.play('sfx/power_on', { volume: 0.8 });
vibrate('success');
- // H1(round4):通电目标句后接播大门方位(下一目标 = gate (2,2))。
+ // H1(round4)/ B2(escape-state round2):通电目标句后接播"目标名+方位"
+ // 一体句 gd_gate_dir(大门 (2,2) 固定为右)。
this._playVoice('voice/fix_box', {
skippable: false, // 关键获得不可跳
- onComplete: () => this._playGoalDir(STAGES.gate.target),
+ 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 + L2 级明示(ui_deny 已退役)
+ // 兜底: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;
- this._speak([
- inKeyRoom ? 'voice/use_fail' : this._inventoryVoice(),
- `voice/stuck_${this._stage()}_2`,
- ]);
+ 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);
}
}
@@ -556,10 +650,16 @@ export class EscapeGame extends Game {
return dy < 0 ? 'voice/gd_room_u' : 'voice/gd_room_d';
}
- /** H1:目标句播完后接播目标房间方位句(游戏反馈层全档位,gd_next_fuse 先例)。 */
- _playGoalDir(target) {
+ /**
+ * H1(round4)/ B2(escape-state round2):目标句播完后接播目标房间方位句
+ * (游戏反馈层全档位,gd_next_fuse 先例)。B2:三处固定接线点传 namedId +
+ * expectedId,计算方位与枚举一致时播"目标名+方位"一体句;异常路径不符时
+ * 回落原 gd_room_*(归因真话,原句保留不删)。
+ */
+ _playGoalDir(target, namedId = null, expectedId = null) {
const dir = this._roomDirVoice(target);
- if (dir) this._playVoice(dir);
+ if (!dir) return;
+ this._playVoice(namedId && dir === expectedId ? namedId : dir);
}
// ---------- 卡关检测与三级提示 ----------
@@ -573,12 +673,19 @@ export class EscapeGame extends Game {
this._lastActiveAt = Date.now();
this._stuckShown = new Set();
this._lastL3At = 0;
+ // B3 无进展计时:进入/重开 PLAY 时起算点与闸门一并复位
+ this._lastProgressAt = Date.now();
+ this._gesturesSinceProgress = 0;
+ this._progShown = new Set();
}
/** 进展事件:清空已播提示级别,下段静默期从 L1 重新分级(计时由手势重置)。 */
_progressEvent() {
this._stuckShown.clear();
this._lastL3At = 0;
+ // B3:无进展计时起算点与活跃手势计数复位(_progShown 按阶段记账,不清)
+ this._lastProgressAt = Date.now();
+ this._gesturesSinceProgress = 0;
}
/**
@@ -609,6 +716,18 @@ export class EscapeGame extends Game {
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' });
+ }
}
/** 从当前坐标到目标房间的导航语音(每轴一句,最多两句)。 */
@@ -677,6 +796,9 @@ export class EscapeGame extends Game {
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)
@@ -696,6 +818,7 @@ export class EscapeGame extends Game {
/** 逃脱结算 BGM:drone 淡出后淡入 bgm/menu(幂等,仅在仍是结算态时生效)。 */
_startEndingBgm() {
if (this._endingBgmStarted || this.phase !== Phase.ENDING) return;
+ this._settleBusy = false; // B7:结算链播完(或 30s 兜底)后解除单击守卫
this._endingBgmStarted = true;
this.audio.play('bgm/menu', { volume: 0.5, fadeMs: 1500 });
}
diff --git a/web/escape/test/playtest.mjs b/web/escape/test/playtest.mjs
index 66019252..5dd59475 100644
--- a/web/escape/test/playtest.mjs
+++ b/web/escape/test/playtest.mjs
@@ -418,13 +418,16 @@ section('A. 关键路径:引导 → 解谜 → 逃脱 → 结算 → 菜单
assert(rec.count('voice/wall') >= 1, '撞墙播 wall 语音');
await finishVoices(1);
- // ---- 边界:双击无目标兜底 = 背包/use_fail + L2 明示,不崩 ----
+ // ---- 边界:双击无目标兜底 = 背包/use_fail + L1 意象暗示,不崩(escape-state
+ // round2 B4 变更登记:原直通 L2 明示 stuck_*_2 把答案白送给没卡关的玩家,
+ // 降级为 L1 意象句 stuck_*_1——不点明房间与动作;答案仍锁在计时器 L2/L3) ----
const errCnt2 = vibrateCount(ERR_PAT);
await gs(game, 'doubletap'); // 电箱房无保险丝
assert(vibrateCount(ERR_PAT) === errCnt2 + 1, '双击无目标发 error 震动');
assert(rec.count('voice/use_fail') === 1, '兜底播 use_fail(关键房间)');
await finishVoices(1);
- assert(rec.count('voice/stuck_key_2') === 1, '兜底追加 L2 明示 stuck_key_2');
+ assert(rec.count('voice/stuck_key_1') === 1, 'B4:兜底追加 L1 意象暗示 stuck_key_1(原 L2 已降级)');
+ assert(rec.count('voice/stuck_key_2') === 0, 'B4:兜底不再直通 L2 明示 stuck_key_2');
await finishVoices(1);
game.stop();
@@ -501,7 +504,16 @@ section('A2. 冷启动引导链:intro 后 g1_move 自动播出,零单击走
await gs(g2b.game, 'down'); // G3 答案 = 左滑,下滑 = 跳过
assert(g2b.rec.count('voice/guide_skip') === 1, 'G3 下滑仍跳过引导');
await tick(VOICE_END_SLACK);
- assert(g2b.game.phase === 'PLAY', 'G3 下滑跳过直达 PLAY');
+ // B8(escape-state round2 变更登记):跳过确认句后、进 PLAY 前补播目标句
+ // g7_goal——跳过链从 1 句变 2 句,第一句播完时仍在 GUIDE
+ assert(g2b.rec.count('voice/g7_goal') === 1, 'B8:guide_skip 后补播 g7_goal');
+ assert(g2b.game.phase === 'GUIDE', 'B8:g7_goal 补播期间尚未进 PLAY');
+ await tick(VOICE_END_SLACK);
+ assert(g2b.game.phase === 'PLAY', 'G3 下滑跳过(补播 g7_goal 后)直达 PLAY');
+ assert(g2b.rec.count('voice/start_room') === 1, 'B8:跳过路径 g7_goal 后播 start_room');
+ const goalT = g2b.rec.last('voice/g7_goal').t;
+ const startT = g2b.rec.last('voice/start_room').t;
+ assert(goalT < startT, 'B8:跳过路径播报顺序 guide_skip → g7_goal → start_room');
g2b.game.stop();
}
@@ -538,7 +550,7 @@ section('B. 引导纠错与跳过');
assert(game.guideStep === 3 && game._guideBusy, '左滑后正常推进(G4 语音中)');
game.stop();
- // 下滑跳过引导
+ // 下滑跳过引导(B8:确认句后补播 g7_goal 再进 PLAY)
store.clear();
const g2 = await newGame();
g2.game.start();
@@ -546,7 +558,9 @@ section('B. 引导纠错与跳过');
await gs(g2.game, 'down');
assert(g2.rec.count('voice/guide_skip') === 1, '引导期下滑播 guide_skip');
await tick(VOICE_END_SLACK);
- assert(g2.game.phase === 'PLAY', '下滑跳过引导直达 PLAY');
+ assert(g2.rec.count('voice/g7_goal') === 1, 'B8:guide_skip 后补播 g7_goal');
+ await tick(VOICE_END_SLACK);
+ assert(g2.game.phase === 'PLAY', '下滑跳过引导(补播 g7_goal 后)直达 PLAY');
assert(store.get('soundgame.escape.guideDone') === '1', '跳过引导也写 guideDone');
assert(g2.rec.count('voice/start_room') === 1, '跳过引导播 start_room');
g2.game.stop();
@@ -807,6 +821,466 @@ section('E. 设置边界');
game.stop();
}
+// ================================================================
+// 场景 G:B1 撞墙方位句(wall_dir_* 接播 + 同向 10s 限频 + 打断作废;引导期不播)
+// ================================================================
+section('G. B1 撞墙方位句(escape-state round2 F1)');
+{
+ // 引导期 G2 伪撞墙不播方位句(B1 仅 PLAY 阶段)
+ store.clear();
+ const gd = await newGame();
+ gd.game.start();
+ await flush();
+ await tick(VOICE_END_SLACK);
+ await gs(gd.game, 'tap'); // G0 → G1
+ await finishVoices(2);
+ await gs(gd.game, 'up'); // G1
+ await finishVoices(2);
+ await gs(gd.game, 'up'); // G2 ×2(伪撞墙)
+ await gs(gd.game, 'left');
+ await finishVoices(2);
+ assert(gd.rec.count('voice/wall') >= 2, '前提:G2 撞墙播过 wall 语音');
+ for (const d of ['l', 'r', 'u', 'd']) {
+ assert(gd.rec.count(`voice/wall_dir_${d}`) === 0, `B1:引导期 G2 撞墙不播 wall_dir_${d}(仅 PLAY)`);
+ }
+ gd.game.stop();
+
+ store.clear();
+ store.set('soundgame.escape.guideDone', '1');
+ const { game, rec } = await newGame();
+ game.start();
+ await flush();
+ await gs(game, 'tap'); // REOPEN → PLAY(start_room → g7_goal)
+ await finishVoices(2);
+ await gs(game, 'left'); // (0,1) B5a 首访 empty_2
+ await gs(game, 'up'); // (0,0) room_power
+ await finishVoices(2);
+
+ // ① 撞墙后 wall 播完接播方位句(顺序:wall → wall_dir_u)
+ await gs(game, 'up'); // 撞北墙
+ assert(rec.count('voice/wall_dir_u') === 0, 'B1:wall 播放中方位句未播(接播不抢话)');
+ await finishVoices(1);
+ assert(rec.count('voice/wall_dir_u') === 1, 'B1:wall 播完接播 wall_dir_u「上边是墙。」');
+ assert(rec.last('voice/wall_dir_u').t >= rec.last('voice/wall').t + 1000, 'B1:方位句在 wall 自然播完之后');
+ await finishVoices(1);
+
+ // ② 打断作废:撞左墙 wall 未播完再撞上墙——左墙方位句作废;
+ // 上墙 10s 限频内(① 刚播过)不再重复
+ await gs(game, 'left'); // wall(左)
+ await gs(game, 'up'); // wall(上)顶掉上一句
+ await finishVoices(2);
+ assert(rec.count('voice/wall_dir_l') === 0, 'B1:wall 被打断则方位句自然作废(wall_dir_l 未播)');
+ assert(rec.count('voice/wall_dir_u') === 1, 'B1:同向 10s 限频——上墙方位句不重复(仍 1 次)');
+
+ // ③ 同向限频与恢复:左墙首播 → 10s 内再撞不重复 → 窗口过后恢复
+ await gs(game, 'left'); // 左墙(② 的接播被作废,未播过 → 本次接播)
+ await finishVoices(2);
+ assert(rec.count('voice/wall_dir_l') === 1, 'B1:被作废的方向补撞后方位句正常接播');
+ await gs(game, 'left'); // 10s 窗口内再撞左墙
+ await finishVoices(2);
+ assert(rec.count('voice/wall_dir_l') === 1, 'B1:同向 10s 内限频 1 次(wall_dir_l 不重复)');
+ await tick(10000);
+ await gs(game, 'left');
+ await finishVoices(2);
+ assert(rec.count('voice/wall_dir_l') === 2, 'B1:限频窗口过后同向方位句恢复');
+ game.stop();
+}
+
+// ================================================================
+// 场景 H:B2 目标名+方位一体句(gd_*_dir 替换三处接播,异常路径回落 gd_room_*)
+// ================================================================
+section('H. B2 目标名+方位一体句(escape-state round2 F2)');
+{
+ store.clear();
+ store.set('soundgame.escape.guideDone', '1');
+ const { game, rec } = await newGame();
+ game.start();
+ await flush();
+ await gs(game, 'tap'); // REOPEN → PLAY (1,1)
+ await finishVoices(2); // start_room + g7_goal
+
+ // ① found_key 链尾 → gd_cabinet_dir(储物间 (0,2) → 铁柜房 (2,1),固定为右)
+ game.x = 0; game.y = 2; // 测试钩子:瞬移储物间
+ await gs(game, 'tap'); // 检查 → 拾钥匙
+ await tick(800); // found_key 起播
+ await finishVoices(2); // found_key → gd_cabinet_dir
+ assert(rec.count('voice/found_key') === 1, '前提:拾钥匙播 found_key');
+ assert(rec.count('voice/gd_cabinet_dir') === 1, 'B2:found_key 链尾播 gd_cabinet_dir「铁柜,在右面的房间。」');
+ assert(rec.last('voice/gd_cabinet_dir').t > rec.last('voice/found_key').t, 'B2:一体句在 found_key 之后');
+ assert(rec.count('voice/gd_room_r') === 0, 'B2:正常路径不再播无目标名的 gd_room_r');
+
+ // ② unlock_cabinet/gd_next_fuse 链尾 → gd_box_dir(铁柜房 → 电箱房 (0,0),固定为左)
+ game.x = 2; game.y = 1; // 钩子:瞬移铁柜房
+ await gs(game, 'doubletap');
+ await finishVoices(3); // unlock_cabinet → gd_next_fuse → gd_box_dir
+ assert(rec.count('voice/gd_box_dir') === 1, 'B2:gd_next_fuse 链尾播 gd_box_dir「电箱,在左面的房间。」');
+ assert(rec.last('voice/gd_box_dir').t > rec.last('voice/gd_next_fuse').t, 'B2:一体句在 gd_next_fuse 之后');
+ assert(rec.count('voice/gd_room_l') === 0, 'B2:正常路径不再播 gd_room_l');
+
+ // ③ fix_box 链尾 → gd_gate_dir(电箱房 → 大门 (2,2),固定为右)
+ game.x = 0; game.y = 0; // 钩子:瞬移电箱房
+ await gs(game, 'doubletap');
+ await finishVoices(2); // fix_box → gd_gate_dir
+ assert(rec.count('voice/gd_gate_dir') === 1, 'B2:fix_box 链尾播 gd_gate_dir「大门,在右面的房间。」');
+ assert(rec.last('voice/gd_gate_dir').t > rec.last('voice/fix_box').t, 'B2:一体句在 fix_box 之后');
+
+ // ④ 异常路径回落:计算方位与枚举不符 → 回落原 gd_room_*(归因真话,原句保留接线)
+ game.x = 1; game.y = 1; // 钩子:异常位置(正常链上不存在)
+ game._playGoalDir([0, 1], 'voice/gd_cabinet_dir', 'voice/gd_room_r'); // 目标在西 ≠ 枚举右
+ await flush();
+ assert(rec.count('voice/gd_room_l') === 1, 'B2:方位与枚举不符时回落 gd_room_*(原句保留接线)');
+ await finishVoices(1);
+ game._playGoalDir([1, 1], 'voice/gd_cabinet_dir', 'voice/gd_room_r'); // 同房间不播(H1 原语义)
+ await flush();
+ assert(rec.count('voice/gd_room_l') === 1 && rec.count('voice/gd_cabinet_dir') === 1,
+ 'B2:目标在同一房间不播(H1 原语义不变)');
+ game.stop();
+}
+
+// ================================================================
+// 场景 I:B3 无进展 150s 活跃计时器(gd_prog 进度重述)
+// 边界:key 阶段不走 / ≥5 手势闸门(挂机不触)/ 每段限 1 次 /
+// 有进展时复位 / 待播期间操作作废且计入已播
+// ================================================================
+section('I. B3 无进展活跃计时器(escape-state round2 F3)');
+{
+ store.clear();
+ store.set('soundgame.escape.guideDone', '1');
+ const { audio, game, rec } = await newGame();
+ game.start();
+ await flush();
+ await gs(game, 'tap'); // REOPEN → PLAY
+ await finishVoices(2);
+ // 往返手势一律 (1,1)↔(0,1)(空房,避开关键房间的首到进展事件)
+ const wander = async (n) => { for (let i = 0; i < n; i++) await gs(game, i % 2 ? 'right' : 'left'); };
+
+ // ① key 阶段不走本通道:无进展 200s + 活跃 5 手势 → 无 gd_prog
+ await wander(5);
+ game._lastProgressAt = now - 200000; // 钩子:模拟 200s 无进展
+ game._checkStuck();
+ await flush();
+ assert(rec.count('voice/gd_prog_1') === 0, 'B3:key 阶段不走本通道(L1/L2/L3 已覆盖)');
+ await tick(1600); // 消化首访 empty_2
+
+ // ② 拾钥匙进 cabinet 阶段(进展事件复位计时)
+ game.x = 0; game.y = 2; // 钩子:瞬移储物间
+ await gs(game, 'tap');
+ await tick(800);
+ await finishVoices(2); // found_key + gd_cabinet_dir
+ assert(game._stage() === 'cabinet' && game._gesturesSinceProgress === 0,
+ 'B3 前提:cabinet 阶段,进展事件已复位计数');
+
+ // ③ 闸门:无进展 151s 但活跃手势 <5(挂机/低活跃)→ 不触
+ game._lastProgressAt = now - 151000;
+ game._checkStuck();
+ await flush();
+ assert(rec.count('voice/gd_prog_1') === 0, 'B3:活跃手势 <5 不触(挂机交给 60s L1 体系)');
+
+ // ④ 活跃触发:5 手势后无进展 151s → gd_prog_1 到达一次
+ game.x = 1; game.y = 1;
+ await wander(5);
+ await tick(1600);
+ game._checkStuck(); // _lastProgressAt 仍为 ② 的钩子值(移动不复位无进展计时)
+ await flush();
+ assert(rec.count('voice/gd_prog_1') === 1, 'B3:活跃(≥5 手势)无进展 150s 播 gd_prog_1');
+
+ // ⑤ 每进展段限 1 次:再到点不重复
+ await finishVoices(1);
+ game._lastProgressAt = now - 200000;
+ game._checkStuck();
+ await flush();
+ assert(rec.count('voice/gd_prog_1') === 1, 'B3:gd_prog_1 每进展段限 1 次');
+
+ // ⑥ 有进展时复位:开柜(进展事件)→ 计数清零、计时重起,不立即触下一段
+ game.x = 2; game.y = 1;
+ await gs(game, 'doubletap'); // 开柜
+ assert(game._gesturesSinceProgress === 0, 'B3:进展事件后活跃手势计数清零(复位)');
+ game._checkStuck(); // _lastProgressAt 刚复位,远不足 150s
+ await flush();
+ assert(rec.count('voice/gd_prog_2') === 0, 'B3:有进展时计时器复位(不立即触 gd_prog_2)');
+ await finishVoices(3); // unlock 链
+
+ // ⑦ box 段可再触一次
+ game.x = 1; game.y = 1;
+ await wander(5);
+ await tick(1600);
+ game._lastProgressAt = now - 151000;
+ game._checkStuck();
+ await flush();
+ assert(rec.count('voice/gd_prog_2') === 1, 'B3:box 阶段活跃无进展 150s 播 gd_prog_2');
+ await finishVoices(1);
+
+ // ⑧ 待播期间操作作废且计入已播(每段限 1 次不重发)
+ game.x = 0; game.y = 0;
+ await gs(game, 'doubletap'); // 修电 → gate 阶段(进展复位)
+ await finishVoices(2); // fix_box + gd_gate_dir
+ game.x = 1; game.y = 1;
+ await wander(5);
+ await tick(1600);
+ await gs(game, 'tap'); // 单击检查
+ await tick(800); // 检查描述播放中
+ assert(audio.isVoicePlaying(), 'B3 前提:检查描述播放中');
+ game._lastProgressAt = now - 151000;
+ game._checkStuck(); // 到点 → gd_prog_3 只入队
+ assert(rec.count('voice/gd_prog_3') === 0, 'B3:语音播放中 gd_prog 只入队不开播(不顶句)');
+ await gs(game, 'left'); // 玩家操作 → 待播 gd_prog_3 作废
+ await finishVoices(2);
+ assert(rec.count('voice/gd_prog_3') === 0, 'B3:待播期间操作即作废(不播出)');
+ assert(game._progShown.has('gate'), 'B3:作废仍计入已播(每进展段限 1 次)');
+ game._lastProgressAt = now - 200000;
+ game._checkStuck();
+ await flush();
+ assert(rec.count('voice/gd_prog_3') === 0, 'B3:作废的 gd_prog 不重发');
+ game.stop();
+}
+
+// ================================================================
+// 场景 J:B4 双击兜底降级(背包 + L1 意象;去重;L2 仍由计时器到达)
+// ================================================================
+section('J. B4 双击兜底降级(escape-state round2 F4)');
+{
+ store.clear();
+ store.set('soundgame.escape.guideDone', '1');
+ const { game, rec } = await newGame();
+ game.start();
+ await flush();
+ await gs(game, 'tap'); // REOPEN → PLAY (1,1) 非关键房
+ await finishVoices(2);
+
+ // 非关键房兜底 = 背包 + L1 意象暗示(不再直通 L2);播了即记账
+ await gs(game, 'doubletap');
+ assert(rec.count('voice/inv_empty') === 1, 'B4:非关键房兜底先播背包 inv_empty');
+ await finishVoices(2);
+ assert(rec.count('voice/stuck_key_1') === 1, 'B4:兜底接 L1 意象暗示 stuck_key_1');
+ assert(rec.count('voice/stuck_key_2') === 0, 'B4:兜底不直通 L2 明示 stuck_key_2');
+ assert(game._stuckShown.has('L1'), 'B4:兜底播 L1 即记账(沿用 _stuckShown 去重)');
+
+ // 同进展段再次兜底不重复 L1 意象句(既有去重)
+ await gs(game, 'doubletap');
+ await finishVoices(2);
+ assert(rec.count('voice/inv_empty') === 2, 'B4:再次兜底仍播背包');
+ assert(rec.count('voice/stuck_key_1') === 1, 'B4:同进展段再次兜底不重复 L1 意象句');
+
+ // 降级后 L2 仍可达:L1 已记账 → 60s 不再重复(整级跳过,stuck_ask 亦不播);
+ // 120s 无操作 L2 明示照常到达(难度不降)
+ await tick(65000);
+ assert(rec.count('voice/stuck_ask') === 0, 'B4:L1 已记账,60s 无操作不再重复 L1 级');
+ await tick(60000);
+ assert(rec.count('voice/stuck_key_2') === 1, 'B4:降级后 L2 仍由计时器到达(120s 无操作)');
+ game.stop();
+}
+
+// ================================================================
+// 场景 K:B5a 空房首访描述(首访到达 / 重访不重复 / 单击检查语义 / 非进展事件)
+// ================================================================
+section('K. B5a 空房首访描述(escape-state round2 F5)');
+{
+ store.clear();
+ store.set('soundgame.escape.guideDone', '1');
+ const { game, rec } = await newGame();
+ game.start();
+ await flush();
+ await gs(game, 'tap'); // REOPEN → PLAY (1,1)
+ await finishVoices(2);
+
+ // 首访空房不算进展事件:不清卡关已播级别
+ game._stuckShown.add('L1'); // 钩子:模拟本段已播 L1
+ await gs(game, 'up'); // (1,0) 首访
+ assert(game._stuckShown.has('L1'), 'B5a:空房首访不算进展事件(不清 _stuckShown)');
+ game._stuckShown.clear(); // 复位钩子
+
+ // 首访 (1,0) → empty_1;本房间内单击不再重复首访句;重访零语音;重访后单击照常
+ assert(rec.count('voice/empty_1') === 1, 'B5a:空房首访补播 empty_1(一次性听觉锚)');
+ await finishVoices(1);
+ await gs(game, 'tap'); // 首访句刚播过的房间内单击
+ await tick(900);
+ assert(rec.count('voice/empty_1') === 1, 'B5a:首访句刚播过的房间内单击不重复同一句');
+ await gs(game, 'down'); // (1,1)
+ await gs(game, 'up'); // (1,0) 重访
+ await tick(900);
+ assert(rec.count('voice/empty_1') === 1, 'B5a:空房重访零语音(不重复首访句)');
+ await gs(game, 'tap'); // 重访后单击检查
+ await tick(900);
+ assert(rec.count('voice/empty_1') === 2, 'B5a:重访后单击检查照常播 empty_1(语义不变)');
+ await finishVoices(1);
+
+ // 四房映射:(0,1)→empty_2、(1,2)→empty_3、(2,0)→empty_3(对称水滴共用合法记忆点)
+ await gs(game, 'down'); // (1,1)
+ await gs(game, 'left'); // (0,1) 首访
+ assert(rec.count('voice/empty_2') === 1, 'B5a:(0,1) 首访播 empty_2');
+ await finishVoices(1);
+ await gs(game, 'right'); // (1,1)
+ await gs(game, 'down'); // (1,2) 首访
+ assert(rec.count('voice/empty_3') === 1, 'B5a:(1,2) 首访播 empty_3');
+ await finishVoices(1);
+ game.x = 2; game.y = 1; // 钩子:瞬移铁柜房(已 visited 与否不影响下行)
+ game.visited.add('2,1');
+ await gs(game, 'up'); // (2,0) 首访
+ assert(rec.count('voice/empty_3') === 2, 'B5a:(2,0) 首访复用 empty_3(两间"远处水滴"对称分布)');
+ await finishVoices(1);
+ game.stop();
+}
+
+// ================================================================
+// 场景 L:B6 引导 busy 窗口手势缓存(补判 / 答错路径 / 不触发跳过 / 仅 1 个)
+// ================================================================
+section('L. B6 引导 busy 手势缓存(escape-state round2 F7)');
+{
+ store.clear();
+ const { game, rec } = await newGame();
+ game.start();
+ await flush();
+ await tick(VOICE_END_SLACK); // intro 播完(自动推进 1.2s 未到点)
+ await gs(game, 'tap'); // G0 → _advance(1) busy
+
+ // ① busy 窗口手势缓存:不当场判定,解锁后按新步骤补判(合法 = 作答)
+ await gs(game, 'up'); // busy 窗口注入滑动 → 缓存
+ assert(game.guideStep === 0 && game._guideBusy, 'B6 前提:busy 窗口内步骤未解锁');
+ const stepsAtBusy = rec.count('sfx/step');
+ assert(rec.count('sfx/step') === stepsAtBusy, 'B6:busy 窗口手势不当场判定(无脚步声)');
+ await finishVoices(2); // g1_move + skip_hint 播完 → 解锁 G1 → 缓存 up 补判
+ assert(rec.count('sfx/step') === stepsAtBusy + 1, 'B6:解锁后缓存手势补判(G1 滑动作答,脚步声)');
+ assert(game.guideStep === 1 && game._guideBusy, 'B6:补判作答后正常推进(G2 语音中)');
+ await finishVoices(2); // g1_done + g2_wall
+ assert(game.guideStep === 2, 'B6:进入 G2');
+
+ // ② 缓存手势对新步骤合法:G2→G3 busy 缓存左滑(G3 的答案)→ 解锁直接作答
+ await gs(game, 'up'); // G2 第一滑
+ await tick(VOICE_END_SLACK); // wall 播完
+ await gs(game, 'right'); // G2 第二滑 → _advance(3) busy
+ await gs(game, 'left'); // busy 缓存左滑
+ assert(game.guideStep === 2, 'B6:busy 窗口仍停 G2');
+ await finishVoices(2); // wall + g3_left 播完 → 解锁 G3 → 缓存 left 补判
+ assert(game.x === 0 && game.y === 1, 'B6:缓存左滑按 G3 补判作答(移到 (0,1))');
+ assert(game.guideStep === 3 && game._guideBusy, 'B6:补判作答后推进 G4');
+ assert(rec.count('voice/guide_skip') === 0, 'B6:缓存手势不触发下滑跳过(S6 事故修复)');
+ await finishVoices(1); // g4_down → G4 解锁
+
+ // ③ 缓存手势对新步骤不合法:按答错路径处理(有纠正语音,可归因)
+ await gs(game, 'down'); // G4 作答 → _advance(5) busy
+ await gs(game, 'up'); // busy 缓存 up
+ await finishVoices(1); // 解锁 G5 → 补判:G5 期望单击,up = 答错
+ assert(game.guideStep === 5, 'B6:进入 G5');
+ assert(rec.count('voice/guide_wrong_gesture') === 1, 'B6:缓存手势不合法按答错路径处理(纠正语音可归因)');
+ assert(game.guideErrs === 1, 'B6:答错计数照常');
+ assert(rec.count('voice/guide_skip') === 0, 'B6:答错路径同样不触发跳过');
+
+ // ④ 仅缓存 1 个:busy 连注两个手势,解锁后只补判首个
+ await gs(game, 'tap'); // G5 作答 → _advance(6) busy
+ await gs(game, 'doubletap'); // 缓存(恰为 G6 答案)
+ await gs(game, 'left'); // 不再缓存(仅 1 个)
+ await finishVoices(2); // g5_key + g6_doubletap → 解锁 G6 → 补判 doubletap
+ assert(game.guideStep === 6 && game._guideBusy, 'B6:仅首个缓存手势补判(doubletap 作答 G6)');
+ await finishVoices(3); // inv_key + g6_done + g7_goal → PLAY
+ assert(game.phase === 'PLAY', 'B6:引导链完整走完(缓存不破坏既有推进)');
+ game.stop();
+
+ // ⑤ 跳过分手势不缓存:busy 窗口下滑(可跳过步骤)仍立即跳过
+ store.clear();
+ const g2b = await newGame();
+ g2b.game.start();
+ await flush();
+ await gs(g2b.game, 'tap'); // G0 → _advance(1) busy
+ assert(g2b.game._guideBusy, '前提:G0→G1 推进语音播放中');
+ await gs(g2b.game, 'down'); // busy 窗口下滑 = 跳过分手势 → 立即跳过,不缓存
+ assert(g2b.rec.count('voice/guide_skip') === 1, 'B6:busy 窗口下滑跳过语义不变(不缓存)');
+ assert(g2b.game._guidePending === null, 'B6:跳过分手势不进缓存');
+ g2b.game.stop();
+}
+
+// ================================================================
+// 场景 M:B7 结算链单击守卫(播完前不重开 / 链完整到达 / 30s 兜底解除)
+// ================================================================
+section('M. B7 结算链单击守卫(escape-state round2 F8)');
+{
+ store.clear();
+ store.set('soundgame.escape.guideDone', '1');
+ const { game, rec } = await newGame();
+ game.start();
+ await flush();
+ await gs(game, 'tap'); // REOPEN → PLAY
+ await finishVoices(2);
+ // 钩子速通:开柜 → 修电 → 大门
+ game.hasKey = true; game.x = 2; game.y = 1;
+ await gs(game, 'doubletap'); // 开柜
+ await finishVoices(3);
+ game.x = 0; game.y = 0;
+ await gs(game, 'doubletap'); // 修电
+ await finishVoices(2);
+ game.x = 2; game.y = 2;
+ await gs(game, 'doubletap'); // 逃脱
+ assert(game.phase === 'ENDING' && game._settleBusy, 'B7 前提:ENDING + 结算守卫挂载');
+
+ // 结算链播放中单击 = 忽略(链不被截断,time_band/again 完整到达)
+ await gs(game, 'tap');
+ assert(game.phase === 'ENDING', 'B7:结算链播完前单击不重开(仍 ENDING)');
+ assert(rec.count('voice/replay') === 0, 'B7:守卫期单击不触发重开(无 replay)');
+ await finishVoices(1);
+ assert(rec.count('voice/time_band1') === 1, 'B7:结算链完整——time_band 到达(不再被吞)');
+ await finishVoices(1);
+ assert(rec.count('voice/again') === 1, 'B7:结算链完整——again 到达');
+ await finishVoices(1); // 链完 → 守卫解除
+ assert(!game._settleBusy, 'B7:链播完守卫解除');
+ await gs(game, 'tap');
+ assert(game.phase === 'PLAY' && rec.count('voice/replay') === 1, 'B7:守卫解除后单击重开原语义不变');
+ game.stop();
+
+ // 30s 资产缺失兜底:again 缺失 → 链停摆 → 守卫靠兜底解除(防静默死锁)
+ blockedAssets = ['again'];
+ store.clear();
+ store.set('soundgame.escape.guideDone', '1');
+ const g2 = await newGame();
+ g2.game.start();
+ await flush();
+ await gs(g2.game, 'tap');
+ await finishVoices(2);
+ g2.game.hasKey = true; g2.game.x = 2; g2.game.y = 1;
+ await gs(g2.game, 'doubletap');
+ await finishVoices(3);
+ g2.game.x = 0; g2.game.y = 0;
+ await gs(g2.game, 'doubletap');
+ await finishVoices(2);
+ g2.game.x = 2; g2.game.y = 2;
+ await gs(g2.game, 'doubletap');
+ assert(g2.game.phase === 'ENDING', '前提:again 缺失下逃脱进 ENDING');
+ await finishVoices(2); // escape + time_band 播完,again 缺失链停摆
+ await gs(g2.game, 'tap');
+ assert(g2.game.phase === 'ENDING', 'B7:again 缺失链停摆时守卫仍生效(单击不重开)');
+ await tick(30000); // 兜底
+ assert(!g2.game._settleBusy, 'B7:30s 资产缺失兜底解除守卫(防静默死锁)');
+ await gs(g2.game, 'tap');
+ assert(g2.game.phase === 'PLAY', 'B7:兜底解除后可重开');
+ blockedAssets = [];
+ g2.game.stop();
+}
+
+// ================================================================
+// 场景 N:B8 REOPEN 直进路径补播目标句(start_room → g7_goal,可单击跳过)
+// ================================================================
+section('N. B8 REOPEN 直进补播 g7_goal(escape-state round2 F9)');
+{
+ store.clear();
+ store.set('soundgame.escape.guideDone', '1');
+ const { game, rec } = await newGame();
+ game.start();
+ await flush();
+ assert(game.phase === 'REOPEN', '前提:二周目 REOPEN');
+ await gs(game, 'tap'); // 直接开始
+ assert(game.phase === 'PLAY', 'REOPEN 单击直进 PLAY');
+ assert(rec.count('voice/start_room') === 1, 'B8:直进播 start_room');
+ await finishVoices(1);
+ assert(rec.count('voice/g7_goal') === 1, 'B8:start_room 后接播 g7_goal');
+ assert(rec.last('voice/g7_goal').t > rec.last('voice/start_room').t, 'B8:直进路径顺序 start_room → g7_goal');
+ // g7_goal 可单击跳过(skippable 默认 true,12 号文档——教学义务句已随首局完成,
+ // 此处是状态告知不是教学)
+ const skips0 = rec.skips();
+ await gs(game, 'tap'); // g7_goal 播放中单击 = 跳过 + 补发检查
+ assert(rec.skips() === skips0 + 1, 'B8:二周目 g7_goal 可单击跳过');
+ assert(game.phase === 'PLAY', 'B8:跳过后仍在 PLAY');
+ await finishVoices(2); // 补发检查描述消化
+ game.stop();
+}
+
// ================================================================
// 资产引用校验:game.js 引用的 id 在音频目录有 ogg + m4a(voice 双性别),零缺失
// ================================================================
@@ -822,6 +1296,7 @@ section('F. 资产引用校验');
ids.add(`voice/guide_wrong_${d}`);
ids.add(`voice/guide_blunt_${d}`);
}
+ for (const d of ['l', 'r', 'u', 'd']) ids.add(`voice/wall_dir_${d}`); // B1 撞墙方位句族
for (const st of ['key', 'cabinet', 'box', 'gate']) {
for (const lv of [1, 2, 3]) ids.add(`voice/stuck_${st}_${lv}`);
}
diff --git a/web/harness/guide.test.mjs b/web/harness/guide.test.mjs
index 6bd59a02..dabd44ed 100644
--- a/web/harness/guide.test.mjs
+++ b/web/harness/guide.test.mjs
@@ -756,9 +756,9 @@ realLog('== ⑩b H5 rhythm:教学应答窗口无任何尝试 → gd_miss_none
resetTimers();
}
-realLog('== ⑩c H1 escape:目标句后按房间图播 gd_room_*(同房间不播,移动坐标系)==');
+realLog('== ⑩c H1/B2 escape:目标句后按房间图播方位句(round2 B2:三处固定接线点升级为"目标名+方位"一体句,异常路径回落 gd_room_*)==');
{
- // 方向映射按图校验(直连单元断言)
+ // 方向映射按图校验(直连单元断言,gd_room_* 映射表保留不动)
const rig0 = makeRig(EscapeGame);
const { game: g0 } = rig0;
await rig0.audio.unlock();
@@ -768,10 +768,15 @@ realLog('== ⑩c H1 escape:目标句后按房间图播 gd_room_*(同房间
assert(g0._roomDirVoice([1, 0]) === 'voice/gd_room_u', 'H1:目标在北(上面的房间)→ gd_room_u');
assert(g0._roomDirVoice([1, 2]) === 'voice/gd_room_d', 'H1:目标在南(下面的房间)→ gd_room_d');
assert(g0._roomDirVoice([1, 1]) === null, 'H1:目标在同一房间不播');
+ // B2 异常路径回落:计算方位与枚举不符 → 原 gd_room_*(原句保留接线,归因真话)
+ g0._playGoalDir([0, 1], 'voice/gd_cabinet_dir', 'voice/gd_room_r'); // 目标在西 ≠ 枚举右
+ await flush();
+ assert(rig0.playLog.some((e) => e.id === 'voice/gd_room_l'), 'B2:方位与枚举不符时回落 gd_room_*');
g0.stop();
resetTimers();
// 三处目标句接线点:found_key → cabinet(2,1);gd_next_fuse → box(0,0);fix_box → gate(2,2)
+ // (B2 换线:gd_room_r/l → gd_cabinet_dir/gd_box_dir/gd_gate_dir,断言同步登记变更)
const rig = makeRig(EscapeGame);
const { audio, playLog, game } = rig;
storageData.set('soundgame.escape.guideDone', '1'); // 二周目直达 REOPEN→PLAY
@@ -779,42 +784,74 @@ realLog('== ⑩c H1 escape:目标句后按房间图播 gd_room_*(同房间
game.start();
await advance(() => game.phase === 'REOPEN');
await advance(() => !audio.isVoicePlaying()); // reopen_prompt 播完
- await gesture(game, 'tap'); // 直接开始 → PLAY (1,1)
+ await gesture(game, 'tap'); // 直接开始 → PLAY (1,1)(B8:start_room → g7_goal 链)
await advance(() => game.phase === 'PLAY' && !audio.isVoicePlaying());
assert(game.x === 1 && game.y === 1 && !game.hasKey, '前提:二周目 PLAY 起点、身上无钥匙');
- // ① found_key:储物间拾钥匙 → 铁柜房在东(右面的房间)
- await gesture(game, 'left'); // (0,1)
+ // ① found_key:储物间拾钥匙 → 铁柜房在东 → B2 一体句 gd_cabinet_dir
+ await gesture(game, 'left'); // (0,1)(B5a:首访播 empty_2)
await gesture(game, 'down'); // (0,2) 储物间(首到播 room_storage)
await advance(() => !audio.isVoicePlaying()); // 房间描述播完再单击(否则单击=跳句)
- await gesture(game, 'tap'); // 检查 → found_key(800ms 后)→ 方位句
- await advance(() => playLog.some((e) => e.id === 'voice/gd_room_r'));
+ await gesture(game, 'tap'); // 检查 → found_key(800ms 后)→ 一体句
+ await advance(() => playLog.some((e) => e.id === 'voice/gd_cabinet_dir'));
const ifk = playLog.findIndex((e) => e.id === 'voice/found_key');
- assert(ifk > -1 && playLog.findIndex((e) => e.id === 'voice/gd_room_r') > ifk,
- 'H1:found_key 后接播 gd_room_r(目标铁柜房按图在东)');
+ assert(ifk > -1 && playLog.findIndex((e) => e.id === 'voice/gd_cabinet_dir') > ifk,
+ 'B2:found_key 后接播 gd_cabinet_dir(铁柜,在右面的房间)');
+ assert(!playLog.some((e) => e.id === 'voice/gd_room_r'), 'B2:正常路径不再播 gd_room_r');
- // ② gd_next_fuse:开柜得保险丝 → 电箱房在西(左面的房间)
+ // ② gd_next_fuse:开柜得保险丝 → 电箱房在西 → B2 一体句 gd_box_dir
await gesture(game, 'right'); await gesture(game, 'right'); // (1,2) (2,2)
await gesture(game, 'up'); // (2,1) 铁柜房
await gesture(game, 'doubletap'); // 开柜
- await advance(() => playLog.some((e) => e.id === 'voice/gd_room_l'));
+ await advance(() => playLog.some((e) => e.id === 'voice/gd_box_dir'));
const inf = playLog.findIndex((e) => e.id === 'voice/gd_next_fuse');
- assert(inf > -1 && playLog.findIndex((e) => e.id === 'voice/gd_room_l') > inf,
- 'H1:gd_next_fuse 后接播 gd_room_l(目标电箱房按图在西)');
+ assert(inf > -1 && playLog.findIndex((e) => e.id === 'voice/gd_box_dir') > inf,
+ 'B2:gd_next_fuse 后接播 gd_box_dir(电箱,在左面的房间)');
+ assert(!playLog.some((e) => e.id === 'voice/gd_room_l'), 'B2:正常路径不再播 gd_room_l');
- // ③ fix_box:修电箱通电 → 大门在东(右面的房间)
+ // ③ fix_box:修电箱通电 → 大门在东 → B2 一体句 gd_gate_dir
+ await advance(() => !audio.isVoicePlaying()); // gd_box_dir 播完再走(首访空房句不顶链)
await gesture(game, 'left'); await gesture(game, 'left'); // (1,1) (0,1)
await gesture(game, 'up'); // (0,0) 电箱房
await gesture(game, 'doubletap'); // 修电箱
- await advance(() => playLog.filter((e) => e.id === 'voice/gd_room_r').length >= 2);
+ await advance(() => playLog.some((e) => e.id === 'voice/gd_gate_dir'));
const ifx = playLog.findIndex((e) => e.id === 'voice/fix_box');
- assert(ifx > -1 && playLog.map((e) => e.id).lastIndexOf('voice/gd_room_r') > ifx,
- 'H1:fix_box 后接播 gd_room_r(目标大门按图在东)');
+ assert(ifx > -1 && playLog.findIndex((e) => e.id === 'voice/gd_gate_dir') > ifx,
+ 'B2:fix_box 后接播 gd_gate_dir(大门,在右面的房间)');
assert(game.powerOn, '前提:电箱已通电(接线点均在真实解谜链上)');
game.stop();
resetTimers();
}
+realLog('== ⑩d B6 escape:引导 busy 窗口手势缓存(full 档)——解锁后补判、答错路径、不触发跳过 ==');
+{
+ const rig = makeRig(EscapeGame);
+ const { audio, playLog, game } = rig;
+ await audio.unlock();
+ game.start();
+ await advance(() => game.phase === 'GUIDE' && !game._guideBusy && !audio.isVoicePlaying()); // G0 待输入
+ await gesture(game, 'tap'); // G0 → _advance(1) busy(g1_move + skip_hint)
+ assert(game._guideBusy, '前提:G0→G1 推进语音播放中(busy 窗口)');
+ await gesture(game, 'up'); // busy 窗口注入滑动 → 缓存(不当场判定)
+ assert(game.guideStep === 0 && game.guideSwipes === 0, 'B6:busy 窗口手势不当场判定');
+ // 解锁 G1 → 缓存 up 按 G1 补判 = 作答(sfx/step + 推进 G2)
+ await advance(() => game.guideStep === 2 && !game._guideBusy);
+ assert(playLog.some((e) => e.id === 'sfx/step'), 'B6:full 档下缓存手势解锁后补判作答(脚步声)');
+
+ // G2 busy 缓存非答案手势 → 解锁 G3 后按答错路径(纠正语音,不触发下滑跳过)
+ await gesture(game, 'up'); // G2 第一滑
+ await advance(() => !audio.isVoicePlaying());
+ await gesture(game, 'right'); // G2 第二滑 → _advance(3) busy
+ await gesture(game, 'down'); // busy 缓存下滑(非 G3 答案)
+ assert(game.guideStep === 2, 'B6:busy 窗口仍停 G2');
+ await advance(() => game.guideStep === 3 && !game._guideBusy);
+ assert(voicePlays(playLog, 'voice/guide_wrong_left') === 1, 'B6:缓存手势不合法按答错路径处理(guide_wrong_left)');
+ assert(voicePlays(playLog, 'voice/guide_skip') === 0, 'B6:缓存的下滑不触发跳过引导(live round1 S6 事故修复)');
+ assert(game.phase === 'GUIDE', 'B6:仍在引导中(未被保送 PLAY)');
+ game.stop();
+ resetTimers();
+}
+
// ---------------------------------------------------------------- 汇总
realLog(`\n通过 ${passCount} 条断言,失败 ${failures.length} 条`);
diff --git a/web/harness/sessions/escape.mjs b/web/harness/sessions/escape.mjs
index 4e78b459..94edf90b 100644
--- a/web/harness/sessions/escape.mjs
+++ b/web/harness/sessions/escape.mjs
@@ -1,8 +1,8 @@
/**
* 《暗室》新手会话剧本:老玩家二周目不算——这里清空过进度走首局引导(G0-G7),
- * 每步听清再做;进 PLAY 后先连撞两次墙(触发向导提前一级的卡关提示),
- * 然后找到铁柜房开柜拿保险丝、照 round4 H1 的目标方位句跨四个房间修电箱、
- * 再摸到大门收工(目标句后 3s 内都能听到方位句)。
+ * 每步听清再做;进 PLAY 后先连撞两次墙(round2 B1:wall 后跟陈述式方位句
+ * 「上边是墙。」,同向 10s 限频),经空房首访描述(B5a)找到铁柜房开柜拿保险丝、
+ * 照 round2 B2 的"目标名+方位"一体句跨四个房间修电箱、再摸到大门收工。
*/
export function describe(game) {
switch (game.phase) {
@@ -55,31 +55,34 @@ export async function session(ctx, mock) {
if (why) ctx.note(why);
};
- // 先往北乱撞探探边界:两步走到 (0,0),再连撞两次北墙——向导把卡关提示提前带上来
+ // 先往北乱撞探探边界:两步走到 (0,0)——(0,1) 首访会播 empty_2(B5a 空房听觉锚),
+ // 再连撞两次北墙——wall 后各跟一句陈述式方位「上边是墙。」(B1,同向 10s 限频)
ctx.silence(3, '辨了辨方向');
- await move('up');
- await move('up');
- await move('up', '北边是墙,撞了一下——「那边是墙」后面还跟了句方向(向上推的是北墙)');
- await move('up', '又撞了一下——听到游戏主动来问我了');
- await ctx.waitVirtual(5200); // 等卡关轮询的 5s tick:连续撞墙 → 提前一级的暗示
+ await move('up', '路过一间空房,它自己开口了:摸到的只有冰冷粗糙的墙壁');
+ await move('up', '电流声——电箱房到了');
+ await move('up', '北边是墙,撞了一下——「那边是墙」后面跟了句「上边是墙。」,这面不通记住了');
+ await move('up', '又撞了一下——同方向 10 秒内不重复报方位,游戏不啰嗦');
+ await ctx.waitVirtual(5200); // 缓一缓(卡关轮询 5s tick 空转,活跃操作不触发提示)
// 去铁柜房:(0,0) → 右×2 = (2,0) → 下 = (2,1),双击用钥匙
- await move('right', '往东走,风声换了个方向');
- await move('right');
+ // ((1,0)/(2,0) 首访各播 empty_1/empty_3——B5a:四间空房从此声学可区分)
+ await move('right', '往东走,一间空房自报家门:空空荡荡只有回声');
+ await move('right', '又一间空房:除了远处的水滴声什么都没有');
await move('down', '到了铁柜房,柜子锁着');
- // 用钥匙 → unlock_cabinet + gd_next_fuse + gd_room_l(round4 H1:目标句带方位,
- // 从铁柜房 (2,1) 看电箱房 (0,0) 在西 = 左面的房间)
+ // 用钥匙 → unlock_cabinet + gd_next_fuse + gd_box_dir(round2 B2:目标名+方位
+ // 一体句,从铁柜房 (2,1) 看电箱房 (0,0) 在西 = 「电箱,在左面的房间。」)
await ctx.gesture('doubletap');
await ctx.waitVirtual(5600);
- ctx.status('柜子开了。「保险丝到手。接下来,去找电箱。」后面还跟了一句「左面的房间。」——这回知道该往哪边摸了');
+ ctx.status('柜子开了。「保险丝到手。接下来,去找电箱。」后面跟的是「电箱,在左面的房间。」——目标名和方位一句话说全了');
// round4 H1 剧本:照方位句跨房间去电箱房——(2,1) → 左×2 = (0,1) → 上 = (0,0)
await move('left', '照方位句往左(西)摸');
await move('left');
await move('up', '电流声越来越清晰——电箱房到了');
- // 修电箱 → fix_box + gd_room_r(round4 H1:从 (0,0) 看大门 (2,2) 在东 = 右面的房间)
+ // 修电箱 → fix_box + gd_gate_dir(round2 B2:从 (0,0) 看大门 (2,2) 在东 =
+ // 「大门,在右面的房间。」)
await ctx.gesture('doubletap');
await ctx.waitVirtual(5600);
- ctx.status('电箱修好了。目标句后面照样跟着方位——「右面的房间。」大门在东边');
+ ctx.status('电箱修好了。目标句后面照样是目标名+方位——「大门,在右面的房间。」');
// 照方位句多跨两个房间摸向大门:(0,0) → 右×2 = (2,0) → 下×2 = (2,2)
await move('right', '照方位句往右(东)摸');
await move('right');
diff --git a/web/harness/voice_text.json b/web/harness/voice_text.json
index 07b8a739..0fab38f1 100644
--- a/web/harness/voice_text.json
+++ b/web/harness/voice_text.json
@@ -1038,7 +1038,17 @@
"time_band4": "你逃出来了,用时超过十分钟。",
"unlock_cabinet": "钥匙插进锁孔,缓缓转动。咔哒一声,柜门开了。柜子深处,躺着一枚保险丝。保险丝到手了。",
"use_fail": "这里没有用得上东西的地方。换个房间试试,或者先单击检查一下周围。",
- "wall": "那边是墙,换个方向。"
+ "wall": "那边是墙,换个方向。",
+ "wall_dir_l": "左边是墙。",
+ "wall_dir_r": "右边是墙。",
+ "wall_dir_u": "上边是墙。",
+ "wall_dir_d": "下边是墙。",
+ "gd_cabinet_dir": "铁柜,在右面的房间。",
+ "gd_box_dir": "电箱,在左面的房间。",
+ "gd_gate_dir": "大门,在右面的房间。",
+ "gd_prog_1": "钥匙已经到手。找到能用它的锁。",
+ "gd_prog_2": "铁柜已经打开。电还没有通。",
+ "gd_prog_3": "电已经通了。去找那扇大门。"
},
"hunt": {
"breach": "有野兽冲进营地了!",
@@ -1958,4 +1968,4 @@
"zhou_r2_plain": "茶是茶,水是水,差点火候。儿子的事,改天再说吧。",
"zhou_r2_wrong": "……今晚这盏不对。儿子的事,不说也罢。"
}
-}
\ No newline at end of file
+}