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, }); }