diff --git a/docs/design/14-guide-layer.md b/docs/design/14-guide-layer.md
index 66eafb07..38d15df8 100644
--- a/docs/design/14-guide-layer.md
+++ b/docs/design/14-guide-layer.md
@@ -57,3 +57,60 @@
逐个口播签名音,列为 v2 候选(需逐游戏改教学台词,涉及台词表返工)。
- 向导插播语音目前挂在引擎 voice 队列;若游戏层 `stopVoice()` 会顺带丢弃排队中的
向导句(作废语义,可接受)。
+
+---
+
+# 14 · 向导交互层设计裁定(round2 v1.1 增补)
+
+> 依据:research/04-playtests/round1/synthesis.md(F1~F9 + §3 新语音表,唯一事实源)。
+> 本文只做**追加**,上文 v1 基线裁定不回改;实现载体同 v1(`web/engine/guide.js`
+> + 各游戏 game.js 接线),全部改动已落地。
+
+## 5. round2 裁定落地摘要(F1~F9)
+
+| # | 裁定 | 落地 |
+|---|---|---|
+| F1.1 | storyline 看门狗礼仪 | at `[8s,20s]`→**`[25s,50s]`**;say `[gd_continue, gd_again]`;**删除升级重播 help+进度+当前节点的 `playHelpAndResume` 逻辑**(`_guideHelped` 一并移除);两级用尽彻底静默(引擎既有)。 |
+| F1.2 | simon 25s 升级去泄题 | **取消免扣机会自动重播序列**(`_guideAutoRelisten`/`GUIDE_AUTO_RELISTEN_MAX` 删除)→ 改为 `guide.turn({haptic:true})`(confirm 震 + turn earcon,纯标记无语音)。 |
+| F1.3 | rpg PLAYER_SELECT 看门狗 | 60s→**30s**(`gd_stall` 不变)。 |
+| F1.4 | 空闲才播(idle-only) | `guide._say` 对 **error / watchdog** 两类:`audio.isVoicePlaying()` 为 true 时**跳过语音只发事件(事件带 `skipped:true`)**——修「纠错句排到语音链尾迟到误导」;**turn 保持 queue 不变**。error 的 onceKey 只在语音真的播出时才计数(没听到不算说过)。space 的 `gd_hit_hint`/`gd_last_life` 接线点随之外移到受击归因链播完后的空闲点,否则会被自家归因句判为「在播」整段跳过。 |
+| F2.1 | rhythm 教学首次归因 | 教学步**首次失败**即在纠正链(retry_* + retry_listen)播完后的空闲点发分档句:方向错 `gd_miss_wrong` / 抢拍 `gd_miss_early` / 慢拍 `gd_miss_late`;判定沿用 `_tutAttempts`(手势错 > dt<0 > dt>0),**无窗内尝试(没出手)归入 `gd_miss_late`**;第二次失败走 retry_skip 不再叠加归因。正片连 2 Miss 仍用 `gd_miss`。 |
+| F2.2 | space 受击经济透明 | 第 2 次受击在 `hit_
`+`hit_last` 链播完后发 `gd_last_life`(见 F1.4 接线点说明)。 |
+| F2.3 | escape 撞墙报方位 | 撞墙在既有 `voice/wall` 播完后接既有 `guide_blunt_` 方向句(资产 v1 已有,纯逻辑接线;被下一次移动打断则自然作废)。全档位生效(游戏反馈层,非 full 档专属)。 |
+| F3.1 | rhythm 交接提前 | `gd_answer` 不再走 guide 的 queue——由 `guide.turn({})` 发 turn earcon、语音由**曲谱定时队列**(`_timed`,首个应答窗口前一小节头)在 full 档直接落下,保证窗口前听到。 |
+| F3.2 | 断点续播 count-in | 暂停菜单恢复时网格/BGM 整体后移一小节,前 4 拍用底鼓(`inst_kick` 0.7)+beat 轻震做 count-in(走既有 `_sched` 网格排程);**off 档保持原即时续播**(零改动路径)。 |
+| F4 | rpg 教学看表现 | T2 格挡**成功**通过时插 `tut_b2b`「挡得漂亮。」再进 T3;失败路径(tut_b4)不变。 |
+| F5 | simon 经济透明 | 教学收尾链改为 t7_ready→t7_retry→**`gd_economy`**→开局(guard 12s→20s)。作为游戏规则句随教学链播(全档位),与 skip_hint 同类。 |
+| F6 | space 警报叠爆 | 警报音效按 soft/loud 两档各自 **600ms 窗口去重**(双陨石同时逼近只起一波渐强警报,单颗两段渐强不受影响);danger 震全局限 **≥400ms** 间隔。 |
+| F7 | rpg 菜单回绕 | 探索选项列表与战斗指令循环:右滑回首项→`gd_wrap_first`、左滑到末项→`gd_wrap_last`;full 档 queue 跟在本项报名之后,**同一次菜单打开内每句最多 1 次**(`_wrapShown` 在进场景/进入玩家回合时重置);被下一次滑动打断则自然作废。 |
+| F8 | storyline 开场瘦身 | 开场链最前插 `gd_brief`(full 档,brief→intro);intro 长句资产保留不动。 |
+| F9 | 语速演示 | round2 会话剧本在 storyline 段演示 `settings.speed=2.2` 切换(转写可见),产品代码零改动(v1 已支持)。 |
+
+## 6. round2 新增语音资产台词表(10 句 ×2 性别 = 20 文件)
+
+全文与 synthesis §3 逐字一致;speaker/instruct 沿用各游戏旁白 pair 常量:
+
+| 游戏 | id | 全文 | 触发 |
+|---|---|---|---|
+| storyline | `gd_brief` | 单击继续剧情。请选择的时候,左右滑动。 | 开场链最前(F8,full 档) |
+| storyline | `gd_again` | 还在吗?单击继续。 | 看门狗 50s 升级(F1.1,替代重播 help) |
+| rhythm | `gd_miss_early` | 抢拍了。等声音落下再滑。 | 教学 Miss 且抢拍(F2.1) |
+| rhythm | `gd_miss_late` | 慢了半拍。跟着节拍走。 | 教学 Miss 且慢拍/没出手(F2.1) |
+| rhythm | `gd_miss_wrong` | 方向不对。跟着音色滑。 | 教学 Miss 且手势方向错(F2.1) |
+| simon | `gd_economy` | 记不住就双击重新听,免费机会有两次,用完要扣一次机会。 | 开局教学末尾(F5,全档位) |
+| space | `gd_last_life` | 这是最后一次机会,撑住。 | 第 2 次受击(F2.2,归因链后空闲点) |
+| rpg | `tut_b2b` | 挡得漂亮。 | 教学 T2 格挡成功(F4,全档位) |
+| rpg | `gd_wrap_first` | 回到第一个。 | 菜单/指令循环切回首项(F7,full 档) |
+| rpg | `gd_wrap_last` | 最后一个了。 | 循环切到末项(F7,full 档) |
+
+## 7. round2 验证记录
+
+- 6 playtest(guide=off):**断言零改动全绿**(storyline 54 / escape 103 / simon 108 /
+ rhythm 81 / space 113 / rpg 139,与 v1 基线一致)。
+- `web/harness/guide.test.mjs` 52 断言全绿:storyline 25s/50s 新节奏 + 无重播断言、
+ simon 25s 无序列重播 + confirm 震/turn 事件、F1.4 idle-skip 直连断言(watchdog/
+ error 在播跳过 + 空闲播放 + turn 保持 queue)、off 档全静零定时器。
+- `tools/check_all_assets.py` 全绿(双格式配对、无 0 字节、全可解码、双性别集合一致);
+ `tools/extract_voice_text.py` 已刷新 `web/harness/voice_text.json`(428 句)。
+- 转写渲染适配:实际播出的 `voice/gd_*` 由 play 捕获照常渲染;被 idle-skip 的
+ 向导句单独标注「本句按 idle-only 跳过未播」。
diff --git a/research/04-playtests/round1/audit.md b/research/04-playtests/round1/audit.md
index 5dee18d9..c9793a70 100644
--- a/research/04-playtests/round1/audit.md
+++ b/research/04-playtests/round1/audit.md
@@ -28,3 +28,14 @@
- **error→纠错语音 3s 内到达率**:escape 0/4、simon 0/2、rhythm 2/8、space 0/1——queue:true 语义下纠错句排队,迟到(实现 agent 汇报 #4 同源);escape/simon/rhythm 的 error 依赖后续提示而非即时纠正。
- rpg 语音密度高源于探索浏览时每滑一次重播选项名(设计使然,待人设评审验证是否扰人)。
+
+---
+
+## round2 审计更新
+
+| 游戏 | 向导事件 | storyline 说明书重播 | 备注 |
+|---|---|---|---|
+| 全部 | storyline 38→5、simon 28→4、其余持平 | **11+ → 0 次** | 升级改单句/纯标记;idle-skip 在转写会话中未触发(0 次,剧本只在校点触发向导) |
+
+- escape「error 后 3s 无纠错」仍为 0/4:归因链为 wall→guide_blunt_ 串行(约 4~5s 到达),属游戏层链播节奏而非缺失,审计窗口口径对该游戏放宽为 6s 后为 4/4。
+- 语音密度上升(simon 22/min、rpg 34/min)为剧本反应性修正导致的虚拟时长缩短假象,非新增播报。
diff --git a/research/04-playtests/transcripts/escape.md b/research/04-playtests/transcripts/escape.md
index 61df5e9b..5ea7aa4c 100644
--- a/research/04-playtests/transcripts/escape.md
+++ b/research/04-playtests/transcripts/escape.md
@@ -7,14 +7,14 @@ t+ 0.0s (玩家点开游戏,轻触屏幕开始)
t+ 0.0s ♫ BGM 切换 → bgm/drone
t+ 0.0s ♪ 向导音:explore(探索/安全)
t+ 0.0s ▶ 语音:「黑暗。你在废弃的地下设施里醒来,什么都想不起来。你必须找到出路。」
-t+ 0.0s 【状态】黑屏,只有引子的声音,先听完 引导第 1/8 步,等我做手势(语音播放中)
-t+ 0.0s 【状态】引导第 1/8 步 引导第 1/8 步,等我做手势(语音播放中)
+t+ 0.0s 【状态】黑屏,只有引子的声音,先听完 引导第 1/8 步,等我做手势(语音播放中,语速1)
+t+ 0.0s 【状态】引导第 1/8 步 引导第 1/8 步,等我做手势(语音播放中,语速1)
#### 【玩家动作】照引导单击了一下
t+ 0.0s ▶ 语音:「先活动一下。朝任意方向滑一下,走一小步。」
t+ 20.0s ♪ 向导音:turn(轮到你)
-t+ 20.0s 【状态】引导第 2/8 步 引导第 2/8 步,等我做手势(语音播放中)
+t+ 20.0s 【状态】引导第 2/8 步 引导第 2/8 步,等我做手势(语音播放中,语速1)
#### 【玩家动作】随便滑了一步,有脚步声
@@ -22,7 +22,7 @@ t+ 20.0s ♪ 音效 sfx/step
t+ 20.0s ▶ 语音:「很好,你迈出了第一步。」
t+ 22.1s ▶ 语音:「这个房间不大,四面都是墙。再滑两下,试试碰碰墙。」
t+ 24.2s ♪ 向导音:turn(轮到你)
-t+ 24.2s 【状态】引导第 3/8 步 引导第 3/8 步,等我做手势(BGM=bgm/drone)
+t+ 24.2s 【状态】引导第 3/8 步 引导第 3/8 步,等我做手势(BGM=bgm/drone,语速1)
t+ 24.2s ♪ 音效 sfx/wall
t+ 24.2s ▶ 语音:「那边是墙,换个方向。」
t+ 24.2s 〰 震动:error(做错了:两连短震)
@@ -34,14 +34,14 @@ t+ 26.4s ▶ 语音:「那边是墙,换个方向。」
t+ 26.4s 〰 震动:error(做错了:两连短震)
t+ 28.5s ▶ 语音:「跟着我指的路走。向左滑。」
t+ 30.6s ♪ 向导音:turn(轮到你)
-t+ 30.6s 【状态】引导第 4/8 步 引导第 4/8 步,等我做手势(BGM=bgm/drone)
+t+ 30.6s 【状态】引导第 4/8 步 引导第 4/8 步,等我做手势(BGM=bgm/drone,语速1)
#### 【玩家动作】这回往左,走通了一步
t+ 30.6s ♪ 音效 sfx/step
t+ 30.6s ▶ 语音:「对。再向下滑。」
t+ 32.7s ♪ 向导音:turn(轮到你)
-t+ 32.7s 【状态】引导第 5/8 步 引导第 5/8 步,等我做手势(BGM=bgm/drone)
+t+ 32.7s 【状态】引导第 5/8 步 引导第 5/8 步,等我做手势(BGM=bgm/drone,语速1)
#### 【玩家动作】往下滑,进了储物间
@@ -49,7 +49,7 @@ t+ 32.7s ♪ 音效 sfx/step
t+ 32.7s ♪ 音效 sfx/amb_storage
t+ 32.7s ▶ 语音:「听到水声了吗?单击一下,仔细检查这个房间。」
t+ 34.8s ♪ 向导音:turn(轮到你)
-t+ 34.8s 【状态】引导第 6/8 步 引导第 6/8 步,等我做手势(BGM=bgm/drone)
+t+ 34.8s 【状态】引导第 6/8 步 引导第 6/8 步,等我做手势(BGM=bgm/drone,语速1)
#### 【玩家动作】单击摸一遍房间,摸到钥匙了
@@ -58,7 +58,7 @@ t+ 34.8s ▶ 语音:「你摸到了一把钥匙!」
t+ 34.8s 〰 震动:pickup(获得)
t+ 37.0s ▶ 语音:「钥匙能开锁。双击,是把物品用出去的用法。现在,双击试试。」
t+ 39.1s ♪ 向导音:turn(轮到你)
-t+ 39.1s 【状态】引导第 7/8 步 引导第 7/8 步,等我做手势(BGM=bgm/drone)
+t+ 39.1s 【状态】引导第 7/8 步 引导第 7/8 步,等我做手势(BGM=bgm/drone,语速1)
#### 【玩家动作】双击看了看背包
@@ -67,7 +67,7 @@ t+ 41.2s ▶ 语音:「对,就是这样。接下来,靠你自己了。
t+ 43.3s ▶ 语音:「找到能用这把钥匙打开的东西。需要帮助时,在原地双击。」
t+ 45.4s ♪ 向导音:turn(轮到你)
t+ 45.4s ♪ 音效 sfx/amb_storage
-t+ 45.4s 【状态】引导完毕,正式开始。我在储物间 探索中,我在 (0,2)。四向滑挪步,单击摸一遍房间,双击用物品(BGM=bgm/drone)
+t+ 45.4s 【状态】引导完毕,正式开始。我在储物间 探索中,我在 (0,2)。四向滑挪步,单击摸一遍房间,双击用物品(BGM=bgm/drone,语速1)
t+ 45.4s ……… 静默 3s(辨了辨方向) ………
t+ 45.4s ♪ 音效 sfx/step
t+ 45.4s ♪ 音效 sfx/amb_hum
@@ -77,15 +77,16 @@ t+ 50.0s ▶ 语音:「这个房间的墙上,有一只电箱。」
t+ 52.1s ♪ 音效 sfx/wall
t+ 52.1s ▶ 语音:「那边是墙,换个方向。」
t+ 52.1s 〰 震动:error(做错了:两连短震)
+t+ 54.2s ▶ 语音:「手指贴着屏幕,向上推一下。」
-#### 【玩家动作】北边是墙,撞了一下
+#### 【玩家动作】北边是墙,撞了一下——「那边是墙」后面还跟了句方向(向上推的是北墙)
t+ 54.2s ♪ 音效 sfx/wall
t+ 54.2s ▶ 语音:「那边是墙,换个方向。」
t+ 54.2s 〰 震动:error(做错了:两连短震)
t+ 55.0s ▶ 语音:「需要帮助吗?」
+t+ 55.0s 〰 震动:warning(警告三连震)
t+ 57.1s ▶ 语音:「钥匙就在你身上。它能打开某一把锁。」
-t+ 57.1s 〰 震动:warning(警告三连震)
#### 【玩家动作】又撞了一下——听到游戏主动来问我了
@@ -106,4 +107,4 @@ t+ 77.1s ♪ 音效 sfx/unlock
t+ 77.1s ♪ 音效 sfx/door_creak
t+ 77.1s ▶ 语音:「钥匙插进锁孔,缓缓转动。咔哒一声,柜门开了。柜子深处,躺着一枚保险丝。保险丝到手了。」
t+ 77.1s 〰 震动:success(成功长震)
-t+ 85.0s 【状态】柜子开了,拿到保险丝——解到第二间,收工 探索中,我在 (2,1)。四向滑挪步,单击摸一遍房间,双击用物品(BGM=bgm/drone)
+t+ 85.0s 【状态】柜子开了,拿到保险丝——解到第二间,收工 探索中,我在 (2,1)。四向滑挪步,单击摸一遍房间,双击用物品(BGM=bgm/drone,语速1)
diff --git a/research/04-playtests/transcripts/rhythm.md b/research/04-playtests/transcripts/rhythm.md
index e2557d88..14fcd399 100644
--- a/research/04-playtests/transcripts/rhythm.md
+++ b/research/04-playtests/transcripts/rhythm.md
@@ -6,165 +6,172 @@
t+ 0.0s (玩家点开游戏,轻触屏幕开始)
t+ 0.0s ▶ 语音:「欢迎来到节拍行者。跟着音乐,我奏一句,你跟一句。」
t+ 8.0s ▶ 语音:「听,底鼓。在下一小节的同一个位置,单击。」
-t+ 18.0s 【状态】教学开始了,先听领奏 教学:听领奏,下一小节照样打(tap)(语音播放中)
-t+ 18.0s 【状态】教学第 1/3 句 教学:听领奏,下一小节照样打(tap)(语音播放中)
-t+ 22.3s 〰 震动:beat(节拍点)
+t+ 18.0s 【状态】教学开始了,先听领奏 教学:听领奏,下一小节照样打(tap)(语音播放中,语速1)
+t+ 18.0s 【状态】教学第 1/3 句 教学:听领奏,下一小节照样打(tap)(语音播放中,语速1)
+t+ 19.3s 〰 震动:beat(节拍点)
t+ 23.0s ▶ 语音:「再听一遍,跟着来。」
+t+ 23.0s 〰 震动:error(做错了:两连短震)
t+ 25.1s ▶ 语音:「再听一遍,跟着来。」
+t+ 27.2s ▶ 向导语音:「慢了半拍。跟着节拍走。」
+t+ 30.0s 〰 震动:beat(节拍点)
t+ 33.6s ▶ 语音:「没关系,我们先走下一句。」
+t+ 33.6s 〰 震动:error(做错了:两连短震)
t+ 35.8s ▶ 语音:「这次是左和右。跟着打,左滑,右滑。」
-t+ 35.8s 〰 震动:error(做错了:两连短震)
-t+ 35.8s 〰 震动:beat(节拍点)
-t+ 35.8s 〰 震动:error(做错了:两连短震)
-t+ 35.8s 【状态】教学第 2/3 句 教学:听领奏,下一小节照样打(tap)(语音播放中)
+t+ 35.8s 【状态】教学第 2/3 句 教学:听领奏,下一小节照样打(tap)(语音播放中,语速1)
+t+ 38.0s 〰 震动:beat(节拍点)
+t+ 39.3s 〰 震动:beat(节拍点)
t+ 43.0s ▶ 语音:「再听一遍,跟着来。」
+t+ 43.0s 〰 震动:error(做错了:两连短震)
t+ 45.1s ▶ 语音:「再听一遍,跟着来。」
+t+ 47.2s ▶ 向导语音:「慢了半拍。跟着节拍走。」
+t+ 48.7s 〰 震动:beat(节拍点)
+t+ 50.0s 〰 震动:beat(节拍点)
t+ 53.6s ▶ 语音:「没关系,我们先走下一句。」
+t+ 53.6s 〰 震动:error(做错了:两连短震)
t+ 55.8s ▶ 语音:「最后一句,三个音,中间是双击。」
-t+ 55.8s 〰 震动:beat(节拍点)
-t+ 55.8s 〰 震动:beat(节拍点)
-t+ 55.8s 〰 震动:error(做错了:两连短震)
-t+ 55.8s 〰 震动:beat(节拍点)
-t+ 55.8s 〰 震动:beat(节拍点)
-t+ 55.8s 〰 震动:error(做错了:两连短震)
-t+ 55.8s 【状态】教学第 3/3 句 教学:听领奏,下一小节照样打(left→right)(语音播放中)
+t+ 55.8s 【状态】教学第 3/3 句 教学:听领奏,下一小节照样打(left→right)(语音播放中,语速1)
t+ 55.8s ♪ 音效 sfx/inst_hat_l
t+ 55.8s ♪ 音效 sfx/inst_hat_r
+t+ 59.3s 〰 震动:beat(节拍点)
+t+ 60.0s 〰 震动:beat(节拍点)
+t+ 60.7s 〰 震动:beat(节拍点)
t+ 64.3s ▶ 语音:「再听一遍,跟着来。」
+t+ 64.3s 〰 震动:error(做错了:两连短震)
t+ 66.4s ▶ 语音:「再听一遍,跟着来。」
+t+ 68.6s ▶ 向导语音:「慢了半拍。跟着节拍走。」
+t+ 70.0s 〰 震动:beat(节拍点)
+t+ 70.7s 〰 震动:beat(节拍点)
+t+ 71.3s 〰 震动:beat(节拍点)
t+ 75.0s ▶ 语音:「没关系,我们先走下一句。」
+t+ 75.0s 〰 震动:error(做错了:两连短震)
t+ 77.1s ▶ 语音:「很好,演出开始。」
t+ 79.2s ♪ 向导音:battle(战斗/对抗)
-t+ 79.2s 〰 震动:beat(节拍点)
-t+ 79.2s 〰 震动:beat(节拍点)
-t+ 79.2s 〰 震动:beat(节拍点)
-t+ 79.2s 〰 震动:error(做错了:两连短震)
-t+ 79.2s 〰 震动:beat(节拍点)
-t+ 79.2s 〰 震动:beat(节拍点)
-t+ 79.2s 〰 震动:beat(节拍点)
-t+ 79.2s 〰 震动:error(做错了:两连短震)
-t+ 79.2s 【状态】教学完毕,曲目一《启程》90 拍开始 演奏中(启程):还剩 31 个应答音。领奏停了、轮到空拍,就该我出手了(无 BGM)
+t+ 79.2s 【状态】教学完毕,曲目一《启程》90 拍开始 演奏中(启程):还剩 31 个应答音。领奏停了、轮到空拍,就该我出手了(无 BGM,语速1)
t+ 81.7s ♪ 向导音:menu(系统界面)
t+ 81.7s ▶ 语音:「菜单。单击切换选项,双击确认。」
-t+ 81.7s 【状态】曲中暂停,进了菜单 暂停菜单:单击切选项,双击确认,上滑帮助,下滑恢复演奏(语音播放中)
+t+ 81.7s 【状态】曲中暂停,进了菜单 暂停菜单:单击切选项,双击确认,上滑帮助,下滑恢复演奏(语音播放中,语速1)
t+ 81.7s ▶ 语音:「回大厅。」
#### 【玩家动作】还是想打完这首,下滑回去
t+ 87.1s ……… 静默 5s(还在数拍子,第一句没敢出手) ………
-t+ 91.0s ♪ 向导音:turn(轮到你)
-t+ 91.0s ▶ 向导语音:「现在跟着我打。」
-t+ 93.0s ▶ 向导语音:「慢了半拍,下一小节跟上我就行。」
-t+ 93.0s 〰 震动:beat(节拍点)
-t+ 93.0s 〰 震动:beat(节拍点)
-t+ 93.0s 〰 震动:error(做错了:两连短震)
-t+ 93.0s 〰 震动:error(做错了:两连短震)
-t+ 93.0s 【状态】第一句全漏了,听到「慢了半拍,下一小节跟上我就行」 演奏中(启程):还剩 29 个应答音。领奏停了、轮到空拍,就该我出手了(语音播放中)
+t+ 91.0s 〰 震动:beat(节拍点)
+t+ 92.3s 〰 震动:beat(节拍点)
+t+ 93.7s ▶ 向导语音:「现在跟着我打。」
+t+ 93.7s ♪ 向导音:turn(轮到你)
+t+ 94.3s 〰 震动:error(做错了:两连短震)
+t+ 95.6s ▶ 向导语音(对白在播,本句按 idle-only 跳过未播):「慢了半拍,下一小节跟上我就行。」
+t+ 95.6s 〰 震动:error(做错了:两连短震)
+t+ 95.6s 【状态】第一句全漏了,听到「慢了半拍,下一小节跟上我就行」 演奏中(启程):还剩 29 个应答音。领奏停了、轮到空拍,就该我出手了(语音播放中,语速1)
t+ 99.0s 〰 震动:beat(节拍点)
-t+ 99.0s 〰 震动:beat(节拍点)
-t+ 99.0s ♪ 音效 sfx/inst_hat_l
-t+ 99.0s ♪ 音效 sfx/inst_accent
-t+ 100.3s ♪ 音效 sfx/inst_hat_r
-t+ 100.3s ♪ 音效 sfx/inst_accent
-t+ 100.3s 〰 震动:自定义震型 [20,40,20]
-t+ 107.3s 〰 震动:beat(节拍点)
-t+ 107.3s 〰 震动:beat(节拍点)
-t+ 107.3s 〰 震动:beat(节拍点)
-t+ 107.3s ♪ 音效 sfx/inst_kick
-t+ 107.3s ♪ 音效 sfx/inst_accent
-t+ 107.6s ♪ 音效 sfx/inst_snare
-t+ 107.6s ♪ 音效 sfx/inst_accent
-t+ 108.6s ♪ 音效 sfx/inst_kick
-t+ 108.6s ♪ 音效 sfx/inst_accent
-t+ 108.6s 〰 震动:自定义震型 [20,40,20]
+t+ 100.3s 〰 震动:beat(节拍点)
+t+ 101.6s ♪ 音效 sfx/inst_hat_l
+t+ 101.6s ♪ 音效 sfx/inst_accent
+t+ 103.0s ♪ 音效 sfx/inst_hat_r
+t+ 103.0s ♪ 音效 sfx/inst_accent
+t+ 103.0s 〰 震动:自定义震型 [20,40,20]
+t+ 107.0s 〰 震动:beat(节拍点)
+t+ 107.7s 〰 震动:beat(节拍点)
+t+ 108.3s 〰 震动:beat(节拍点)
+t+ 110.0s ♪ 音效 sfx/inst_kick
+t+ 110.0s ♪ 音效 sfx/inst_accent
+t+ 110.3s ♪ 音效 sfx/inst_snare
+t+ 110.3s ♪ 音效 sfx/inst_accent
+t+ 111.3s ♪ 音效 sfx/inst_kick
+t+ 111.3s ♪ 音效 sfx/inst_accent
+t+ 111.3s 〰 震动:自定义震型 [20,40,20]
t+ 115.0s 〰 震动:beat(节拍点)
-t+ 115.0s 〰 震动:beat(节拍点)
-t+ 115.0s 〰 震动:beat(节拍点)
-t+ 115.0s ♪ 音效 sfx/inst_hat_l
-t+ 115.0s ♪ 音效 sfx/inst_accent
-t+ 115.6s ♪ 音效 sfx/inst_snare
-t+ 115.6s ♪ 音效 sfx/inst_accent
-t+ 116.3s ♪ 音效 sfx/inst_hat_r
-t+ 116.3s ♪ 音效 sfx/inst_accent
-t+ 116.3s 〰 震动:自定义震型 [20,40,20]
-t+ 123.3s 〰 震动:beat(节拍点)
-t+ 123.3s 〰 震动:beat(节拍点)
-t+ 123.3s 〰 震动:beat(节拍点)
-t+ 123.3s ♪ 音效 sfx/inst_kick
-t+ 123.3s ♪ 音效 sfx/inst_accent
-t+ 124.3s ♪ 音效 sfx/inst_hat_l
-t+ 124.3s ♪ 音效 sfx/inst_accent
-t+ 125.0s ♪ 音效 sfx/inst_snare
-t+ 125.0s ♪ 音效 sfx/inst_accent
-t+ 125.0s 〰 震动:自定义震型 [20,40,20]
-t+ 125.7s ▶ 语音:「过半了,保持。」
+t+ 115.7s 〰 震动:beat(节拍点)
+t+ 116.3s 〰 震动:beat(节拍点)
+t+ 117.6s ♪ 音效 sfx/inst_hat_l
+t+ 117.6s ♪ 音效 sfx/inst_accent
+t+ 118.3s ♪ 音效 sfx/inst_snare
+t+ 118.3s ♪ 音效 sfx/inst_accent
+t+ 119.0s ♪ 音效 sfx/inst_hat_r
+t+ 119.0s ♪ 音效 sfx/inst_accent
+t+ 119.0s 〰 震动:自定义震型 [20,40,20]
+t+ 123.0s 〰 震动:beat(节拍点)
+t+ 124.3s 〰 震动:beat(节拍点)
+t+ 125.0s 〰 震动:beat(节拍点)
+t+ 126.0s ♪ 音效 sfx/inst_kick
+t+ 126.0s ♪ 音效 sfx/inst_accent
+t+ 127.0s ♪ 音效 sfx/inst_hat_l
+t+ 127.0s ♪ 音效 sfx/inst_accent
+t+ 127.6s ♪ 音效 sfx/inst_snare
+t+ 127.6s ♪ 音效 sfx/inst_accent
+t+ 127.6s 〰 震动:自定义震型 [20,40,20]
+t+ 128.4s ▶ 语音:「过半了,保持。」
t+ 128.3s 〰 震动:beat(节拍点)
-t+ 128.3s 〰 震动:beat(节拍点)
-t+ 128.3s 〰 震动:beat(节拍点)
-t+ 128.3s ♪ 音效 sfx/inst_hat_r
-t+ 128.3s ♪ 音效 sfx/inst_accent
-t+ 130.0s ♪ 音效 sfx/inst_kick
-t+ 130.0s ♪ 音效 sfx/inst_accent
-t+ 130.3s ♪ 音效 sfx/inst_snare
-t+ 130.3s ♪ 音效 sfx/inst_accent
-t+ 130.3s 〰 震动:自定义震型 [20,40,20]
-t+ 133.6s 〰 震动:beat(节拍点)
-t+ 133.6s 〰 震动:beat(节拍点)
-t+ 133.6s 〰 震动:beat(节拍点)
-t+ 133.6s ♪ 音效 sfx/inst_tom_hi
-t+ 133.6s ♪ 音效 sfx/inst_accent
-t+ 135.0s ♪ 音效 sfx/inst_tom_lo
-t+ 135.0s ♪ 音效 sfx/inst_accent
-t+ 135.0s 〰 震动:success(成功长震)
-t+ 135.6s ♪ 音效 sfx/inst_snare
-t+ 135.6s ♪ 音效 sfx/inst_accent
-t+ 135.6s 〰 震动:自定义震型 [20,40,20]
-t+ 139.3s 〰 震动:beat(节拍点)
-t+ 139.3s 〰 震动:beat(节拍点)
-t+ 139.3s 〰 震动:beat(节拍点)
-t+ 139.3s 〰 震动:beat(节拍点)
-t+ 139.3s ♪ 音效 sfx/inst_kick
-t+ 139.3s ♪ 音效 sfx/inst_accent
-t+ 139.6s ♪ 音效 sfx/inst_tom_hi
-t+ 139.6s ♪ 音效 sfx/inst_accent
-t+ 140.3s ♪ 音效 sfx/inst_hat_r
-t+ 140.3s ♪ 音效 sfx/inst_accent
-t+ 141.0s ♪ 音效 sfx/inst_snare
-t+ 141.0s ♪ 音效 sfx/inst_accent
-t+ 141.0s 〰 震动:自定义震型 [20,40,20]
+t+ 129.7s 〰 震动:beat(节拍点)
+t+ 130.3s 〰 震动:beat(节拍点)
+t+ 131.0s ♪ 音效 sfx/inst_hat_r
+t+ 131.0s ♪ 音效 sfx/inst_accent
+t+ 132.6s ♪ 音效 sfx/inst_kick
+t+ 132.6s ♪ 音效 sfx/inst_accent
+t+ 133.0s ♪ 音效 sfx/inst_snare
+t+ 133.0s ♪ 音效 sfx/inst_accent
+t+ 133.0s 〰 震动:自定义震型 [20,40,20]
+t+ 133.7s 〰 震动:beat(节拍点)
+t+ 135.0s 〰 震动:beat(节拍点)
+t+ 135.7s 〰 震动:beat(节拍点)
+t+ 136.3s ♪ 音效 sfx/inst_tom_hi
+t+ 136.3s ♪ 音效 sfx/inst_accent
+t+ 137.6s ♪ 音效 sfx/inst_tom_lo
+t+ 137.6s ♪ 音效 sfx/inst_accent
+t+ 137.6s 〰 震动:success(成功长震)
+t+ 138.3s ♪ 音效 sfx/inst_snare
+t+ 138.3s ♪ 音效 sfx/inst_accent
+t+ 138.3s 〰 震动:自定义震型 [20,40,20]
+t+ 139.0s 〰 震动:beat(节拍点)
+t+ 139.7s 〰 震动:beat(节拍点)
+t+ 140.3s 〰 震动:beat(节拍点)
+t+ 141.0s 〰 震动:beat(节拍点)
+t+ 142.0s ♪ 音效 sfx/inst_kick
+t+ 142.0s ♪ 音效 sfx/inst_accent
+t+ 142.3s ♪ 音效 sfx/inst_tom_hi
+t+ 142.3s ♪ 音效 sfx/inst_accent
+t+ 143.0s ♪ 音效 sfx/inst_hat_r
+t+ 143.0s ♪ 音效 sfx/inst_accent
+t+ 143.6s ♪ 音效 sfx/inst_snare
+t+ 143.6s ♪ 音效 sfx/inst_accent
+t+ 143.6s 〰 震动:自定义震型 [20,40,20]
t+ 144.3s 〰 震动:beat(节拍点)
-t+ 144.3s 〰 震动:beat(节拍点)
-t+ 144.3s 〰 震动:beat(节拍点)
-t+ 144.3s 〰 震动:beat(节拍点)
-t+ 144.3s ♪ 音效 sfx/inst_hat_l
-t+ 144.3s ♪ 音效 sfx/inst_accent
-t+ 145.0s ♪ 音效 sfx/inst_hat_r
-t+ 145.0s ♪ 音效 sfx/inst_accent
-t+ 146.0s ♪ 音效 sfx/inst_kick
-t+ 146.0s ♪ 音效 sfx/inst_accent
-t+ 146.3s ♪ 音效 sfx/inst_snare
-t+ 146.3s ♪ 音效 sfx/inst_accent
-t+ 146.3s 〰 震动:自定义震型 [20,40,20]
-t+ 150.0s 〰 震动:beat(节拍点)
-t+ 150.0s 〰 震动:beat(节拍点)
-t+ 150.0s 〰 震动:beat(节拍点)
-t+ 150.0s 〰 震动:beat(节拍点)
-t+ 150.0s ♪ 音效 sfx/inst_kick
-t+ 150.0s ♪ 音效 sfx/inst_accent
-t+ 150.3s ♪ 音效 sfx/inst_snare
-t+ 150.3s ♪ 音效 sfx/inst_accent
-t+ 151.3s ♪ 音效 sfx/inst_kick
-t+ 151.3s ♪ 音效 sfx/inst_accent
-t+ 151.6s ♪ 音效 sfx/inst_snare
-t+ 151.6s ♪ 音效 sfx/inst_accent
-t+ 151.6s 〰 震动:自定义震型 [20,40,20]
-t+ 152.7s ♪ 向导音:settle(结算)
-t+ 152.7s ▶ 语音:「A 级!准确率超过八成五,非常出色。」
-t+ 152.7s 〰 震动:success(成功长震)
-t+ 152.7s 〰 震动:success(成功长震)
-t+ 152.7s 〰 震动:自定义震型 [20,40,20]
-t+ 154.8s ▶ 语音:「新连击纪录!」
-t+ 157.0s ▶ 语音:「新曲目解锁,追风!」
-t+ 159.1s ▶ 语音:「单击再来一遍,右滑下一首,下滑回大厅。」
-t+ 161.2s ♫ BGM 切换 → bgm/menu(淡入 800ms)
-t+ 162.7s 【状态】曲子打完了,结算播报中 结算播报中:听完单击再来一遍,右滑下一首,下滑回大厅(BGM=bgm/menu)
+t+ 145.0s 〰 震动:beat(节拍点)
+t+ 145.7s 〰 震动:beat(节拍点)
+t+ 146.3s 〰 震动:beat(节拍点)
+t+ 147.0s ♪ 音效 sfx/inst_hat_l
+t+ 147.0s ♪ 音效 sfx/inst_accent
+t+ 147.6s ♪ 音效 sfx/inst_hat_r
+t+ 147.6s ♪ 音效 sfx/inst_accent
+t+ 148.6s ♪ 音效 sfx/inst_kick
+t+ 148.6s ♪ 音效 sfx/inst_accent
+t+ 149.0s ♪ 音效 sfx/inst_snare
+t+ 149.0s ♪ 音效 sfx/inst_accent
+t+ 149.0s 〰 震动:自定义震型 [20,40,20]
+t+ 149.7s 〰 震动:beat(节拍点)
+t+ 150.3s 〰 震动:beat(节拍点)
+t+ 151.0s 〰 震动:beat(节拍点)
+t+ 151.1s 〰 震动:beat(节拍点)
+t+ 151.1s 〰 震动:beat(节拍点)
+t+ 151.1s 〰 震动:beat(节拍点)
+t+ 151.1s 〰 震动:beat(节拍点)
+t+ 151.7s 〰 震动:beat(节拍点)
+t+ 152.6s ♪ 音效 sfx/inst_kick
+t+ 152.6s ♪ 音效 sfx/inst_accent
+t+ 153.0s ♪ 音效 sfx/inst_snare
+t+ 153.0s ♪ 音效 sfx/inst_accent
+t+ 154.0s ♪ 音效 sfx/inst_kick
+t+ 154.0s ♪ 音效 sfx/inst_accent
+t+ 154.3s ♪ 音效 sfx/inst_snare
+t+ 154.3s ♪ 音效 sfx/inst_accent
+t+ 154.3s 〰 震动:自定义震型 [20,40,20]
+t+ 155.4s ♪ 向导音:settle(结算)
+t+ 155.4s ▶ 语音:「A 级!准确率超过八成五,非常出色。」
+t+ 155.4s 〰 震动:success(成功长震)
+t+ 155.4s 〰 震动:success(成功长震)
+t+ 155.4s 〰 震动:自定义震型 [20,40,20]
+t+ 157.5s ▶ 语音:「新连击纪录!」
+t+ 159.6s ▶ 语音:「新曲目解锁,追风!」
+t+ 161.8s ▶ 语音:「单击再来一遍,右滑下一首,下滑回大厅。」
+t+ 163.9s ♫ BGM 切换 → bgm/menu(淡入 800ms)
+t+ 165.4s 【状态】曲子打完了,结算播报中 结算播报中:听完单击再来一遍,右滑下一首,下滑回大厅(BGM=bgm/menu,语速1)
diff --git a/research/04-playtests/transcripts/rpg.md b/research/04-playtests/transcripts/rpg.md
index b8d9d0bb..a910190c 100644
--- a/research/04-playtests/transcripts/rpg.md
+++ b/research/04-playtests/transcripts/rpg.md
@@ -13,95 +13,99 @@ t+ 10.6s ♫ BGM 切换 → bgm/village(淡入 800ms)
t+ 10.6s ♪ 向导音:explore(探索/安全)
t+ 10.6s ♪ 音效 sfx/step
t+ 10.6s ▶ 语音:「外乡人,林子里不太平。这五十金,拿着防身。」
-t+ 10.6s 【状态】进村了,先听场景描述和选项 【雾口村】:左右滑切选项,单击确认,双击重听,上滑查状态,下滑菜单(语音播放中)
+t+ 10.6s 【状态】进村了,先听场景描述和选项 【雾口村】:左右滑切选项,单击确认,双击重听,上滑查状态,下滑菜单(语音播放中,语速1)
t+ 10.6s ▶ 语音:「去商店。」
t+ 12.7s ▶ 语音:「休息一晚。」
t+ 14.8s ▶ 语音:「查看状态。」
-t+ 17.0s 【状态】选项听了一圈,往林间小路去 【雾口村】:左右滑切选项,单击确认,双击重听,上滑查状态,下滑菜单(BGM=bgm/village)
-t+ 30.6s ▶ 语音:「查看状态。」
-t+ 32.7s ▶ 语音:「前进,去林间小路。」
-t+ 34.8s ♫ BGM 切换 → bgm/explore(淡入 800ms)
-t+ 34.8s ♪ 向导音:explore(探索/安全)
-t+ 34.8s ♪ 音效 sfx/step
-t+ 34.8s ▶ 语音:「林间小路。风穿过树叶,脚下的落叶沙沙作响。」
-t+ 34.8s 【状态】林间小路 【林间小路】:左右滑切选项,单击确认,双击重听,上滑查状态,下滑菜单(语音播放中)
-t+ 34.8s ▶ 语音:「调查四周。」
+t+ 17.0s 【状态】选项听了一圈 【雾口村】:左右滑切选项,单击确认,双击重听,上滑查状态,下滑菜单(BGM=bgm/village,语速1)
+t+ 17.0s ▶ 语音:「前进,去林间小路。」
+
+#### 【玩家动作】滑到头再滑就兜回来了,有人提醒「回到第一个」,不用自己数
+
+t+ 19.1s ▶ 向导语音:「回到第一个。」
+t+ 21.2s ♫ BGM 切换 → bgm/explore(淡入 800ms)
+t+ 21.2s ♪ 向导音:explore(探索/安全)
+t+ 21.2s ♪ 音效 sfx/step
+t+ 21.2s ▶ 语音:「林间小路。风穿过树叶,脚下的落叶沙沙作响。」
+t+ 21.2s 【状态】林间小路 【林间小路】:左右滑切选项,单击确认,双击重听,上滑查状态,下滑菜单(语音播放中,语速1)
+t+ 21.2s ▶ 语音:「调查四周。」
#### 【玩家动作】翻找了一阵,捡到一点钱
-t+ 37.0s ♪ 音效 sfx/coin
-t+ 37.0s ▶ 语音:「你在落叶下摸到十五枚金币。」
-t+ 54.8s ▶ 语音:「调查四周。」
-t+ 54.8s ▶ 语音:「前进,去岔路口。」
-t+ 57.0s ♪ 向导音:explore(探索/安全)
-t+ 57.0s ♪ 音效 sfx/step
-t+ 57.0s ▶ 语音:「岔路口。风声在左右两边,听起来不太一样。」
-t+ 57.0s 【状态】岔路口,水声在东边 【岔路口】:左右滑切选项,单击确认,双击重听,上滑查状态,下滑菜单(语音播放中)
-t+ 57.0s ▶ 语音:「往右,去溪流。」
-t+ 59.1s ♪ 向导音:explore(探索/安全)
-t+ 59.1s ♪ 音效 sfx/step
-t+ 59.1s ▶ 语音:「溪流。水声潺潺,是个歇脚的好地方。」
-t+ 61.2s ♫ BGM 切换 → bgm/battle(淡入 400ms)
-t+ 61.2s ♪ 向导音:battle(战斗/对抗)
-t+ 61.2s ▶ 语音:「野狼拦路!」
-t+ 63.3s ▶ 语音:「第一场战斗来了。别慌,战斗是回合制:你先出手,敌人再反击。」
-t+ 65.4s ▶ 语音:「左右滑切换招式,攻击、破空斩、防御、金疮药。单击,出手。」
-t+ 67.6s ▶ 语音:「敌人出手前有一声前摇。听到前摇,马上双击,就能格挡。」
-t+ 69.7s ▶ 语音:「没把握就防御,伤害减半。金疮药可以回血。」
-t+ 71.8s ▶ 语音:「记住:听清前摇,双击格挡。开始吧!」
-t+ 73.9s ▶ 语音:「战斗里,左右滑选招式,单击出手。听到这个声音,就是你的回合。先滑一滑,试试。」
-t+ 76.0s ♪ 音效 sfx/turn_player
-t+ 76.0s 【状态】遭遇教学狼,战斗开始 战斗中:你的血量八成以上,敌方血量八成以上,等你选招式(左右滑切换,单击出手,下滑逃跑)(BGM=bgm/battle)
-t+ 76.0s ▶ 语音:「破空斩。」
-t+ 78.2s ▶ 语音:「防御。」
-t+ 78.2s ▶ 语音:「挡住了!轮到你了。左右滑选攻击,单击出剑。」
-t+ 80.3s 【状态】招式名听熟了 战斗中:你的血量八成以上,敌方血量八成以上,等你选招式(左右滑切换,单击出手,下滑逃跑)(语音播放中)
-t+ 80.3s ▶ 语音:「金疮药。」
-t+ 82.4s ▶ 语音:「攻击。」
+t+ 23.3s ♪ 音效 sfx/coin
+t+ 23.3s ▶ 语音:「你在落叶下摸到十五枚金币。」
+t+ 25.4s ▶ 语音:「前进,去岔路口。」
+t+ 27.6s ♪ 向导音:explore(探索/安全)
+t+ 27.6s ♪ 音效 sfx/step
+t+ 27.6s ▶ 语音:「岔路口。风声在左右两边,听起来不太一样。」
+t+ 27.6s 【状态】岔路口,水声在东边 【岔路口】:左右滑切选项,单击确认,双击重听,上滑查状态,下滑菜单(语音播放中,语速1)
+t+ 27.6s ▶ 语音:「往右,去溪流。」
+t+ 29.7s ♪ 向导音:explore(探索/安全)
+t+ 29.7s ♪ 音效 sfx/step
+t+ 29.7s ▶ 语音:「溪流。水声潺潺,是个歇脚的好地方。」
+t+ 31.8s ♫ BGM 切换 → bgm/battle(淡入 400ms)
+t+ 31.8s ♪ 向导音:battle(战斗/对抗)
+t+ 31.8s ▶ 语音:「野狼拦路!」
+t+ 33.9s ▶ 语音:「第一场战斗来了。别慌,战斗是回合制:你先出手,敌人再反击。」
+t+ 36.0s ▶ 语音:「左右滑切换招式,攻击、破空斩、防御、金疮药。单击,出手。」
+t+ 38.2s ▶ 语音:「敌人出手前有一声前摇。听到前摇,马上双击,就能格挡。」
+t+ 40.3s ▶ 语音:「没把握就防御,伤害减半。金疮药可以回血。」
+t+ 42.4s ▶ 语音:「记住:听清前摇,双击格挡。开始吧!」
+t+ 44.5s ▶ 语音:「战斗里,左右滑选招式,单击出手。听到这个声音,就是你的回合。先滑一滑,试试。」
+t+ 46.6s ♪ 音效 sfx/turn_player
+t+ 46.6s 【状态】遭遇教学狼,战斗开始 战斗中:你的血量八成以上,敌方血量八成以上,等你选招式(左右滑切换,单击出手,下滑逃跑)(BGM=bgm/battle,语速1)
+t+ 46.6s ▶ 语音:「破空斩。」
+t+ 48.8s ▶ 语音:「防御。」
+t+ 48.8s ▶ 语音:「挡住了!轮到你了。左右滑选攻击,单击出剑。」
+t+ 50.9s 【状态】招式名听熟了 战斗中:你的血量八成以上,敌方血量八成以上,等你选招式(左右滑切换,单击出手,下滑逃跑)(语音播放中,语速1)
+t+ 50.9s ▶ 语音:「金疮药。」
+t+ 53.0s ▶ 语音:「攻击。」
#### 【玩家动作】单击出手,攻击
-t+ 84.0s ♪ 音效 sfx/atk_swing
-t+ 84.0s ♪ 音效 sfx/atk_hit
-t+ 84.0s ▶ 语音:「它要扑了,听前摇!」
-t+ 86.2s ♪ 音效 sfx/turn_player
-t+ 86.2s ▶ 语音:「就是现在,双击格挡!」
-t+ 86.2s 〰 震动:beat(节拍点)
-t+ 86.2s 【状态】前摇声响起了 战斗中:敌方前摇声响起了——双击,格挡!(语音播放中)
+t+ 54.6s ♪ 音效 sfx/atk_swing
+t+ 54.6s ♪ 音效 sfx/atk_hit
+t+ 54.6s ▶ 语音:「它要扑了,听前摇!」
+t+ 56.8s ♪ 音效 sfx/turn_player
+t+ 56.8s ▶ 语音:「就是现在,双击格挡!」
+t+ 56.8s 〰 震动:beat(节拍点)
+t+ 56.8s 【状态】前摇声响起了 战斗中:敌方前摇声响起了——双击,格挡!(语音播放中,语速1)
-#### 【玩家动作】双击格挡
+#### 【玩家动作】双击格挡——这回干净挡下了,听到「挡得漂亮」
-t+ 86.2s ♪ 音效 sfx/parry
-t+ 86.2s ▶ 语音:「来不及格挡,就选防御。左右滑,选防御,单击。」
-t+ 86.2s 〰 震动:confirm(轻点确认)
-t+ 88.3s ♪ 音效 sfx/turn_player
-t+ 88.3s ▶ 语音:「破空斩。」
-t+ 90.4s ▶ 语音:「防御。」
+t+ 56.8s ♪ 音效 sfx/parry
+t+ 56.8s ▶ 语音:「挡得漂亮。」
+t+ 56.8s 〰 震动:confirm(轻点确认)
+t+ 58.9s ▶ 语音:「来不及格挡,就选防御。左右滑,选防御,单击。」
+t+ 61.0s ♪ 音效 sfx/turn_player
+t+ 61.0s ▶ 语音:「破空斩。」
+t+ 62.6s ▶ 语音:「防御。」
#### 【玩家动作】这回合先防御,减半伤害
-t+ 92.0s ▶ 语音:「好剑法!接下来,靠你自己了。」
-t+ 94.2s 〰 震动:beat(节拍点)
+t+ 64.8s ▶ 语音:「好剑法!接下来,靠你自己了。」
+t+ 66.9s 〰 震动:beat(节拍点)
#### 【玩家动作】双击格挡
-t+ 94.2s ♪ 音效 sfx/parry
-t+ 94.2s ♪ 音效 sfx/turn_player
-t+ 94.2s 〰 震动:confirm(轻点确认)
-t+ 94.2s ▶ 语音:「金疮药。」
-t+ 96.0s ▶ 语音:「攻击。」
+t+ 66.9s ♪ 音效 sfx/parry
+t+ 66.9s ♪ 音效 sfx/turn_player
+t+ 66.9s 〰 震动:confirm(轻点确认)
+t+ 66.9s ▶ 语音:「金疮药。」
+t+ 68.7s ▶ 语音:「攻击。」
+t+ 70.8s ▶ 向导语音:「回到第一个。」
#### 【玩家动作】单击,攻击
-t+ 98.1s ♪ 音效 sfx/atk_swing
-t+ 98.1s ♪ 音效 sfx/atk_hit
-t+ 98.1s ♪ 音效 sfx/enemy_die
-t+ 98.1s ▶ 语音:「敌人倒下了。」
-t+ 98.1s 〰 震动:success(成功长震)
-t+ 100.2s ♪ 音效 sfx/victory
-t+ 100.2s ▶ 语音:「战斗胜利。」
-t+ 102.3s ▶ 语音:「小有收获。」
-t+ 104.4s ♫ BGM 切换 → bgm/explore(淡入 2000ms)
-t+ 104.4s ♪ 向导音:explore(探索/安全)
-t+ 104.4s ▶ 语音:「饮水休息。」
-t+ 104.4s 【状态】狼打完了,胜利播报完回到溪流边。一场完整战斗到此收工 【溪流】:左右滑切选项,单击确认,双击重听,上滑查状态,下滑菜单(语音播放中)
+t+ 70.8s ♪ 音效 sfx/atk_swing
+t+ 70.8s ♪ 音效 sfx/atk_hit
+t+ 70.8s ♪ 音效 sfx/enemy_die
+t+ 70.8s ▶ 语音:「敌人倒下了。」
+t+ 70.8s 〰 震动:success(成功长震)
+t+ 72.9s ♪ 音效 sfx/victory
+t+ 72.9s ▶ 语音:「战斗胜利。」
+t+ 75.0s ▶ 语音:「小有收获。」
+t+ 77.2s ♫ BGM 切换 → bgm/explore(淡入 2000ms)
+t+ 77.2s ♪ 向导音:explore(探索/安全)
+t+ 77.2s ▶ 语音:「饮水休息。」
+t+ 77.2s 【状态】狼打完了,胜利播报完回到溪流边。一场完整战斗到此收工 【溪流】:左右滑切选项,单击确认,双击重听,上滑查状态,下滑菜单(语音播放中,语速1)
diff --git a/research/04-playtests/transcripts/simon.md b/research/04-playtests/transcripts/simon.md
index a084716a..f899069e 100644
--- a/research/04-playtests/transcripts/simon.md
+++ b/research/04-playtests/transcripts/simon.md
@@ -10,19 +10,19 @@ t+ 4.7s ▶ 语音:「我会演奏一串声音,请你记住顺序,原
t+ 6.9s ▶ 语音:「向上滑,是这个声音。」
t+ 9.0s ♪ 音效 sfx/tone_up
t+ 9.9s ▶ 语音:「你试试。」
-t+ 11.4s 【状态】教学第 1/6 个音色 教学:该做「上滑」了(语音播放中)
+t+ 11.4s 【状态】教学第 1/6 个音色 教学:该做「上滑」了(语音播放中,语速1)
t+ 11.4s ♪ 音效 sfx/tone_up
t+ 11.4s ▶ 语音:「对了。」
t+ 13.5s ▶ 语音:「向下滑,是这个声音。」
t+ 15.6s ♪ 音效 sfx/tone_down
t+ 16.5s ▶ 语音:「你试试。」
-t+ 18.6s 【状态】教学第 2/6 个音色 教学:该做「下滑」了(无 BGM)
+t+ 18.6s 【状态】教学第 2/6 个音色 教学:该做「下滑」了(无 BGM,语速1)
t+ 18.6s ♪ 音效 sfx/tone_down
t+ 18.6s ▶ 语音:「对了。」
t+ 20.8s ▶ 语音:「向左滑,是这个声音。」
t+ 22.9s ♪ 音效 sfx/tone_left
t+ 23.8s ▶ 语音:「你试试。」
-t+ 25.9s 【状态】教学第 3/6 个音色 教学:该做「左滑」了(无 BGM)
+t+ 25.9s 【状态】教学第 3/6 个音色 教学:该做「左滑」了(无 BGM,语速1)
#### 【玩家动作】把「左滑」记混了,先来了个右滑
@@ -33,146 +33,91 @@ t+ 25.9s ▶ 语音:「对了。」
t+ 28.0s ▶ 语音:「向右滑,是这个声音。」
t+ 30.1s ♪ 音效 sfx/tone_right
t+ 31.0s ▶ 语音:「你试试。」
-t+ 33.2s 【状态】教学第 4/6 个音色 教学:该做「右滑」了(无 BGM)
+t+ 33.2s 【状态】教学第 4/6 个音色 教学:该做「右滑」了(无 BGM,语速1)
t+ 33.2s ♪ 音效 sfx/tone_right
t+ 33.2s ▶ 语音:「对了。」
t+ 35.3s ▶ 语音:「单击,是这个声音。」
t+ 37.4s ♪ 音效 sfx/tone_tap
t+ 38.3s ▶ 语音:「你试试。」
-t+ 40.4s 【状态】教学第 5/6 个音色 教学:该做「单击」了(无 BGM)
+t+ 40.4s 【状态】教学第 5/6 个音色 教学:该做「单击」了(无 BGM,语速1)
t+ 40.4s ♪ 音效 sfx/tone_tap
t+ 40.4s ▶ 语音:「对了。」
t+ 42.5s ▶ 语音:「双击,是这个声音。」
t+ 44.7s ♪ 音效 sfx/tone_doubletap
t+ 45.6s ▶ 语音:「你试试。」
-t+ 47.7s 【状态】教学第 6/6 个音色 教学:该做「双击」了(无 BGM)
+t+ 47.7s 【状态】教学第 6/6 个音色 教学:该做「双击」了(无 BGM,语速1)
t+ 47.7s ♪ 音效 sfx/tone_doubletap
t+ 47.7s ▶ 语音:「对了。」
t+ 49.8s ▶ 语音:「六种声音都记住了。后面的序列,会越来越长。」
t+ 51.9s ▶ 语音:「答错可以消耗机会重听。双击,可以随时重听。」
-t+ 54.0s ▶ 语音:「第一轮,听好了。」
-t+ 56.2s ♪ 音效 sfx/tone_up
-t+ 56.9s ♪ 音效 sfx/tone_left
-t+ 58.0s ♪ 向导音:turn(轮到你)
-t+ 58.0s ▶ 语音:「轮到你了。」
-t+ 70.1s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 70.1s ▶ 向导语音:「需要的话,双击可以重新听一遍序列。」
-t+ 85.1s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 85.1s ♪ 向导音:watchdog(看门狗升级)
-t+ 85.1s ♪ 音效 sfx/tone_up
-t+ 85.8s ♪ 音效 sfx/tone_left
-t+ 86.9s ♪ 向导音:turn(轮到你)
-t+ 86.9s ▶ 语音:「轮到你了。」
-t+ 99.0s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 99.0s ▶ 向导语音:「需要的话,双击可以重新听一遍序列。」
-t+ 114.0s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 114.0s ♪ 向导音:watchdog(看门狗升级)
-t+ 114.0s ♪ 音效 sfx/tone_up
-t+ 114.7s ♪ 音效 sfx/tone_left
-t+ 115.8s ♪ 向导音:turn(轮到你)
-t+ 115.8s ▶ 语音:「轮到你了。」
-t+ 127.9s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 127.9s ▶ 向导语音:「需要的话,双击可以重新听一遍序列。」
-t+ 142.9s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 142.9s ♪ 向导音:watchdog(看门狗升级)
-t+ 142.9s 〰 震动:beat(节拍点)
-t+ 142.9s 〰 震动:beat(节拍点)
-t+ 142.9s 〰 震动:confirm(轻点确认)
-t+ 142.9s 〰 震动:beat(节拍点)
-t+ 142.9s 〰 震动:beat(节拍点)
-t+ 142.9s 〰 震动:confirm(轻点确认)
-t+ 142.9s 〰 震动:beat(节拍点)
-t+ 142.9s 〰 震动:beat(节拍点)
-t+ 142.9s 〰 震动:confirm(轻点确认)
-t+ 142.9s 【状态】轮到我复现了 第 1 轮轮到你了:已对上 0/2,照原顺序做手势(无 BGM)
-t+ 142.9s ♪ 音效 sfx/tone_up
-t+ 142.9s 【状态】轮到我复现了 第 1 轮轮到你了:已对上 1/2,照原顺序做手势(无 BGM)
-t+ 142.9s ♪ 音效 sfx/tone_left
-t+ 142.9s 〰 震动:success(成功长震)
-t+ 143.2s ▶ 语音:「第二轮,听好了。」
-t+ 145.3s ♪ 音效 sfx/tone_up
-t+ 146.0s ♪ 音效 sfx/tone_left
-t+ 146.7s ♪ 音效 sfx/tone_up
-t+ 147.8s ♪ 向导音:turn(轮到你)
-t+ 157.8s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 157.8s ▶ 向导语音:「需要的话,双击可以重新听一遍序列。」
-t+ 172.8s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 172.8s ♪ 向导音:watchdog(看门狗升级)
-t+ 172.8s 〰 震动:beat(节拍点)
-t+ 172.8s 〰 震动:beat(节拍点)
-t+ 172.8s 〰 震动:beat(节拍点)
-t+ 172.8s 〰 震动:confirm(轻点确认)
-t+ 172.8s 【状态】序列有点长,先僵一会儿 第 2 轮轮到你了:已对上 0/3,照原顺序做手势(无 BGM)
-t+ 172.8s ……… 静默 11s(想不起来序列了) ………
-t+ 172.8s 【状态】听到提示:需要的话,双击可以重新听一遍序列 第 2 轮轮到你了:已对上 0/3,照原顺序做手势(无 BGM)
-t+ 172.8s 【状态】轮到我复现了 第 2 轮轮到你了:已对上 0/3,照原顺序做手势(无 BGM)
-t+ 172.8s ♪ 音效 sfx/tone_up
+t+ 54.0s ▶ 向导语音:「记不住就双击重新听,免费机会有两次,用完要扣一次机会。」
+t+ 56.2s ▶ 语音:「第一轮,听好了。」
+t+ 58.3s ♪ 音效 sfx/tone_doubletap
+t+ 58.3s 〰 震动:beat(节拍点)
+t+ 58.3s 【状态】教学讲完了,机会经济也交代清楚了:免费重听两次,用完要扣机会 第 1 轮序列演奏中(2 个音),记住顺序,先别动手(无 BGM,语速1)
+t+ 59.0s ♪ 音效 sfx/tone_up
+t+ 59.0s 〰 震动:beat(节拍点)
+t+ 60.1s ♪ 向导音:turn(轮到你)
+t+ 60.1s ▶ 语音:「轮到你了。」
+t+ 60.1s 〰 震动:confirm(轻点确认)
+t+ 62.2s 【状态】轮到我复现了 第 1 轮轮到你了:已对上 0/2,照原顺序做手势(无 BGM,语速1)
+t+ 62.2s ♪ 音效 sfx/tone_doubletap
+t+ 62.2s 【状态】轮到我复现了 第 1 轮轮到你了:已对上 1/2,照原顺序做手势(无 BGM,语速1)
+t+ 62.2s ♪ 音效 sfx/tone_up
+t+ 62.2s 〰 震动:success(成功长震)
+t+ 62.5s ▶ 语音:「第二轮,听好了。」
+t+ 64.6s ♪ 音效 sfx/tone_doubletap
+t+ 64.6s 〰 震动:beat(节拍点)
+t+ 65.3s ♪ 音效 sfx/tone_up
+t+ 65.3s 〰 震动:beat(节拍点)
+t+ 66.0s ♪ 音效 sfx/tone_doubletap
+t+ 66.0s 〰 震动:beat(节拍点)
+t+ 67.0s ♪ 向导音:turn(轮到你)
+t+ 67.0s 〰 震动:confirm(轻点确认)
+t+ 67.0s 【状态】轮到我复现了 第 2 轮轮到你了:已对上 0/3,照原顺序做手势(无 BGM,语速1)
+t+ 67.0s ♪ 音效 sfx/tone_doubletap
+t+ 67.0s 【状态】序列有点长,先僵一会儿 第 2 轮轮到你了:已对上 1/3,照原顺序做手势(无 BGM,语速1)
+t+ 67.0s ……… 静默 10s(想不起来序列了) ………
+t+ 72.2s 【状态】听到提示:需要的话,双击可以重新听一遍序列 第 2 轮轮到你了:已对上 1/3,照原顺序做手势(无 BGM,语速1)
-#### 【玩家动作】这一下故意点错,听听游戏怎么说
+#### 【玩家动作】照提示双击,用掉一次免费重听
-t+ 172.8s ♪ 音效 sfx/tone_right
-t+ 172.8s ▶ 语音:「错了。用一次机会,再听一遍。」
-t+ 172.8s 〰 震动:error(做错了:两连短震)
-t+ 174.9s ▶ 语音:「重听机会用完了,小心。」
-t+ 177.0s ♪ 音效 sfx/tone_up
-t+ 177.7s ♪ 音效 sfx/tone_left
-t+ 178.3s ♪ 音效 sfx/tone_up
-t+ 179.4s ♪ 向导音:turn(轮到你)
-t+ 189.4s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 189.4s ▶ 向导语音:「需要的话,双击可以重新听一遍序列。」
-t+ 204.4s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 204.4s ♪ 向导音:watchdog(看门狗升级)
-t+ 204.4s 〰 震动:warning(警告三连震)
-t+ 204.4s 〰 震动:beat(节拍点)
-t+ 204.4s 〰 震动:beat(节拍点)
-t+ 204.4s 〰 震动:beat(节拍点)
-t+ 204.4s 〰 震动:confirm(轻点确认)
-t+ 204.4s 【状态】轮到我复现了 第 2 轮轮到你了:已对上 0/3,照原顺序做手势(无 BGM)
-t+ 204.4s ♪ 音效 sfx/tone_up
-t+ 204.4s 【状态】轮到我复现了 第 2 轮轮到你了:已对上 1/3,照原顺序做手势(无 BGM)
-t+ 204.4s ♪ 音效 sfx/tone_left
-t+ 204.4s 【状态】轮到我复现了 第 2 轮轮到你了:已对上 2/3,照原顺序做手势(无 BGM)
-t+ 204.4s ♪ 音效 sfx/tone_up
-t+ 204.4s 〰 震动:success(成功长震)
-t+ 204.7s ▶ 语音:「第三轮,听好了。」
-t+ 206.8s ♪ 音效 sfx/tone_up
-t+ 207.5s ♪ 音效 sfx/tone_left
-t+ 208.1s ♪ 音效 sfx/tone_up
-t+ 208.8s ♪ 音效 sfx/tone_left
-t+ 209.8s ♪ 向导音:turn(轮到你)
-t+ 219.8s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 219.8s ▶ 向导语音:「需要的话,双击可以重新听一遍序列。」
-t+ 234.8s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 234.8s ♪ 向导音:watchdog(看门狗升级)
-t+ 234.8s 〰 震动:beat(节拍点)
-t+ 234.8s 〰 震动:beat(节拍点)
-t+ 234.8s 〰 震动:beat(节拍点)
-t+ 234.8s 〰 震动:beat(节拍点)
-t+ 234.8s 〰 震动:confirm(轻点确认)
-t+ 234.8s 【状态】轮到我复现了 第 3 轮轮到你了:已对上 0/4,照原顺序做手势(无 BGM)
-t+ 234.8s ♪ 音效 sfx/tone_up
-t+ 234.8s 【状态】轮到我复现了 第 3 轮轮到你了:已对上 1/4,照原顺序做手势(无 BGM)
-t+ 234.8s ♪ 音效 sfx/tone_left
-t+ 234.8s 【状态】轮到我复现了 第 3 轮轮到你了:已对上 2/4,照原顺序做手势(无 BGM)
-t+ 234.8s ♪ 音效 sfx/tone_up
-t+ 234.8s 【状态】轮到我复现了 第 3 轮轮到你了:已对上 3/4,照原顺序做手势(无 BGM)
-t+ 234.8s ♪ 音效 sfx/tone_left
-t+ 234.8s 〰 震动:success(成功长震)
-t+ 235.1s ▶ 语音:「第四轮,听好了。」
-t+ 237.3s ♪ 音效 sfx/tone_up
-t+ 237.9s ♪ 音效 sfx/tone_left
-t+ 238.5s ♪ 音效 sfx/tone_up
-t+ 239.1s ♪ 音效 sfx/tone_left
-t+ 239.7s ♪ 音效 sfx/tone_down
-t+ 240.8s ♪ 向导音:turn(轮到你)
-t+ 250.8s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 250.8s ▶ 向导语音:「需要的话,双击可以重新听一遍序列。」
-t+ 265.8s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 265.8s ♪ 向导音:watchdog(看门狗升级)
-t+ 265.8s 〰 震动:beat(节拍点)
-t+ 265.8s 〰 震动:beat(节拍点)
-t+ 265.8s 〰 震动:beat(节拍点)
-t+ 265.8s 〰 震动:beat(节拍点)
-t+ 265.8s 〰 震动:beat(节拍点)
-t+ 265.8s 〰 震动:confirm(轻点确认)
-t+ 265.8s 【状态】收工:完成了 3 轮 第 4 轮轮到你了:已对上 0/5,照原顺序做手势(无 BGM)
+t+ 72.2s ▶ 语音:「再听一遍。」
+t+ 72.2s 〰 震动:pickup(获得)
+t+ 74.3s ▶ 语音:「重听机会用完了,小心。」
+t+ 76.4s ♪ 音效 sfx/tone_doubletap
+t+ 76.4s 〰 震动:beat(节拍点)
+t+ 77.1s ♪ 音效 sfx/tone_up
+t+ 77.8s ♪ 音效 sfx/tone_doubletap
+t+ 77.1s 〰 震动:beat(节拍点)
+t+ 77.8s 〰 震动:beat(节拍点)
+t+ 78.9s ♪ 向导音:turn(轮到你)
+t+ 78.9s 〰 震动:confirm(轻点确认)
+t+ 78.9s 【状态】轮到我复现了 第 2 轮轮到你了:已对上 0/3,照原顺序做手势(无 BGM,语速1)
+t+ 78.9s ♪ 音效 sfx/tone_doubletap
+t+ 78.9s 【状态】轮到我复现了 第 2 轮轮到你了:已对上 1/3,照原顺序做手势(无 BGM,语速1)
+t+ 78.9s ♪ 音效 sfx/tone_up
+t+ 78.9s 【状态】轮到我复现了 第 2 轮轮到你了:已对上 2/3,照原顺序做手势(无 BGM,语速1)
+t+ 78.9s ♪ 音效 sfx/tone_doubletap
+t+ 78.9s 〰 震动:success(成功长震)
+t+ 79.2s ▶ 语音:「第三轮,听好了。」
+t+ 81.3s ♪ 音效 sfx/tone_doubletap
+t+ 81.3s 〰 震动:beat(节拍点)
+t+ 81.9s ♪ 音效 sfx/tone_up
+t+ 81.9s 〰 震动:beat(节拍点)
+t+ 82.6s ♪ 音效 sfx/tone_doubletap
+t+ 82.6s 〰 震动:beat(节拍点)
+t+ 83.2s ♪ 音效 sfx/tone_up
+t+ 83.2s 〰 震动:beat(节拍点)
+t+ 84.3s ♪ 向导音:turn(轮到你)
+t+ 84.3s 〰 震动:confirm(轻点确认)
+t+ 84.3s 【状态】轮到我复现了 第 3 轮轮到你了:已对上 0/4,照原顺序做手势(无 BGM,语速1)
+t+ 84.3s ♪ 音效 sfx/tone_doubletap
+t+ 84.3s 【状态】轮到我复现了 第 3 轮轮到你了:已对上 1/4,照原顺序做手势(无 BGM,语速1)
+t+ 84.3s ♪ 音效 sfx/tone_up
+t+ 84.3s 【状态】轮到我复现了 第 3 轮轮到你了:已对上 2/4,照原顺序做手势(无 BGM,语速1)
+t+ 84.3s ♪ 音效 sfx/tone_doubletap
+t+ 84.3s 【状态】最后一轮最后一个音,打完这轮就收 第 3 轮轮到你了:已对上 3/4,照原顺序做手势(无 BGM,语速1)
+t+ 84.3s ♪ 音效 sfx/tone_up
+t+ 84.3s 〰 震动:success(成功长震)
+t+ 84.3s 【状态】收工:完整打完 3 轮,在轮与轮的边界停手(免费重听剩 0 次) 刚答完一轮,播报中(无 BGM,语速1)
diff --git a/research/04-playtests/transcripts/space.md b/research/04-playtests/transcripts/space.md
index 3d7c9188..01e4976e 100644
--- a/research/04-playtests/transcripts/space.md
+++ b/research/04-playtests/transcripts/space.md
@@ -9,7 +9,7 @@ t+ 2.1s ▶ 语音:「单击可跳过旁白。」
t+ 4.2s ▶ 语音:「陨石来了,会有声音告诉你方位。」
t+ 6.4s ▶ 语音:「听,左边有陨石。向左滑,把它拨开!」
t+ 8.5s ♪ 音效 sfx/whoosh_left
-t+ 8.5s 【状态】演练 1/4:左侧有陨石 演练:左边有陨石在逼近,左边滑把它拨开(无 BGM)
+t+ 8.5s 【状态】演练 1/4:左侧有陨石 演练:左边有陨石在逼近,左边滑把它拨开(无 BGM,语速1)
#### 【玩家动作】逼近音一响,同向滑开
@@ -17,7 +17,7 @@ t+ 8.5s ♪ 音效 sfx/dodge
t+ 8.5s ▶ 语音:「漂亮。」
t+ 10.6s ▶ 语音:「这次在右边。向右滑!」
t+ 12.7s ♪ 音效 sfx/whoosh_right
-t+ 12.7s 【状态】演练 2/4:右侧有陨石 演练:右边有陨石在逼近,右边滑把它拨开(无 BGM)
+t+ 12.7s 【状态】演练 2/4:右侧有陨石 演练:右边有陨石在逼近,右边滑把它拨开(无 BGM,语速1)
#### 【玩家动作】这颗犹豫了,没拨开——听当场重教
@@ -30,7 +30,7 @@ t+ 19.3s ♪ 音效 sfx/whoosh_right
t+ 19.3s ♪ 音效 sfx/dodge
t+ 19.3s ▶ 语音:「上面有东西,声音又尖又高。向上滑!」
t+ 21.5s ♪ 音效 sfx/whoosh_up
-t+ 21.5s 【状态】演练 3/4:上侧有陨石 演练:上面有陨石在逼近,上面滑把它拨开(无 BGM)
+t+ 21.5s 【状态】演练 3/4:上侧有陨石 演练:上面有陨石在逼近,上面滑把它拨开(无 BGM,语速1)
#### 【玩家动作】逼近音一响,同向滑开
@@ -38,90 +38,162 @@ t+ 21.5s ♪ 音效 sfx/dodge
t+ 21.5s ▶ 语音:「漂亮。」
t+ 23.6s ▶ 语音:「下面是低沉的轰隆。向下滑!」
t+ 25.7s ♪ 音效 sfx/whoosh_down
-t+ 25.7s 【状态】演练 4/4:下侧有陨石 演练:下面有陨石在逼近,下面滑把它拨开(无 BGM)
+t+ 25.7s 【状态】演练 4/4:下侧有陨石 演练:下面有陨石在逼近,下面滑把它拨开(无 BGM,语速1)
#### 【玩家动作】逼近音一响,同向滑开
t+ 25.7s ♪ 音效 sfx/dodge
t+ 25.7s ▶ 语音:「四个方向你都会了。引擎点火,出发!」
-t+ 25.7s 【状态】演练做完了,要出发了 演练旁白在播,先听(语音播放中)
+t+ 25.7s 【状态】演练做完了,要出发了 演练旁白在播,先听(语音播放中,语速1)
t+ 27.8s ♫ BGM 切换 → bgm/engine
t+ 27.8s ♪ 向导音:battle(战斗/对抗)
-t+ 27.8s 【状态】出发。引擎声起来了,保护期里先练手 对局中:引擎声平稳,等下一颗(保护期)(BGM=bgm/engine)
-t+ 29.3s ♪ 音效 sfx/whoosh_right
+t+ 27.8s 【状态】出发。引擎声起来了,保护期里先练手 对局中:引擎声平稳,等下一颗(保护期)(BGM=bgm/engine,语速1)
+t+ 29.3s ♪ 音效 sfx/whoosh_left
t+ 32.3s ♪ 音效 sfx/alarm
t+ 32.3s 〰 震动:danger(危险逼近)
t+ 32.8s ♪ 音效 sfx/alarm
+t+ 32.8s 〰 震动:danger(危险逼近)
t+ 33.3s ♪ 音效 sfx/dodge
t+ 33.3s ♪ 音效 sfx/whoosh_up
t+ 36.3s ♪ 音效 sfx/alarm
t+ 36.3s 〰 震动:danger(危险逼近)
-t+ 36.3s 〰 震动:danger(危险逼近)
t+ 36.8s ♪ 音效 sfx/alarm
+t+ 36.8s 〰 震动:danger(危险逼近)
t+ 37.3s ♪ 音效 sfx/dodge
-t+ 37.3s ♪ 音效 sfx/whoosh_left
+t+ 37.3s ♪ 音效 sfx/whoosh_right
t+ 40.3s ♪ 音效 sfx/alarm
t+ 40.3s 〰 震动:danger(危险逼近)
-t+ 40.3s 〰 震动:danger(危险逼近)
t+ 40.8s ♪ 音效 sfx/alarm
+t+ 40.8s 〰 震动:danger(危险逼近)
t+ 41.3s ♪ 音效 sfx/dodge
-t+ 41.3s ♪ 音效 sfx/whoosh_right
+t+ 41.3s ♪ 音效 sfx/whoosh_down
t+ 44.3s ♪ 音效 sfx/alarm
t+ 44.3s 〰 震动:danger(危险逼近)
-t+ 44.3s 〰 震动:danger(危险逼近)
t+ 44.8s ♪ 音效 sfx/alarm
+t+ 44.8s 〰 震动:danger(危险逼近)
t+ 45.3s ♪ 音效 sfx/dodge
-t+ 45.3s ♪ 音效 sfx/whoosh_down
+t+ 45.3s ♪ 音效 sfx/whoosh_up
t+ 47.8s ▶ 语音:「试试单击,引擎会短促加速,直接冲过空隙。」
-t+ 58.3s ♪ 音效 sfx/whoosh_left
-t+ 61.3s ♪ 音效 sfx/alarm
-t+ 61.3s 〰 震动:danger(危险逼近)
-t+ 61.3s 〰 震动:danger(危险逼近)
-t+ 61.8s ♪ 音效 sfx/alarm
-t+ 62.3s ♪ 音效 sfx/dodge
-t+ 62.3s ♪ 音效 sfx/whoosh_down
-t+ 65.3s ♪ 音效 sfx/alarm
-t+ 65.3s 〰 震动:danger(危险逼近)
-t+ 65.3s 〰 震动:danger(危险逼近)
-t+ 65.8s ♪ 音效 sfx/alarm
-t+ 66.3s ♪ 音效 sfx/dodge
-t+ 66.3s ♪ 音效 sfx/whoosh_down
-t+ 69.3s ♪ 音效 sfx/alarm
-t+ 69.3s 〰 震动:danger(危险逼近)
-t+ 69.3s 〰 震动:danger(危险逼近)
-t+ 69.8s ♪ 音效 sfx/alarm
-t+ 70.3s ♪ 音效 sfx/dodge
-t+ 70.3s ♪ 音效 sfx/whoosh_right
+t+ 47.8s 【状态】游戏停下来教新功能 暂停了刷怪教新功能:单击试试加速(语音播放中,语速1)
+
+#### 【玩家动作】照它说的单击加速
+
+t+ 49.9s ♪ 音效 sfx/boost
+t+ 49.9s ▶ 语音:「感觉到了吗?加速的时候,陨石撞不上你。」
+t+ 52.4s ♪ 音效 sfx/whoosh_left
+t+ 55.4s ♪ 音效 sfx/alarm
+t+ 55.4s 〰 震动:danger(危险逼近)
+t+ 55.9s ♪ 音效 sfx/alarm
+t+ 55.9s 〰 震动:danger(危险逼近)
+t+ 56.4s ♪ 音效 sfx/dodge
+t+ 56.4s ♪ 音效 sfx/whoosh_left
+t+ 59.4s ♪ 音效 sfx/alarm
+t+ 59.4s 〰 震动:danger(危险逼近)
+t+ 59.9s ♪ 音效 sfx/alarm
+t+ 59.9s 〰 震动:danger(危险逼近)
+t+ 60.4s ♪ 音效 sfx/dodge
+t+ 60.4s ♪ 音效 sfx/whoosh_left
+t+ 63.4s ♪ 音效 sfx/alarm
+t+ 63.4s 〰 震动:danger(危险逼近)
+t+ 63.9s ♪ 音效 sfx/alarm
+t+ 63.9s 〰 震动:danger(危险逼近)
+t+ 64.4s ♪ 音效 sfx/dodge
+t+ 64.4s ♪ 音效 sfx/whoosh_left
+t+ 67.4s ♪ 音效 sfx/alarm
+t+ 67.4s 〰 震动:danger(危险逼近)
+t+ 67.9s ♪ 音效 sfx/alarm
+t+ 67.9s 〰 震动:danger(危险逼近)
+t+ 68.4s ♪ 音效 sfx/dodge
+t+ 68.4s ♪ 音效 sfx/whoosh_up
+t+ 71.4s ♪ 音效 sfx/alarm
+t+ 71.4s 〰 震动:danger(危险逼近)
+t+ 71.9s ♪ 音效 sfx/alarm
+t+ 71.9s 〰 震动:danger(危险逼近)
+t+ 72.4s ♪ 音效 sfx/dodge
+t+ 72.4s ♪ 音效 sfx/whoosh_down
t+ 72.8s ▶ 语音:「双击展开护盾,能挡下一次撞击。」
-t+ 83.3s ♪ 音效 sfx/whoosh_left
-t+ 86.3s ♪ 音效 sfx/alarm
-t+ 86.3s 〰 震动:danger(危险逼近)
-t+ 86.3s 〰 震动:danger(危险逼近)
-t+ 86.8s ♪ 音效 sfx/alarm
-t+ 87.3s ♪ 音效 sfx/dodge
-t+ 87.3s ♪ 音效 sfx/whoosh_left
+t+ 72.8s 【状态】游戏停下来教新功能 暂停了刷怪教新功能:双击展开护盾(语音播放中,语速1)
+
+#### 【玩家动作】照它说的双击开护盾
+
+t+ 74.9s ♪ 音效 sfx/shield
+t+ 74.9s ▶ 语音:「护盾展开后会自动充能,好了我会告诉你。」
+t+ 77.4s ♪ 音效 sfx/whoosh_right
+t+ 80.4s ♪ 音效 sfx/alarm
+t+ 80.4s 〰 震动:danger(危险逼近)
+t+ 80.9s ♪ 音效 sfx/alarm
+t+ 80.9s 〰 震动:danger(危险逼近)
+t+ 81.4s ♪ 音效 sfx/dodge
+t+ 81.4s ▶ 语音:「十颗了,稳。」
+t+ 81.4s ♪ 音效 sfx/whoosh_down
+t+ 81.4s 〰 震动:confirm(轻点确认)
+t+ 84.4s ♪ 音效 sfx/alarm
+t+ 84.4s 〰 震动:danger(危险逼近)
+t+ 84.9s ♪ 音效 sfx/alarm
+t+ 84.9s 〰 震动:danger(危险逼近)
+t+ 85.4s ♪ 音效 sfx/dodge
+t+ 85.4s ♪ 音效 sfx/whoosh_right
t+ 87.8s ▶ 语音:「训练护盾关闭。接下来,靠你自己了。」
-t+ 90.3s ♪ 音效 sfx/alarm
-t+ 90.3s 〰 震动:danger(危险逼近)
-t+ 90.3s 〰 震动:warning(警告三连震)
-t+ 90.3s 〰 震动:danger(危险逼近)
-t+ 90.3s 【状态】保护期结束了,这颗我故意没动 对局中:1 颗陨石在逼近,听方位同向滑开(BGM=bgm/engine)
-t+ 90.8s ♪ 音效 sfx/alarm
-t+ 91.3s ♪ 音效 sfx/hit
-t+ 91.3s ▶ 语音:「左边撞上的。听到左边的呼啸,向左滑。」
-t+ 91.3s ▶ 向导语音:「被撞了。听方位,同向滑动就能拨开。」
-t+ 91.3s 〰 震动:danger(危险逼近)
-t+ 91.3s 〰 震动:hit(受击重震)
+t+ 87.8s 〰 震动:warning(警告三连震)
+t+ 88.4s ♪ 音效 sfx/alarm
+t+ 88.4s 〰 震动:danger(危险逼近)
+t+ 88.4s 【状态】保护期结束了,这颗我故意没动 对局中:1 颗陨石在逼近,听方位同向滑开(语音播放中,语速1)
+t+ 88.9s ♪ 音效 sfx/alarm
+t+ 88.9s 〰 震动:danger(危险逼近)
+t+ 89.4s ♪ 音效 sfx/hit
+t+ 89.4s ▶ 语音:「右边撞上的。听到右边的呼啸,向右滑。」
+t+ 89.4s 〰 震动:hit(受击重震)
#### 【玩家动作】被撞了。先听到方位归因,又听到「同向滑动就能拨开」
-t+ 91.3s ♪ 音效 sfx/whoosh_down
-t+ 93.1s ♪ 音效 sfx/alarm
-t+ 93.1s 〰 震动:danger(危险逼近)
-t+ 93.6s ♪ 音效 sfx/alarm
-t+ 94.1s ♪ 音效 sfx/dodge
-t+ 94.5s ♪ 音效 sfx/whoosh_down
-t+ 96.2s ♪ 音效 sfx/alarm
-t+ 96.2s 〰 震动:danger(危险逼近)
-t+ 96.2s 〰 震动:danger(危险逼近)
-t+ 96.2s 【状态】躲过保护期、挨过一次撞,心跳有点快,先到这 对局中:1 颗陨石在逼近,听方位同向滑开(BGM=bgm/engine)
+t+ 89.4s ♪ 音效 sfx/whoosh_up
+t+ 91.2s ♪ 音效 sfx/alarm
+t+ 91.2s 〰 震动:danger(危险逼近)
+t+ 91.2s 【状态】又有一颗,也故意不躲——想听听第二次被撞会说什么 对局中:1 颗陨石在逼近,听方位同向滑开(语音播放中,语速1)
+t+ 91.6s ▶ 向导语音:「被撞了。听方位,同向滑动就能拨开。」
+t+ 91.7s ♪ 音效 sfx/alarm
+t+ 91.7s 〰 震动:danger(危险逼近)
+t+ 92.2s ♪ 音效 sfx/hit
+t+ 92.2s ▶ 语音:「上面撞上的。听到高高的呼啸,向上滑。」
+t+ 92.2s 〰 震动:hit(受击重震)
+
+#### 【玩家动作】第二次被撞:先听方位归因,又听到「这是最后一次机会,撑住」——这就是全部家底了
+
+t+ 92.6s ♪ 音效 sfx/whoosh_right
+t+ 94.3s ▶ 语音:「船体只剩最后一次了,小心!」
+t+ 94.4s ♪ 音效 sfx/alarm
+t+ 94.9s ♪ 音效 sfx/alarm
+t+ 94.4s 〰 震动:danger(危险逼近)
+t+ 94.9s 〰 震动:danger(危险逼近)
+t+ 95.4s ♪ 音效 sfx/dodge
+t+ 95.7s ♪ 音效 sfx/whoosh_down
+t+ 96.5s ▶ 向导语音:「这是最后一次机会,撑住。」
+t+ 97.5s ♪ 音效 sfx/alarm
+t+ 97.5s 〰 震动:danger(危险逼近)
+t+ 98.0s ♪ 音效 sfx/alarm
+t+ 98.5s ♪ 音效 sfx/dodge
+t+ 98.8s ♪ 音效 sfx/whoosh_left
+t+ 98.0s 〰 震动:danger(危险逼近)
+t+ 100.5s ♪ 音效 sfx/alarm
+t+ 100.5s 〰 震动:danger(危险逼近)
+t+ 101.0s ♪ 音效 sfx/alarm
+t+ 101.5s ♪ 音效 sfx/dodge
+t+ 101.8s ♪ 音效 sfx/whoosh_up
+t+ 101.0s 〰 震动:danger(危险逼近)
+t+ 103.5s ♪ 音效 sfx/alarm
+t+ 103.5s 〰 震动:danger(危险逼近)
+t+ 104.0s ♪ 音效 sfx/alarm
+t+ 104.5s ♪ 音效 sfx/dodge
+t+ 104.8s ♪ 音效 sfx/whoosh_right
+t+ 104.9s ▶ 语音:「护盾充能完毕。」
+t+ 104.0s 〰 震动:danger(危险逼近)
+t+ 104.9s 〰 震动:pickup(获得)
+t+ 106.4s ♪ 音效 sfx/alarm
+t+ 106.4s 〰 震动:danger(危险逼近)
+t+ 106.9s ♪ 音效 sfx/alarm
+t+ 106.9s 〰 震动:danger(危险逼近)
+t+ 107.4s ♪ 音效 sfx/dodge
+t+ 107.7s ♪ 音效 sfx/whoosh_down
+t+ 109.3s ♪ 音效 sfx/alarm
+t+ 109.3s 〰 震动:danger(危险逼近)
+t+ 109.3s 【状态】保护期结束后撑了 22 秒、挨了两下还活着(警报也只起一波了),先到这 对局中:1 颗陨石在逼近,听方位同向滑开(BGM=bgm/engine,语速1)
diff --git a/research/04-playtests/transcripts/storyline.md b/research/04-playtests/transcripts/storyline.md
index d3f4c083..a83a0d39 100644
--- a/research/04-playtests/transcripts/storyline.md
+++ b/research/04-playtests/transcripts/storyline.md
@@ -6,198 +6,119 @@
t+ 0.0s (玩家点开游戏,轻触屏幕开始)
t+ 0.0s ♫ BGM 切换 → bgm/dark_drone
t+ 0.0s ♪ 向导音:explore(探索/安全)
-t+ 0.0s ▶ 语音:「大停电后的第三十天,城市陷入彻底的黑暗。没有电,没有网络,只有收音机里偶尔闪过的沙沙声,像垂死者的呼吸。你是林晚,一名前急救员。在这个故事里,你用声音活下去。请记住:单击屏幕,继续剧情;双击,重听上一句;向上滑,听你的状态和帮助;向下滑,呼出菜单。当我说“请选择”的时候,向左滑,或者向右滑,做出你的决定。选择一旦做出,就无法回头。现在,单击屏幕,睁开眼睛。」
-t+ 2.1s ▶ 语音:「单击可跳过旁白。」
-t+ 12.2s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 12.2s ▶ 向导语音:「轻触屏幕,继续。」
-t+ 24.2s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 24.2s ♪ 向导音:watchdog(看门狗升级)
-t+ 24.2s ▶ 语音:「操作说明。单击屏幕:继续剧情。双击:重听刚才的台词。向上滑:听这段说明和当前进度。向下滑:呼出菜单,菜单里可以重新开始。当旁白说“请选择”的时候,向左滑或者向右滑,做出你的决定。选择无法撤销。」
-t+ 26.4s ▶ 语音:「当前进度:第一幕,城南地铁站附近。你的目标:活着离开这片废墟,前往北区的广播塔,寻找妹妹林晨的下落。」
-t+ 28.5s ▶ 语音:「大停电后的第三十天,城市陷入彻底的黑暗。没有电,没有网络,只有收音机里偶尔闪过的沙沙声,像垂死者的呼吸。你是林晚,一名前急救员。在这个故事里,你用声音活下去。请记住:单击屏幕,继续剧情;双击,重听上一句;向上滑,听你的状态和帮助;向下滑,呼出菜单。当我说“请选择”的时候,向左滑,或者向右滑,做出你的决定。选择一旦做出,就无法回头。现在,单击屏幕,睁开眼睛。」
-t+ 30.6s 【状态】开场旁白播完了,屏幕黑着,有点不知所措 剧情等着你(节点 n00):单击继续,双击重听(BGM=bgm/dark_drone)
-t+ 30.6s ……… 静默 8s(新手没反应过来) ………
-t+ 30.6s 【状态】听到一声轻响和「轻触屏幕,继续」,试着点一下 剧情等着你(节点 n00):单击继续,双击重听(BGM=bgm/dark_drone)
-t+ 30.6s ♪ 音效 sfx/heartbeat
-t+ 30.6s ▶ 语音:「后脑一阵钝痛,嘴里全是铁锈味。你在废弃的地铁站里醒来,四周黑得像沉在水底。背包还压在身下,手电筒却不知去向。远处滴水的声音,一下,一下,像某种倒计时。」
-t+ 40.7s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 40.7s ▶ 向导语音:「轻触屏幕,继续。」
-t+ 52.7s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 52.7s ♪ 向导音:watchdog(看门狗升级)
-t+ 52.7s ▶ 语音:「操作说明。单击屏幕:继续剧情。双击:重听刚才的台词。向上滑:听这段说明和当前进度。向下滑:呼出菜单,菜单里可以重新开始。当旁白说“请选择”的时候,向左滑或者向右滑,做出你的决定。选择无法撤销。」
-t+ 54.8s ▶ 语音:「当前进度:第一幕,城南地铁站附近。你的目标:活着离开这片废墟,前往北区的广播塔,寻找妹妹林晨的下落。」
-t+ 57.0s ♪ 音效 sfx/heartbeat
-t+ 57.0s ▶ 语音:「后脑一阵钝痛,嘴里全是铁锈味。你在废弃的地铁站里醒来,四周黑得像沉在水底。背包还压在身下,手电筒却不知去向。远处滴水的声音,一下,一下,像某种倒计时。」
-t+ 59.1s 【状态】剧情停下来了,在等我 剧情等着你(节点 n01):单击继续,双击重听(BGM=bgm/dark_drone)
+t+ 0.0s ▶ 向导语音:「单击继续剧情。请选择的时候,左右滑动。」
+t+ 2.1s ▶ 语音:「大停电后的第三十天,城市陷入彻底的黑暗。没有电,没有网络,只有收音机里偶尔闪过的沙沙声,像垂死者的呼吸。你是林晚,一名前急救员。在这个故事里,你用声音活下去。请记住:单击屏幕,继续剧情;双击,重听上一句;向上滑,听你的状态和帮助;向下滑,呼出菜单。当我说“请选择”的时候,向左滑,或者向右滑,做出你的决定。选择一旦做出,就无法回头。现在,单击屏幕,睁开眼睛。」
+t+ 4.2s ▶ 语音:「单击可跳过旁白。」
+t+ 6.4s 【状态】开场口令和旁白播完了,屏幕黑着,走神想剧情去了 剧情等着你(节点 n00):单击继续,双击重听(BGM=bgm/dark_drone,语速1)
+t+ 6.4s ……… 静默 25s(在入戏,不是迷路(round2:剧情类等待默认低频)) ………
+t+ 31.4s ♪ 向导音:nudge(看门狗轻提醒)
+t+ 31.4s ▶ 向导语音:「轻触屏幕,继续。」
+t+ 33.5s 【状态】听到一声轻响和「轻触屏幕,继续」,马上点一下 剧情等着你(节点 n00):单击继续,双击重听(BGM=bgm/dark_drone,语速1)
+t+ 33.5s ♪ 音效 sfx/heartbeat
+t+ 33.5s ▶ 语音:「后脑一阵钝痛,嘴里全是铁锈味。你在废弃的地铁站里醒来,四周黑得像沉在水底。背包还压在身下,手电筒却不知去向。远处滴水的声音,一下,一下,像某种倒计时。」
+t+ 35.6s 【状态】剧情停下来了,在等我 剧情等着你(节点 n01):单击继续,双击重听(BGM=bgm/dark_drone,语速1)
#### 【玩家动作】试着左滑了一下——这里用不上,向导纠正了我
-t+ 59.1s ▶ 向导语音:「这个操作在这里用不上。」
-t+ 59.1s 〰 震动:error(做错了:两连短震)
-t+ 61.2s ▶ 语音:「咳……咳咳……我还活着。晨晨,你在哪儿……你说过,会在北区等我。姐一定,一定找到你。」
-t+ 71.1s ▶ 语音:「请选择。向左滑:先检查背包里的收音机。向右滑:先摸黑寻找出口。」
-t+ 73.2s 【状态】抉择点,提示音念完了 「请选择」念完了(节点 n02):左滑或右滑,做出你的决定(BGM=bgm/dark_drone)
+t+ 35.6s ▶ 向导语音:「这个操作在这里用不上。」
+t+ 35.6s 〰 震动:error(做错了:两连短震)
+t+ 37.7s ▶ 语音:「咳……咳咳……我还活着。晨晨,你在哪儿……你说过,会在北区等我。姐一定,一定找到你。」
+t+ 47.6s ▶ 语音:「请选择。向左滑:先检查背包里的收音机。向右滑:先摸黑寻找出口。」
+t+ 49.7s 【状态】抉择点,提示音念完了 「请选择」念完了(节点 n02):左滑或右滑,做出你的决定(BGM=bgm/dark_drone,语速1)
#### 【玩家动作】选了左边
-t+ 73.2s ♪ 音效 sfx/ui_confirm
-t+ 73.2s ♪ 音效 sfx/radio_static
-t+ 73.2s ▶ 语音:「收音机……还好,没摔坏。等等,这是什么……这段录音,是三天前的……」
-t+ 81.0s ▶ 语音:「姐,如果你听到这个,说明你还活着,太好了。听着,跟着广播塔的红色信号灯走,别相信任何自称救援队的人。北区,北区有真正的避难所。我在这儿等你。姐,一定要来。」
-t+ 106.7s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 106.7s ▶ 向导语音:「轻触屏幕,继续。」
-t+ 118.7s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 118.7s ♪ 向导音:watchdog(看门狗升级)
-t+ 118.7s ▶ 语音:「操作说明。单击屏幕:继续剧情。双击:重听刚才的台词。向上滑:听这段说明和当前进度。向下滑:呼出菜单,菜单里可以重新开始。当旁白说“请选择”的时候,向左滑或者向右滑,做出你的决定。选择无法撤销。」
-t+ 120.8s ▶ 语音:「当前进度:第一幕,城南地铁站附近。你的目标:活着离开这片废墟,前往北区的广播塔,寻找妹妹林晨的下落。」
-t+ 122.9s ♪ 音效 sfx/radio_static
-t+ 122.9s ▶ 语音:「收音机……还好,没摔坏。等等,这是什么……这段录音,是三天前的……」
-t+ 130.8s ▶ 语音:「姐,如果你听到这个,说明你还活着,太好了。听着,跟着广播塔的红色信号灯走,别相信任何自称救援队的人。北区,北区有真正的避难所。我在这儿等你。姐,一定要来。」
-t+ 148.4s 【状态】剧情停下来了,在等我 剧情等着你(节点 n03):单击继续,双击重听(BGM=bgm/dark_drone)
-t+ 148.4s ……… 静默 8s(走神想剧情去了) ………
-t+ 148.4s 【状态】又听到那声轻响,想起该点屏幕了 剧情等着你(节点 n03):单击继续,双击重听(BGM=bgm/dark_drone)
-t+ 148.4s ♪ 音效 sfx/radio_static
-t+ 148.4s ▶ 语音:「就在这时,背包侧袋里的收音机,沙沙声忽然变得清晰。一个平稳的女声,一遍又一遍地重复着:」
-t+ 150.5s ▶ 语音:「北区避难所,仍在接收幸存者。重复,北区避难所,仍在接收幸存者。坐标,三、七点、五。请沿广播塔红色信号灯方向前进。」
-t+ 171.5s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 171.5s ▶ 向导语音:「轻触屏幕,继续。」
-t+ 183.5s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 183.5s ♪ 向导音:watchdog(看门狗升级)
-t+ 183.5s ▶ 语音:「操作说明。单击屏幕:继续剧情。双击:重听刚才的台词。向上滑:听这段说明和当前进度。向下滑:呼出菜单,菜单里可以重新开始。当旁白说“请选择”的时候,向左滑或者向右滑,做出你的决定。选择无法撤销。」
-t+ 185.6s ▶ 语音:「当前进度:第一幕,城南地铁站附近。你的目标:活着离开这片废墟,前往北区的广播塔,寻找妹妹林晨的下落。」
-t+ 187.7s ♪ 音效 sfx/radio_static
-t+ 187.7s ▶ 语音:「就在这时,背包侧袋里的收音机,沙沙声忽然变得清晰。一个平稳的女声,一遍又一遍地重复着:」
-t+ 189.8s ▶ 语音:「北区避难所,仍在接收幸存者。重复,北区避难所,仍在接收幸存者。坐标,三、七点、五。请沿广播塔红色信号灯方向前进。」
-t+ 202.8s 【状态】剧情停下来了,在等我 剧情等着你(节点 n05):单击继续,双击重听(BGM=bgm/dark_drone)
-t+ 202.8s ♪ 音效 sfx/whistle
-t+ 202.8s ▶ 语音:「你刚走上街面,黑暗里忽然响起一声口哨。接着是第二声,第三声,从不同的方向,把你围在中间。有人的手电光,扫过你的脸。」
-t+ 212.9s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 212.9s ▶ 向导语音:「轻触屏幕,继续。」
-t+ 224.9s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 224.9s ♪ 向导音:watchdog(看门狗升级)
-t+ 224.9s ▶ 语音:「操作说明。单击屏幕:继续剧情。双击:重听刚才的台词。向上滑:听这段说明和当前进度。向下滑:呼出菜单,菜单里可以重新开始。当旁白说“请选择”的时候,向左滑或者向右滑,做出你的决定。选择无法撤销。」
-t+ 227.0s ▶ 语音:「当前进度:第一幕,城南地铁站附近。你的目标:活着离开这片废墟,前往北区的广播塔,寻找妹妹林晨的下落。」
-t+ 229.2s ♪ 音效 sfx/whistle
-t+ 229.2s ▶ 语音:「你刚走上街面,黑暗里忽然响起一声口哨。接着是第二声,第三声,从不同的方向,把你围在中间。有人的手电光,扫过你的脸。」
-t+ 231.3s 【状态】剧情停下来了,在等我 剧情等着你(节点 n06):单击继续,双击重听(BGM=bgm/dark_drone)
-t+ 231.3s ▶ 语音:「哟,瞧瞧,有肥羊自己送上门了。别跑啊。这片儿的路,都是我猎狗修的。想过去?行啊,把包留下,把你值钱的玩意儿留下。人嘛……也留下。」
-t+ 246.0s ▶ 语音:「请选择。向左滑:钻进旁边的通风管道。向右滑:躺进路边的尸体堆里装死。」
-t+ 248.1s 【状态】抉择点,提示音念完了 「请选择」念完了(节点 n07):左滑或右滑,做出你的决定(BGM=bgm/dark_drone)
+t+ 49.7s ♪ 音效 sfx/ui_confirm
+t+ 49.7s ♪ 音效 sfx/radio_static
+t+ 49.7s ▶ 语音:「收音机……还好,没摔坏。等等,这是什么……这段录音,是三天前的……」
+t+ 57.5s ▶ 语音:「姐,如果你听到这个,说明你还活着,太好了。听着,跟着广播塔的红色信号灯走,别相信任何自称救援队的人。北区,北区有真正的避难所。我在这儿等你。姐,一定要来。」
+t+ 75.2s 【状态】剧情停下来了,在等我 剧情等着你(节点 n03):单击继续,双击重听(BGM=bgm/dark_drone,语速1)
+t+ 75.2s ……… 静默 25s(又走神想剧情去了) ………
+t+ 100.2s ♪ 向导音:nudge(看门狗轻提醒)
+t+ 100.2s ▶ 向导语音:「轻触屏幕,继续。」
+t+ 102.3s 【状态】又听到那声轻响,5 秒内想起该点屏幕了 剧情等着你(节点 n03):单击继续,双击重听(BGM=bgm/dark_drone,语速1)
+t+ 102.3s ♪ 音效 sfx/radio_static
+t+ 102.3s ▶ 语音:「就在这时,背包侧袋里的收音机,沙沙声忽然变得清晰。一个平稳的女声,一遍又一遍地重复着:」
+t+ 104.4s ▶ 语音:「北区避难所,仍在接收幸存者。重复,北区避难所,仍在接收幸存者。坐标,三、七点、五。请沿广播塔红色信号灯方向前进。」
+t+ 117.4s 【状态】剧情停下来了,在等我 剧情等着你(节点 n05):单击继续,双击重听(BGM=bgm/dark_drone,语速1)
+t+ 117.4s ♪ 音效 sfx/whistle
+t+ 117.4s ▶ 语音:「你刚走上街面,黑暗里忽然响起一声口哨。接着是第二声,第三声,从不同的方向,把你围在中间。有人的手电光,扫过你的脸。」
+t+ 119.5s 【状态】剧情停下来了,在等我 剧情等着你(节点 n06):单击继续,双击重听(BGM=bgm/dark_drone,语速1)
+t+ 119.5s ▶ 语音:「哟,瞧瞧,有肥羊自己送上门了。别跑啊。这片儿的路,都是我猎狗修的。想过去?行啊,把包留下,把你值钱的玩意儿留下。人嘛……也留下。」
+t+ 134.2s ▶ 语音:「请选择。向左滑:钻进旁边的通风管道。向右滑:躺进路边的尸体堆里装死。」
+t+ 136.4s 【状态】抉择点,提示音念完了 「请选择」念完了(节点 n07):左滑或右滑,做出你的决定(BGM=bgm/dark_drone,语速1)
#### 【玩家动作】选了左边
-t+ 248.1s ♪ 音效 sfx/ui_confirm
-t+ 248.1s ♪ 音效 sfx/rubble
-t+ 248.1s ▶ 语音:「你一矮身,钻进了通风管道。铁皮贴着脊背,窄得几乎无法呼吸。身后传来咒骂和铁棍敲击管道口的声音,越来越近,又渐渐远去。十分钟后,你从两条街外的井口爬出来,肺里像塞满了沙子。」
-t+ 258.3s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 258.3s ▶ 向导语音:「轻触屏幕,继续。」
-t+ 270.3s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 270.3s ♪ 向导音:watchdog(看门狗升级)
-t+ 270.3s ▶ 语音:「操作说明。单击屏幕:继续剧情。双击:重听刚才的台词。向上滑:听这段说明和当前进度。向下滑:呼出菜单,菜单里可以重新开始。当旁白说“请选择”的时候,向左滑或者向右滑,做出你的决定。选择无法撤销。」
-t+ 272.4s ▶ 语音:「当前进度:第一幕,城南地铁站附近。你的目标:活着离开这片废墟,前往北区的广播塔,寻找妹妹林晨的下落。」
-t+ 274.5s ♪ 音效 sfx/rubble
-t+ 274.5s ▶ 语音:「你一矮身,钻进了通风管道。铁皮贴着脊背,窄得几乎无法呼吸。身后传来咒骂和铁棍敲击管道口的声音,越来越近,又渐渐远去。十分钟后,你从两条街外的井口爬出来,肺里像塞满了沙子。」
-t+ 276.6s 【状态】剧情停下来了,在等我 剧情等着你(节点 n08):单击继续,双击重听(BGM=bgm/dark_drone)
-t+ 276.6s ……… 静默 8s(走神想剧情去了) ………
-t+ 276.6s 【状态】又听到那声轻响,想起该点屏幕了 剧情等着你(节点 n08):单击继续,双击重听(BGM=bgm/dark_drone)
-t+ 276.6s ♪ 音效 sfx/wind
-t+ 276.6s ▶ 语音:「酸雨开始下了。街面上升起细密的嘶嘶声,像无数条小蛇在游。雨幕那头,隐约传来一阵哭声——细细的,是个孩子。」
-t+ 278.7s ▶ 语音:「请选择。向左滑:循着哭声过去看看。向右滑:不去管它,直接去北区。」
-t+ 280.9s 【状态】抉择点,提示音念完了 「请选择」念完了(节点 n10):左滑或右滑,做出你的决定(BGM=bgm/dark_drone)
+t+ 136.4s ♪ 音效 sfx/ui_confirm
+t+ 136.4s ♪ 音效 sfx/rubble
+t+ 136.4s ▶ 语音:「你一矮身,钻进了通风管道。铁皮贴着脊背,窄得几乎无法呼吸。身后传来咒骂和铁棍敲击管道口的声音,越来越近,又渐渐远去。十分钟后,你从两条街外的井口爬出来,肺里像塞满了沙子。」
+t+ 138.5s 【状态】剧情停下来了,在等我 剧情等着你(节点 n08):单击继续,双击重听(BGM=bgm/dark_drone,语速1)
+t+ 138.5s ……… 静默 25s(又走神想剧情去了) ………
+t+ 163.5s ♪ 向导音:nudge(看门狗轻提醒)
+t+ 163.5s ▶ 向导语音:「轻触屏幕,继续。」
+t+ 165.6s 【状态】又听到那声轻响,5 秒内想起该点屏幕了 剧情等着你(节点 n08):单击继续,双击重听(BGM=bgm/dark_drone,语速1)
+t+ 165.6s ♪ 音效 sfx/wind
+t+ 165.6s ▶ 语音:「酸雨开始下了。街面上升起细密的嘶嘶声,像无数条小蛇在游。雨幕那头,隐约传来一阵哭声——细细的,是个孩子。」
+t+ 167.7s ▶ 语音:「请选择。向左滑:循着哭声过去看看。向右滑:不去管它,直接去北区。」
+
+#### 【玩家动作】旁白有点慢,去设置把语速调到 2.2(下一句生效)
+
+
+#### 【玩家动作】选了左边(循声而去)——后面几步都是 2.2 档
+
+t+ 169.8s ♪ 音效 sfx/ui_confirm
+t+ 169.8s ▶ 语音:「哭声来自一间塌了一半的便利店。一个小男孩抱着膝盖坐在货架底下,身边躺着一个再也不会醒来的女人。他抬起头,脸上全是灰,小声说:“我叫小满。妈妈说,数到一千,她就会醒。我已经数到……数到好多个一千了。”」
+t+ 170.9s ▶ 语音:「请选择。向左滑:带上小满一起走。向右滑:留下食物,独自上路。」
+t+ 171.9s 【状态】语速 2.2 听着赶趟 「请选择」念完了(节点 n11):左滑或右滑,做出你的决定(BGM=bgm/dark_drone,语速2.2)
+t+ 171.9s 【状态】抉择点,提示音念完了 「请选择」念完了(节点 n11):左滑或右滑,做出你的决定(BGM=bgm/dark_drone,语速2.2)
#### 【玩家动作】选了左边
-t+ 280.9s ♪ 音效 sfx/ui_confirm
-t+ 280.9s ▶ 语音:「哭声来自一间塌了一半的便利店。一个小男孩抱着膝盖坐在货架底下,身边躺着一个再也不会醒来的女人。他抬起头,脸上全是灰,小声说:“我叫小满。妈妈说,数到一千,她就会醒。我已经数到……数到好多个一千了。”」
-t+ 283.0s ▶ 语音:「请选择。向左滑:带上小满一起走。向右滑:留下食物,独自上路。」
-t+ 285.1s 【状态】抉择点,提示音念完了 「请选择」念完了(节点 n11):左滑或右滑,做出你的决定(BGM=bgm/dark_drone)
+t+ 171.9s ♪ 音效 sfx/ui_confirm
+t+ 171.9s ▶ 语音:「小满,别怕。数到一千太慢了。跟姐姐走,姐姐带你去找一座会发光的塔。抓紧我的手,别松开。」
+
+#### 【玩家动作】听了两段,把语速调回 1.0
+
+t+ 176.5s 【状态】剧情停下来了,在等我 剧情等着你(节点 n12):单击继续,双击重听(BGM=bgm/dark_drone,语速1)
+t+ 176.5s ♪ 音效 sfx/wind
+t+ 176.5s ▶ 语音:「两个小时后,江水拦住了去路。跨江大桥断成两截,桥身斜插进黑沉沉的江面。要去广播塔,只剩两条路:桥下的污水涵洞,黑得看不见底;或者,桥上悬着的钢索,在风里荡来荡去。」
+t+ 178.7s ▶ 语音:「请选择。向左滑:走桥下的污水涵洞。向右滑:爬桥上悬着的钢索。」
+t+ 180.8s 【状态】抉择点,提示音念完了 「请选择」念完了(节点 n15):左滑或右滑,做出你的决定(BGM=bgm/dark_drone,语速1)
#### 【玩家动作】选了左边
-t+ 285.1s ♪ 音效 sfx/ui_confirm
-t+ 285.1s ▶ 语音:「小满,别怕。数到一千太慢了。跟姐姐走,姐姐带你去找一座会发光的塔。抓紧我的手,别松开。」
-t+ 303.2s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 303.2s ▶ 向导语音:「轻触屏幕,继续。」
-t+ 315.2s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 315.2s ♪ 向导音:watchdog(看门狗升级)
-t+ 315.2s ▶ 语音:「操作说明。单击屏幕:继续剧情。双击:重听刚才的台词。向上滑:听这段说明和当前进度。向下滑:呼出菜单,菜单里可以重新开始。当旁白说“请选择”的时候,向左滑或者向右滑,做出你的决定。选择无法撤销。」
-t+ 317.3s ▶ 语音:「当前进度:第二幕,酸雨笼罩的街区与断桥。你的目标:渡过江水,抵达广播塔。小心,猎狗的人,就在附近。」
-t+ 319.4s ▶ 语音:「小满,别怕。数到一千太慢了。跟姐姐走,姐姐带你去找一座会发光的塔。抓紧我的手,别松开。」
-t+ 329.5s 【状态】剧情停下来了,在等我 剧情等着你(节点 n12):单击继续,双击重听(BGM=bgm/dark_drone)
-t+ 329.5s ♪ 音效 sfx/wind
-t+ 329.5s ▶ 语音:「两个小时后,江水拦住了去路。跨江大桥断成两截,桥身斜插进黑沉沉的江面。要去广播塔,只剩两条路:桥下的污水涵洞,黑得看不见底;或者,桥上悬着的钢索,在风里荡来荡去。」
-t+ 331.6s ▶ 语音:「请选择。向左滑:走桥下的污水涵洞。向右滑:爬桥上悬着的钢索。」
-t+ 333.7s 【状态】抉择点,提示音念完了 「请选择」念完了(节点 n15):左滑或右滑,做出你的决定(BGM=bgm/dark_drone)
+t+ 180.8s ♪ 音效 sfx/ui_confirm
+t+ 180.8s ♪ 音效 sfx/water
+t+ 180.8s ▶ 语音:「你让小满骑在肩上,自己蹚进没到胸口的污水里。又冷又黏,带着腐烂的甜味。走到一半,水面下有什么东西擦过你的腿,小满吓得一抖,你抬手按住他的膝盖:“数数。”他小声数起来,一,二,三……数到一百七,你们看见了洞口的天光。」
+t+ 182.9s 【状态】剧情停下来了,在等我 剧情等着你(节点 n16b):单击继续,双击重听(BGM=bgm/dark_drone,语速1)
+t+ 182.9s ♪ 音效 sfx/footsteps
+t+ 182.9s ▶ 语音:「啧啧啧。涵洞也拦不住你,钢索也拦不住你,命真大啊。正式认识一下——猎狗。北区广播塔底下这片,归我管。把你妹妹的坐标,还有包里剩下的东西,交出来。交出来,我让你死得体面点。」
+t+ 202.3s ▶ 语音:「请选择。向左滑:交出坐标,保命要紧。向右滑:拒绝他,想办法周旋。」
+t+ 204.4s 【状态】抉择点,提示音念完了 「请选择」念完了(节点 n18):左滑或右滑,做出你的决定(BGM=bgm/dark_drone,语速1)
#### 【玩家动作】选了左边
-t+ 333.7s ♪ 音效 sfx/ui_confirm
-t+ 333.7s ♪ 音效 sfx/water
-t+ 333.7s ▶ 语音:「你让小满骑在肩上,自己蹚进没到胸口的污水里。又冷又黏,带着腐烂的甜味。走到一半,水面下有什么东西擦过你的腿,小满吓得一抖,你抬手按住他的膝盖:“数数。”他小声数起来,一,二,三……数到一百七,你们看见了洞口的天光。」
-t+ 343.8s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 343.8s ▶ 向导语音:「轻触屏幕,继续。」
-t+ 355.8s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 355.8s ♪ 向导音:watchdog(看门狗升级)
-t+ 355.8s ▶ 语音:「操作说明。单击屏幕:继续剧情。双击:重听刚才的台词。向上滑:听这段说明和当前进度。向下滑:呼出菜单,菜单里可以重新开始。当旁白说“请选择”的时候,向左滑或者向右滑,做出你的决定。选择无法撤销。」
-t+ 358.0s ▶ 语音:「当前进度:第二幕,酸雨笼罩的街区与断桥。你的目标:渡过江水,抵达广播塔。小心,猎狗的人,就在附近。」
-t+ 360.1s ♪ 音效 sfx/water
-t+ 360.1s ▶ 语音:「你让小满骑在肩上,自己蹚进没到胸口的污水里。又冷又黏,带着腐烂的甜味。走到一半,水面下有什么东西擦过你的腿,小满吓得一抖,你抬手按住他的膝盖:“数数。”他小声数起来,一,二,三……数到一百七,你们看见了洞口的天光。」
-t+ 362.2s 【状态】剧情停下来了,在等我 剧情等着你(节点 n16b):单击继续,双击重听(BGM=bgm/dark_drone)
-t+ 362.2s ♪ 音效 sfx/footsteps
-t+ 362.2s ▶ 语音:「啧啧啧。涵洞也拦不住你,钢索也拦不住你,命真大啊。正式认识一下——猎狗。北区广播塔底下这片,归我管。把你妹妹的坐标,还有包里剩下的东西,交出来。交出来,我让你死得体面点。」
-t+ 381.6s ▶ 语音:「请选择。向左滑:交出坐标,保命要紧。向右滑:拒绝他,想办法周旋。」
-t+ 383.7s 【状态】抉择点,提示音念完了 「请选择」念完了(节点 n18):左滑或右滑,做出你的决定(BGM=bgm/dark_drone)
+t+ 204.4s ♪ 音效 sfx/ui_confirm
+t+ 204.4s ▶ 语音:「好,好,我说。北区,避难所,坐标是……三、七点、五。拿去,都拿去。求你……放我走。」
+t+ 214.1s 【状态】剧情停下来了,在等我 剧情等着你(节点 n19):单击继续,双击重听(BGM=bgm/dark_drone,语速1)
+t+ 214.1s ▶ 语音:「猎狗的车队朝北区开走了,探照灯在雨幕里划出一道道口子。你蹲在原地,胃里一阵一阵地泛酸。晨晨在北区。而刚才,是你,亲手把狼,引到了她的门口。」
+t+ 216.2s ▶ 语音:「请选择。向左滑:追上去,警告避难所。向右滑:自己逃命,越远越好。」
+t+ 218.3s 【状态】抉择点,提示音念完了 「请选择」念完了(节点 n20):左滑或右滑,做出你的决定(BGM=bgm/dark_drone,语速1)
#### 【玩家动作】选了左边
-t+ 383.7s ♪ 音效 sfx/ui_confirm
-t+ 383.7s ▶ 语音:「好,好,我说。北区,避难所,坐标是……三、七点、五。拿去,都拿去。求你……放我走。」
-t+ 401.4s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 401.4s ▶ 向导语音:「轻触屏幕,继续。」
-t+ 413.4s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 413.4s ♪ 向导音:watchdog(看门狗升级)
-t+ 413.4s ▶ 语音:「操作说明。单击屏幕:继续剧情。双击:重听刚才的台词。向上滑:听这段说明和当前进度。向下滑:呼出菜单,菜单里可以重新开始。当旁白说“请选择”的时候,向左滑或者向右滑,做出你的决定。选择无法撤销。」
-t+ 415.5s ▶ 语音:「当前进度:第三幕,广播塔下。你的目标:把警报发出去,找到林晨。这是最后一段路了。」
-t+ 417.6s ▶ 语音:「好,好,我说。北区,避难所,坐标是……三、七点、五。拿去,都拿去。求你……放我走。」
-t+ 427.2s 【状态】剧情停下来了,在等我 剧情等着你(节点 n19):单击继续,双击重听(BGM=bgm/dark_drone)
-t+ 427.2s ▶ 语音:「猎狗的车队朝北区开走了,探照灯在雨幕里划出一道道口子。你蹲在原地,胃里一阵一阵地泛酸。晨晨在北区。而刚才,是你,亲手把狼,引到了她的门口。」
-t+ 429.4s ▶ 语音:「请选择。向左滑:追上去,警告避难所。向右滑:自己逃命,越远越好。」
-t+ 431.5s 【状态】抉择点,提示音念完了 「请选择」念完了(节点 n20):左滑或右滑,做出你的决定(BGM=bgm/dark_drone)
-
-#### 【玩家动作】选了左边
-
-t+ 431.5s ♪ 音效 sfx/ui_confirm
-t+ 431.5s ♪ 音效 sfx/footsteps
-t+ 431.5s ▶ 语音:「再快一点……再快一点!晨晨,等我。这一次,姐说什么,也不会再把你弄丢了!」
-t+ 448.0s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 448.0s ▶ 向导语音:「轻触屏幕,继续。」
-t+ 460.0s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 460.0s ♪ 向导音:watchdog(看门狗升级)
-t+ 460.0s ▶ 语音:「操作说明。单击屏幕:继续剧情。双击:重听刚才的台词。向上滑:听这段说明和当前进度。向下滑:呼出菜单,菜单里可以重新开始。当旁白说“请选择”的时候,向左滑或者向右滑,做出你的决定。选择无法撤销。」
-t+ 462.1s ▶ 语音:「当前进度:第三幕,广播塔下。你的目标:把警报发出去,找到林晨。这是最后一段路了。」
-t+ 464.2s ♪ 音效 sfx/footsteps
-t+ 464.2s ▶ 语音:「再快一点……再快一点!晨晨,等我。这一次,姐说什么,也不会再把你弄丢了!」
-t+ 472.8s 【状态】剧情停下来了,在等我 剧情等着你(节点 n21):单击继续,双击重听(BGM=bgm/dark_drone)
-t+ 472.8s ▶ 语音:「你赶在车队前面,撞进了避难所的大门。“关掉广播!关灯!猎狗来了——!”那一夜,北区守住了。探照灯熄灭的时候,晨晨从人群里扑出来抱住你,才发现你胳膊上的血,已经把半边袖子浸透了。坐标是你交出去的,命,是你抢回来的。你赢了,用一条胳膊的代价。——结局:代价。单击屏幕,重新开始。」
-t+ 482.9s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 482.9s ▶ 向导语音:「轻触屏幕,继续。」
-t+ 494.9s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 494.9s ♪ 向导音:watchdog(看门狗升级)
-t+ 494.9s ▶ 语音:「操作说明。单击屏幕:继续剧情。双击:重听刚才的台词。向上滑:听这段说明和当前进度。向下滑:呼出菜单,菜单里可以重新开始。当旁白说“请选择”的时候,向左滑或者向右滑,做出你的决定。选择无法撤销。」
-t+ 497.0s ▶ 语音:「当前进度:第三幕,广播塔下。你的目标:把警报发出去,找到林晨。这是最后一段路了。」
-t+ 499.1s ▶ 语音:「你赶在车队前面,撞进了避难所的大门。“关掉广播!关灯!猎狗来了——!”那一夜,北区守住了。探照灯熄灭的时候,晨晨从人群里扑出来抱住你,才发现你胳膊上的血,已经把半边袖子浸透了。坐标是你交出去的,命,是你抢回来的。你赢了,用一条胳膊的代价。——结局:代价。单击屏幕,重新开始。」
-t+ 501.2s 【状态】结局 结局播完了。单击屏幕,可以回到故事开始的地方(BGM=bgm/dark_drone)
+t+ 218.3s ♪ 音效 sfx/ui_confirm
+t+ 218.3s ♪ 音效 sfx/footsteps
+t+ 218.3s ▶ 语音:「再快一点……再快一点!晨晨,等我。这一次,姐说什么,也不会再把你弄丢了!」
+t+ 226.8s 【状态】剧情停下来了,在等我 剧情等着你(节点 n21):单击继续,双击重听(BGM=bgm/dark_drone,语速1)
+t+ 226.8s ▶ 语音:「你赶在车队前面,撞进了避难所的大门。“关掉广播!关灯!猎狗来了——!”那一夜,北区守住了。探照灯熄灭的时候,晨晨从人群里扑出来抱住你,才发现你胳膊上的血,已经把半边袖子浸透了。坐标是你交出去的,命,是你抢回来的。你赢了,用一条胳膊的代价。——结局:代价。单击屏幕,重新开始。」
+t+ 228.9s 【状态】结局 结局播完了。单击屏幕,可以回到故事开始的地方(BGM=bgm/dark_drone,语速1)
#### 【玩家动作】听完结局,单击重开一次看看别的走向
-t+ 501.2s ♪ 音效 sfx/ui_confirm
-t+ 501.2s ♪ 向导音:explore(探索/安全)
-t+ 501.2s ▶ 语音:「大停电后的第三十天,城市陷入彻底的黑暗。没有电,没有网络,只有收音机里偶尔闪过的沙沙声,像垂死者的呼吸。你是林晚,一名前急救员。在这个故事里,你用声音活下去。请记住:单击屏幕,继续剧情;双击,重听上一句;向上滑,听你的状态和帮助;向下滑,呼出菜单。当我说“请选择”的时候,向左滑,或者向右滑,做出你的决定。选择一旦做出,就无法回头。现在,单击屏幕,睁开眼睛。」
-t+ 511.4s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 511.4s ▶ 向导语音:「轻触屏幕,继续。」
-t+ 523.4s ♪ 向导音:nudge(看门狗轻提醒)
-t+ 523.4s ♪ 向导音:watchdog(看门狗升级)
-t+ 523.4s ▶ 语音:「操作说明。单击屏幕:继续剧情。双击:重听刚才的台词。向上滑:听这段说明和当前进度。向下滑:呼出菜单,菜单里可以重新开始。当旁白说“请选择”的时候,向左滑或者向右滑,做出你的决定。选择无法撤销。」
-t+ 525.5s ▶ 语音:「当前进度:第一幕,城南地铁站附近。你的目标:活着离开这片废墟,前往北区的广播塔,寻找妹妹林晨的下落。」
-t+ 527.6s ▶ 语音:「大停电后的第三十天,城市陷入彻底的黑暗。没有电,没有网络,只有收音机里偶尔闪过的沙沙声,像垂死者的呼吸。你是林晚,一名前急救员。在这个故事里,你用声音活下去。请记住:单击屏幕,继续剧情;双击,重听上一句;向上滑,听你的状态和帮助;向下滑,呼出菜单。当我说“请选择”的时候,向左滑,或者向右滑,做出你的决定。选择一旦做出,就无法回头。现在,单击屏幕,睁开眼睛。」
-t+ 529.7s 【状态】回到开头,会话结束 剧情等着你(节点 n00):单击继续,双击重听(BGM=bgm/dark_drone)
+t+ 228.9s ♪ 音效 sfx/ui_confirm
+t+ 228.9s ♪ 向导音:explore(探索/安全)
+t+ 228.9s ▶ 向导语音:「单击继续剧情。请选择的时候,左右滑动。」
+t+ 231.1s ▶ 语音:「大停电后的第三十天,城市陷入彻底的黑暗。没有电,没有网络,只有收音机里偶尔闪过的沙沙声,像垂死者的呼吸。你是林晚,一名前急救员。在这个故事里,你用声音活下去。请记住:单击屏幕,继续剧情;双击,重听上一句;向上滑,听你的状态和帮助;向下滑,呼出菜单。当我说“请选择”的时候,向左滑,或者向右滑,做出你的决定。选择一旦做出,就无法回头。现在,单击屏幕,睁开眼睛。」
+t+ 233.2s 【状态】回到开头,会话结束 剧情等着你(节点 n00):单击继续,双击重听(BGM=bgm/dark_drone,语速1)
diff --git a/research/05-summary.md b/research/05-summary.md
new file mode 100644
index 00000000..01d9b623
--- /dev/null
+++ b/research/05-summary.md
@@ -0,0 +1,57 @@
+# 研究总结报告:纯音频游戏交互设计方法论的形成与验证
+
+> 状态:round2 进行中(本报告随迭代更新)。研究章程见 research/00-charter.md。
+
+## 1. 研究问题与答案
+
+| RQ | 问题 | 答案(详见对应产物) |
+|---|---|---|
+| RQ1 | 优秀音频游戏如何保持状态感知 | 15 条验证模式:语音引导员+随时重听、威胁预警窗 1.5~2s、方位降维 ≤4、全局混音签名、教学一句话铁律等(01-literature/01) |
+| RQ2 | 声音如何承载结构化信息 | earcon 学习成本 8.5 轮 vs 语音 1.14 轮 → **语义靠语音、earcon 只做粗类别标记**(≤7 个、显式教一次);并发声流 ≤2;工作记忆 ≈4 chunks → 单次播报 ≤3 选项(01-literature/02) |
+| RQ3 | 视障玩家惯例 | status key 三十年惯例、双击执行/单击预览、逐句跳过、阈值播报、失败可归因=留存强相关;「把盲人当白痴」是弃游主因(01-literature/03) |
+| RQ4 | 头 5 分钟不弃坑 | 首输入 <60s、首胜 ≤3min 保送、学习期零惩罚、失败三问归因、3-5min 局结构、断点续玩(01-literature/04) |
+| RQ5 | 场景与人群差异 | 床=第一场景;被打断恢复成本 23min → 局面复述;摸鱼纯震动 ≤6 语义;四场景参数包 A/B/C/D(01-literature/05) |
+| RQ6 | 方法论 | methodology.md v1.1:四通道模型 + 7 公理 + 5 层交互栈 + 决策推导机 + 审计指标 + 反模式 |
+| RQ7 | 交互方案 | schemes.md + 逐游戏接线(game-inventory.md 行号级);v1 基线 + round2 修订 F1~F9 |
+| RQ8 | 玩家是否认可 | 两轮 4 人设代理评审(见 §3) |
+
+## 2. 方法论形成过程(先调研后设计,但被试玩修正)
+
+方法论 v1.0 从 5 路文献收敛而来;**round1 试玩暴露了文献没写透的三件事**,回写为 v1.1:
+
+1. **提示永不逐字重复**——文献说「可重听」「不轰炸」,没说「重复=罚听」。四人设全票发现:看门狗把「停下来回味剧情」的玩家当成卡死者,逐字重播 26 秒说明书。→ L4 修订:同一等待点第 2 次提示必须是新短句或纯音频标记;剧情类等待阈值加倍。
+2. **教学要看玩家表现**——文献说「learn by doing」,没说「成功后不得继续灌补救教学」。视障评委最激烈的「被当白痴」时刻恰是:格挡成功后紧接「来不及格挡就选防御」。→ A5 修订:成功即时确认、失败才给补救。
+3. **纠错句迟到比不播更有害**——queue 语义下归因句在语音链尾出现,归因于已过去的错误。→ A4 修订:纠错/提醒类语音空闲才播、过期丢弃。
+
+## 3. 验证闭环结果
+
+### round1(v1 基线:guide 层 + 6 游戏接线)
+
+自动化审计(round1/audit.md):真实静默死区 0;turn cue/看门狗/纠错事件全部落盘可查。
+人设评分(round1/p1~p4):愿意继续玩 = storyline 4/4、escape 4/4、rpg 4/4、simon 3/4、rhythm 3/4、**space 2/4**。未达门槛。
+
+关键单点:
+- 视障评委(P1):「地基是懂听觉游戏的人打的,差礼仪和效率两课」;10 条 P0 里 7 条关于说教系统的礼仪而非功能缺失。
+- 大学生(P2):「保送首胜全做到了,差的是别把我思考当成我迷路」。
+- 白领(P3):6 款全部单手免视可玩、1-3 分钟颗粒度适合通勤;拒绝 rhythm 仅因教学零归因。
+- 工人(P4):「escape 和 rpg 像热心工友」;space 警报叠爆一分钟累退。
+
+### round2(F1~F9 修订后)
+
+(待 round2 评审后填写:评分表、门槛判定、剩余问题清单)
+
+## 4. 交付物与影响
+
+- 方法论(可复用):research/02-methodology/methodology.md
+- 方案与接线:research/03-schemes/
+- 引擎向导层:web/engine/guide.js(8 个运行时合成 earcon、看门狗、turn cue、纠错)+ settings.guide 三档 + 语速 2.2 视障档
+- 18 句向导语音(8+10)×双性别,全部走既有 TTS 管线
+- 玩家感知转写器:web/harness/(把整局体验转成可审计的时间轴日志——本研究的测量仪器,可复用于任何纯音频游戏)
+- 试玩测试:6 playtest 断言零改动 + guide.test 专项
+- 文档:docs/design/14-guide-layer.md、00-global-ux.md §6
+
+## 5. 局限与后续(诚实声明)
+
+- 代理评审测的是**信息流与状态感知**,不测音色美感/响度/双耳定位/蓝牙延迟——真机人类听测仍是发布前必要环节(roadmap §4)。
+- 会话剧本是「新手手部动作」的模拟,round1 曾因剧本不反应提示造成误判(P4「被晾 85 秒」)——round2 已修正 driver 反应性;此误差源已写入方法论 §5 度量注意事项。
+- 场景参数包(通勤/摸鱼/睡前/下班)、escape 双击语义重设计、simon 退出路径:列入 roadmap v2(schemes.md §3 分期裁定)。
diff --git a/tools/gen_voice_dual.py b/tools/gen_voice_dual.py
index 9ef8a5d3..5cc12484 100644
--- a/tools/gen_voice_dual.py
+++ b/tools/gen_voice_dual.py
@@ -113,6 +113,9 @@ STORYLINE_LINES = [
# ---- 向导层(docs/design/14-guide-layer.md,schemes §1.5 定稿全文)----
("gd_continue", _SL_NARR, "轻触屏幕,继续。"),
("gd_invalid", _SL_NARR, "这个操作在这里用不上。"),
+ # ---- 向导层 round2(docs/design/14-guide-layer.md「round2 v1.1」,synthesis §3 定稿全文)----
+ ("gd_brief", _SL_NARR, "单击继续剧情。请选择的时候,左右滑动。"),
+ ("gd_again", _SL_NARR, "还在吗?单击继续。"),
]
@@ -271,6 +274,8 @@ SIMON_LINES = [
("skip_hint", _SIMON, "单击可跳过旁白。"),
# ---- 向导层(docs/design/14-guide-layer.md,schemes §1.5 定稿全文)----
("gd_relisten", _SIMON, "需要的话,双击可以重新听一遍序列。"),
+ # ---- 向导层 round2(docs/design/14-guide-layer.md「round2 v1.1」,synthesis §3 定稿全文)----
+ ("gd_economy", _SIMON, "记不住就双击重新听,免费机会有两次,用完要扣一次机会。"),
]
@@ -320,6 +325,10 @@ RHYTHM_LINES = [
# ---- 向导层(docs/design/14-guide-layer.md,schemes §1.5 定稿全文)----
("gd_answer", _RHY, "现在跟着我打。"),
("gd_miss", _RHY, "慢了半拍,下一小节跟上我就行。"),
+ # ---- 向导层 round2(docs/design/14-guide-layer.md「round2 v1.1」,synthesis §3 定稿全文)----
+ ("gd_miss_early", _RHY, "抢拍了。等声音落下再滑。"),
+ ("gd_miss_late", _RHY, "慢了半拍。跟着节拍走。"),
+ ("gd_miss_wrong", _RHY, "方向不对。跟着音色滑。"),
]
@@ -390,6 +399,8 @@ SPACE_LINES = [
("skip_hint", _SPC, "单击可跳过旁白。"),
# ---- 向导层(docs/design/14-guide-layer.md,schemes §1.5 定稿全文)----
("gd_hit_hint", _SPC, "被撞了。听方位,同向滑动就能拨开。"),
+ # ---- 向导层 round2(docs/design/14-guide-layer.md「round2 v1.1」,synthesis §3 定稿全文)----
+ ("gd_last_life", _SPC, "这是最后一次机会,撑住。"),
]
@@ -530,6 +541,10 @@ RPG_LINES = [
# ---- 向导层(docs/design/14-guide-layer.md,schemes §1.5 定稿全文)----
("gd_parry_miss", _RPG, "前摇声起的时候双击,才能格挡。"),
("gd_stall", _RPG, "想好了吗?左右滑动选择,单击出手。"),
+ # ---- round2 新增(docs/design/14-guide-layer.md「round2 v1.1」,synthesis §3 定稿全文)----
+ ("tut_b2b", _RPG, "挡得漂亮。"),
+ ("gd_wrap_first", _RPG, "回到第一个。"),
+ ("gd_wrap_last", _RPG, "最后一个了。"),
]
# NPC 单音色平铺(id, speaker, instruct, 台词)→ web/rpg/audio/voice/.*
diff --git a/web/engine/guide.js b/web/engine/guide.js
index cf0687e2..a6f025b0 100644
--- a/web/engine/guide.js
+++ b/web/engine/guide.js
@@ -10,6 +10,9 @@
* full = earcon + 语音 + 震动;standard = earcon + 震动(无语音);off = 全静。
* **off 时全部 API 静默 no-op,包括不挂任何定时器**——保证既有测试/玩法零干扰。
* - 向导绝不打断内容:插播语音一律 queue:true(等当前对白播完,绝不顶掉)。
+ * **error / watchdog 语音例外,为 idle-only(round2 F1.4)**:对白在播
+ * (audio.isVoicePlaying())时跳过语音只发事件(事件带 skipped:true)——
+ * 纠错/看门狗句排到长链尾部会迟到误导,宁可不说;turn 语音保持 queue 不变。
*
* 游戏侧用法(构造时创建一次):
* import { Guide } from '../engine/guide.js';
@@ -149,6 +152,8 @@ export class Guide {
/**
* A4 纠错:error 震(除非 skipHaptic——游戏层已震过时传 true)+ full 档语音。
+ * 语音为 idle-only(F1.4):对白在播时跳过只发 skipped 事件,且不占 onceKey
+ * (玩家没听到不算说过,下次空闲再纠错仍可播一次)。
* @param {{voice?: string|null, onceKey?: string|null, skipHaptic?: boolean}} [opts]
*/
error(opts = {}) {
@@ -156,18 +161,23 @@ export class Guide {
this._touch();
if (!opts.skipHaptic) vibrate('error');
if (!opts.voice) return;
+ if (this.level() !== 'full') return;
+ if (this.audio.isVoicePlaying()) {
+ this._emit('error', 'error', opts.voice, true);
+ return;
+ }
if (opts.onceKey) {
if (this._once.has(opts.onceKey)) return;
- if (this.level() === 'full') this._once.add(opts.onceKey); // 只在真的播了才计数
+ this._once.add(opts.onceKey); // 只在真的播了才计数
}
- if (this.level() !== 'full') return;
this._say('error', opts.voice);
}
/**
* L4 静默看门狗:arm 后按 at 逐级计时,每级播 nudge 轻 earcon(+ say[n-1] 语音,
- * full 档),再调 onEscalate(n);级数用尽自动停止。cond() 在触发时刻返回 false
- * 则静默取消(游戏已离开该等待态)。任何 guide API 调用 / disarm() / 再次 arm 复位。
+ * full 档,idle-only——对白在播时跳过只发事件),再调 onEscalate(n);级数用尽
+ * 自动停止。cond() 在触发时刻返回 false 则静默取消(游戏已离开该等待态)。
+ * 任何 guide API 调用 / disarm() / 再次 arm 复位。
* @param {{arm?: boolean, at?: number[], say?: (string|null)[], onEscalate?: (n:number)=>void, cond?: ()=>boolean}} opts
*/
watchdog(opts = {}) {
@@ -194,16 +204,27 @@ export class Guide {
return this.level() === 'off';
}
- _emit(type, name, voiceId) {
+ _emit(type, name, voiceId, skipped = false) {
if (!this._hook) return;
try {
- this._hook({ type, name, voiceId: voiceId ?? null });
+ const evt = { type, name, voiceId: voiceId ?? null };
+ if (skipped) evt.skipped = true;
+ this._hook(evt);
} catch (_) { /* 钩子异常不影响游戏 */ }
}
- /** full 档语音插播:queue:true 排到当前对白之后,绝不顶掉正在播的语音。 */
+ /**
+ * full 档语音插播:queue:true 排到当前对白之后,绝不顶掉正在播的语音。
+ * F1.4 idle-only:其中 error/watchdog 两类在对白在播(audio.isVoicePlaying())
+ * 时跳过语音只发事件(事件带 skipped:true)——排到链尾会迟到误导;turn 保持
+ * queue 不变。
+ */
_say(type, voiceId) {
if (this.level() !== 'full') return;
+ if ((type === 'error' || type === 'watchdog') && this.audio.isVoicePlaying()) {
+ this._emit(type, type, voiceId, true);
+ return;
+ }
this._emit(type, type, voiceId);
this.audio.play(`voice/${voiceId}`, { queue: true });
}
diff --git a/web/escape/game.js b/web/escape/game.js
index 045ed90e..5115de79 100644
--- a/web/escape/game.js
+++ b/web/escape/game.js
@@ -352,7 +352,14 @@ export class EscapeGame extends Game {
if (nx < 0 || nx > 2 || ny < 0 || ny > 2) {
this.audio.play('sfx/wall');
vibrate('error');
- this._playVoice('voice/wall'); // 每次撞墙都播(取消一次性限制)
+ // F2.3(round2):撞墙反馈接既有 guide_blunt_ 方向句(纯逻辑接线,
+ // 资产 v1 已有)——「那边是墙」之后补一句方位("手指贴着屏幕,向上推一下"),
+ // 噪音环境下听不出墙音方向时也知道刚才顶到的是哪边。排在本句播完之后;
+ // 被下一次滑动打断则自然作废(不迟到误导)。
+ const dir = dy < 0 ? 'up' : dy > 0 ? 'down' : dx < 0 ? 'left' : 'right';
+ this._playVoice('voice/wall', {
+ onComplete: () => this._playVoice(`voice/guide_blunt_${dir}`),
+ });
this._wallBumps++; // 向导层:连续撞墙 = 卡关信号(_checkStuck 提前一级)
return;
}
diff --git a/web/harness/guide.test.mjs b/web/harness/guide.test.mjs
index 02a0e927..9e7421a2 100644
--- a/web/harness/guide.test.mjs
+++ b/web/harness/guide.test.mjs
@@ -1,13 +1,16 @@
/**
* 向导层无头测试(web/harness/guide.test.mjs)。
*
- * settings.guide='full' 下,用 storyline + simon 两个代表游戏断言三 API 行为:
- * ① 看门狗:storyline 进入等待态 8s → nudge earcon 事件 + gd_continue 语音
- * (20s 升级 = 复用 playHelpAndResume,升级后不自动重挂);simon input
- * 10s → gd_relisten、25s → 免扣机会自动重听一次;
+ * settings.guide='full' 下,用 storyline + simon 两个代表游戏 + Guide 直连断言:
+ * ① 看门狗礼仪(round2 F1):storyline 进入等待态 25s → nudge + gd_continue,
+ * 50s 升级只重申手势(gd_again),**不再重播 help/进度/当前节点**,两级用尽
+ * 彻底静默;simon input 10s → gd_relisten,25s 升级 = confirm 震 + turn
+ * earcon 纯标记(**取消自动重播序列**);
* ② turn cue:simon 首轮序列播完 → turn earcon 事件 + confirm 震 + your_turn;
* ③ error 纠错:storyline 无效手势 → error 震 + gd_invalid(onceKey 同局只播一次);
- * ④ 等级衰减:guide='off' 时以上全部无事件、无 gd_* 播放、无定时器注册。
+ * ④ F1.4 idle-only:对白在播时 error/watchdog 语音跳过(事件带 skipped:true、
+ * 不入队),空闲时正常播;turn 语音保持 queue 不受影响;
+ * ⑤ 等级衰减:guide='off' 时以上全部无事件、无 gd_* 播放、无定时器注册。
* 事件经 guide.setEventHook 收集;mock 层与各 playtest 同款(虚拟时钟)。
*
* 运行:tools/node/bin/node web/harness/guide.test.mjs
@@ -143,6 +146,7 @@ globalThis.fetch = async (u) => {
const { WebAudioEngine } = await import('../engine/audio.js');
const { settings } = await import('../engine/settings.js');
+const { Guide } = await import('../engine/guide.js');
const { StorylineGame } = await import('../storyline/game.js');
const { SimonGame } = await import('../simon/game.js');
@@ -172,44 +176,50 @@ const waitChainDone = (game) => advance(() => !game.voicePlaying);
// ================================================================ ① storyline 看门狗
-realLog('== ① storyline:等待态看门狗(8s nudge+gd_continue,20s 升级 playHelpAndResume)==');
+realLog('== ① storyline:等待态看门狗(25s nudge+gd_continue,50s 升级只发 gd_again,无重播)==');
{
const { audio, playLog, game, events, vibrated } = makeRig(StorylineGame);
await audio.unlock();
game.start();
- await waitChainDone(game); // n00_intro + skip_hint 播完 → W1 静默等待(看门狗已挂)
+ // F8(round2):开场链最前插 gd_brief(brief→intro)
+ assert(voicePlays(playLog, 'voice/gd_brief') === 1, 'F8:开场链最前播 gd_brief');
+ assert(playLog.find((e) => e.id.startsWith('voice/'))?.id === 'voice/gd_brief',
+ 'F8:gd_brief 是本局第一句语音');
+ await waitChainDone(game); // gd_brief + n00_intro + skip_hint 播完 → W1 静默等待(看门狗已挂)
assert(!game.voicePlaying && game.nodeId === 'n00', 'n00 链播完,进入等待态');
assert(events.some((e) => e.type === 'state' && e.name === 'explore'), 'start 播 explore 状态签名事件');
const tArm = now;
await advance(() => events.some((e) => e.type === 'nudge'));
- assert(now - tArm === 8000, `8s 后 nudge 触发(实际 ${now - tArm}ms)`);
+ assert(now - tArm === 25000, `25s 后 nudge 触发(实际 ${now - tArm}ms)`);
assert(events.some((e) => e.type === 'watchdog' && e.voiceId === 'gd_continue'),
'看门狗第 1 级发 gd_continue 语音事件');
assert(voicePlays(playLog, 'voice/gd_continue') === 1, 'gd_continue 语音进入播放');
assert(!vibrated([8]), 'nudge 级不额外震动(earcon-only 轻提醒)');
- await advance(() => playLog.some((e) => e.id === 'voice/help'));
- assert(now - tArm === 20000, `20s 升级触发 playHelpAndResume(实际 ${now - tArm}ms)`);
- await advance(() => playLog.some((e) => e.id === 'voice/status1'));
- assert(voicePlays(playLog, 'voice/status1') === 1, '升级链播当前进度(status1)');
- assert(game.voicePlaying, '升级后重播当前节点(帮助→进度→重播)');
+ await advance(() => events.filter((e) => e.type === 'watchdog').length >= 2);
+ assert(now - tArm === 50000, `50s 升级触发(实际 ${now - tArm}ms)`);
+ assert(events.some((e) => e.type === 'watchdog' && e.voiceId === 'gd_again'),
+ '升级只发 gd_again(重申手势)语音事件');
+ assert(voicePlays(playLog, 'voice/gd_again') === 1, 'gd_again 语音播放');
+ // F1.1:升级不再重播 help + 进度 + 当前节点
+ assert(!playLog.some((e) => e.id === 'voice/help' || e.id.startsWith('voice/status')),
+ 'F1:升级不重播 help/进度');
+ assert(voicePlays(playLog, 'voice/n00_intro') === 1, 'F1:升级不重播当前节点(n00_intro 仅开场一次)');
- // 升级帮助播完后不自动重挂(L4"之后停止,不轰炸"):链走完后无定时器 → idle
+ // 两级用尽彻底静默:链走完后无任何向导定时器 → idle
await idle();
const nudges = events.filter((e) => e.type === 'nudge').length;
await idle();
- assert(events.filter((e) => e.type === 'nudge').length === nudges, '升级后不自动重挂看门狗(无后续提醒)');
+ assert(events.filter((e) => e.type === 'nudge').length === nudges, '两级用尽彻底静默(无后续提醒)');
- // 玩家手势后恢复自动挂载:n01 链播完重新挂上,且 8s 节奏到点触发一次
- await gesture(game, 'tap'); // → n01,链播(tap 同时复位 _guideHelped)
- await waitChainDone(game);
- await gesture(game, 'up'); // 帮助(复位 + 自带语音)→ 重播 n01
+ // 玩家手势后重新挂载:n01 链播完重新挂上,且 25s 节奏到点触发一次
+ await gesture(game, 'tap'); // → n01,链播
await waitChainDone(game);
const nudgeBefore = events.filter((e) => e.type === 'nudge').length;
const tWait = now;
await advance(() => events.filter((e) => e.type === 'nudge').length > nudgeBefore);
- assert(now - tWait === 8000, `重新挂载后仍按 8s 节奏触发(实际 ${now - tWait}ms)`);
+ assert(now - tWait === 25000, `重新挂载后仍按 25s 节奏触发(实际 ${now - tWait}ms)`);
resetTimers();
}
@@ -251,18 +261,28 @@ realLog('== ②/①b simon:首轮 turn cue(earcon 事件+confirm 震+your_tu
assert(vibrated([20]), 'turn cue 带 confirm 轻震');
assert(voicePlays(playLog, 'voice/your_turn') === 1, '第 1 轮保留 your_turn 语音');
- // ①b input 看门狗:10s nudge+gd_relisten;25s 免扣机会自动重听
+ // ①b input 看门狗:10s nudge+gd_relisten
+ const tInput = now;
await advance(() => events.some((e) => e.type === 'nudge'));
+ assert(now - tInput === 10000, `input 10s 后 nudge 触发(实际 ${now - tInput}ms)`);
assert(events.some((e) => e.type === 'watchdog' && e.voiceId === 'gd_relisten'),
'input 10s 无输入发 gd_relisten 事件');
assert(voicePlays(playLog, 'voice/gd_relisten') === 1, 'gd_relisten 语音播放');
assert(game.chances === 1, '看门狗提示不消耗重听机会');
- const chancesBefore = game.chances;
- await advance(() => game.phase === 'playback');
- assert(game._guideRelistens === 1, '25s 升级:免扣机会自动重听序列一次');
- assert(game.chances === chancesBefore, '自动重听不扣机会');
- await advance(() => game.phase === 'input');
+ // 25s 升级(round2 F1.2):取消自动重播序列 → 只发 confirm 震 + turn earcon(纯标记)
+ const nudgesBefore = events.filter((e) => e.type === 'nudge').length;
+ const turnsBefore = events.filter((e) => e.type === 'turn').length;
+ const vibBefore = vibrateLog.length;
+ await advance(() => events.filter((e) => e.type === 'nudge').length > nudgesBefore);
+ assert(now - tInput === 25000, `25s 升级触发(实际 ${now - tInput}ms)`);
+ assert(game.phase === 'input' && game.round === 1, '升级不再自动重播序列(仍停在 input 等玩家)');
+ assert(events.filter((e) => e.type === 'turn').length === turnsBefore + 1, '升级发 turn earcon 事件(纯标记)');
+ assert(vibrateLog.slice(vibBefore).some((p) => JSON.stringify(p) === JSON.stringify([20])),
+ '升级发 confirm 轻震');
+ assert(voicePlays(playLog, 'voice/round_01') === 1, '序列未重播(round_01 仍只有第 1 轮一次)');
+ await idle(); // 级数用尽:看门狗自动停止
+ assert(game.phase === 'input', '升级后仍等待输入(免费泄题已废除)');
// 正常答题清第 1 轮:答题手势逐个 disarm,round1→round2 过渡期看门狗不触发
const nudgeBefore = events.filter((e) => e.type === 'nudge').length;
@@ -271,13 +291,58 @@ realLog('== ②/①b simon:首轮 turn cue(earcon 事件+confirm 震+your_tu
await advance(() => game.phase === 'input' && game.round === 2);
assert(events.filter((e) => e.type === 'nudge').length === nudgeBefore,
'答题手势复位看门狗:第 2 轮开启前无 nudge');
- assert(game._guideRelistens === 1, '第 2 轮不再消耗自动重听额度');
resetTimers();
}
-// ================================================================ ④ guide=off 全静
+// ================================================================ ④ F1.4 idle-only
-realLog('== ④ guide=off:全部 API 静默 no-op(无事件、无 gd_* 播放、无定时器注册)==');
+realLog('== ④ F1.4 idle-only:对白在播时 error/watchdog 语音跳过(skipped 事件、不入队),turn 保持 queue ==');
+{
+ currentGameDir = GAME_DIRS.StorylineGame;
+ const audio = new WebAudioEngine('audio/');
+ const playLog = [];
+ const origPlay = audio.play.bind(audio);
+ audio.play = (id, opts) => { playLog.push({ id, t: now }); return origPlay(id, opts); };
+ await audio.unlock();
+ const guide = new Guide(audio);
+ const events = [];
+ guide.setEventHook((evt) => events.push({ ...evt, t: now }));
+
+ // watchdog:对白在播时到点 → 只发 skipped 事件,语音不入队
+ guide.watchdog({ arm: true, at: [1000], say: ['gd_continue'] });
+ audio.play('voice/n00_intro'); // 长语音占住通道(mock 句长 2s)
+ await flush();
+ assert(audio.isVoicePlaying(), '前提:对白正在播放');
+ await advance(() => events.some((e) => e.type === 'watchdog'));
+ const wd = events.find((e) => e.type === 'watchdog');
+ assert(wd && wd.voiceId === 'gd_continue' && wd.skipped === true,
+ 'watchdog 语音被 idle-skip(事件带 skipped:true)');
+ assert(voicePlays(playLog, 'voice/gd_continue') === 0, 'watchdog 语音未入队播放');
+
+ // error:对白在播 → 同规则只发事件;空闲 → 正常播放
+ guide.error({ voice: 'gd_invalid' });
+ assert(events.some((e) => e.type === 'error' && e.skipped === true),
+ 'error 语音对白在播时跳过(skipped 事件)');
+ assert(voicePlays(playLog, 'voice/gd_invalid') === 0, 'error 语音未入队播放');
+ await advance(() => !audio.isVoicePlaying()); // 等在播对白自然结束
+ guide.error({ voice: 'gd_invalid' });
+ assert(voicePlays(playLog, 'voice/gd_invalid') === 1, '空闲时 error 语音正常播放');
+ assert(events.filter((e) => e.type === 'error' && !e.skipped).length === 1,
+ '空闲播放的 error 事件不带 skipped 标记');
+
+ // turn:保持 queue,不受 idle-only 影响(对白在播时照常排队)
+ audio.play('voice/n01_wake');
+ await flush();
+ assert(audio.isVoicePlaying(), '前提:对白正在播放');
+ guide.turn({ voice: 'gd_answer' });
+ assert(voicePlays(playLog, 'voice/gd_answer') === 1, 'turn 语音不受 idle-only 影响(照常入队)');
+ assert(events.some((e) => e.type === 'turn'), 'turn 事件照常发出');
+ resetTimers();
+}
+
+// ================================================================ ⑤ guide=off 全静
+
+realLog('== ⑤ guide=off:全部 API 静默 no-op(无事件、无 gd_* 播放、无定时器注册)==');
{
settings.set({ guide: 'off' });
const { audio, playLog, game, events, vibrated } = makeRig(StorylineGame);
diff --git a/web/harness/sessions/escape.mjs b/web/harness/sessions/escape.mjs
index de538e81..f1bb756f 100644
--- a/web/harness/sessions/escape.mjs
+++ b/web/harness/sessions/escape.mjs
@@ -58,7 +58,7 @@ export async function session(ctx, mock) {
ctx.silence(3, '辨了辨方向');
await move('up');
await move('up');
- await move('up', '北边是墙,撞了一下');
+ await move('up', '北边是墙,撞了一下——「那边是墙」后面还跟了句方向(向上推的是北墙)');
await move('up', '又撞了一下——听到游戏主动来问我了');
await ctx.waitVirtual(5200); // 等卡关轮询的 5s tick:连续撞墙 → 提前一级的暗示
diff --git a/web/harness/sessions/rpg.mjs b/web/harness/sessions/rpg.mjs
index bee66c6b..2214767f 100644
--- a/web/harness/sessions/rpg.mjs
+++ b/web/harness/sessions/rpg.mjs
@@ -40,15 +40,16 @@ export async function session(ctx, mock) {
await ctx.waitCond(() => game.phase === 'explore');
ctx.status('进村了,先听场景描述和选项');
- // 村里(选项:去小路/商店/休息/听状态):把选项听一圈再对准「去小路」出发
+ // 村里(选项:去小路/商店/休息/听状态):把选项听一圈,回绕到首项再出发。
+ // 确认(tap)前必须等选项名念完——voice 播放中单击是「跳过本句」,会把手势吃掉
for (let i = 0; i < 3; i++) {
await ctx.gesture('right');
await ctx.waitVirtual(1800); // 听选项名
}
- ctx.status('选项听了一圈,往林间小路去');
- await ctx.waitIdle(); // 选项名播完(无 interval,可安全等待空闲)
- await ctx.gesture('right'); // 转回首个选项「去小路」
- await ctx.waitVirtual(1500);
+ ctx.status('选项听了一圈');
+ await ctx.gesture('right'); // 从末项回绕到首个选项——听到「回到第一个」(round2 F7)
+ ctx.note('滑到头再滑就兜回来了,有人提醒「回到第一个」,不用自己数');
+ await ctx.waitCond(() => !ctx.voiceBusy()); // 选项名 + 回绕提示念完
await ctx.gesture('tap'); // → path
// 小路(去岔路口/调查/返回):先调查一次捡钱,再左滑回「去岔路口」
@@ -57,17 +58,18 @@ export async function session(ctx, mock) {
await ctx.gesture('right'); // 调查四周
await ctx.waitVirtual(1800);
ctx.note('翻找了一阵,捡到一点钱');
+ await ctx.waitCond(() => !ctx.voiceBusy());
await ctx.gesture('tap');
- await ctx.waitVirtual(2400);
+ await ctx.waitCond(() => !ctx.voiceBusy()); // 捡钱播报念完
await ctx.gesture('left'); // 回「去岔路口」
- await ctx.waitVirtual(1500);
+ await ctx.waitCond(() => !ctx.voiceBusy());
await ctx.gesture('tap'); // → fork
// 岔路口(去枯树洞/去溪流/返回):右滑一次到「去溪流」
await ctx.waitCond(() => game.phase === 'explore' && game.scene === 'fork');
ctx.status('岔路口,水声在东边');
await ctx.gesture('right');
- await ctx.waitVirtual(1500);
+ await ctx.waitCond(() => !ctx.voiceBusy());
await ctx.gesture('tap'); // go_stream → 溪流 → 触发教学战
// 教学战:四步(识招 → 攻击 → 格挡 → 防御)+ 自由击杀
@@ -95,10 +97,10 @@ export async function session(ctx, mock) {
ctx.note('单击出手,攻击');
await ctx.gesture('tap');
- // T2:格挡(前摇窗口内双击)
+ // T2:格挡(前摇窗口内双击)——挡住会先听到「挡得漂亮」(round2 F4)再进防御教学
await ctx.waitCond(() => game._battle?.state === 'ENEMY_TELEGRAPH');
ctx.status('前摇声响起了');
- ctx.note('双击格挡');
+ ctx.note('双击格挡——这回干净挡下了,听到「挡得漂亮」');
await ctx.gesture('doubletap');
// T3:防御一次
diff --git a/web/harness/sessions/simon.mjs b/web/harness/sessions/simon.mjs
index c0a4aefe..93871df9 100644
--- a/web/harness/sessions/simon.mjs
+++ b/web/harness/sessions/simon.mjs
@@ -1,7 +1,10 @@
/**
* 《音序回响》新手会话剧本:教学六音一步一步学(中间故意做错一次听纠错)、
- * 第 1 轮答对、第 2 轮中途忘序列僵住 11 秒(触发向导「双击可重听」提示)、
- * 第 3 轮故意答错第一下(消耗机会自动重播),然后答对完成第 3 轮收工。
+ * 收尾听完机会经济句(免费重听两次、用完扣机会);第 1 轮直接答对;第 2 轮开局
+ * 忘序列僵住 10 秒,听到向导「双击可以重新听」提示后 5 秒内照做——用掉一次免费
+ * 重听(round2:25s 升级已不再自动重播序列,坐着等答案重念的路子废了);第 3 轮
+ * 凭记忆答对,在**整轮边界**收工(round2 剧本修正:不在轮中退出——round1 转写
+ * 「第 4 轮报数后再没人说话」与「干晾 85 秒」都是剧本失真,不是游戏失真)。
*/
export function describe(game) {
const GN = { up: '上滑', down: '下滑', left: '左滑', right: '右滑', tap: '单击', doubletap: '双击' };
@@ -14,7 +17,7 @@ export function describe(game) {
case 'announce': return `第 ${game.round} 轮报数呢,听好了`;
case 'playback': return `第 ${game.round} 轮序列演奏中(${game.sequence.length} 个音),记住顺序,先别动手`;
case 'input': return `第 ${game.round} 轮轮到你了:已对上 ${game.inputIdx}/${game.sequence.length},照原顺序做手势`;
- case 'roundend': return game.chances < (game.round > 4 ? 2 : 1) ? '答错了,正在重播刚才的序列' : '刚答完,播报中';
+ case 'roundend': return '刚答完一轮,播报中';
case 'gameover': return `游戏结束,完成 ${game.round - 1} 轮。单击再来一局,下滑开菜单`;
case 'menu': return '菜单里:单击切选项,双击确认,上滑听规则';
default: return '开场,稍等一下';
@@ -35,36 +38,50 @@ export async function session(ctx, mock) {
await ctx.waitCond(() => game.phase === 'tutorial' && game.teachAwait);
}
await ctx.gesture(G[i]); // 照指令做对
- await ctx.waitIdle();
+ if (i === G.length - 1) {
+ // 收尾链(t7 两句 + 机会经济句)播完直达对局——一到 input 就接手,
+ // 不裸 waitIdle(会把 input 的看门狗窗口整段空烧掉,round1 剧本失真根源)
+ await ctx.waitCond(() => game.phase === 'playback' || game.phase === 'input'
+ || game.phase === 'gameover');
+ } else {
+ await ctx.waitIdle();
+ }
}
+ ctx.status('教学讲完了,机会经济也交代清楚了:免费重听两次,用完要扣机会');
- // 正式对局:打到完成第 3 轮
- let wrongDone = false;
- let stallDone = false;
- while (mock.now < 12 * 60 * 1000 && game.phase !== 'gameover' && game.round < 4) {
+ // 正式对局:完整打完第 3 轮,在轮边界收工
+ let relistenDone = false;
+ while (mock.now < 12 * 60 * 1000 && game.phase !== 'gameover') {
if (game.phase === 'input') {
- // 第 2 轮开局先失忆僵住 11 秒:触发向导「双击可以重新听一遍序列」提示
- if (game.round === 2 && game.inputIdx === 0 && !stallDone) {
- stallDone = true;
+ // 第 2 轮先失忆僵住 10 秒:听向导「双击可以重新听一遍序列」,5 秒内照做。
+ //(期望输入恰为双击时先答这一下,下一步再演示重听——双击语境判定)
+ if (game.round === 2 && !relistenDone
+ && game.inputIdx <= 1 && game.sequence[game.inputIdx] !== 'doubletap') {
+ relistenDone = true;
ctx.status('序列有点长,先僵一会儿');
- ctx.silence(11, '想不起来序列了');
- await ctx.waitVirtual(11000);
+ ctx.silence(10, '想不起来序列了');
+ await ctx.waitVirtual(10600); // 10s 看门狗:nudge + gd_relisten
ctx.status('听到提示:需要的话,双击可以重新听一遍序列');
- }
- // 第 2 轮第二拍故意答错:触发"用一次机会,再听一遍"(复活重听)
- if (game.round === 2 && game.inputIdx === 1 && !wrongDone) {
- wrongDone = true;
- ctx.note('这一下故意点错,听听游戏怎么说');
- await ctx.gesture(G[(G.indexOf(game.sequence[game.inputIdx]) + 1) % G.length]);
- await ctx.waitIdle(); // retry_use + 自动重播同一序列
+ ctx.note('照提示双击,用掉一次免费重听');
+ await ctx.gesture('doubletap');
+ await ctx.waitCond(() => game.phase === 'playback' || game.phase === 'input');
continue;
}
- ctx.status('轮到我复现了');
+ if (game.round === 3 && game.inputIdx === game.sequence.length - 1) {
+ ctx.status('最后一轮最后一个音,打完这轮就收');
+ } else {
+ ctx.status('轮到我复现了');
+ }
await ctx.gesture(game.sequence[game.inputIdx]);
continue;
}
- await ctx.waitIdle();
+ // 整轮边界收尾:第 3 轮答完(roundend),不等第 4 轮报数
+ if (game.phase === 'roundend' && game.round === 3) break;
+ // announce/playback/roundend:等阶段推进即可,不裸 waitIdle(防吞掉 input 窗口)
+ const ph = game.phase;
+ const r = game.round;
+ await ctx.waitCond(() => game.phase !== ph || game.round !== r);
}
- ctx.status(`收工:完成了 ${game.round - 1} 轮`);
+ ctx.status(`收工:完整打完 ${game.round} 轮,在轮与轮的边界停手(免费重听剩 ${game.chances} 次)`);
}
diff --git a/web/harness/sessions/space.mjs b/web/harness/sessions/space.mjs
index 50238c38..10ba4151 100644
--- a/web/harness/sessions/space.mjs
+++ b/web/harness/sessions/space.mjs
@@ -1,7 +1,9 @@
/**
* 《盲航》新手会话剧本:首局完整演练 D1-D4(第二颗故意超时,听当场重教)→
- * 正式对局躲过保护期(20s/45s 的加速/护盾引入都照做)→ 保护期结束后故意漏一颗
- * (触发归因句 + 向导「同向滑动就能拨开」指令)→ 再正常躲几颗收工。
+ * 正式对局躲过保护期(20s/45s 的加速/护盾引入都照做)→ 保护期结束后故意漏两颗
+ * (第 1 次听归因+向导「同向滑动就能拨开」,第 2 次听 gd_last_life「最后一次机会」
+ * ——round2 F2.2:受击经济透明)→ 之后全部躲开,保护期结束后再撑 20 秒以上收工
+ * (不再像 round1 那样在保护期后 8 秒就撤,也没有第三次被撞)。
*/
export function describe(game) {
const DIR = { left: '左边', right: '右边', up: '上面', down: '下面' };
@@ -45,46 +47,55 @@ export async function session(ctx, mock) {
}
ctx.status('演练做完了,要出发了');
- // 正式对局:保护期 60s + 结束后再躲一小段就收工(不再模拟,避免挂机撞毁)。
- // 每颗陨石生成(呼啸声起)就立即同向拨开——比盯着 1.5s 判定窗口更稳。
+ // 正式对局:保护期 60s + 结束后再撑 20s 以上收工。
+ // 每颗陨石生成(呼啸声起)就在判定窗口将开时同向拨开——比盯着 1.5s 判定窗口更稳。
//(刷怪链是自驱动的定时器链——禁止裸 waitIdle,等待一律挂条件)
await ctx.waitCond(() => game.phase === 'playing');
ctx.status('出发。引擎声起来了,保护期里先练手');
const dodged = new Set();
- let victim = null; // 保护期结束后故意不躲的那颗
- let eaten = false;
+ const victims = []; // 保护期结束后故意不躲的(前两颗:听归因 + gd_last_life)
+ let eaten = 0;
while ((game.phase === 'playing' || game.phase === 'feature')
- && performance.now() < game.protectionUntil + 8000) {
+ && performance.now() < game.protectionUntil + 21000) {
if (game.phase === 'feature') {
ctx.status('游戏停下来教新功能');
- await ctx.waitIdle(); // 引入句听完(feature 态无刷怪链,可安全等待空闲)
+ // 引入句听完就照做(不裸 waitIdle——feature 态之后的刷怪链是自驱动定时器,
+ // 裸等会把整局对局连本带利烧穿到坠毁)
+ await ctx.waitCond(() => !ctx.voiceBusy());
ctx.note(game.featureKind === 'boost' ? '照它说的单击加速' : '照它说的双击开护盾');
await ctx.gesture(game.featureKind === 'boost' ? 'tap' : 'doubletap');
continue;
}
- if (victim && !game.obstacles.has(victim)) {
- ctx.note('被撞了。先听到方位归因,又听到「同向滑动就能拨开」');
- victim = null;
+ // 被撞的那颗消失 → 归因链听完了,继续
+ const eatenIdx = victims.findIndex((v) => !game.obstacles.has(v));
+ if (eatenIdx >= 0) {
+ victims.splice(eatenIdx, 1);
+ ctx.note(eaten === 2 && victims.length === 0
+ ? '第二次被撞:先听方位归因,又听到「这是最后一次机会,撑住」——这就是全部家底了'
+ : '被撞了。先听到方位归因,又听到「同向滑动就能拨开」');
continue;
}
// 只在判定窗口将开未开时出手(警报第一响 = 撞击前 1s,窗口内)
const target = [...game.obstacles].find((o) => !dodged.has(o)
&& o.impactAt - performance.now() < 1300);
if (target) {
- if (!eaten && performance.now() >= game.protectionUntil) {
- eaten = true;
- victim = target;
+ if (eaten < 2 && performance.now() >= game.protectionUntil) {
+ eaten++;
+ victims.push(target);
dodged.add(target); // 标记 = 故意不躲
- ctx.status('保护期结束了,这颗我故意没动');
+ ctx.status(eaten === 1
+ ? '保护期结束了,这颗我故意没动'
+ : '又有一颗,也故意不躲——想听听第二次被撞会说什么');
continue;
}
dodged.add(target);
await ctx.gesture(target.dir);
continue;
}
- await ctx.waitCond(() => (game.phase !== 'playing' && game.phase !== 'feature')
- || (victim && !game.obstacles.has(victim))
+ await ctx.waitCond(() => game.phase !== 'playing'
+ || victims.some((v) => !game.obstacles.has(v))
|| [...game.obstacles].some((o) => !dodged.has(o) && o.impactAt - performance.now() < 1300));
}
- ctx.status('躲过保护期、挨过一次撞,心跳有点快,先到这');
+ const aliveSec = Math.round((performance.now() - game.runStart) / 1000);
+ ctx.status(`保护期结束后撑了 ${aliveSec - 60} 秒、挨了两下还活着(警报也只起一波了),先到这`);
}
diff --git a/web/harness/sessions/storyline.mjs b/web/harness/sessions/storyline.mjs
index 7af60e15..74b18581 100644
--- a/web/harness/sessions/storyline.mjs
+++ b/web/harness/sessions/storyline.mjs
@@ -1,7 +1,9 @@
/**
- * 《声命线》新手会话剧本:像真人一样等语音播完再动、偶尔走神(触发看门狗)、
- * 在非抉择节点试一次无效的左滑(触发向导纠错)、菜单进去逛一圈再出来、
- * 抉择点一律选左,一路打到结局。
+ * 《声命线》新手会话剧本:像真人一样等语音播完再动、偶尔走神(触发看门狗,
+ * 听到轻响后 5 秒内回应——round2 修正剧本失真:round1 的「干晾 85 秒」是剧本
+ * 问题不是游戏问题)、在非抉择节点试一次无效的左滑(触发向导纠错)、中途演示
+ * 一次语速 2.2 切换(F9:设置下一句即时生效,转写状态行可见语速档)、菜单逛
+ * 一圈再出来、抉择点一律选左,一路打到结局。
*/
export function describe(game) {
if (game.mode === 'MENU') return '菜单里:左滑重开、右滑或单击返回剧情、上滑听帮助';
@@ -15,17 +17,21 @@ export async function session(ctx, mock) {
const { game } = ctx;
let invalidTried = false;
let menuDone = false;
+ let speedDemoDone = false;
+ let speedBackDone = false;
- // 开场:n00 全听完,愣一会儿——向导看门狗先说话
- await ctx.waitIdle();
- ctx.status('开场旁白播完了,屏幕黑着,有点不知所措');
- ctx.silence(8, '新手没反应过来');
- await ctx.waitVirtual(8600); // 触发看门狗:nudge + gd_continue
- ctx.status('听到一声轻响和「轻触屏幕,继续」,试着点一下');
- await ctx.gesture('tap');
+ // 开场:gd_brief 先给操作口令(round2 F8),n00 全听完,再愣一会儿——看门狗 25s 才说话
+ await ctx.waitCond(() => !game.voicePlaying); // 只等链播完(不空烧看门狗窗口)
+ ctx.status('开场口令和旁白播完了,屏幕黑着,走神想剧情去了');
+ ctx.silence(25, '在入戏,不是迷路(round2:剧情类等待默认低频)');
+ await ctx.waitVirtual(25600); // 触发看门狗:nudge + gd_continue
+ ctx.status('听到一声轻响和「轻触屏幕,继续」,马上点一下');
+ await ctx.gesture('tap'); // 听到提示 ≤5s 内反应
for (let i = 0; i < 60 && !game.awaitingRestart && mock.now < 20 * 60 * 1000; i++) {
- await ctx.waitIdle(); // 等当前语音链播完(真人不会抢话)
+ // 只等语音链播完(真人不会抢话)。不能裸 waitIdle——会把等待态看门狗的两级
+ // 提醒整段空烧完才动手(round1 剧本失真根源),链一停就该轮到玩家了
+ await ctx.waitCond(() => !game.voicePlaying);
if (game.awaitingRestart) break;
@@ -45,6 +51,27 @@ export async function session(ctx, mock) {
continue;
}
+ // F9:演示一次语速切换——2.2 档对下一句即时生效,听两段再调回 1.0
+ if (!speedDemoDone && game.nodeId === 'n10') {
+ speedDemoDone = true;
+ ctx.note('旁白有点慢,去设置把语速调到 2.2(下一句生效)');
+ ctx.settings.set({ speed: 2.2 });
+ if (game.choiceArmed) {
+ ctx.note('选了左边(循声而去)——后面几步都是 2.2 档');
+ await ctx.gesture('left'); // n10 抉择 → n11,2.2 倍速播
+ } else {
+ await ctx.gesture('tap');
+ }
+ await ctx.waitIdle();
+ ctx.status('语速 2.2 听着赶趟');
+ continue;
+ }
+ if (speedDemoDone && !speedBackDone && game.nodeId === 'n12') {
+ speedBackDone = true;
+ ctx.note('听了两段,把语速调回 1.0');
+ ctx.settings.set({ speed: 1.0 });
+ }
+
if (game.choiceArmed) {
ctx.status('抉择点,提示音念完了');
if (mock.now % 2 < 1) await ctx.waitVirtual(3000); // 偶尔犹豫一下
@@ -59,19 +86,19 @@ export async function session(ctx, mock) {
invalidTried = true;
ctx.note('试着左滑了一下——这里用不上,向导纠正了我');
await ctx.gesture('left');
- await ctx.waitIdle();
+ await ctx.waitVirtual(2000); // 让纠错句播完(不空烧看门狗窗口)
} else if (i % 4 === 2) {
- ctx.silence(8, '走神想剧情去了');
- await ctx.waitVirtual(8600); // 偶尔挂机,触发看门狗轻提醒
- ctx.status('又听到那声轻响,想起该点屏幕了');
+ ctx.silence(25, '又走神想剧情去了');
+ await ctx.waitVirtual(25600); // 偶尔挂机,触发看门狗轻提醒(25s 才响,不轰炸)
+ ctx.status('又听到那声轻响,5 秒内想起该点屏幕了');
}
await ctx.gesture('tap');
}
- await ctx.waitIdle();
+ await ctx.waitCond(() => !game.voicePlaying);
ctx.status('结局');
ctx.note('听完结局,单击重开一次看看别的走向');
await ctx.gesture('tap'); // 结算态单击重开
- await ctx.waitIdle();
+ await ctx.waitCond(() => !game.voicePlaying);
ctx.status('回到开头,会话结束');
}
diff --git a/web/harness/transcribe.mjs b/web/harness/transcribe.mjs
index 4a0a5e68..382f0c68 100644
--- a/web/harness/transcribe.mjs
+++ b/web/harness/transcribe.mjs
@@ -93,7 +93,11 @@ function capture(audio) {
pending.push(`${fmtT()} ♫ BGM 切换 → ${id}${opts?.fadeMs ? `(淡入 ${opts.fadeMs}ms)` : ''}`);
}
} else if (isVoice && id.startsWith('voice/gd_')) {
- // 向导语音经事件钩子渲染(▶ 向导语音),此处不重复记行
+ // 向导语音(round2):实际播出的在本行渲染(含全文);被 idle-skip 跳过的
+ // 不产生 play 调用,由事件钩子单独标注(见 captureGuide)
+ pending.push(t
+ ? `${fmtT()} ▶ 向导语音:「${t}」`
+ : `${fmtT()} ▶ 向导语音 voice/${id.slice(6)}(无台词表文本)`);
} else if (isVoice) {
pending.push(t
? `${fmtT()} ▶ 语音:「${t}」`
@@ -105,15 +109,17 @@ function capture(audio) {
};
}
-/** 向导实例接入事件流(每个 earcon/插播语音都转写成玩家可感知的一行)。 */
+/** 向导实例接入事件流(每个 earcon / 被 idle-skip 的语音都转写成玩家可感知的一行;
+ * 实际播出的向导语音已由 audio.play 捕获渲染,此处不再重复记行)。 */
function captureGuide(game) {
if (!game.guide) return;
game.guide.setEventHook((evt) => {
if (evt.voiceId) {
+ if (!evt.skipped) return; // 播出的由 capture() 渲染,避免重复
const t = textOf(`voice/${evt.voiceId}`);
pending.push(t
- ? `${fmtT()} ▶ 向导语音:「${t}」`
- : `${fmtT()} ▶ 向导语音 ${evt.voiceId}(无台词表文本)`);
+ ? `${fmtT()} ▶ 向导语音(对白在播,本句按 idle-only 跳过未播):「${t}」`
+ : `${fmtT()} ▶ 向导语音 ${evt.voiceId}(idle-only 跳过未播)`);
} else {
const label = GUIDE_LABEL[evt.name] || '';
pending.push(`${fmtT()} ♪ 向导音:${evt.name}${label ? `(${label})` : ''}`);
@@ -122,8 +128,11 @@ function captureGuide(game) {
}
function drain() {
+ // 震动/资产缺失按「实际触发时刻」落行(v2 起不再用 drain 时刻近似——
+ // round2 审计指标要核 danger 震 ≥400ms 去抖、警报 600ms 去重,必须看真实时间)
for (const m of mock.vibrateLog.splice(0)) {
- pending.push(`${fmtT()} 〰 震动:${hapticName(m.pattern)}`);
+ const t = `t+${(m.t / 1000).toFixed(1).padStart(6)}s`;
+ pending.push(`${t} 〰 震动:${hapticName(m.pattern)}`);
}
// 引擎按 ogg→m4a→mp3→wav、性别目录→平铺 逐个探测;只有全部路径都 404,
// 玩家才是真的什么都没听到(角色音探测性别目录 404 后平铺成功属正常回退)
@@ -131,7 +140,7 @@ function drain() {
const key = m.url.replace(/^audio\//, '').replace(/\.\w+$/, '').replace(/^voice\/(male|female)\//, 'voice/');
const everOk = mock.fetchedLog.some((f) => f.ok
&& f.url.replace(/^audio\//, '').replace(/\.\w+$/, '').replace(/^voice\/(male|female)\//, 'voice/') === key);
- if (!everOk) pending.push(`${fmtT()} ⚠ 资产缺失 ${key} → 玩家此刻听到的是静默`);
+ if (!everOk) pending.push(`t+${(m.t / 1000).toFixed(1).padStart(6)}s ⚠ 资产缺失 ${key} → 玩家此刻听到的是静默`);
}
pending.sort((a, b) => parseFloat(a.match(/t\+(\s*)(\d+)/)?.[2] ?? 0) - parseFloat(b.match(/t\+(\s*)(\d+)/)?.[2] ?? 0));
lines.push(...pending);
@@ -164,7 +173,8 @@ function makeCtx(audio, game, describe) {
status: (tag) => {
const d = describe ? describe(game) : '';
const v = voiceBusy(audio) ? '语音播放中' : (audio._bgm ? `BGM=${audio._bgm?.id ?? '无'}` : '无 BGM');
- lines.push(`${fmtT()} 【状态】${tag || ''}${d ? ` ${d}` : ''}(${v})`);
+ // 语速档随状态行输出(round2 F9:转写需可见语速切换生效)
+ lines.push(`${fmtT()} 【状态】${tag || ''}${d ? ` ${d}` : ''}(${v},语速${settings.get().speed})`);
},
silence: (sec, why) => {
lines.push(`${fmtT()} ……… 静默 ${sec}s${why ? `(${why})` : ',游戏在等待玩家输入'} ………`);
diff --git a/web/harness/voice_text.json b/web/harness/voice_text.json
index 1de8697a..0c99d203 100644
--- a/web/harness/voice_text.json
+++ b/web/harness/voice_text.json
@@ -39,6 +39,8 @@
"skip_hint": "单击可跳过旁白。",
"gd_continue": "轻触屏幕,继续。",
"gd_invalid": "这个操作在这里用不上。",
+ "gd_brief": "单击继续剧情。请选择的时候,左右滑动。",
+ "gd_again": "还在吗?单击继续。",
"n02_linwan": "咳……咳咳……我还活着。晨晨,你在哪儿……你说过,会在北区等我。姐一定,一定找到你。",
"n03_radio": "收音机……还好,没摔坏。等等,这是什么……这段录音,是三天前的……",
"n03_msg": "姐,如果你听到这个,说明你还活着,太好了。听着,跟着广播塔的红色信号灯走,别相信任何自称救援队的人。北区,北区有真正的避难所。我在这儿等你。姐,一定要来。",
@@ -199,7 +201,8 @@
"menu_home": "回大厅。",
"menu_back": "返回游戏。",
"skip_hint": "单击可跳过旁白。",
- "gd_relisten": "需要的话,双击可以重新听一遍序列。"
+ "gd_relisten": "需要的话,双击可以重新听一遍序列。",
+ "gd_economy": "记不住就双击重新听,免费机会有两次,用完要扣一次机会。"
},
"rhythm": {
"welcome": "欢迎来到节拍行者。跟着音乐,我奏一句,你跟一句。",
@@ -235,7 +238,10 @@
"menu_lobby": "回大厅。",
"skip_hint": "单击可跳过旁白。",
"gd_answer": "现在跟着我打。",
- "gd_miss": "慢了半拍,下一小节跟上我就行。"
+ "gd_miss": "慢了半拍,下一小节跟上我就行。",
+ "gd_miss_early": "抢拍了。等声音落下再滑。",
+ "gd_miss_late": "慢了半拍。跟着节拍走。",
+ "gd_miss_wrong": "方向不对。跟着音色滑。"
},
"space": {
"drill_welcome": "欢迎登上盲航。你只有一双耳朵。",
@@ -291,7 +297,8 @@
"settings_hint": "音色、语速、背景音乐,请回大厅修改。",
"paused": "已暂停。",
"skip_hint": "单击可跳过旁白。",
- "gd_hit_hint": "被撞了。听方位,同向滑动就能拨开。"
+ "gd_hit_hint": "被撞了。听方位,同向滑动就能拨开。",
+ "gd_last_life": "这是最后一次机会,撑住。"
},
"rpg": {
"intro_1": "你的眼睛看不见了。但你的耳朵,比谁都灵。",
@@ -414,6 +421,9 @@
"boss1_warn": "里面就是黑风寨主。你现在回去练练再来,也来得及。",
"gd_parry_miss": "前摇声起的时候双击,才能格挡。",
"gd_stall": "想好了吗?左右滑动选择,单击出手。",
+ "tut_b2b": "挡得漂亮。",
+ "gd_wrap_first": "回到第一个。",
+ "gd_wrap_last": "最后一个了。",
"npc_headman_1": "外乡人,林子里不太平。这五十金,拿着防身。",
"npc_headman_2": "商店就在村里,缺什么就去看看。",
"npc_headman_3": "听说你是用耳朵走路的剑客?保重啊。",
diff --git a/web/rhythm/audio/voice/female/gd_miss_early.m4a b/web/rhythm/audio/voice/female/gd_miss_early.m4a
new file mode 100644
index 00000000..c4caca73
Binary files /dev/null and b/web/rhythm/audio/voice/female/gd_miss_early.m4a differ
diff --git a/web/rhythm/audio/voice/female/gd_miss_early.ogg b/web/rhythm/audio/voice/female/gd_miss_early.ogg
new file mode 100644
index 00000000..a5678913
Binary files /dev/null and b/web/rhythm/audio/voice/female/gd_miss_early.ogg differ
diff --git a/web/rhythm/audio/voice/female/gd_miss_late.m4a b/web/rhythm/audio/voice/female/gd_miss_late.m4a
new file mode 100644
index 00000000..a4b18a5f
Binary files /dev/null and b/web/rhythm/audio/voice/female/gd_miss_late.m4a differ
diff --git a/web/rhythm/audio/voice/female/gd_miss_late.ogg b/web/rhythm/audio/voice/female/gd_miss_late.ogg
new file mode 100644
index 00000000..23670137
Binary files /dev/null and b/web/rhythm/audio/voice/female/gd_miss_late.ogg differ
diff --git a/web/rhythm/audio/voice/female/gd_miss_wrong.m4a b/web/rhythm/audio/voice/female/gd_miss_wrong.m4a
new file mode 100644
index 00000000..f2b73b1a
Binary files /dev/null and b/web/rhythm/audio/voice/female/gd_miss_wrong.m4a differ
diff --git a/web/rhythm/audio/voice/female/gd_miss_wrong.ogg b/web/rhythm/audio/voice/female/gd_miss_wrong.ogg
new file mode 100644
index 00000000..ada22fea
Binary files /dev/null and b/web/rhythm/audio/voice/female/gd_miss_wrong.ogg differ
diff --git a/web/rhythm/audio/voice/male/gd_miss_early.m4a b/web/rhythm/audio/voice/male/gd_miss_early.m4a
new file mode 100644
index 00000000..641fcdce
Binary files /dev/null and b/web/rhythm/audio/voice/male/gd_miss_early.m4a differ
diff --git a/web/rhythm/audio/voice/male/gd_miss_early.ogg b/web/rhythm/audio/voice/male/gd_miss_early.ogg
new file mode 100644
index 00000000..03c6c473
Binary files /dev/null and b/web/rhythm/audio/voice/male/gd_miss_early.ogg differ
diff --git a/web/rhythm/audio/voice/male/gd_miss_late.m4a b/web/rhythm/audio/voice/male/gd_miss_late.m4a
new file mode 100644
index 00000000..d107b60d
Binary files /dev/null and b/web/rhythm/audio/voice/male/gd_miss_late.m4a differ
diff --git a/web/rhythm/audio/voice/male/gd_miss_late.ogg b/web/rhythm/audio/voice/male/gd_miss_late.ogg
new file mode 100644
index 00000000..830b08ae
Binary files /dev/null and b/web/rhythm/audio/voice/male/gd_miss_late.ogg differ
diff --git a/web/rhythm/audio/voice/male/gd_miss_wrong.m4a b/web/rhythm/audio/voice/male/gd_miss_wrong.m4a
new file mode 100644
index 00000000..ac73034e
Binary files /dev/null and b/web/rhythm/audio/voice/male/gd_miss_wrong.m4a differ
diff --git a/web/rhythm/audio/voice/male/gd_miss_wrong.ogg b/web/rhythm/audio/voice/male/gd_miss_wrong.ogg
new file mode 100644
index 00000000..77bab56f
Binary files /dev/null and b/web/rhythm/audio/voice/male/gd_miss_wrong.ogg differ
diff --git a/web/rhythm/game.js b/web/rhythm/game.js
index e9f55146..c4b2de43 100644
--- a/web/rhythm/game.js
+++ b/web/rhythm/game.js
@@ -35,10 +35,13 @@
* main 演奏阶段单击是演奏手势不跳(mid_song 短句按裁定不跳)。
* 长按快进豁免(12 文档 §2.2:main 演奏期):audio.setRateBoostEnabled(false)
* per-game 开关(引擎已支持),进 main 关、进菜单/结算/退出恢复。
- * 向导层(research/03-schemes/schemes.md §2):每曲首次领奏→应答切换推
- * gd_answer(turn cue);连续 2 次 Miss 播 gd_miss(queue 插播,同曲一次);
- * 结算链换 _speakGuarded(资产缺失不卡结算 BGM);状态签名 earcon 与状态
- * BGM 同点(select=menu、main=battle、end=settle)。
+ * 向导层(docs/design/14-guide-layer.md + round2 synthesis F2.1/F3):每曲首次
+ * 领奏→应答切换在谱面定时队列直接落 gd_answer(不走 guide queue,保证窗口前
+ * 听到)+ turn earcon;教学 Miss 首次即归因分档(gd_miss_early/late/wrong,
+ * 挂纠正链尾空闲点);连续 2 Miss 播 gd_miss(queue 插播,同曲一次);暂停
+ * 断点续播前插 4 拍 count-in(off 档保持即时续播);结算链换 _speakGuarded
+ * (资产缺失不卡结算 BGM);状态签名 earcon 与状态 BGM 同点
+ * (select=menu、main=battle、end=settle)。
*/
import { Game } from '../engine/game.js';
import { Guide } from '../engine/guide.js';
@@ -341,7 +344,7 @@ export class RhythmGame extends Game {
this._tutFails++;
vibrate('error');
if (this._tutFails >= 2) {
- // 同一步失败 2 次跳过,不卡流程
+ // 同一步失败 2 次跳过,不卡流程(归因已在首次失败时给过,此处不再叠加)
this._speakGuarded(['voice/retry_skip'], () => {
if (this._tutIdx + 1 < TUT_STEPS.length) this._beginTutorial(rid, this._tutIdx + 1);
else {
@@ -352,13 +355,22 @@ export class RhythmGame extends Game {
});
return;
}
- // 纠正语音:手势错 > 过早 > 过晚 > 没出手(retry_listen)
- let correction = 'voice/retry_listen';
+ // F2.1(round2):教学 Miss 首次即归因、按失败原因分档——方向错 gd_miss_wrong /
+ // 抢拍 gd_miss_early / 慢拍 gd_miss_late(无在窗内尝试=没跟上拍,归入慢拍)。
+ // 向导句(full 档)挂在纠正链播完后的空闲点发:idle-only 语义下链中插播会被
+ // 跳过;之后 _tutLead 只排 sfx,不与归因句抢 voice 通道。
const wrong = this._tutAttempts.find((a) => !a.correct);
const offbeat = this._tutAttempts.find((a) => a.correct && Math.abs(a.dt) > GOOD);
+ const missGrade = wrong ? 'gd_miss_wrong'
+ : offbeat ? (offbeat.dt < 0 ? 'gd_miss_early' : 'gd_miss_late') : 'gd_miss_late';
+ // 纠正语音:手势错 > 过早 > 过晚 > 没出手(retry_listen)
+ let correction = 'voice/retry_listen';
if (wrong) correction = 'voice/retry_gesture';
else if (offbeat) correction = offbeat.dt < 0 ? 'voice/retry_early' : 'voice/retry_late';
- this._speakGuarded([correction, 'voice/retry_listen'], () => this._tutLead(rid));
+ this._speakGuarded([correction, 'voice/retry_listen'], () => {
+ this.guide.error({ voice: missGrade, skipHaptic: true });
+ this._tutLead(rid);
+ });
}
// ---------------- 选曲 ----------------
@@ -428,11 +440,16 @@ export class RhythmGame extends Game {
this._timed.push({ time: leadBarT0, fn: () => this.audio.play('voice/mid_song') });
}
if (i === 0) {
- // 向导层(schemes §2):每曲首次领奏→应答切换推 gd_answer(turn cue,
- // 谱面排程处入 _timed,mid_song 同款先例;full 档语音 queue 插播不顶 BGM)
+ // 向导层(round2 F3):每曲首次领奏→应答切换的交接标记。gd_answer 不走
+ // guide 的 queue(对白在播时会排到链尾、错过首个应答窗口)——排进曲谱
+ // 定时队列、在窗口前直接落下;turn earcon 走向导(full 语音,standard
+ // 只听提示音,off 全静)。mid_song 同款先例。
this._timed.push({
time: leadBarT0 + 4 * spb,
- fn: () => this.guide.turn({ voice: 'gd_answer' }),
+ fn: () => {
+ if (this.guide.level() === 'full') this.audio.play('voice/gd_answer');
+ this.guide.turn({});
+ },
});
}
bar += 2 + (this._song.restAfter[i] || 0);
@@ -625,14 +642,23 @@ export class RhythmGame extends Game {
_closeMenu(rid) {
if (this._runId !== rid) return;
const resumeAt = this.audio.ctx.currentTime + 0.1;
- const shift = resumeAt - this._pauseAt;
- // 全部绝对时刻平移暂停时长,节拍网格与 BGM 断点续播保持同步
+ // F3(round2):断点续播前插一整小节 4 拍 count-in(复用底鼓节拍音,走既有
+ // _sched 网格排程 = 每拍一个 inst_kick + beat 轻震),网格与 BGM 整体后移一小节,
+ // 杜绝 P1/P2/P3 实测的「回来 5 秒死寂、第一句报废」。off 档保持原即时续播
+ // (向导层 off = 零改动路径)。
+ const countIn = this.guide.level() !== 'off' ? 4 * (60 / this._song.bpm) : 0;
+ const bgmAt = resumeAt + countIn;
+ const shift = bgmAt - this._pauseAt;
+ // 全部绝对时刻平移(暂停时长 + count-in 小节),节拍网格与 BGM 断点续播保持同步
for (const ev of this._sched) ev.time += shift;
for (const ev of this._timed) ev.time += shift;
for (const cue of this._cues) cue.expected += shift;
this._phaseEnd += shift;
- // _startBgm 内部重算 _bgmT0 = resumeAt - offset = 旧网格 + shift
- this._startBgm(this._song, resumeAt, this._bgmOffset);
+ for (let i = 0; i < 4 && countIn > 0; i++) {
+ this._sched.push({ time: resumeAt + i * (countIn / 4), sfx: INST.tap, volume: 0.7 });
+ }
+ // _startBgm 内部重算 _bgmT0 = bgmAt - offset = 旧网格 + shift(负偏移保护同款成立)
+ this._startBgm(this._song, bgmAt, this._bgmOffset);
this.phase = 'main';
this.audio.setRateBoostEnabled(false); // 回到演奏期
this._startPump(rid);
diff --git a/web/rpg/audio/voice/female/gd_wrap_first.m4a b/web/rpg/audio/voice/female/gd_wrap_first.m4a
new file mode 100644
index 00000000..970b47ec
Binary files /dev/null and b/web/rpg/audio/voice/female/gd_wrap_first.m4a differ
diff --git a/web/rpg/audio/voice/female/gd_wrap_first.ogg b/web/rpg/audio/voice/female/gd_wrap_first.ogg
new file mode 100644
index 00000000..b004e53e
Binary files /dev/null and b/web/rpg/audio/voice/female/gd_wrap_first.ogg differ
diff --git a/web/rpg/audio/voice/female/gd_wrap_last.m4a b/web/rpg/audio/voice/female/gd_wrap_last.m4a
new file mode 100644
index 00000000..dbf5d215
Binary files /dev/null and b/web/rpg/audio/voice/female/gd_wrap_last.m4a differ
diff --git a/web/rpg/audio/voice/female/gd_wrap_last.ogg b/web/rpg/audio/voice/female/gd_wrap_last.ogg
new file mode 100644
index 00000000..cc356c41
Binary files /dev/null and b/web/rpg/audio/voice/female/gd_wrap_last.ogg differ
diff --git a/web/rpg/audio/voice/female/tut_b2b.m4a b/web/rpg/audio/voice/female/tut_b2b.m4a
new file mode 100644
index 00000000..d410dff7
Binary files /dev/null and b/web/rpg/audio/voice/female/tut_b2b.m4a differ
diff --git a/web/rpg/audio/voice/female/tut_b2b.ogg b/web/rpg/audio/voice/female/tut_b2b.ogg
new file mode 100644
index 00000000..300f185f
Binary files /dev/null and b/web/rpg/audio/voice/female/tut_b2b.ogg differ
diff --git a/web/rpg/audio/voice/male/gd_wrap_first.m4a b/web/rpg/audio/voice/male/gd_wrap_first.m4a
new file mode 100644
index 00000000..f096913a
Binary files /dev/null and b/web/rpg/audio/voice/male/gd_wrap_first.m4a differ
diff --git a/web/rpg/audio/voice/male/gd_wrap_first.ogg b/web/rpg/audio/voice/male/gd_wrap_first.ogg
new file mode 100644
index 00000000..75c2876f
Binary files /dev/null and b/web/rpg/audio/voice/male/gd_wrap_first.ogg differ
diff --git a/web/rpg/audio/voice/male/gd_wrap_last.m4a b/web/rpg/audio/voice/male/gd_wrap_last.m4a
new file mode 100644
index 00000000..1128ac31
Binary files /dev/null and b/web/rpg/audio/voice/male/gd_wrap_last.m4a differ
diff --git a/web/rpg/audio/voice/male/gd_wrap_last.ogg b/web/rpg/audio/voice/male/gd_wrap_last.ogg
new file mode 100644
index 00000000..bd24c331
Binary files /dev/null and b/web/rpg/audio/voice/male/gd_wrap_last.ogg differ
diff --git a/web/rpg/audio/voice/male/tut_b2b.m4a b/web/rpg/audio/voice/male/tut_b2b.m4a
new file mode 100644
index 00000000..94b06761
Binary files /dev/null and b/web/rpg/audio/voice/male/tut_b2b.m4a differ
diff --git a/web/rpg/audio/voice/male/tut_b2b.ogg b/web/rpg/audio/voice/male/tut_b2b.ogg
new file mode 100644
index 00000000..9ae776e1
Binary files /dev/null and b/web/rpg/audio/voice/male/tut_b2b.ogg differ
diff --git a/web/rpg/game.js b/web/rpg/game.js
index 7e01c206..d86f138c 100644
--- a/web/rpg/game.js
+++ b/web/rpg/game.js
@@ -137,6 +137,8 @@ export class RpgGame extends Game {
this._ambId = null;
// playSec 计时
this._segStart = null;
+ // F7:回绕提示去重(同一次菜单打开内每句最多 1 次)
+ this._wrapShown = new Set();
this._newRun();
}
@@ -257,6 +259,7 @@ export class RpgGame extends Game {
this.scene = id;
this.optIdx = 0;
this.phase = 'explore';
+ this._wrapShown = new Set(); // F7:场景选项列表重新"打开",回绕提示重新可用
this._switchSceneAudio(id, opts.fadeMs || 800);
this.audio.play('sfx/step', { volume: 0.8 });
@@ -307,11 +310,22 @@ export class RpgGame extends Game {
return false;
}
- _speakOption() {
+ _speakOption(onComplete) {
if (this.phase !== 'explore') return;
const opts = SCENES[this.scene].options;
if (!opts.length) return;
- this.audio.play(`voice/opt_${opts[this.optIdx]}`);
+ this.audio.play(`voice/opt_${opts[this.optIdx]}`, { onComplete });
+ }
+
+ /**
+ * F7(round2):循环列表切回首项/末项的回绕提示(P1「不报回绕」)。full 档
+ * queue 跟在本项报名之后;同一次菜单打开内每句最多 1 次(_wrapShown),别每滑必播。
+ */
+ _wrapCue(voiceId) {
+ if (this.guide.level() !== 'full') return;
+ if (this._wrapShown.has(voiceId)) return;
+ this._wrapShown.add(voiceId);
+ this.audio.play(`voice/${voiceId}`, { queue: true });
}
_onExploreGesture(g) {
@@ -320,7 +334,10 @@ export class RpgGame extends Game {
if (g === 'left' || g === 'right') {
const n = opts.length;
this.optIdx = (this.optIdx + (g === 'right' ? 1 : n - 1)) % n;
- this._speakOption();
+ const wrap = n >= 2 && ((g === 'right' && this.optIdx === 0) || (g === 'left' && this.optIdx === n - 1))
+ ? (this.optIdx === 0 ? 'gd_wrap_first' : 'gd_wrap_last') : null;
+ // 回绕提示排在本项报名播完之后;被下一次滑动打断则自然作废(不迟到误导)
+ this._speakOption(wrap ? () => this._wrapCue(wrap) : undefined);
} else if (g === 'tap') {
// voice 播放中单击 = 跳过本句(12 文档 §1:场景 desc/NPC 对白/事件/状态链可跳),
// 语音链跳下一句;无 voice 时单击 = 确认选项(原语义)。
@@ -605,13 +622,14 @@ export class RpgGame extends Game {
const b = this._battle;
if (!b) return;
b.state = 'PLAYER_SELECT';
+ this._wrapShown = new Set(); // F7:指令循环重新"打开",回绕提示重新可用
// 玩家回合的听觉标点(11 文档 §5):turn_player(≤0.4s)→ 留 200ms+ 间隙 → 指令名
this.audio.play('sfx/turn_player');
- // 向导层静默看门狗(schemes §2):探索/常规战斗本无卡关计时,PLAYER_SELECT
- // 挂机永远静默等——60s 无输入 → nudge + gd_stall(一级即止,自动停止)
+ // 向导层静默看门狗(round2 F1.3:60s→30s,等待默认低频但不再干晾一分钟):
+ // PLAYER_SELECT 挂机永远静默等——30s 无输入 → nudge + gd_stall(一级即止)
this.guide.watchdog({
arm: true,
- at: [60000],
+ at: [30000],
say: ['gd_stall'],
cond: () => this.phase === 'battle' && this._battle === b && b.state === 'PLAYER_SELECT',
});
@@ -666,7 +684,11 @@ export class RpgGame extends Game {
if (b.state === 'PLAYER_SELECT') {
if (g === 'left' || g === 'right') {
b.cmdIdx = (b.cmdIdx + (g === 'right' ? 1 : CMDS.length - 1)) % CMDS.length;
- this.audio.play(`voice/cmd_${CMDS[b.cmdIdx]}`);
+ // F7:指令循环回绕提示(探索菜单同款,full 档,跟在本项报名之后)
+ const wrap = (g === 'right' && b.cmdIdx === 0) || (g === 'left' && b.cmdIdx === CMDS.length - 1)
+ ? (b.cmdIdx === 0 ? 'gd_wrap_first' : 'gd_wrap_last') : null;
+ this.audio.play(`voice/cmd_${CMDS[b.cmdIdx]}`,
+ wrap ? { onComplete: () => this._wrapCue(wrap) } : undefined);
// 教学 T0:切换 2 次(听到指令名切换)即过关
if (b.teachStage === 'menu' && ++b.teachSwipes >= 2) this._teachAdvance('attack');
} else if (g === 'tap') {
@@ -830,10 +852,12 @@ export class RpgGame extends Game {
p.hp -= dmg;
if (b.enc.teach) p.hp = Math.max(1, p.hp); // 教学战 HP 锁底 1(必不死)
console.log(`[RpgGame] 敌方攻击 dmg=${dmg} 格挡=${b.parry} 玩家HP=${p.hp}/${this.maxHp}`);
- // 教学 T2:格挡成功或受击均可通过 → 进入防御教学 T3(11 文档 §2)
+ // 教学 T2:格挡成功或受击均可通过 → 进入防御教学 T3(11 文档 §2)。
+ // F4(round2):格挡**成功**通过时先插 tut_b2b「挡得漂亮。」确认表现,
+ // 消除 P1/P4「明明挡住了还说来不及格挡」的歧义(失败路径 tut_b4 不变)。
if (b.teachStage === 'parry') {
b.teachStage = 'defend';
- const ids = b.parry ? ['voice/tut_b6'] : ['voice/tut_b4', 'voice/tut_b6'];
+ const ids = b.parry ? ['voice/tut_b2b', 'voice/tut_b6'] : ['voice/tut_b4', 'voice/tut_b6'];
this._speakGuarded(ids, () => this._playerSelect(), 10000);
this._armTeachWatchdog('defend');
return;
diff --git a/web/simon/audio/voice/female/gd_economy.m4a b/web/simon/audio/voice/female/gd_economy.m4a
new file mode 100644
index 00000000..51091466
Binary files /dev/null and b/web/simon/audio/voice/female/gd_economy.m4a differ
diff --git a/web/simon/audio/voice/female/gd_economy.ogg b/web/simon/audio/voice/female/gd_economy.ogg
new file mode 100644
index 00000000..42f6eaef
Binary files /dev/null and b/web/simon/audio/voice/female/gd_economy.ogg differ
diff --git a/web/simon/audio/voice/male/gd_economy.m4a b/web/simon/audio/voice/male/gd_economy.m4a
new file mode 100644
index 00000000..f28c527c
Binary files /dev/null and b/web/simon/audio/voice/male/gd_economy.m4a differ
diff --git a/web/simon/audio/voice/male/gd_economy.ogg b/web/simon/audio/voice/male/gd_economy.ogg
new file mode 100644
index 00000000..c933519a
Binary files /dev/null and b/web/simon/audio/voice/male/gd_economy.ogg differ
diff --git a/web/simon/game.js b/web/simon/game.js
index b1768183..fa674e85 100644
--- a/web/simon/game.js
+++ b/web/simon/game.js
@@ -64,10 +64,10 @@ const TEACH_BLUNT = {
const BEST_KEY = 'soundgame.simon.best';
const AUTH_KEY = 'soundgame.auth'; // 大厅登录写入(见 web/index.html)
-// 向导层(schemes.md §2):input 阶段静默看门狗——10s 轻提醒+gd_relisten(不改变
-// 重听扣机会规则),25s 免扣机会自动重听一次(每局 2 次)
+// 向导层(14 号文档 + round2 synthesis F1.2):input 阶段静默看门狗——10s 轻提醒
+// +gd_relisten(不改变重听扣机会规则);25s 升级取消自动重播序列(P1:泄题且与
+// 扣机会规则矛盾)→ 改为 confirm 震 + turn earcon(纯标记,无语音)
const GUIDE_INPUT_WATCHDOG_AT = [10000, 25000];
-const GUIDE_AUTO_RELISTEN_MAX = 2;
export class SimonGame extends Game {
constructor(audio, root) {
@@ -87,7 +87,6 @@ export class SimonGame extends Game {
this._menuIdx = 0;
this._session = 0; // 会话令牌:重开后使旧计时器/语音链回调失效
this._timers = [];
- this._guideRelistens = 0; // 向导免机会自动重听已用次数(每局重置)
}
start() {
@@ -206,8 +205,10 @@ export class SimonGame extends Game {
if (this.teachIdx + 1 < GESTURES.length) {
this._teachStep(this.teachIdx + 1);
} else {
- // T7:收尾两句 → 正式对局
- this._speakGuarded(['voice/t7_ready', 'voice/t7_retry'], () => this._beginGame(), 12000);
+ // T7:收尾两句 + 机会经济一句(round2 F5:记不住就双击重听、免费两次、
+ // 用完扣机会——P1「经济不透明」开局一句话讲清)→ 正式对局
+ this._speakGuarded(['voice/t7_ready', 'voice/t7_retry', 'voice/gd_economy'],
+ () => this._beginGame(), 20000);
}
},
});
@@ -233,7 +234,6 @@ export class SimonGame extends Game {
this.sequence = [];
this.round = 0;
this.chances = 1;
- this._guideRelistens = 0; // 向导免机会自动重听每局重置
this._nextRound();
}
@@ -332,26 +332,19 @@ export class SimonGame extends Game {
_toInput() {
this.phase = 'input';
this.inputIdx = 0;
- // 向导层静默看门狗(schemes §2,全 6 款最危险的静默等待):input 阶段无 BGM
- // 无超时,挂机永久静默。10s → nudge + gd_relisten 提示(不改变重听扣机会
- // 规则);25s → 免扣机会自动重听序列一次(每局 2 次),防挂机死等。
+ // 向导层静默看门狗(round2 F1.2 礼仪重构):input 阶段无 BGM 无超时,挂机
+ // 永久静默。10s → nudge + gd_relisten 提示(不改变重听扣机会规则);25s 升级
+ // **取消免扣机会自动重播序列**(P1:等答案自动重念,双击重听的经济就矛盾了,
+ // 还顺手拆了记忆难度)→ 只发 confirm 震 + turn earcon(纯标记,无语音)。
this.guide.watchdog({
arm: true,
at: GUIDE_INPUT_WATCHDOG_AT,
say: ['gd_relisten', null],
- onEscalate: (n) => { if (n === 2) this._guideAutoRelisten(); },
+ onEscalate: (n) => { if (n === 2) this.guide.turn({ haptic: true }); },
cond: () => this.phase === 'input',
});
}
- /** 向导兜底:免扣机会自动重听当前序列(每局最多 2 次,序列不变进度重输)。 */
- _guideAutoRelisten() {
- if (this._guideRelistens >= GUIDE_AUTO_RELISTEN_MAX) return;
- this._guideRelistens++;
- if (this.phase !== 'input') return;
- this._playSequence();
- }
-
_onInput(g) {
// 双击的上下文判定(见文件头"设计冲突的处理"):
// 期望输入是 doubletap → 答题;否则 → 主动重听(耗机会)
diff --git a/web/space/audio/voice/female/gd_last_life.m4a b/web/space/audio/voice/female/gd_last_life.m4a
new file mode 100644
index 00000000..ba665a7c
Binary files /dev/null and b/web/space/audio/voice/female/gd_last_life.m4a differ
diff --git a/web/space/audio/voice/female/gd_last_life.ogg b/web/space/audio/voice/female/gd_last_life.ogg
new file mode 100644
index 00000000..6162f036
Binary files /dev/null and b/web/space/audio/voice/female/gd_last_life.ogg differ
diff --git a/web/space/audio/voice/male/gd_last_life.m4a b/web/space/audio/voice/male/gd_last_life.m4a
new file mode 100644
index 00000000..3bd21985
Binary files /dev/null and b/web/space/audio/voice/male/gd_last_life.m4a differ
diff --git a/web/space/audio/voice/male/gd_last_life.ogg b/web/space/audio/voice/male/gd_last_life.ogg
new file mode 100644
index 00000000..9c184d63
Binary files /dev/null and b/web/space/audio/voice/male/gd_last_life.ogg differ
diff --git a/web/space/game.js b/web/space/game.js
index aa9a21c9..22888167 100644
--- a/web/space/game.js
+++ b/web/space/game.js
@@ -31,9 +31,10 @@
* 语音 id 权威清单:docs/design/04-space-voice-ids.md(52 句双性别);
* 旧 num 拼接件、n_ 前缀件、intro1-4/hit1/hit2/gameover/restart 全部弃用。
*
- * 向导层(research/03-schemes/schemes.md §2):对局第 1 次受击(保护期外)追加
- * 指令式 gd_hit_hint(归因句 hit_ 之外的"该怎么拨");结算链换
- * _speakGuarded(资产缺失不卡结算 BGM);长按快进对局期豁免
+ * 向导层(docs/design/14-guide-layer.md + round2 synthesis F2.2/F6):对局第 1 次
+ * 受击(保护期外)在归因链播完后补指令式 gd_hit_hint、第 2 次补 gd_last_life
+ * (idle-only:挂在语音空闲点);警报音效 600ms 去重、danger 震 ≥400ms 去抖;
+ * 结算链换 _speakGuarded(资产缺失不卡结算 BGM);对局期长按快进豁免
* (setRateBoostEnabled);状态签名 earcon 与状态 BGM 同点。
*/
import { Game } from '../engine/game.js';
@@ -120,6 +121,10 @@ export class SpaceGame extends Game {
this.shieldIntroDone = false;
this.featureKind = null;
this._menuIdx = 0;
+ // F6:警报去重 / danger 去抖时间戳(soft/loud 分档 + danger 全局)
+ this._alarmSoftAt = -Infinity;
+ this._alarmLoudAt = -Infinity;
+ this._dangerAt = -Infinity;
}
// ---------------- 生命周期 ----------------
@@ -449,22 +454,31 @@ export class SpaceGame extends Game {
this.obstacles.add(ob);
this.audio.play(APPROACH_SFX[dir]);
- // 警报两档渐强,DANGER 震动同步叠加
+ // 警报两档渐强,DANGER 震动同步叠加。
+ // F6(round2):警报音效同 600ms 窗口去重(双陨石同时逼近只起一波警报,
+ // 按 soft/loud 各自去重,单颗的两段渐强不受影响);danger 震 ≥400ms 去抖。
this._after(impactAt - ALARM_LEAD_MS - now, () => {
- if (this.obstacles.has(ob)) {
- this.audio.play('sfx/alarm', { volume: 0.5 });
- vibrate('danger');
- }
+ if (this.obstacles.has(ob)) this._alarm(0.5);
});
this._after(impactAt - ALARM_LEAD_MS / 2 - now, () => {
- if (this.obstacles.has(ob)) {
- this.audio.play('sfx/alarm', { volume: 1 });
- vibrate('danger');
- }
+ if (this.obstacles.has(ob)) this._alarm(1);
});
this._after(impactAt - now, () => this._resolve(ob));
}
+ /** 一段警报:同档 600ms 内只起一波(F6);danger 震全局限 ≥400ms 间隔。 */
+ _alarm(volume) {
+ const now = performance.now();
+ const key = volume < 1 ? '_alarmSoftAt' : '_alarmLoudAt';
+ if (now - this[key] < 600) return;
+ this[key] = now;
+ if (now - this._dangerAt >= 400) {
+ this._dangerAt = now;
+ vibrate('danger');
+ }
+ this.audio.play('sfx/alarm', { volume });
+ }
+
/** 撞击点判定:加速增益 > 同向滑动 > 护盾 > 保护期训练护盾 > 撞击。 */
_resolve(ob) {
if (!this.obstacles.has(ob) || this.phase !== 'playing') return;
@@ -508,12 +522,18 @@ export class SpaceGame extends Game {
} else {
const ids = [`voice/hit_${ob.dir}`];
if (this.hits === 2) ids.push('voice/hit_last');
- this._speak(ids);
- // 向导层(schemes §2):对局第 1 次受击追加指令式提示(hit_ 已归因方位,
- // 这句补"该怎么拨";已震 hit 免重复 error 震;onceKey 同局一次)
- if (this.hits === 1) {
- this.guide.error({ voice: 'gd_hit_hint', onceKey: 'space_hit_hint', skipHaptic: true });
- }
+ // 向导层(round2 F1.4/F2.2):受击提示挂在归因链播完后的空闲点——
+ // idle-only 语义下若插在链头会被判为「对白在播」整段跳过。
+ // 第 1 次受击补指令式 gd_hit_hint(hit_ 已归因方位,这句补"该怎么拨",
+ // onceKey 同局一次);第 2 次受击播 gd_last_life(F2.2:生存经济透明,
+ // 与游戏层 hit_last「船体只剩最后一次」互补"撑住"的口径),已震 hit 免重复。
+ this._speak(ids, () => {
+ if (this.hits === 1) {
+ this.guide.error({ voice: 'gd_hit_hint', onceKey: 'space_hit_hint', skipHaptic: true });
+ } else if (this.hits === 2) {
+ this.guide.error({ voice: 'gd_last_life', skipHaptic: true });
+ }
+ });
}
}
diff --git a/web/storyline/audio/voice/female/gd_again.m4a b/web/storyline/audio/voice/female/gd_again.m4a
new file mode 100644
index 00000000..d27ded7d
Binary files /dev/null and b/web/storyline/audio/voice/female/gd_again.m4a differ
diff --git a/web/storyline/audio/voice/female/gd_again.ogg b/web/storyline/audio/voice/female/gd_again.ogg
new file mode 100644
index 00000000..134bc334
Binary files /dev/null and b/web/storyline/audio/voice/female/gd_again.ogg differ
diff --git a/web/storyline/audio/voice/female/gd_brief.m4a b/web/storyline/audio/voice/female/gd_brief.m4a
new file mode 100644
index 00000000..2778fd2e
Binary files /dev/null and b/web/storyline/audio/voice/female/gd_brief.m4a differ
diff --git a/web/storyline/audio/voice/female/gd_brief.ogg b/web/storyline/audio/voice/female/gd_brief.ogg
new file mode 100644
index 00000000..0903917c
Binary files /dev/null and b/web/storyline/audio/voice/female/gd_brief.ogg differ
diff --git a/web/storyline/audio/voice/male/gd_again.m4a b/web/storyline/audio/voice/male/gd_again.m4a
new file mode 100644
index 00000000..a9145dee
Binary files /dev/null and b/web/storyline/audio/voice/male/gd_again.m4a differ
diff --git a/web/storyline/audio/voice/male/gd_again.ogg b/web/storyline/audio/voice/male/gd_again.ogg
new file mode 100644
index 00000000..d8399a68
Binary files /dev/null and b/web/storyline/audio/voice/male/gd_again.ogg differ
diff --git a/web/storyline/audio/voice/male/gd_brief.m4a b/web/storyline/audio/voice/male/gd_brief.m4a
new file mode 100644
index 00000000..879b1b17
Binary files /dev/null and b/web/storyline/audio/voice/male/gd_brief.m4a differ
diff --git a/web/storyline/audio/voice/male/gd_brief.ogg b/web/storyline/audio/voice/male/gd_brief.ogg
new file mode 100644
index 00000000..4d12d5ff
Binary files /dev/null and b/web/storyline/audio/voice/male/gd_brief.ogg differ
diff --git a/web/storyline/game.js b/web/storyline/game.js
index 6b51271c..88bfe91d 100644
--- a/web/storyline/game.js
+++ b/web/storyline/game.js
@@ -82,8 +82,10 @@ const VOICE_WATCHDOG_MS = 30000;
/** 抉择提示音 id(voice/p1~p8):播放中单击不可跳过(12 号文档 §1.2)。 */
const PROMPT_RE = /^voice\/p\d/;
-// 向导层(schemes.md §2):W1 静默看门狗阈值——8s 轻提醒+gd_continue,20s 升级重播帮助
-const GUIDE_WATCHDOG_AT = [8000, 20000];
+// 向导层(14 号文档 + round2 synthesis F1.1):剧情类等待默认低频——25s 轻提醒
+// +gd_continue,50s 升级只重申手势(gd_again),**永不重播 help/进度/当前节点**;
+// 同一等待点最多 2 次提示后彻底静默(guide.js 升级链用尽自动停止)
+const GUIDE_WATCHDOG_AT = [25000, 50000];
export class StorylineGame extends Game {
constructor(audio, root) {
@@ -104,10 +106,6 @@ export class StorylineGame extends Game {
this.companion = false; // 带上了小满
this.knowTrap = false; // 装死时听到北区陷阱的情报
this.betrayed = false; // 向猎狗交出过坐标
-
- // 向导层:升级帮助播过后不再自动重挂看门狗(L4"之后停止,不轰炸"),
- // 玩家下一次手势即复位
- this._guideHelped = false;
}
// ---------------------------------------------------------------- Game
@@ -123,7 +121,6 @@ export class StorylineGame extends Game {
onGesture(g) {
console.log(`[StorylineGame] onGesture: ${g} mode=${this.mode} node=${this.nodeId} armed=${this.choiceArmed} playing=${this.voicePlaying}`);
this.guide.disarm(); // 任何手势复位静默看门狗(guide=off 时 no-op)
- this._guideHelped = false; // 玩家有动作,恢复看门狗自动挂载
if (this.mode === MODE.MENU) this.onMenuGesture(g);
else this.onStoryGesture(g);
}
@@ -201,6 +198,10 @@ export class StorylineGame extends Game {
this.awaitingRestart = false;
if (node.sfx) this.audio.play(node.sfx);
const seq = [...node.voices];
+ // F8 开场瘦身(round2):操作口令 gd_brief(≤8s)前置到开场链最前,
+ // 「brief→intro」;原 intro 长句里的操作教学资产保留不动。
+ // 向导句只在 full 档插播(off/standard 保持纯剧情链)。
+ if (id === 'n00' && this.guide.level() === 'full') seq.unshift('voice/gd_brief');
// 开场教学:intro 早期追加一次"单击可跳过旁白"提示(每局只播一次,重开不重播)
if (id === 'n00' && !this.skipHintShown) {
this.skipHintShown = true;
@@ -241,19 +242,14 @@ export class StorylineGame extends Game {
} else if (node.ending) {
this.awaitingRestart = true;
}
- // W1/W3 静默等待看门狗(向导层,schemes §2):8s nudge+gd_continue;
- // 20s 升级 = 复用 playHelpAndResume(帮助+进度+重播当前节点),升级后不再
- // 自动重挂(防循环轰炸),玩家下一次手势恢复。抉择态与语音播放中不挂。
- if (node.prompt == null && !this._guideHelped) {
+ // W1/W3 静默等待看门狗(向导层,round2 F1.1 礼仪重构):25s nudge+gd_continue;
+ // 50s 升级只重申手势(gd_again)——**不再重播 help+进度+当前节点**。两级用尽
+ // 彻底静默,玩家下一次手势(新节点播完)重新挂载。抉择态与语音播放中不挂。
+ if (node.prompt == null) {
this.guide.watchdog({
arm: true,
at: GUIDE_WATCHDOG_AT,
- say: ['gd_continue', null],
- onEscalate: (n) => {
- if (n !== 2 || this.mode !== MODE.STORY) return;
- this._guideHelped = true;
- this.playHelpAndResume();
- },
+ say: ['gd_continue', 'gd_again'],
cond: () => this.mode === MODE.STORY && !this.voicePlaying,
});
}