round4: H1 escape目标带实时方位 + H2 simon轮次占用预告(根治手势两义) + H3 重听账目真实化(诊断=初始发放1次vs承诺2次) + H4 input看门狗5s + H5 归因真话(gd_miss_none) + 20语音 + guide120断言全绿

This commit is contained in:
SoundGame Dev
2026-08-31 02:51:41 +08:00
parent 6be3d29b78
commit 3d37d4656d
55 changed files with 822 additions and 215 deletions
+62
View File
@@ -184,3 +184,65 @@
其后再走两步;simon 看门狗提示上滑重听 + 期望恰为 up 时上滑作答 + 双击音
双击作答,全程无双击歧义时刻;space 每次撞击前 500ms 警报与「左!/右!/上!/
下!」同窗到达。
---
# 14 · 向导交互层设计裁定(round4 v1.3 增补)
> 依据:research/04-playtests/round3/synthesis.md(H1~H5 + §3 新语音表,唯一事实源)。
> 本文只做**追加**,上文 v1 / round2 / round3 裁定不回改;实现载体同前
> (`web/engine/guide.js` + 各游戏 game.js 接线),全部改动已落地。
## 11. round4 裁定落地摘要(H1~H5)
| # | 裁定 | 落地 |
|---|---|---|
| H1 | escape 目标带方位 | 目标句接线点盘点:**found_key**(储物间拾钥匙→cabinet (2,1))、**gd_next_fuse**(开柜得保险丝→box (0,0))、**fix_box**(修电箱通电→gate (2,2))三处,句尾接播 `gd_room_l/r/u/d`(游戏反馈层全档位,gd_next_fuse 先例)。方位按房间图对当前坐标实时计算(x 0西..2东 / y 0北..2南;方向语义 = escape 移动坐标系,up/down/left/right 滑动方向即方位);斜对角只播一句、轴序与 `_navVoices` 一致(先东西后南北);目标在同一房间不播。**引导期 g7_goal 不接**:引导本身逐句带路(g3_left/g4_down),方位句冗余,且引导链被 escape playtest 场景 A/D 的 `finishVoices(3)→PLAY` 断言冻结(断言零改动红线)。 |
| H2 | simon 占用手势预告 | `_playSequence` 序列播完 400ms 的「轮到你了」回调里按本轮序列插占用预告链:含 up→`gd_occ_up`、含 doubletap→`gd_occ_dt`、两者皆有→`gd_occ_both`(都不含则不播、直达 input);链尾接第 1 轮 your_turn → `_toInput`(`_speakGuarded`,每句 2.5s 缺失兜底)。游戏反馈层全档位,插在轮次链中(**预告先于「轮到你」**)。被占用轮次(序列含 up)input 看门狗 `say` 动态置空——只发 nudge earcon 不播重听建议句(实现从简,say 按 arm 时刻序列选择);仅 doubletap 占用不影响建议(建议只关联上滑重听键)。 |
| H3 | simon 重听账目真实 | **诊断结论:教学/演练/看门狗均无隐性扣减**(教学期不触 chances;watchdog 提示不消耗机会——guide.test 既有断言)。缺口在**初始发放**:`_beginGame()` chances=1,而教学句 gd_economy 承诺「免费机会有两次」——第二次免费要打完第 4 轮才发放(每 4 轮 +1、上限 2),P1 在第 1 轮用掉唯一一次即听到绝对化「用完了」,账面对不上。**修法=使用时如实报余量**:`_relisten()` 消耗后余 1 播 `gd_chances_left1`、余 0 播 `gd_chances_left0`(替换该路径旧「用完了」句,全档位)——「用完了」从此只在真实余量为 0 时出现。答错自动消耗(retry_use + chances_out)与余 0 再求重听的拒绝(chances_out)两处保留:前者是 t7_retry 明说过的「答错可以消耗机会重听」的机会账、后者播报时点余量确为 0;且两处被 simon playtest 断言锁定(零改动红线)。初始发放数值 1 不动(playtest「初始 1 次重听机会」断言冻结;「不动玩法数值」红线仅豁免 H3 的账目真实化,不做经济重发)。 |
| H4 | simon input 看门狗首级提前 | `GUIDE_INPUT_WATCHDOG_AT` [10000,25000]→**[5000,25000]**:仅 input 首级提前(P4「10 秒干等」),升级级 25s 与两级次数不变;该常量只用于 input 看门狗,非 input 阶段不动。 |
| H5 | rhythm 归因真话 | `_tutEvaluate` 教学首次失败归因改四档:先判**应答窗口无任何尝试**(任一 cue 在窗内无尝试——出窗的自由演奏不算尝试)→ `gd_miss_none`「这拍没接」(none 优先级最高);真出手再分 wrong/early/late(沿用 `_tutAttempts` 判据)。纠正链(retry_*)不变;正片连 2 Miss 仍 `gd_miss`(`_applyMiss` 不动)。 |
## 12. round4 新增语音资产台词表(10 句 ×2 性别 = 20 文件)
全文与 synthesis §3 逐字一致(speaker/instruct 沿用各游戏旁白 pair 常量):
| 游戏 | id | 全文 | 触发 |
|---|---|---|---|
| escape | `gd_room_l` / `gd_room_r` / `gd_room_u` / `gd_room_d` | 左面的房间。/ 右面的房间。/ 上面的房间。/ 下面的房间。 | 目标句(found_key/gd_next_fuse/fix_box)播完后接播(H1,全档位) |
| simon | `gd_occ_up` | 这一轮,向上滑是作答。 | 序列含 up(H2,全档位) |
| simon | `gd_occ_dt` | 这一轮,双击是作答。 | 序列含 doubletap(H2,全档位) |
| simon | `gd_occ_both` | 这一轮,向上滑和双击都是作答。 | 两者皆含(H2,全档位) |
| simon | `gd_chances_left1` | 免费重听还剩一次。 | 免费重听用掉后余 1(H3,全档位) |
| simon | `gd_chances_left0` | 免费重听用完了。之后重听会花一次机会,别怕,慢慢来。 | 用掉后余 0(H3,全档位) |
| rhythm | `gd_miss_none` | 这拍没接。我停下的时候,就轮到你了。 | 教学应答窗口无尝试(H5,full 档,guide.error 通道) |
资产落位与管线同前:`tools/gen_voice_dual.py --game <g> --only <id>` GPU 逐条生成 →
`--convert` 落盘 → `tools/check_all_assets.py` 终检。
## 13. round4 验证记录
- 6 playtest(guide=off)**断言零改动全绿**:storyline 54 / escape 103 / simon 108 /
rhythm 81 / space 113 / rpg 139(与 round3 基线一致)。
- `web/harness/guide.test.mjs` **120 条断言全绿**(round3 基线 93 条中仅 ② 块两条
input 看门狗时刻断言随 H4 10s→5s 更新,并固定第 1 轮序列以隔离占用预告的动态
say;其余既有断言零改动):新增 H 组 27 条——H1 方向映射按图校验(l/r/u/d +
同房间 null)+ 三处目标句接线在真实解谜链上验证;H2 三种占用预告 + 预告先于
「轮到你」+ up 占用轮看门狗无建议句 + 仅 dt 占用不影响建议;H3 余量句与真实
余量一致(2→1 播 left1、1→0 播 left0、余 0 拒绝仍 chances_out);H4 首级 5s
(升级级不变);H5 无尝试 gd_miss_none 且不再误报 late/wrong。
- `tools/check_all_assets.py` 全绿(双格式配对、无 0 字节、全可解码、双性别集合
一致);`tools/extract_voice_text.py` 已刷新 `web/harness/voice_text.json`
(447 句)。`gen_voice_dual.py --check` 新增 10 句无时长离群;rpg/tut_b0 等
4 句「缺 staging」为 R5 既有已知项,非本轮引入。
- 转写抽查(research/04-playtests/transcripts/,6 份全部重跑、零资产缺失):
simon——R1 占用预告 t+60.1 先于「轮到你了」t+62.2,5s 首级(t+69.3)与 25s
升级(t+89.3)两级均无重听建议、期望恰为 up 时上滑作答;R5/R7 建议句 5s 准时
(t+125.4 / t+179.7),两次免费重听分别跟播「免费重听还剩一次」(t+127.5)与
「免费重听用完了。之后重听会花一次机会」(t+181.8),收工态余量 0 与播报一致;
escape——
gd_next_fuse(t+64.2)后 2.2s 接「左面的房间。」(t+66.4)、fix_box(t+85.0)后
2.1s 接「右面的房间。」(t+87.1),均在目标句后 3s 内,剧本照方位句多跨四个
房间摸到大门;rhythm——教学整拍不出手归因「这拍没接。我停下的时候,就轮到你了」
(t+23.2)而非「慢了半拍」,正片首句全漏仍 gd_miss(t+71.6,正片规则不变);
storyline/space/rpg 行为零改动。
+32 -8
View File
@@ -109,15 +109,39 @@ t+ 62.1s ♪ 音效 sfx/door_creak
t+ 62.1s ▶ 语音:「钥匙插进锁孔,缓缓转动。咔哒一声,柜门开了。柜子深处,躺着一枚保险丝。保险丝到手了。」
t+ 62.1s 〰 震动:success(成功长震)
t+ 64.2s ▶ 向导语音:「保险丝到手。接下来,去找电箱。」
t+ 66.4s 【状态】柜子开了,听到「保险丝到手。接下来,去找电箱」——目标又有了 探索中,我在 (2,1)。四向滑挪步,单击摸一遍房间,双击用物品(BGM=bgm/drone,语速1)
t+ 66.4s ♪ 音效 sfx/step
t+ 66.4s ♪ 音效 sfx/amb_drip
t+ 66.4s ▶ 向导语音:「左面的房间。」
t+ 68.5s 【状态】柜子开了。「保险丝到手。接下来,去找电箱。」后面还跟了一句「左面的房间。」——这回知道该往哪边摸了 探索中,我在 (2,1)。四向滑挪步,单击摸一遍房间,双击用物品(BGM=bgm/drone,语速1)
t+ 68.5s ♪ 音效 sfx/step
t+ 68.5s ♪ 音效 sfx/amb_drip
#### 【玩家动作】照新目标往西摸
#### 【玩家动作】照方位句往左(西)摸
t+ 70.0s ♪ 音效 sfx/step
t+ 70.0s ♪ 音效 sfx/amb_hum
t+ 75.0s ♪ 音效 sfx/step
t+ 75.0s ♪ 音效 sfx/amb_hum
t+ 80.0s ♪ 音效 sfx/step
t+ 80.0s ♪ 音效 sfx/amb_power
#### 【玩家动作】再走一步,西边隐隐有电流声——电箱在那边
#### 【玩家动作】电流声越来越清晰——电箱房到了
t+ 75.0s 【状态】解到第二间、照下一目标句又摸了两步,收工 探索中,我在 (0,1)。四向滑挪步,单击摸一遍房间,双击用物品(BGM=bgm/drone,语速1)
t+ 85.0s ♪ 音效 sfx/spark
t+ 85.0s ♪ 音效 sfx/power_on
t+ 85.0s ▶ 语音:「你把保险丝用力按进卡座。火花一闪,嗡鸣声响起——头顶的线路恢复供电了!」
t+ 85.0s 〰 震动:success(成功长震)
t+ 87.1s ▶ 向导语音:「右面的房间。」
t+ 95.0s 【状态】电箱修好了。目标句后面照样跟着方位——「右面的房间。」大门在东边 探索中,我在 (0,0)。四向滑挪步,单击摸一遍房间,双击用物品(BGM=bgm/drone,语速1)
t+ 95.0s ♪ 音效 sfx/step
t+ 95.0s ♪ 音效 sfx/amb_wind
#### 【玩家动作】照方位句往右(东)摸
t+ 100.0s ♪ 音效 sfx/step
t+ 100.0s ♪ 音效 sfx/amb_hum
t+ 105.0s ♪ 音效 sfx/step
t+ 105.0s ♪ 音效 sfx/amb_cabinet
t+ 110.0s ♪ 音效 sfx/step
t+ 110.0s ♪ 音效 sfx/amb_gate_live
t+ 110.0s ▶ 语音:「面前,是一扇沉重的铁门。」
#### 【玩家动作】脚步声停下来——面前这间就是大门
t+ 112.1s 【状态】解到通电、照目标句的方位跨了四个房间摸到大门,收工 探索中,我在 (2,2)。四向滑挪步,单击摸一遍房间,双击用物品(BGM=bgm/drone,语速1)
+2 -2
View File
@@ -16,9 +16,9 @@ t+ 14.0s 〰 震动:beat(节拍点)
t+ 15.3s 〰 震动:beat(节拍点)
t+ 19.0s ▶ 语音:「再听一遍,跟着来。」
t+ 19.0s 〰 震动:error(做错了:两连短震)
t+ 19.0s 【状态】第一遍没敢出手,先听它归因 教学:听领奏,下一小节照样打(left→right)(语音播放中,语速1)
t+ 19.0s 【状态】第一遍整拍没出手——听到的是「这拍没接。我停下的时候,就轮到你了」,不是「慢了半拍」 教学:听领奏,下一小节照样打(left→right)(语音播放中,语速1)
t+ 21.1s ▶ 语音:「再听一遍,跟着来。」
t+ 23.2s ▶ 向导语音:「慢了半拍。跟着节拍走。」
t+ 23.2s ▶ 向导语音:「这拍没接。我停下的时候,就轮到你了。」
t+ 23.2s 【状态】又重新领奏了一遍,还是没敢出手 教学:听领奏,下一小节照样打(left→right)(语音播放中,语速1)
t+ 24.7s 〰 震动:beat(节拍点)
t+ 26.0s 〰 震动:beat(节拍点)
+215 -63
View File
@@ -52,72 +52,224 @@ t+ 49.8s ▶ 语音:「六种声音都记住了。后面的序列,会越
t+ 51.9s ▶ 语音:「答错可以消耗机会重听。向上滑,可以随时重听。」
t+ 54.0s ▶ 向导语音:「记不住就向上滑,重新听一遍。免费机会有两次,用完要扣一次机会。」
t+ 56.2s ▶ 语音:「第一轮,听好了。」
t+ 58.3s ♪ 音效 sfx/tone_tap
t+ 56.2s 【状态】教学讲完了:免费重听两次、用完扣机会,想重听就向上滑 第 1 轮报数呢,听好了(语音播放中,语速1)
t+ 58.3s ♪ 音效 sfx/tone_up
t+ 58.3s 〰 震动:beat(节拍点)
t+ 58.3s 【状态】教学讲完了:免费重听两次、用完扣机会,想重听就向上滑 第 1 轮序列演奏中(2 个音),记住顺序,先别动手(无 BGM,语速1)
t+ 59.0s ♪ 音效 sfx/tone_doubletap
t+ 59.0s ♪ 音效 sfx/tone_tap
t+ 59.0s 〰 震动:beat(节拍点)
t+ 60.1s ♪ 向导音:turn(轮到你)
t+ 60.1s ▶ 语音:「轮到你了。」
t+ 60.1s ▶ 向导语音:「这一轮,向上滑是作答。」
t+ 60.1s 〰 震动:confirm(轻点确认)
t+ 62.2s 【状态】第 1 轮有点懵,先僵一会儿 第 1 轮轮到你了:已对上 0/2,照原顺序做手势(双击就是双击那个音,要重听就向上滑)(无 BGM,语速1)
t+ 62.2s ……… 静默 10s(想不起来序列了) ………
t+ 72.2s ♪ 向导音:nudge(看门狗轻提醒)
t+ 72.2s ▶ 向导语音:「需要的话,向上滑可以重新听一遍序列。」
t+ 72.2s 【状态】听到提示:需要的话,向上滑可以重新听一遍序列 第 1 轮轮到你了:已对上 0/2,照原顺序做手势(双击就是双击那个音,要重听就向上滑)(语音播放中,语速1)
t+ 62.2s ▶ 语音:「轮到你了。」
t+ 64.3s 【状态】序列一播完就听到「这一轮,向上滑是作答」——重听键这轮被占了 第 1 轮轮到你了:已对上 0/2,照原顺序做手势(这轮上滑是作答,占用了)(要重听就向上滑)(无 BGM,语速1)
t+ 64.3s ……… 静默 5s(第一轮先不动手,看看它催不催) ………
t+ 69.3s ♪ 向导音:nudge(看门狗轻提醒)
t+ 69.3s ♪ 向导音:watchdog(看门狗升级)
t+ 89.3s ♪ 向导音:nudge(看门狗轻提醒)
t+ 89.3s ♪ 向导音:watchdog(看门狗升级)
t+ 89.3s ♪ 向导音:turn(轮到你)
t+ 89.3s 〰 震动:confirm(轻点确认)
#### 【玩家动作】照提示向上滑,用掉一次免费重听
#### 【玩家动作】5 秒一声轻提醒、25 秒又一声加确认震——两级提醒都没建议「向上滑重听」,它知道这轮上滑是作答
t+ 72.2s ▶ 语音:「再听一遍。」
t+ 72.2s 〰 震动:pickup(获得)
t+ 74.3s ▶ 语音:「重听机会用完了,小心。」
t+ 76.4s ♪ 音效 sfx/tone_tap
t+ 76.4s 〰 震动:beat(节拍点)
t+ 77.1s ♪ 音效 sfx/tone_doubletap
t+ 77.1s 〰 震动:beat(节拍点)
t+ 78.2s ♪ 向导音:turn(轮到你)
t+ 78.2s ▶ 语音:「轮到你了。」
t+ 78.2s 〰 震动:confirm(轻点确认)
t+ 80.4s 【状态】轮到我复现了 第 1 轮轮到你了:已对上 0/2,照原顺序做手势(双击就是双击那个音,要重听就向上滑)(无 BGM,语速1)
t+ 80.4s ♪ 音效 sfx/tone_tap
t+ 80.4s 【状态】轮到我复现了 第 1 轮轮到你了:已对上 1/2,照原顺序做手势(双击就是双击那个音,要重听就向上滑)(无 BGM,语速1)
t+ 80.4s ♪ 音效 sfx/tone_doubletap
t+ 80.4s ▶ 向导语音:「漂亮,过了。」
t+ 80.4s 〰 震动:success(成功长震)
t+ 82.8s ▶ 语音:「第二轮,听好了。」
t+ 84.9s ♪ 音效 sfx/tone_tap
t+ 84.9s 〰 震动:beat(节拍点)
t+ 85.6s ♪ 音效 sfx/tone_doubletap
t+ 85.6s 〰 震动:beat(节拍点)
t+ 86.2s ♪ 音效 sfx/tone_up
t+ 86.2s 〰 震动:beat(节拍点)
t+ 87.3s ♪ 向导音:turn(轮到你)
t+ 87.3s 〰 震动:confirm(轻点确认)
t+ 87.3s 【状态】这个是上行音——上滑作答(期望恰为 up 时上滑=作答) 第 2 轮轮到你了:已对上 0/3,照原顺序做手势(双击就是双击那个音,要重听就向上滑)(无 BGM,语速1)
t+ 87.3s ♪ 音效 sfx/tone_up
t+ 87.3s 【状态】轮到我复现了 第 2 轮轮到你了:已对上 1/3,照原顺序做手势(双击就是双击那个音,要重听就向上滑)(无 BGM,语速1)
t+ 87.3s ♪ 音效 sfx/tone_doubletap
t+ 87.3s 【状态】轮到我复现了 第 2 轮轮到你了:已对上 2/3,照原顺序做手势(双击就是双击那个音,要重听就向上滑)(无 BGM,语速1)
t+ 87.3s ♪ 音效 sfx/tone_up
t+ 87.3s 〰 震动:success(成功长震)
t+ 87.6s ▶ 语音:「第三轮,听好了。」
t+ 89.7s ♪ 音效 sfx/tone_up
t+ 89.7s 〰 震动:beat(节拍点)
t+ 90.4s ♪ 音效 sfx/tone_doubletap
t+ 90.4s 〰 震动:beat(节拍点)
t+ 91.0s ♪ 音效 sfx/tone_up
t+ 91.7s ♪ 音效 sfx/tone_right
t+ 91.0s 〰 震动:beat(节拍点)
t+ 91.7s 〰 震动:beat(节拍点)
t+ 92.7s ♪ 向导音:turn(轮到你)
t+ 92.7s 〰 震动:confirm(轻点确认)
t+ 92.7s 【状态】轮到我复现了 第 3 轮轮到你了:已对上 0/4,照原顺序做手势(双击就是双击那个音,要重听就向上滑)(无 BGM,语速1)
t+ 92.7s ♪ 音效 sfx/tone_up
t+ 92.7s 【状态】轮到我复现了 第 3 轮轮到你了:已对上 1/4,照原顺序做手势(双击就是双击那个音,要重听就向上滑)(无 BGM,语速1)
t+ 92.7s ♪ 音效 sfx/tone_doubletap
t+ 92.7s 【状态】轮到我复现了 第 3 轮轮到你了:已对上 2/4,照原顺序做手势(双击就是双击那个音,要重听就向上滑)(无 BGM,语速1)
t+ 92.7s ♪ 音效 sfx/tone_up
t+ 92.7s 【状态】这个是双击音——双击作答(round3:input 双击永远=作答) 第 3 轮轮到你了:已对上 3/4,照原顺序做手势(双击就是双击那个音,要重听就向上滑)(无 BGM,语速1)
t+ 92.7s ♪ 音效 sfx/tone_doubletap
t+ 92.7s 〰 震动:success(成功长震)
t+ 92.7s 【状态】收工:完整打完 3 轮,在轮与轮的边界停手(免费重听剩 0 次) 刚答完一轮,播报中(无 BGM,语速1)
t+ 89.3s 【状态】先来一发上滑——期望恰为 up 时上滑就是作答 第 1 轮轮到你了:已对上 0/2,照原顺序做手势(这轮上滑是作答,占用了)(要重听就向上滑)(无 BGM,语速1)
t+ 89.3s ♪ 音效 sfx/tone_up
t+ 89.3s ♪ 音效 sfx/tone_tap
t+ 89.3s ▶ 向导语音:「漂亮,过了。」
t+ 89.3s 〰 震动:success(成功长震)
t+ 91.7s ▶ 语音:「第二轮,听好了。」
t+ 93.9s ♪ 音效 sfx/tone_down
t+ 93.9s 〰 震动:beat(节拍点)
t+ 94.5s ♪ 音效 sfx/tone_doubletap
t+ 94.5s 〰 震动:beat(节拍点)
t+ 95.2s ♪ 音效 sfx/tone_left
t+ 95.2s 〰 震动:beat(节拍点)
t+ 96.3s ♪ 向导音:turn(轮到你)
t+ 96.3s ▶ 向导语音:「这一轮,双击是作答。」
t+ 96.3s 〰 震动:confirm(轻点确认)
t+ 98.4s 【状态】这轮预告的是「双击是作答」 第 2 轮轮到你了:已对上 0/3,照原顺序做手势(这轮双击是作答,占用了)(要重听就向上滑)(无 BGM,语速1)
t+ 98.4s ♪ 音效 sfx/tone_down
t+ 98.4s ♪ 音效 sfx/tone_doubletap
t+ 98.4s ♪ 音效 sfx/tone_left
t+ 98.4s 〰 震动:success(成功长震)
t+ 98.7s ▶ 语音:「第三轮,听好了。」
t+ 100.8s ♪ 音效 sfx/tone_down
t+ 100.8s 〰 震动:beat(节拍点)
t+ 101.5s ♪ 音效 sfx/tone_right
t+ 101.5s 〰 震动:beat(节拍点)
t+ 102.1s ♪ 音效 sfx/tone_tap
t+ 102.8s ♪ 音效 sfx/tone_left
t+ 102.1s 〰 震动:beat(节拍点)
t+ 102.8s 〰 震动:beat(节拍点)
t+ 103.8s ♪ 向导音:turn(轮到你)
t+ 103.8s 〰 震动:confirm(轻点确认)
t+ 103.8s 【状态】这轮开场没有占用预告——上滑还能当重听键用 第 3 轮轮到你了:已对上 0/4,照原顺序做手势(要重听就向上滑)(无 BGM,语速1)
t+ 103.8s ♪ 音效 sfx/tone_down
t+ 103.8s ♪ 音效 sfx/tone_right
t+ 103.8s ♪ 音效 sfx/tone_tap
t+ 103.8s ♪ 音效 sfx/tone_left
t+ 103.8s 〰 震动:success(成功长震)
t+ 104.1s ▶ 语音:「第四轮,听好了。」
t+ 106.2s ♪ 音效 sfx/tone_down
t+ 106.9s ♪ 音效 sfx/tone_right
t+ 106.2s 〰 震动:beat(节拍点)
t+ 106.9s 〰 震动:beat(节拍点)
t+ 107.5s ♪ 音效 sfx/tone_tap
t+ 107.5s 〰 震动:beat(节拍点)
t+ 108.1s ♪ 音效 sfx/tone_left
t+ 108.7s ♪ 音效 sfx/tone_down
t+ 108.1s 〰 震动:beat(节拍点)
t+ 108.7s 〰 震动:beat(节拍点)
t+ 109.7s ♪ 向导音:turn(轮到你)
t+ 109.7s 〰 震动:confirm(轻点确认)
t+ 109.7s 【状态】机会加一之后,免费重听真的有两次了 第 4 轮轮到你了:已对上 0/5,照原顺序做手势(要重听就向上滑)(无 BGM,语速1)
t+ 109.7s ♪ 音效 sfx/tone_down
t+ 109.7s ♪ 音效 sfx/tone_right
t+ 109.7s ♪ 音效 sfx/tone_tap
t+ 109.7s ♪ 音效 sfx/tone_left
t+ 109.7s ♪ 音效 sfx/tone_down
t+ 109.7s ▶ 语音:「答对了!」
t+ 109.7s 〰 震动:success(成功长震)
t+ 109.7s 〰 震动:pickup(获得)
t+ 111.9s ▶ 语音:「机会加一。」
t+ 114.3s ▶ 语音:「第五轮,听好了。」
t+ 116.4s ♪ 音效 sfx/tone_down
t+ 116.4s 〰 震动:beat(节拍点)
t+ 117.0s ♪ 音效 sfx/tone_right
t+ 117.6s ♪ 音效 sfx/tone_tap
t+ 117.0s 〰 震动:beat(节拍点)
t+ 117.6s 〰 震动:beat(节拍点)
t+ 118.2s ♪ 音效 sfx/tone_left
t+ 118.8s ♪ 音效 sfx/tone_down
t+ 118.2s 〰 震动:beat(节拍点)
t+ 118.8s 〰 震动:beat(节拍点)
t+ 119.4s ♪ 音效 sfx/tone_right
t+ 119.4s 〰 震动:beat(节拍点)
t+ 120.4s ♪ 向导音:turn(轮到你)
t+ 120.4s 〰 震动:confirm(轻点确认)
t+ 120.4s 【状态】第 5 轮序列有点长,先不动手 第 5 轮轮到你了:已对上 0/6,照原顺序做手势(要重听就向上滑)(无 BGM,语速1)
t+ 120.4s ……… 静默 5s(想不起来序列了) ………
t+ 125.4s ♪ 向导音:nudge(看门狗轻提醒)
t+ 125.4s ▶ 向导语音:「需要的话,向上滑可以重新听一遍序列。」
t+ 125.4s 【状态】5 秒就来了提示:需要的话,向上滑可以重新听一遍序列 第 5 轮轮到你了:已对上 0/6,照原顺序做手势(要重听就向上滑)(语音播放中,语速1)
#### 【玩家动作】照提示向上滑——第 1 次免费重听用掉,跟着听到「免费重听还剩一次」
t+ 125.4s ▶ 语音:「再听一遍。」
t+ 125.4s 〰 震动:pickup(获得)
t+ 127.5s ▶ 向导语音:「免费重听还剩一次。」
t+ 129.6s ♪ 音效 sfx/tone_down
t+ 129.6s 〰 震动:beat(节拍点)
t+ 130.2s ♪ 音效 sfx/tone_right
t+ 130.8s ♪ 音效 sfx/tone_tap
t+ 130.2s 〰 震动:beat(节拍点)
t+ 130.8s 〰 震动:beat(节拍点)
t+ 131.4s ♪ 音效 sfx/tone_left
t+ 131.4s 〰 震动:beat(节拍点)
t+ 132.0s ♪ 音效 sfx/tone_down
t+ 132.6s ♪ 音效 sfx/tone_right
t+ 132.0s 〰 震动:beat(节拍点)
t+ 132.6s 〰 震动:beat(节拍点)
t+ 133.6s ♪ 向导音:turn(轮到你)
t+ 133.6s 〰 震动:confirm(轻点确认)
t+ 133.6s ♪ 音效 sfx/tone_down
t+ 133.6s ♪ 音效 sfx/tone_right
t+ 133.6s ♪ 音效 sfx/tone_tap
t+ 133.6s ♪ 音效 sfx/tone_left
t+ 133.6s ♪ 音效 sfx/tone_down
t+ 133.6s ♪ 音效 sfx/tone_right
t+ 133.6s 〰 震动:success(成功长震)
t+ 133.9s ▶ 语音:「第六轮,听好了。」
t+ 136.0s ♪ 音效 sfx/tone_up
t+ 136.6s ♪ 音效 sfx/tone_doubletap
t+ 136.0s 〰 震动:beat(节拍点)
t+ 136.6s 〰 震动:beat(节拍点)
t+ 137.2s ♪ 音效 sfx/tone_tap
t+ 137.7s ♪ 音效 sfx/tone_left
t+ 137.2s 〰 震动:beat(节拍点)
t+ 137.7s 〰 震动:beat(节拍点)
t+ 138.3s ♪ 音效 sfx/tone_down
t+ 138.9s ♪ 音效 sfx/tone_right
t+ 138.3s 〰 震动:beat(节拍点)
t+ 138.9s 〰 震动:beat(节拍点)
t+ 139.4s ♪ 音效 sfx/tone_tap
t+ 139.4s 〰 震动:beat(节拍点)
t+ 140.4s ♪ 向导音:turn(轮到你)
t+ 140.4s ▶ 向导语音:「这一轮,向上滑和双击都是作答。」
t+ 140.4s 〰 震动:confirm(轻点确认)
t+ 142.5s 【状态】这轮说「向上滑和双击都是作答」——两个键都被占用了 第 6 轮轮到你了:已对上 0/7,照原顺序做手势(这轮上滑和双击是作答,占用了)(要重听就向上滑)(无 BGM,语速1)
t+ 142.5s ……… 静默 5s(轮到我了,先心里过一遍序列) ………
t+ 147.5s ♪ 向导音:nudge(看门狗轻提醒)
t+ 147.5s ♪ 向导音:watchdog(看门狗升级)
t+ 167.5s ♪ 向导音:nudge(看门狗轻提醒)
t+ 167.5s ♪ 向导音:watchdog(看门狗升级)
t+ 167.5s ♪ 向导音:turn(轮到你)
t+ 167.5s 〰 震动:confirm(轻点确认)
#### 【玩家动作】又是两级提醒都没有重听建议——占用轮里那句建议自动闭嘴
t+ 167.5s ♪ 音效 sfx/tone_up
t+ 167.5s ♪ 音效 sfx/tone_doubletap
t+ 167.5s ♪ 音效 sfx/tone_tap
t+ 167.5s ♪ 音效 sfx/tone_left
t+ 167.5s ♪ 音效 sfx/tone_down
t+ 167.5s ♪ 音效 sfx/tone_right
t+ 167.5s ♪ 音效 sfx/tone_tap
t+ 167.5s 〰 震动:success(成功长震)
t+ 167.8s ▶ 语音:「第七轮,听好了。」
t+ 169.9s ♪ 音效 sfx/tone_down
t+ 169.9s 〰 震动:beat(节拍点)
t+ 170.5s ♪ 音效 sfx/tone_right
t+ 170.5s 〰 震动:beat(节拍点)
t+ 171.0s ♪ 音效 sfx/tone_tap
t+ 171.6s ♪ 音效 sfx/tone_left
t+ 171.0s 〰 震动:beat(节拍点)
t+ 171.6s 〰 震动:beat(节拍点)
t+ 172.1s ♪ 音效 sfx/tone_down
t+ 172.7s ♪ 音效 sfx/tone_right
t+ 172.1s 〰 震动:beat(节拍点)
t+ 172.7s 〰 震动:beat(节拍点)
t+ 173.2s ♪ 音效 sfx/tone_tap
t+ 173.8s ♪ 音效 sfx/tone_left
t+ 173.2s 〰 震动:beat(节拍点)
t+ 173.8s 〰 震动:beat(节拍点)
t+ 174.7s ♪ 向导音:turn(轮到你)
t+ 174.7s 〰 震动:confirm(轻点确认)
t+ 174.7s ……… 静默 5s(长序列,又想不起来了) ………
t+ 179.7s ♪ 向导音:nudge(看门狗轻提醒)
t+ 179.7s ▶ 向导语音:「需要的话,向上滑可以重新听一遍序列。」
t+ 179.7s 【状态】又是那句提示:向上滑可以重新听一遍序列 第 7 轮轮到你了:已对上 0/8,照原顺序做手势(要重听就向上滑)(语音播放中,语速1)
#### 【玩家动作】向上滑——第 2 次免费重听用掉,这回听到「免费重听用完了。之后重听会花一次机会」
t+ 179.7s ▶ 语音:「再听一遍。」
t+ 179.7s 〰 震动:pickup(获得)
t+ 181.8s ▶ 向导语音:「免费重听用完了。之后重听会花一次机会,别怕,慢慢来。」
t+ 183.9s ♪ 音效 sfx/tone_down
t+ 183.9s 〰 震动:beat(节拍点)
t+ 184.5s ♪ 音效 sfx/tone_right
t+ 184.5s 〰 震动:beat(节拍点)
t+ 185.0s ♪ 音效 sfx/tone_tap
t+ 185.6s ♪ 音效 sfx/tone_left
t+ 185.0s 〰 震动:beat(节拍点)
t+ 185.6s 〰 震动:beat(节拍点)
t+ 186.1s ♪ 音效 sfx/tone_down
t+ 186.7s ♪ 音效 sfx/tone_right
t+ 186.1s 〰 震动:beat(节拍点)
t+ 186.7s 〰 震动:beat(节拍点)
t+ 187.2s ♪ 音效 sfx/tone_tap
t+ 187.7s ♪ 音效 sfx/tone_left
t+ 187.2s 〰 震动:beat(节拍点)
t+ 187.7s 〰 震动:beat(节拍点)
t+ 188.7s ♪ 向导音:turn(轮到你)
t+ 188.7s 〰 震动:confirm(轻点确认)
t+ 188.7s ♪ 音效 sfx/tone_down
t+ 188.7s ♪ 音效 sfx/tone_right
t+ 188.7s ♪ 音效 sfx/tone_tap
t+ 188.7s ♪ 音效 sfx/tone_left
t+ 188.7s ♪ 音效 sfx/tone_down
t+ 188.7s ♪ 音效 sfx/tone_right
t+ 188.7s ♪ 音效 sfx/tone_tap
t+ 188.7s ♪ 音效 sfx/tone_left
t+ 188.7s 〰 震动:success(成功长震)
t+ 188.7s 【状态】收工:打完 7 轮在轮边界停手(免费重听剩 0 次,账目和播的对上了) 刚答完一轮,播报中(无 BGM,语速1)
+33 -33
View File
@@ -58,37 +58,37 @@ t+ 32.8s 〰 震动:danger(危险逼近)
#### 【玩家动作】听到第二段警报和方向呼喊,同向滑开
t+ 33.3s ♪ 音效 sfx/dodge
t+ 33.3s ♪ 音效 sfx/whoosh_down
t+ 33.3s ♪ 音效 sfx/whoosh_left
t+ 36.3s ♪ 音效 sfx/alarm
t+ 36.8s ♪ 音效 sfx/alarm
t+ 36.8s ▶ 语音:「下!」
t+ 36.8s ▶ 语音:「左!」
t+ 36.3s 〰 震动:danger(危险逼近)
t+ 36.8s 〰 震动:danger(危险逼近)
#### 【玩家动作】听到第二段警报和方向呼喊,同向滑开
t+ 37.3s ♪ 音效 sfx/dodge
t+ 37.3s ♪ 音效 sfx/whoosh_left
t+ 37.3s ♪ 音效 sfx/whoosh_up
t+ 40.3s ♪ 音效 sfx/alarm
t+ 40.8s ♪ 音效 sfx/alarm
t+ 40.8s ▶ 语音:「左!」
t+ 40.8s ▶ 语音:「上!」
t+ 40.3s 〰 震动:danger(危险逼近)
t+ 40.8s 〰 震动:danger(危险逼近)
#### 【玩家动作】听到第二段警报和方向呼喊,同向滑开
t+ 41.3s ♪ 音效 sfx/dodge
t+ 41.3s ♪ 音效 sfx/whoosh_right
t+ 41.3s ♪ 音效 sfx/whoosh_left
t+ 44.3s ♪ 音效 sfx/alarm
t+ 44.8s ♪ 音效 sfx/alarm
t+ 44.8s ▶ 语音:「右!」
t+ 44.8s ▶ 语音:「左!」
t+ 44.3s 〰 震动:danger(危险逼近)
t+ 44.8s 〰 震动:danger(危险逼近)
#### 【玩家动作】听到第二段警报和方向呼喊,同向滑开
t+ 45.3s ♪ 音效 sfx/dodge
t+ 45.3s ♪ 音效 sfx/whoosh_left
t+ 45.3s ♪ 音效 sfx/whoosh_down
t+ 47.8s ▶ 语音:「试试单击,引擎会短促加速,直接冲过空隙。」
t+ 47.8s 【状态】游戏停下来教新功能 暂停了刷怪教新功能:单击试试加速(语音播放中,语速1)
@@ -96,30 +96,30 @@ t+ 47.8s 【状态】游戏停下来教新功能 暂停了刷怪教新功能
t+ 49.9s ♪ 音效 sfx/boost
t+ 49.9s ▶ 语音:「感觉到了吗?加速的时候,陨石撞不上你。」
t+ 52.4s ♪ 音效 sfx/whoosh_up
t+ 52.4s ♪ 音效 sfx/whoosh_right
t+ 55.4s ♪ 音效 sfx/alarm
t+ 55.9s ♪ 音效 sfx/alarm
t+ 55.9s ▶ 语音:「上!」
t+ 55.9s ▶ 语音:「右!」
t+ 55.4s 〰 震动:danger(危险逼近)
t+ 55.9s 〰 震动:danger(危险逼近)
#### 【玩家动作】听到第二段警报和方向呼喊,同向滑开
t+ 56.4s ♪ 音效 sfx/dodge
t+ 56.4s ♪ 音效 sfx/whoosh_right
t+ 56.4s ♪ 音效 sfx/whoosh_up
t+ 59.4s ♪ 音效 sfx/alarm
t+ 59.9s ♪ 音效 sfx/alarm
t+ 59.9s ▶ 语音:「右!」
t+ 59.9s ▶ 语音:「上!」
t+ 59.4s 〰 震动:danger(危险逼近)
t+ 59.9s 〰 震动:danger(危险逼近)
#### 【玩家动作】听到第二段警报和方向呼喊,同向滑开
t+ 60.4s ♪ 音效 sfx/dodge
t+ 60.4s ♪ 音效 sfx/whoosh_down
t+ 60.4s ♪ 音效 sfx/whoosh_right
t+ 63.4s ♪ 音效 sfx/alarm
t+ 63.9s ♪ 音效 sfx/alarm
t+ 63.9s ▶ 语音:「下!」
t+ 63.9s ▶ 语音:「右!」
t+ 63.4s 〰 震动:danger(危险逼近)
t+ 63.9s 〰 震动:danger(危险逼近)
@@ -136,10 +136,10 @@ t+ 67.9s 〰 震动:danger(危险逼近)
#### 【玩家动作】听到第二段警报和方向呼喊,同向滑开
t+ 68.4s ♪ 音效 sfx/dodge
t+ 68.4s ♪ 音效 sfx/whoosh_up
t+ 68.4s ♪ 音效 sfx/whoosh_left
t+ 71.4s ♪ 音效 sfx/alarm
t+ 71.9s ♪ 音效 sfx/alarm
t+ 71.9s ▶ 语音:「上!」
t+ 71.9s ▶ 语音:「左!」
t+ 71.4s 〰 震动:danger(危险逼近)
t+ 71.9s 〰 震动:danger(危险逼近)
@@ -154,10 +154,10 @@ t+ 72.8s 【状态】游戏停下来教新功能 暂停了刷怪教新功能
t+ 74.9s ♪ 音效 sfx/shield
t+ 74.9s ▶ 语音:「护盾展开后会自动充能,好了我会告诉你。」
t+ 77.4s ♪ 音效 sfx/whoosh_right
t+ 77.4s ♪ 音效 sfx/whoosh_down
t+ 80.4s ♪ 音效 sfx/alarm
t+ 80.9s ♪ 音效 sfx/alarm
t+ 80.9s ▶ 语音:「右!」
t+ 80.9s ▶ 语音:「下!」
t+ 80.4s 〰 震动:danger(危险逼近)
t+ 80.9s 〰 震动:danger(危险逼近)
@@ -165,72 +165,72 @@ t+ 80.9s 〰 震动:danger(危险逼近)
t+ 81.4s ♪ 音效 sfx/dodge
t+ 81.4s ▶ 语音:「十颗了,稳。」
t+ 81.4s ♪ 音效 sfx/whoosh_right
t+ 81.4s ♪ 音效 sfx/whoosh_down
t+ 81.4s 〰 震动:confirm(轻点确认)
t+ 84.4s ♪ 音效 sfx/alarm
t+ 84.9s ♪ 音效 sfx/alarm
t+ 84.9s ▶ 语音:「右!」
t+ 84.9s ▶ 语音:「下!」
t+ 84.4s 〰 震动:danger(危险逼近)
t+ 84.9s 〰 震动:danger(危险逼近)
#### 【玩家动作】听到第二段警报和方向呼喊,同向滑开
t+ 85.4s ♪ 音效 sfx/dodge
t+ 85.4s ♪ 音效 sfx/whoosh_right
t+ 85.4s ♪ 音效 sfx/whoosh_down
t+ 87.8s ▶ 语音:「训练护盾关闭。接下来,靠你自己了。」
t+ 87.8s 〰 震动:warning(警告三连震)
t+ 88.4s ♪ 音效 sfx/alarm
t+ 88.9s ♪ 音效 sfx/alarm
t+ 88.9s ▶ 语音:「右!」
t+ 88.9s ▶ 语音:「下!」
t+ 88.4s 〰 震动:danger(危险逼近)
t+ 88.9s 〰 震动:danger(危险逼近)
t+ 88.9s 【状态】保护期结束了,这颗我故意没动 对局中:1 颗陨石在逼近,听方位同向滑开(语音播放中,语速1)
t+ 89.4s ♪ 音效 sfx/hit
t+ 89.4s ▶ 语音:「右边撞上的。听到右边的呼啸,向右滑。」
t+ 89.4s ▶ 语音:「下面撞上的。听到低沉的轰隆,向下滑。」
t+ 89.4s 〰 震动:hit(受击重震)
#### 【玩家动作】被撞了。先听到方位归因,又听到「同向滑动就能拨开」
t+ 89.4s ♪ 音效 sfx/whoosh_up
t+ 89.4s ♪ 音效 sfx/whoosh_left
t+ 91.2s ♪ 音效 sfx/alarm
t+ 91.6s ▶ 向导语音:「被撞了。听方位,同向滑动就能拨开。」
t+ 91.7s ♪ 音效 sfx/alarm
t+ 91.7s ▶ 语音:「上!」
t+ 91.7s ▶ 语音:「左!」
t+ 91.2s 〰 震动:danger(危险逼近)
t+ 91.7s 〰 震动:danger(危险逼近)
t+ 91.7s 【状态】又有一颗,也故意不躲——想听听第二次被撞会说什么 对局中:1 颗陨石在逼近,听方位同向滑开(语音播放中,语速1)
t+ 92.2s ♪ 音效 sfx/hit
t+ 92.2s ▶ 语音:「上面撞上的。听到高高的呼啸,向上滑。」
t+ 92.2s ▶ 语音:「左边撞上的。听到左边的呼啸,向左滑。」
t+ 92.2s 〰 震动:hit(受击重震)
#### 【玩家动作】第二次被撞:先听方位归因,又听到「这是最后一次机会,撑住」——这就是全部家底了
t+ 92.6s ♪ 音效 sfx/whoosh_left
t+ 92.6s ♪ 音效 sfx/whoosh_right
t+ 94.3s ▶ 语音:「船体只剩最后一次了,小心!」
t+ 94.4s ♪ 音效 sfx/alarm
t+ 94.9s ♪ 音效 sfx/alarm
t+ 94.9s ▶ 语音:「左!」
t+ 94.9s ▶ 语音:「右!」
t+ 94.4s 〰 震动:danger(危险逼近)
t+ 94.9s 〰 震动:danger(危险逼近)
#### 【玩家动作】听到第二段警报和方向呼喊,同向滑开
t+ 95.4s ♪ 音效 sfx/dodge
t+ 95.7s ♪ 音效 sfx/whoosh_right
t+ 95.7s ♪ 音效 sfx/whoosh_down
t+ 97.5s ♪ 音效 sfx/alarm
t+ 97.5s 〰 震动:danger(危险逼近)
t+ 98.0s ♪ 音效 sfx/alarm
t+ 98.0s ▶ 语音:「右!」
t+ 98.0s ▶ 语音:「下!」
t+ 98.0s 〰 震动:danger(危险逼近)
#### 【玩家动作】听到第二段警报和方向呼喊,同向滑开
t+ 98.5s ♪ 音效 sfx/dodge
t+ 98.8s ♪ 音效 sfx/whoosh_left
t+ 98.8s ♪ 音效 sfx/whoosh_down
t+ 100.5s ♪ 音效 sfx/alarm
t+ 100.5s 〰 震动:danger(危险逼近)
t+ 101.0s ♪ 音效 sfx/alarm
t+ 101.0s ▶ 语音:「左!」
t+ 101.0s ▶ 语音:「下!」
t+ 101.0s 〰 震动:danger(危险逼近)
#### 【玩家动作】听到第二段警报和方向呼喊,同向滑开
@@ -258,10 +258,10 @@ t+ 106.9s 〰 震动:danger(危险逼近)
#### 【玩家动作】听到第二段警报和方向呼喊,同向滑开
t+ 107.4s ♪ 音效 sfx/dodge
t+ 107.7s ♪ 音效 sfx/whoosh_right
t+ 107.7s ♪ 音效 sfx/whoosh_left
t+ 109.3s ♪ 音效 sfx/alarm
t+ 109.8s ♪ 音效 sfx/alarm
t+ 109.8s ▶ 语音:「右!」
t+ 109.8s ▶ 语音:「左!」
t+ 109.3s 〰 震动:danger(危险逼近)
t+ 109.8s 〰 震动:danger(危险逼近)
t+ 109.8s 【状态】保护期结束后撑了 22 秒、挨了两下还活着;每回撞击前半秒都能听到方向呼喊,先到这 对局中:1 颗陨石在逼近,听方位同向滑开(语音播放中,语速1)
+10
View File
@@ -44,6 +44,16 @@
未结题原因(跨人设/跨轮复现的状态混淆 P0):rhythm「轮到你」排程迟到 bug(两人设)、escape 拿保险丝后目标断链(round1 P1 提出、round2 P2 复现)、simon 双击双义判据不可感知(P1)、rhythm 教学无降难路径(P4)。→ round3 裁定 G1~G8(round2/synthesis.md)。
### round3(G1~G8 修订后)
愿意继续玩:**P1/P2/P4 全 6 游戏通过;P3 5/6(escape)**。rhythm 从 3/4 → 4/4(「放慢陪练」被 P4 实测认可);space 5/5(P3:「威胁自报家门是全场最佳设计」)。
P1 终评:「六款工程素养高于 audiogames.net 平均线、无一款把我当白痴」。剩余 P0 全部收敛到三类**信息真实性问题**:simon 手势两义(P1/P2/P4 三人设收敛 → 根治=轮次占用预告而非换键)、simon 重听账目不实(教学说两次用一次报完)、rhythm 无出手被误报「慢了半拍」(证明 round2 偏离裁定错误——归因真话律由此入方法论 v1.2)。→ round4 裁定 H1~H5(round3/synthesis.md)。
### round4(H1~H5 修订后)
(待 round4 终评后填写:门槛判定与结题结论)
## 4. 交付物与影响
- 方法论(可复用):research/02-methodology/methodology.md
+17
View File
@@ -209,6 +209,12 @@ ESCAPE_LINES = [
("skip_hint", _ESC, "单击可跳过旁白。"),
# ---- 向导层 round3(docs/design/14-guide-layer.md「round3 v1.2」,synthesis §3 定稿全文)----
("gd_next_fuse", _ESC, "保险丝到手。接下来,去找电箱。"),
# ---- 向导层 round4(docs/design/14-guide-layer.md「round4 v1.3」,synthesis §3 定稿全文)----
# H1:目标句后按房间图接播目标房间方位句(移动坐标系:up/down/left/right 滑动方向即方位)。
("gd_room_l", _ESC, "左面的房间。"),
("gd_room_r", _ESC, "右面的房间。"),
("gd_room_u", _ESC, "上面的房间。"),
("gd_room_d", _ESC, "下面的房间。"),
]
@@ -283,6 +289,14 @@ SIMON_LINES = [
# ---- 向导层 round3(docs/design/14-guide-layer.md「round3 v1.2」,synthesis §3 定稿全文)----
("gd_clear", _SIMON, "漂亮,过了。"),
("gd_record", _SIMON, "破纪录了。"),
# ---- 向导层 round4(docs/design/14-guide-layer.md「round4 v1.3」,synthesis §3 定稿全文)----
# H2:轮次开场占用预告(序列播放后、轮到你提示前;重听键与答案音共用词表的根治形态)。
("gd_occ_up", _SIMON, "这一轮,向上滑是作答。"),
("gd_occ_dt", _SIMON, "这一轮,双击是作答。"),
("gd_occ_both", _SIMON, "这一轮,向上滑和双击都是作答。"),
# H3:使用时如实报余量(替换主动重听路径的旧「用完了」绝对句)。
("gd_chances_left1", _SIMON, "免费重听还剩一次。"),
("gd_chances_left0", _SIMON, "免费重听用完了。之后重听会花一次机会,别怕,慢慢来。"),
]
@@ -339,6 +353,9 @@ RHYTHM_LINES = [
# ---- 向导层 round3(docs/design/14-guide-layer.md「round3 v1.2」,synthesis §3 定稿全文)----
("gd_slow", _RHY, "我放慢一些,跟着试试。"),
("gd_count", _RHY, "四拍之后,继续。"),
# ---- 向导层 round4(docs/design/14-guide-layer.md「round4 v1.3」,synthesis §3 定稿全文)----
# H5:归因真话律——应答窗口无任何尝试播此句(早/晚/错分档只留给真出手的情况)。
("gd_miss_none", _RHY, "这拍没接。我停下的时候,就轮到你了。"),
]
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+43 -3
View File
@@ -21,6 +21,12 @@
* 60s 无进展 L1 暗示(stuck_ask + stuck_*_1)、120s L2 明示(stuck_*_2)、
* 180s L3 带路(stuck_*_3 + 按当前坐标算路链播 nav_*,此后每 60s 重复)。
*
* round4 H1(docs/design/14-guide-layer.md「round4 v1.3」):阶段目标句
* (found_key / gd_next_fuse / fix_box)播完后,按房间图计算目标房间相对当前
* 房间的真实方位,接播 gd_room_l/r/u/d 之一(游戏反馈层全档位;目标在同一
* 房间不播;方向 = escape 移动坐标系,up/down/left/right 滑动方向即方位)。
* 引导期 g7_goal 不接(引导本身逐句带路,且引导链被 playtest 断言冻结)。
*
* 语音 id 权威清单:docs/audio/round2-voice-ids.md(已删除的 n0–n9 数字拼接链、
* hint_ 四级提示、intro/tutorial 旧开场不再引用)。震动分工:滑动/点按的识别反馈由 gesture.js 内置,本层只发语义震动
* (error/success/warning/pickup)。
@@ -406,7 +412,14 @@ export class EscapeGame extends Game {
this._progressEvent();
this.audio.play('sfx/pickup');
vibrate('pickup');
describe('voice/found_key', false);
// H1(round4):钥匙到手句后接播铁柜房方位(下一目标 = cabinet (2,1))。
this._timer(() => {
if (s !== this._session) return;
this._playVoice('voice/found_key', {
skippable: false, // 关键获得不可跳
onComplete: () => this._playGoalDir(STAGES.cabinet.target),
});
}, 800); // 先让环境音进耳朵再播描述
} else {
describe('voice/empty_2');
}
@@ -441,9 +454,12 @@ export class EscapeGame extends Game {
// G4(round3):拾取反馈链尾接下一目标句「保险丝到手。接下来,去找电箱。」
// (游戏反馈层全档位,gd_economy 先例——走游戏语音链不走向导通道)——堵上
// P2-round2 P0「保险丝到手后目标悬空:没有新指令、没有下文」。
// H1(round4):目标句播完接播目标房间方位句(P3 唯一拒因:目标无方位)。
this._playVoice('voice/unlock_cabinet', {
skippable: false, // 关键获得不可跳
onComplete: () => this._playVoice('voice/gd_next_fuse'),
onComplete: () => this._playVoice('voice/gd_next_fuse', {
onComplete: () => this._playGoalDir(STAGES.box.target),
}),
});
} else if (x === 0 && y === 0 && this.hasFuse && !this.powerOn) {
// 保险丝修电箱 -> 恢复供电(保险丝消耗)
@@ -453,7 +469,11 @@ export class EscapeGame extends Game {
this.audio.play('sfx/spark');
this.audio.play('sfx/power_on', { volume: 0.8 });
vibrate('success');
this._playVoice('voice/fix_box', { skippable: false }); // 关键获得不可跳
// H1(round4):通电目标句后接播大门方位(下一目标 = gate (2,2))。
this._playVoice('voice/fix_box', {
skippable: false, // 关键获得不可跳
onComplete: () => this._playGoalDir(STAGES.gate.target),
});
} else if (x === 2 && y === 2 && this.powerOn) {
// 通电后开大门 -> 逃脱
this._escape();
@@ -483,6 +503,26 @@ export class EscapeGame extends Game {
return 'gate';
}
/**
* H1(round4):目标房间相对当前房间的方位句(gd_room_l/r/u/d 之一)。
* 方向语义用本游戏移动坐标系:up/down/left/right 滑动方向即方位
* (y 0北..2南、x 0西..2东,向上滑 dy=-1)。斜对角只播一句,轴序与
* _navVoices 一致(先东西后南北);目标在同一房间不播(返回 null)。
*/
_roomDirVoice([tx, ty]) {
const dx = tx - this.x;
const dy = ty - this.y;
if (dx === 0 && dy === 0) return null;
if (dx !== 0) return dx < 0 ? 'voice/gd_room_l' : 'voice/gd_room_r';
return dy < 0 ? 'voice/gd_room_u' : 'voice/gd_room_d';
}
/** H1:目标句播完后接播目标房间方位句(游戏反馈层全档位,gd_next_fuse 先例)。 */
_playGoalDir(target) {
const dir = this._roomDirVoice(target);
if (dir) this._playVoice(dir);
}
// ---------- 卡关检测与三级提示 ----------
_startStuckTimer() {
+190 -3
View File
@@ -25,6 +25,7 @@ const GAME_DIRS = {
SimonGame: path.join(ROOT, 'web', 'simon'),
RhythmGame: path.join(ROOT, 'web', 'rhythm'),
SpaceGame: path.join(ROOT, 'web', 'space'),
EscapeGame: path.join(ROOT, 'web', 'escape'),
};
let currentGameDir = GAME_DIRS.StorylineGame;
@@ -169,6 +170,7 @@ const { StorylineGame } = await import('../storyline/game.js');
const { SimonGame } = await import('../simon/game.js');
const { RhythmGame } = await import('../rhythm/game.js');
const { SpaceGame } = await import('../space/game.js');
const { EscapeGame } = await import('../escape/game.js');
// guide='full':本文件验证向导层行为(既有 playtest 以 guide='off' 验证零干扰)
settings.set({ guide: 'full', voice: 'female', speed: 1.0, bgm: 1.0 });
@@ -275,6 +277,12 @@ realLog('== ②/①b simon:首轮 turn cue(earcon 事件+confirm 震+your_tu
await advance(() => game.teachAwait && game.phase === 'tutorial');
await gesture(game, g);
}
// round4 H2/H4 前提锚定:announce 时点固定第 1 轮序列为无 up/doubletap 的两音
//(guide.test 的 Math.random 是真随机——不固定则 gd_relisten 断言会随占用预告
// 动态置空而偶发跳过;固定后本块行为与 round3 基线一致)
await advance(() => game.phase === 'announce');
game.sequence[0] = 'down';
game.sequence[1] = 'tap';
await advance(() => game.phase === 'input');
assert(game.round === 1, '教学走完进入第 1 轮 input');
@@ -283,12 +291,12 @@ 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
// ①b input 看门狗:round4 H4 首级 10s→5s,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(now - tInput === 5000, `H4:input 首级 5s 后 nudge 触发(实际 ${now - tInput}ms)`);
assert(events.some((e) => e.type === 'watchdog' && e.voiceId === 'gd_relisten'),
'input 10s 无输入发 gd_relisten 事件');
'input 5s 无输入发 gd_relisten 事件');
assert(voicePlays(playLog, 'voice/gd_relisten') === 1, 'gd_relisten 语音播放');
assert(game.chances === 1, '看门狗提示不消耗重听机会');
@@ -599,6 +607,185 @@ realLog('== ⑨ G7 space:loud 警报档(撞击前 500ms)同窗 dir_* 超
resetTimers();
}
// ================================================================ ⑩ H1~H5(round4)
realLog('== ⑩a H2/H3/H4 simon:占用预告三句、被占用轮无重听建议、5s 看门狗、余量句 ==');
async function driveSimonToAnnounce(rig) {
const { audio, game } = rig;
await audio.unlock();
game.start();
await advance(() => game.phase === 'tutorial' && game.teachAwait);
for (const g of ['up', 'down', 'left', 'right', 'tap', 'doubletap']) {
await advance(() => game.teachAwait && game.phase === 'tutorial');
await gesture(game, g);
}
await advance(() => game.phase === 'announce');
return game;
}
/** 测试钩子(13 号文档允许直改实例字段):固定当前轮序列;未指定位用无 up/dt 填充。 */
function fixSequence(game, recipe) {
const fill = ['down', 'right', 'tap', 'left'];
for (let i = 0; i < game.sequence.length; i++) {
game.sequence[i] = i < recipe.length ? recipe[i] : fill[i % fill.length];
}
}
{
const rig = makeRig(SimonGame);
const { playLog, game, events } = rig;
await driveSimonToAnnounce(rig);
// R1(含 up):序列播放完成后、「轮到你」提示前播 gd_occ_up
fixSequence(game, ['up', 'tap']);
await advance(() => game.phase === 'input');
assert(voicePlays(playLog, 'voice/gd_occ_up') === 1, 'H2:含 up 轮次播 gd_occ_up(序列播放完成后)');
const iOcc = playLog.findIndex((e) => e.id === 'voice/gd_occ_up');
const iTurn = playLog.findIndex((e) => e.id === 'voice/your_turn');
assert(iOcc > -1 && iTurn > iOcc, 'H2:占用预告在「轮到你」提示之前(插在轮次链中)');
// 被占用轮 input 看门狗:H4 首级 5s 只发 nudge,不播重听建议句(say 动态置空)
const tInput = now;
const rl0 = voicePlays(playLog, 'voice/gd_relisten');
const nb0 = events.filter((e) => e.type === 'nudge').length;
await advance(() => events.filter((e) => e.type === 'nudge').length > nb0);
assert(now - tInput === 5000, `H4:input 看门狗首级 5s(实际 ${now - tInput}ms)`);
assert(voicePlays(playLog, 'voice/gd_relisten') === rl0,
'H2:up 被占用轮看门狗只发 nudge earcon,不播重听建议句');
// 期望恰为 up 时上滑=作答(00 §5.3 豁免同款)→ 清第 1 轮
for (const g of game.sequence) await gesture(game, g);
await advance(() => game.phase === 'announce' && game.round === 2);
// R2(仅含 doubletap):gd_occ_dt;重听建议不受影响(建议只关联 up 占用)
fixSequence(game, ['down', 'doubletap', 'left']);
await advance(() => game.phase === 'input');
assert(voicePlays(playLog, 'voice/gd_occ_dt') === 1, 'H2:含 doubletap 轮次播 gd_occ_dt');
const t2 = now;
const rl1 = voicePlays(playLog, 'voice/gd_relisten');
const nb1 = events.filter((e) => e.type === 'nudge').length;
await advance(() => events.filter((e) => e.type === 'nudge').length > nb1);
assert(now - t2 === 5000, 'H4:看门狗首级恒 5s(升级级 25s 与级数不变)');
assert(voicePlays(playLog, 'voice/gd_relisten') === rl1 + 1,
'H2:仅 doubletap 占用不影响上滑重听建议');
for (const g of game.sequence) await gesture(game, g);
await advance(() => game.phase === 'announce' && game.round === 3);
// R3(up+doubletap 双占用):gd_occ_both;看门狗同样静默只 nudge
fixSequence(game, ['up', 'doubletap', 'left', 'right']);
await advance(() => game.phase === 'input');
assert(voicePlays(playLog, 'voice/gd_occ_both') === 1, 'H2:up+doubletap 双占用播 gd_occ_both');
const rl2 = voicePlays(playLog, 'voice/gd_relisten');
const nb2 = events.filter((e) => e.type === 'nudge').length;
await advance(() => events.filter((e) => e.type === 'nudge').length > nb2);
assert(voicePlays(playLog, 'voice/gd_relisten') === rl2, 'H2:双占用轮看门狗同样不播重听建议');
for (const g of game.sequence) await gesture(game, g);
await advance(() => game.phase === 'announce' && game.round === 4);
// H3:余量句与真实余量一致——第 1 次免费重听用掉(2→1)播 left1,第 2 次(1→0)播 left0
fixSequence(game, []); // 无占用填充轮
await advance(() => game.phase === 'input');
game.chances = 2; // 测试钩子:满额免费重听(初始 1 + 第 4 轮完成 +1,上限 2)
const rel0 = voicePlays(playLog, 'voice/relisten');
const l1_0 = voicePlays(playLog, 'voice/gd_chances_left1');
await gesture(game, 'up'); // 期望非 up → 主动重听①:2→1
await advance(() => voicePlays(playLog, 'voice/gd_chances_left1') === l1_0 + 1); // 余量句在 relisten 链尾
assert(game.chances === 1 && voicePlays(playLog, 'voice/gd_chances_left1') === l1_0 + 1,
'H3:第 1 次免费重听用掉播 gd_chances_left1(余量句与真实余量一致)');
assert(voicePlays(playLog, 'voice/relisten') === rel0 + 1, 'H3:重听照常播 relisten 并重播序列');
await advance(() => game.phase === 'input');
assert(game.inputIdx === 0, 'H3:重听后输入进度清零');
const l0_0 = voicePlays(playLog, 'voice/gd_chances_left0');
await gesture(game, 'up'); // 重听②:1→0
await advance(() => voicePlays(playLog, 'voice/gd_chances_left0') === l0_0 + 1);
assert(game.chances === 0 && voicePlays(playLog, 'voice/gd_chances_left0') === l0_0 + 1,
'H3:第 2 次免费重听用掉播 gd_chances_left0(「用完了」只在真实余量为 0 时出现)');
await advance(() => game.phase === 'input');
const co0 = voicePlays(playLog, 'voice/chances_out');
await gesture(game, 'up'); // 余 0 再求重听:拒绝路径(机会账)行为不变
assert(game.phase === 'input' && voicePlays(playLog, 'voice/chances_out') === co0 + 1,
'H3:余 0 再求重听仍拒绝并播 chances_out(答错救援的机会账路径不变)');
while (game.phase === 'input') await gesture(game, game.sequence[game.inputIdx]);
game.stop();
resetTimers();
}
realLog('== ⑩b H5 rhythm:教学应答窗口无任何尝试 → gd_miss_none(none 优先级最高)==');
{
const rig = makeRig(RhythmGame);
const { audio, playLog, game } = rig;
await audio.unlock();
game.start();
await advance(() => audio.isVoicePlaying()); // welcome 开播
await gesture(game, 'tap'); // 播放中单击 = 跳过 → 教学 T1
await advance(() => game.phase === 'tut' && game._cues.length > 0);
// T1 整句不出手(窗口关闭时该音未判定)→ 首次失败归因 gd_miss_none
await advance(() => voicePlays(playLog, 'voice/gd_miss_none') === 1);
assert(game._tutFails === 1, '前提:T1 首次失败(整句不出手)');
assert(voicePlays(playLog, 'voice/gd_miss_none') === 1, 'H5:无尝试播 gd_miss_none(这拍没接)');
assert(voicePlays(playLog, 'voice/gd_miss_late') === 0 && voicePlays(playLog, 'voice/gd_miss_wrong') === 0,
'H5:没出手不再被归因为「慢了半拍」(归因真话律:宁可粗不可假)');
game.stop();
resetTimers();
}
realLog('== ⑩c H1 escape:目标句后按房间图播 gd_room_*(同房间不播,移动坐标系)==');
{
// 方向映射按图校验(直连单元断言)
const rig0 = makeRig(EscapeGame);
const { game: g0 } = rig0;
await rig0.audio.unlock();
g0.x = 1; g0.y = 1;
assert(g0._roomDirVoice([0, 1]) === 'voice/gd_room_l', 'H1:目标在西 → gd_room_l');
assert(g0._roomDirVoice([2, 1]) === 'voice/gd_room_r', 'H1:目标在东 → gd_room_r');
assert(g0._roomDirVoice([1, 0]) === 'voice/gd_room_u', 'H1:目标在北(上面的房间)→ gd_room_u');
assert(g0._roomDirVoice([1, 2]) === 'voice/gd_room_d', 'H1:目标在南(下面的房间)→ gd_room_d');
assert(g0._roomDirVoice([1, 1]) === null, 'H1:目标在同一房间不播');
g0.stop();
resetTimers();
// 三处目标句接线点:found_key → cabinet(2,1);gd_next_fuse → box(0,0);fix_box → gate(2,2)
const rig = makeRig(EscapeGame);
const { audio, playLog, game } = rig;
storageData.set('soundgame.escape.guideDone', '1'); // 二周目直达 REOPEN→PLAY
await audio.unlock();
game.start();
await advance(() => game.phase === 'REOPEN');
await advance(() => !audio.isVoicePlaying()); // reopen_prompt 播完
await gesture(game, 'tap'); // 直接开始 → PLAY (1,1)
await advance(() => game.phase === 'PLAY' && !audio.isVoicePlaying());
assert(game.x === 1 && game.y === 1 && !game.hasKey, '前提:二周目 PLAY 起点、身上无钥匙');
// ① found_key:储物间拾钥匙 → 铁柜房在东(右面的房间)
await gesture(game, 'left'); // (0,1)
await gesture(game, 'down'); // (0,2) 储物间(首到播 room_storage)
await advance(() => !audio.isVoicePlaying()); // 房间描述播完再单击(否则单击=跳句)
await gesture(game, 'tap'); // 检查 → found_key(800ms 后)→ 方位句
await advance(() => playLog.some((e) => e.id === 'voice/gd_room_r'));
const ifk = playLog.findIndex((e) => e.id === 'voice/found_key');
assert(ifk > -1 && playLog.findIndex((e) => e.id === 'voice/gd_room_r') > ifk,
'H1:found_key 后接播 gd_room_r(目标铁柜房按图在东)');
// ② gd_next_fuse:开柜得保险丝 → 电箱房在西(左面的房间)
await gesture(game, 'right'); await gesture(game, 'right'); // (1,2) (2,2)
await gesture(game, 'up'); // (2,1) 铁柜房
await gesture(game, 'doubletap'); // 开柜
await advance(() => playLog.some((e) => e.id === 'voice/gd_room_l'));
const inf = playLog.findIndex((e) => e.id === 'voice/gd_next_fuse');
assert(inf > -1 && playLog.findIndex((e) => e.id === 'voice/gd_room_l') > inf,
'H1:gd_next_fuse 后接播 gd_room_l(目标电箱房按图在西)');
// ③ fix_box:修电箱通电 → 大门在东(右面的房间)
await gesture(game, 'left'); await gesture(game, 'left'); // (1,1) (0,1)
await gesture(game, 'up'); // (0,0) 电箱房
await gesture(game, 'doubletap'); // 修电箱
await advance(() => playLog.filter((e) => e.id === 'voice/gd_room_r').length >= 2);
const ifx = playLog.findIndex((e) => e.id === 'voice/fix_box');
assert(ifx > -1 && playLog.map((e) => e.id).lastIndexOf('voice/gd_room_r') > ifx,
'H1:fix_box 后接播 gd_room_r(目标大门按图在东)');
assert(game.powerOn, '前提:电箱已通电(接线点均在真实解谜链上)');
game.stop();
resetTimers();
}
// ---------------------------------------------------------------- 汇总
realLog(`\n通过 ${passCount} 条断言,失败 ${failures.length} 条`);
+21 -8
View File
@@ -1,7 +1,8 @@
/**
* 《暗室》新手会话剧本:老玩家二周目不算——这里清空过进度走首局引导(G0-G7),
* 每步听清再做;进 PLAY 后先连撞两次墙(触发向导提前一级的卡关提示),
* 然后找到储物间拿钥匙、去铁柜房开柜(解到第二间),收工。
* 然后找到铁柜房开柜拿保险丝、照 round4 H1 的目标方位句跨四个房间修电箱、
* 再摸到大门收工(目标句后 3s 内都能听到方位句)。
*/
export function describe(game) {
switch (game.phase) {
@@ -66,11 +67,23 @@ export async function session(ctx, mock) {
await move('right', '往东走,风声换了个方向');
await move('right');
await move('down', '到了铁柜房,柜子锁着');
await ctx.gesture('doubletap'); // 用钥匙 → unlock_cabinet + gd_next_fuse(round3 G4)
await ctx.waitVirtual(4200);
ctx.status('柜子开了,听到「保险丝到手。接下来,去找电箱」——目标又有了');
// round3 G4 剧本:拿到保险丝后再走两步(朝西边的电箱房方向),目标链不断
await move('left', '照新目标往西摸');
await move('left', '再走一步,西边隐隐有电流声——电箱在那边');
ctx.status('解到第二间、照下一目标句又摸了两步,收工');
// 用钥匙 → unlock_cabinet + gd_next_fuse + gd_room_l(round4 H1:目标句带方位,
// 从铁柜房 (2,1) 看电箱房 (0,0) 在西 = 左面的房间)
await ctx.gesture('doubletap');
await ctx.waitVirtual(5600);
ctx.status('柜子开了。「保险丝到手。接下来,去找电箱。」后面还跟了一句「左面的房间。」——这回知道该往哪边摸了');
// round4 H1 剧本:照方位句跨房间去电箱房——(2,1) → 左×2 = (0,1) → 上 = (0,0)
await move('left', '照方位句往左(西)摸');
await move('left');
await move('up', '电流声越来越清晰——电箱房到了');
// 修电箱 → fix_box + gd_room_r(round4 H1:从 (0,0) 看大门 (2,2) 在东 = 右面的房间)
await ctx.gesture('doubletap');
await ctx.waitVirtual(5600);
ctx.status('电箱修好了。目标句后面照样跟着方位——「右面的房间。」大门在东边');
// 照方位句多跨两个房间摸向大门:(0,0) → 右×2 = (2,0) → 下×2 = (2,2)
await move('right', '照方位句往右(东)摸');
await move('right');
await move('down');
await move('down', '脚步声停下来——面前这间就是大门');
ctx.status('解到通电、照目标句的方位跨了四个房间摸到大门,收工');
}
+7 -5
View File
@@ -1,10 +1,11 @@
/**
* 《节拍行者》新手会话剧本:首局完整走教学 T1-T3(第 2 句故意连错两遍,
* 《节拍行者》新手会话剧本:首局完整走教学 T1-T3(第 2 句先整拍不出手——
* round4 H5 归因真话律:听到的是「这拍没接」而不是「慢了半拍」;再连错一遍
* 触发 round3 G2 的「降速 25% 重演 + gd_slow」,跟着慢速领奏做对后恢复正常)
* → 曲目一(90 BPM);曲中下滑呼出菜单逛一圈再恢复(round3 G3:恢复先听
* gd_count「四拍之后,继续」,再跟 4 拍底鼓 count-in 进场;首个应答窗口前
* 整整 1 小节有 gd_answer+turn 交接标记);第一乐句故意整句漏掉(连 2 Miss
* 触发向导「慢了半拍」提示),之后跟着领奏把一曲打完到结算。
* 整整 1 小节有 gd_answer+turn 交接标记);正片第一乐句故意整句漏掉(连 2 Miss
* 触发向导 gd_miss「慢了半拍」——正片规则不变),之后跟着领奏把一曲打完到结算。
*/
export function describe(game) {
switch (game.phase) {
@@ -37,11 +38,12 @@ export async function session(ctx, mock) {
await ctx.waitCond(() => game.phase === 'tut' && game._cues.length > 0 && game._tutAttempts.length === 0);
ctx.status(`教学第 ${step + 1}/3 句`);
if (step === 1) {
// round3 G2 剧本:这一句故意连错两遍(整句不出手),听它放慢一些再带着打。
// round4 H5 剧本:这一句先整拍不出手——归因必须是「这拍没接」(gd_miss_none),
// 不是「慢了半拍」;再连错一遍(整句不出手)听它放慢一些再带着打(G2)。
// 注意 _tutEvaluate 判定即把 _phaseEnd 置 Infinity 防重入——等待一律挂
// 「持久里程碑」条件(_tutFails/_tutSlowUsed),别挂阶段末时刻。
await ctx.waitCond(() => game._tutFails === 1); // 失败①:没出手
ctx.status('第一遍没敢出手,先听它归因');
ctx.status('第一遍整拍没出手——听到的是「这拍没接。我停下的时候,就轮到你了」,不是「慢了半拍」');
await ctx.waitCond(() => game._phaseEnd < 1e8 && t() < game._phaseEnd - 1); // 纠正链播完、重新领奏
ctx.status('又重新领奏了一遍,还是没敢出手');
await ctx.waitCond(() => game._tutSlowUsed === true); // 失败② → 触发降速重演
+104 -63
View File
@@ -1,10 +1,12 @@
/**
* 《音序回响》新手会话剧本:教学六音一步一步学(中间故意做错一次听纠错)、
* 收尾听完机会经济句(round3 G5 重录口径:记不住就向上滑重新听);第 1 轮
* 直接答对(途中有双击音就演示「双击=作答」);第 2 轮开局忘序列僵住 10 秒,
* 听到向导「向上滑可以重新听」提示后照做——用掉一次免费重听(round3 G5:
* input 双击永远=作答,重听改绑上滑);第 3 轮凭记忆答对,在**整轮边界**收工
* (round2 剧本修正:不在轮中退出)。
* 《音序回响》新手会话剧本(round4):教学六音一步一步学(中间故意做错一次听纠错)、
* 收尾听完机会经济句;对局段制造 round4 H 组断言点——
* - 含 up 的轮次(R1):序列播完先听占用预告 gd_occ_up,僵 5s 验证被占用轮看门狗
* 只发 nudge 不建议重听(H2),期望恰为 up 时上滑=作答;
* - 含 doubletap 的轮次(R2)与 up+doubletap 双占用轮(R6):gd_occ_dt / gd_occ_both;
* - 无尝试超时(R5/R7):干净轮僵 5s → 看门狗建议上滑重听 → 用掉两次免费重听,
* 第 1 次听到「免费重听还剩一次」(H3:余量句与真实余量一致——第 4 轮完成机会
* +1 到 2 后才重听),第 2 次听到「免费重听用完了」;整轮边界收工。
*/
export function describe(game) {
const GN = { up: '上滑', down: '下滑', left: '左滑', right: '右滑', tap: '单击', doubletap: '双击' };
@@ -16,7 +18,13 @@ 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 'input': {
const occ = [];
if (game.sequence.includes('up')) occ.push('上滑');
if (game.sequence.includes('doubletap')) occ.push('双击');
const occNote = occ.length ? `(这轮${occ.join('和')}是作答,占用了)` : '';
return `第 ${game.round} 轮轮到你了:已对上 ${game.inputIdx}/${game.sequence.length},照原顺序做手势${occNote}(要重听就向上滑)`;
}
case 'roundend': return '刚答完一轮,播报中';
case 'gameover': return `游戏结束,完成 ${game.round - 1} 轮。单击再来一局,下滑开菜单`;
case 'menu': return '菜单里:单击切选项,双击确认,上滑听规则';
@@ -39,69 +47,102 @@ export async function session(ctx, mock) {
}
await ctx.gesture(G[i]); // 照指令做对
if (i === G.length - 1) {
// 收尾链(t7 两句 + 机会经济句)播完直达对局——一到 input 就接手,
// 收尾链(t7 两句 + 机会经济句)播完直达对局——一到 announce 就接手,
// 不裸 waitIdle(会把 input 的看门狗窗口整段空烧掉,round1 剧本失真根源)
await ctx.waitCond(() => game.phase === 'playback' || game.phase === 'input'
|| game.phase === 'gameover');
await ctx.waitCond(() => game.phase === 'announce' || game.phase === 'playback'
|| game.phase === 'input' || game.phase === 'gameover');
} else {
await ctx.waitIdle();
}
}
ctx.status('教学讲完了:免费重听两次、用完扣机会,想重听就向上滑');
// 正式对局:完整打完第 3 轮,在轮边界收工
let relistenDone = false;
while (mock.now < 12 * 60 * 1000 && game.phase !== 'gameover') {
if (game.phase === 'input') {
// 第 1 轮开局先失忆僵 10 秒(还没动过手,input 看门狗在挂):听向导
// 「向上滑可以重新听一遍序列」,照做——round3 G5:重听已改绑上滑。
// 测试钩子把序列摆成无 up 的两音,保证上滑=重听不被「期望恰为 up」豁免干扰。
if (game.round === 1 && !relistenDone && game.inputIdx === 0) {
relistenDone = true;
if (game.sequence.includes('up')) { game.sequence[0] = 'left'; game.sequence[1] = 'tap'; }
ctx.status('第 1 轮有点懵,先僵一会儿');
ctx.silence(10, '想不起来序列了');
// 10s 看门狗到点:nudge + gd_relisten。直接等提示语音开播(input 态无
// BGM、无别的语音,voiceBusy=提示在播;别用「等满 10.6s」——定时刻度会
// 恰好迈过语音结束点,等成第二次升级)
await ctx.waitCond(() => ctx.voiceBusy());
ctx.status('听到提示:需要的话,向上滑可以重新听一遍序列');
ctx.note('照提示向上滑,用掉一次免费重听');
await ctx.gesture('up');
await ctx.waitCond(() => game.phase === 'playback' || game.phase === 'input');
continue;
}
// 第 2 轮开头演示「期望恰为 up 时上滑=作答」(00 §5.3 豁免同款口径)
if (game.round === 2 && !game.__upDemoDone && game.inputIdx === 0) {
game.__upDemoDone = true;
game.sequence[0] = 'up';
ctx.status('这个是上行音——上滑作答(期望恰为 up 时上滑=作答)');
await ctx.gesture('up');
continue;
}
if (game.round === 3 && game.inputIdx === game.sequence.length - 1) {
// round3 G5 演示钩子(13 号文档允许直读/改实例字段):把最后一个期望音
// 摆成 doubletap,保证转写演示到「input 双击永远=作答」
if (game.sequence[game.inputIdx] !== 'doubletap' && !game.__dtDemoDone) {
game.__dtDemoDone = true;
game.sequence[game.inputIdx] = 'doubletap';
}
ctx.status(game.sequence[game.inputIdx] === 'doubletap'
? '这个是双击音——双击作答(round3:input 双击永远=作答)'
: '最后一轮最后一个音,打完这轮就收');
} else {
ctx.status('轮到我复现了');
}
await ctx.gesture(game.sequence[game.inputIdx]);
continue;
// 测试钩子(13 号文档允许直改实例字段):每轮报数时点把序列摆成剧本需要的
// 形状——占用音(up/doubletap)摆进指定轮,其余用不含占用音的手势补齐。
const fixSeq = (highlights) => {
const fill = ['down', 'right', 'tap', 'left'];
for (let i = 0; i < game.sequence.length; i++) {
game.sequence[i] = i < highlights.length ? highlights[i] : fill[i % fill.length];
}
// 整轮边界收尾:第 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);
}
};
const waitAnnounce = () => ctx.waitCond(() => game.phase === 'announce' || game.phase === 'gameover');
const answerRound = async () => {
await ctx.waitCond(() => game.phase === 'input' || game.phase === 'gameover');
while (game.phase === 'input') await ctx.gesture(game.sequence[game.inputIdx]);
};
// 「无尝试超时」剧本点(干净轮):僵到 input 看门狗 5s 首级的提示语音开播
//(input 态无 BGM 无别的语音,voiceBusy=提示在播;别盲等固定时长——harness 的
// advance 没有时间上限,会恰好迈过提示语音结束点、一路空烧到 25s 升级)
const stallForAdvice = async (why) => {
ctx.silence(5, why);
await ctx.waitCond(() => ctx.voiceBusy());
};
ctx.status(`收工:完整打完 ${game.round} 轮,在轮与轮的边界停手(免费重听剩 ${game.chances} 次)`);
// R1:含 up 的轮次——占用预告 + 被占用轮看门狗只 nudge(H2)+ 上滑恰为作答
await waitAnnounce();
fixSeq(['up', 'tap']);
await ctx.waitCond(() => game.phase === 'input');
ctx.status('序列一播完就听到「这一轮,向上滑是作答」——重听键这轮被占了');
ctx.silence(5, '第一轮先不动手,看看它催不催');
// 被占用轮 5s 级没有提示语音可等:僵到 25s 升级(turn+confirm 震)作为里程碑
await ctx.waitVirtual(25600);
ctx.note('5 秒一声轻提醒、25 秒又一声加确认震——两级提醒都没建议「向上滑重听」,它知道这轮上滑是作答');
ctx.status('先来一发上滑——期望恰为 up 时上滑就是作答');
await answerRound();
// R2:含 doubletap 的轮次——占用预告 dt(重听建议不受影响,不僵)
await waitAnnounce();
fixSeq(['down', 'doubletap', 'left']);
await ctx.waitCond(() => game.phase === 'input');
ctx.status('这轮预告的是「双击是作答」');
await answerRound();
// R3/R4:无占用轮直答(第 4 轮完成 → 机会 +1,免费重听凑满两次)
await waitAnnounce();
fixSeq([]);
await ctx.waitCond(() => game.phase === 'input');
ctx.status('这轮开场没有占用预告——上滑还能当重听键用');
await answerRound();
await waitAnnounce();
fixSeq([]);
await ctx.waitCond(() => game.phase === 'input');
ctx.status('机会加一之后,免费重听真的有两次了');
await answerRound();
// R5:无尝试超时 + 第 1 次免费重听(余量如实报「还剩一次」,H3)
await waitAnnounce();
fixSeq([]);
await ctx.waitCond(() => game.phase === 'input');
ctx.status('第 5 轮序列有点长,先不动手');
await stallForAdvice('想不起来序列了');
ctx.status('5 秒就来了提示:需要的话,向上滑可以重新听一遍序列');
ctx.note('照提示向上滑——第 1 次免费重听用掉,跟着听到「免费重听还剩一次」');
await ctx.gesture('up');
await ctx.waitCond(() => game.phase === 'playback' || game.phase === 'input');
await answerRound();
// R6:up+doubletap 双占用轮——gd_occ_both + 被占用轮看门狗只 nudge
await waitAnnounce();
fixSeq(['up', 'doubletap']);
await ctx.waitCond(() => game.phase === 'input');
ctx.status('这轮说「向上滑和双击都是作答」——两个键都被占用了');
ctx.silence(5, '轮到我了,先心里过一遍序列');
await ctx.waitVirtual(25600); // 同 R1:两级提醒都只发 earcon/标记,无重听建议
ctx.note('又是两级提醒都没有重听建议——占用轮里那句建议自动闭嘴');
await answerRound();
// R7:无尝试超时 + 第 2 次免费重听(余量归 0,H3)
await waitAnnounce();
fixSeq([]);
await ctx.waitCond(() => game.phase === 'input');
await stallForAdvice('长序列,又想不起来了');
ctx.status('又是那句提示:向上滑可以重新听一遍序列');
ctx.note('向上滑——第 2 次免费重听用掉,这回听到「免费重听用完了。之后重听会花一次机会」');
await ctx.gesture('up');
await ctx.waitCond(() => game.phase === 'playback' || game.phase === 'input');
await answerRound();
// 轮边界收工
await ctx.waitCond(() => game.phase === 'roundend' || game.phase === 'gameover');
ctx.status(`收工:打完 ${game.round} 轮在轮边界停手(免费重听剩 ${game.chances} 次,账目和播的对上了)`);
}
+13 -3
View File
@@ -130,7 +130,11 @@
"menu_home": "回大厅。",
"menu_back": "返回游戏。",
"skip_hint": "单击可跳过旁白。",
"gd_next_fuse": "保险丝到手。接下来,去找电箱。"
"gd_next_fuse": "保险丝到手。接下来,去找电箱。",
"gd_room_l": "左面的房间。",
"gd_room_r": "右面的房间。",
"gd_room_u": "上面的房间。",
"gd_room_d": "下面的房间。"
},
"simon": {
"t0_welcome": "欢迎来到音序回响。先认识六种声音。",
@@ -205,7 +209,12 @@
"gd_relisten": "需要的话,向上滑可以重新听一遍序列。",
"gd_economy": "记不住就向上滑,重新听一遍。免费机会有两次,用完要扣一次机会。",
"gd_clear": "漂亮,过了。",
"gd_record": "破纪录了。"
"gd_record": "破纪录了。",
"gd_occ_up": "这一轮,向上滑是作答。",
"gd_occ_dt": "这一轮,双击是作答。",
"gd_occ_both": "这一轮,向上滑和双击都是作答。",
"gd_chances_left1": "免费重听还剩一次。",
"gd_chances_left0": "免费重听用完了。之后重听会花一次机会,别怕,慢慢来。"
},
"rhythm": {
"welcome": "欢迎来到节拍行者。跟着音乐,我奏一句,你跟一句。",
@@ -246,7 +255,8 @@
"gd_miss_late": "慢了半拍。跟着节拍走。",
"gd_miss_wrong": "方向不对。跟着音色滑。",
"gd_slow": "我放慢一些,跟着试试。",
"gd_count": "四拍之后,继续。"
"gd_count": "四拍之后,继续。",
"gd_miss_none": "这拍没接。我停下的时候,就轮到你了。"
},
"space": {
"drill_welcome": "欢迎登上盲航。你只有一双耳朵。",
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+16 -6
View File
@@ -35,12 +35,14 @@
* main 演奏阶段单击是演奏手势不跳(mid_song 短句按裁定不跳)。
* 长按快进豁免(12 文档 §2.2:main 演奏期):audio.setRateBoostEnabled(false)
* per-game 开关(引擎已支持),进 main 关、进菜单/结算/退出恢复。
* 向导层(docs/design/14-guide-layer.md + round2 synthesis F2.1/F3 + round3 G1~G3):
* 向导层(docs/design/14-guide-layer.md + round2 synthesis F2.1/F3 + round3 G1~G3
* + round4 H5):
* 每曲首次领奏→应答切换在谱面定时队列直接落 gd_answer(不走 guide queue,保证
* 窗口前听到)+ turn earcon;round3 G1 修正排程点:落在首个应答窗口**前整整
* 1 小节**(= 领奏小节头;round2 排在窗口起点本身,P4 实测窗口开了才听到),
* 菜单恢复对下一个未判定窗口重排同款标记;教学 Miss 首次即归因分档
* (gd_miss_early/late/wrong,挂纠正链尾空闲点);连续 2 Miss 播 gd_miss(queue
* (round4 H5 四档:无窗内尝试 gd_miss_none 优先、真出手再分 early/late/wrong,
* 挂纠正链尾空闲点;正片连 2 Miss 仍 gd_miss);连续 2 Miss 播 gd_miss(queue
* 插播,同曲一次);暂停断点续播前插 4 拍 count-in(round3 G3:底鼓 sfx 层可辨
* 音量 + full 档 gd_count 每局恢复限 1 次 + beat 震;round2 版因 push 后未重排
* _sched 被队头卡死从未按时响过;off 档保持即时续播);教学 DDA(round3 G2):
@@ -388,14 +390,22 @@ export class RhythmGame extends Game {
});
return;
}
// F2.1(round2):教学 Miss 首次即归因、按失败原因分档——方向错 gd_miss_wrong /
// 抢拍 gd_miss_early / 慢拍 gd_miss_late(无在窗内尝试=没跟上拍,归入慢拍)。
// F2.1(round2)+ H5(round4):教学 Miss 首次即归因、按失败原因分档。
// H5 归因真话律:应答窗口无任何尝试(窗口关闭时该音未判定——出窗的自由
// 演奏不算尝试)优先播 gd_miss_none「这拍没接」,早/晚/错分档只保留给
// 真出手的情况(P1 round3:没出手被报「慢了半拍」是归因说假话——"宁可粗
// 不可假";正片连 2 Miss 仍走 gd_miss,本档只改教学归因)。
// 向导句(full 档)挂在纠正链播完后的空闲点发:idle-only 语义下链中插播会被
// 跳过;之后 _tutLead 只排 sfx,不与归因句抢 voice 通道。
const noShow = this._cues.some((cue) => {
const a = this._tutAttempts.find((x) => x.cue === cue);
return !a || Math.abs(a.dt) > GOOD; // 窗内无尝试 = 该音没接
});
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';
const missGrade = noShow ? 'gd_miss_none' // none 优先级最高
: 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';
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+57 -18
View File
@@ -26,6 +26,14 @@
* 答题输入的 00 §5.3 豁免同款)。非输入阶段行为不变(gameover/菜单上滑仍是规则/
* 帮助)。同一手势在 input 阶段不再有第二种含义(P1 round2 P0 根治)。
*
* round4(docs/design/14-guide-layer.md「round4 v1.3」,synthesis H2~H4):
* - H2 占用手势预告:每轮序列播放完成后、「轮到你」提示前,序列含 up/doubletap
* 则播 gd_occ_up/dt/both(游戏反馈层全档位,插在轮次链中);被占用轮次 input
* 看门狗只发 nudge earcon 不播重听建议句(say 动态置空)。
* - H3 重听账目真实:主动重听使用时如实报余量(2→1 播 gd_chances_left1、
* 1→0 播 gd_chances_left0,替换该路径旧「用完了」绝对句,全档位)。
* - H4 input 看门狗首级 10s→5s(升级级 25s 与次数不变;非 input 阶段不动)。
*
* 手势 → 音色映射:
* up 上行琶音 / down 下行低音 / left 左声道弹拨 / right 右声道弹拨 /
* tap 底鼓 / doubletap 双连响
@@ -66,10 +74,11 @@ const TEACH_BLUNT = {
const BEST_KEY = 'soundgame.simon.best';
const AUTH_KEY = 'soundgame.auth'; // 大厅登录写入(见 web/index.html)
// 向导层(14 号文档 + round2 synthesis F1.2):input 阶段静默看门狗——10s 轻提醒
// +gd_relisten(不改变重听扣机会规则);25s 升级取消自动重播序列(P1:泄题且与
// 扣机会规则矛盾)→ 改为 confirm 震 + turn earcon(纯标记,无语音)
const GUIDE_INPUT_WATCHDOG_AT = [10000, 25000];
// 向导层(14 号文档 + round2 synthesis F1.2 + round4 H4):input 阶段静默看门狗
// ——首级 5s 轻提醒 +gd_relisten(H4 round4:10s→5s,P4「10 秒干等」;升级级
// 不变、次数不变);25s 升级取消自动重播序列(P1:泄题且与扣机会规则矛盾)→
// 改为 confirm 震 + turn earcon(纯标记,无语音)
const GUIDE_INPUT_WATCHDOG_AT = [5000, 25000];
export class SimonGame extends Game {
constructor(audio, root) {
@@ -321,13 +330,15 @@ export class SimonGame extends Game {
if (s !== this._session || this.phase !== 'playback') return;
vibrate('confirm'); // "轮到你了"
this.guide.turn({}); // turn earcon 每轮泛化(第 1 轮另有 your_turn 语音,下行)
if (this.round === 1) {
this.audio.play('voice/your_turn', {
onComplete: () => {
if (s === this._session && this.phase === 'playback') this._toInput();
},
});
this._timer(() => { if (this.phase === 'playback') this._toInput(); }, 2500); // 兜底
// H2(round4):序列播放完成后、「轮到你」提示前,按本轮序列播占用预告
//(gd_occ_up/dt/both,游戏反馈层全档位,插在轮次链中)——重听键与答案音
// 共用词表的结构性冲突,换手势无解,改为轮次开场讲清判据(P1#1+P2)。
const ids = this._occupancyVoices();
if (this.round === 1) ids.push('voice/your_turn');
if (ids.length) {
this._speakGuarded(ids, () => {
if (s === this._session && this.phase === 'playback') this._toInput();
}, 2500 * ids.length); // 语音缺失兜底:每句 2.5s
} else {
this._toInput();
}
@@ -337,17 +348,33 @@ export class SimonGame extends Game {
step();
}
/**
* H2(round4):本轮占用预告句。序列含 up → gd_occ_up、含 doubletap →
* gd_occ_dt、两者皆有 → gd_occ_both;都不含则不播(空链直达 input)。
*/
_occupancyVoices() {
const hasUp = this.sequence.includes('up');
const hasDt = this.sequence.includes('doubletap');
if (hasUp && hasDt) return ['voice/gd_occ_both'];
if (hasUp) return ['voice/gd_occ_up'];
if (hasDt) return ['voice/gd_occ_dt'];
return [];
}
_toInput() {
this.phase = 'input';
this.inputIdx = 0;
// 向导层静默看门狗(round2 F1.2 礼仪重构):input 阶段无 BGM 无超时,挂机
// 永久静默。10s → nudge + gd_relisten 提示(不改变重听扣机会规则);25s 升级
// **取消免扣机会自动重播序列**(P1:等答案自动重念,双击重听的经济就矛盾了,
// 还顺手拆了记忆难度)→ 只发 confirm 震 + turn earcon(纯标记,无语音)。
// 向导层静默看门狗(round2 F1.2 礼仪重构 + round4 H4/H2):input 阶段无 BGM
// 无超时,挂机永久静默。5s → nudge + gd_relisten 提示(H4:首级 10s→5s,
// P4「10 秒干等」;升级级 25s 与级数不变);25s 升级**取消免扣机会自动重播
// 序列**(P1:等答案自动重念,双击重听的经济就矛盾了,还顺手拆了记忆难度)
// → 只发 confirm 震 + turn earcon(纯标记,无语音)。
// H2:本轮 up 已被占用为作答时,重听建议句不再建议上滑(实现从简:被占用
// 轮次看门狗只发 nudge earcon 不播重听建议句)。
this.guide.watchdog({
arm: true,
at: GUIDE_INPUT_WATCHDOG_AT,
say: ['gd_relisten', null],
say: [this.sequence.includes('up') ? null : 'gd_relisten', null],
onEscalate: (n) => { if (n === 2) this.guide.turn({ haptic: true }); },
cond: () => this.phase === 'input',
});
@@ -371,7 +398,18 @@ export class SimonGame extends Game {
if (this.inputIdx >= this.sequence.length) this._roundClear();
}
/** 主动重听:消耗一次机会,重播当前序列(进度清零重输,序列不变)。 */
/**
* 主动重听:消耗一次机会,重播当前序列(进度清零重输,序列不变)。
* H3(round4):使用时如实报余量——免费重听第 1 次用掉(余 1)播
* gd_chances_left1「免费重听还剩一次。」、第 2 次用掉(余 0)播
* gd_chances_left0「免费重听用完了。之后重听会花一次机会,别怕,慢慢来。」
* (游戏档全级别,替换本路径旧「用完了」绝对句;P1:教学说两次、用一次即报
* 用完了——账目不可知)。账目诊断:教学/演练/看门狗均无隐性扣减(watchdog
* 提示不消耗机会,教学期不触 chances),缺口在「免费两次」中的第二次要打完
* 第 4 轮才发放(每 4 轮 +1、上限 2)——「用完了」因此只在真实余量为 0 时出现。
* 答错自动消耗路径(retry_use + chances_out)与 0 次时再求重听的拒绝路径
* (chances_out)是「答错可以消耗机会重听」的机会账,语义如实、维持不变。
*/
_relisten() {
if (this.chances <= 0) {
vibrate('error');
@@ -381,7 +419,8 @@ export class SimonGame extends Game {
this.chances--;
vibrate('pickup');
const ids = ['voice/relisten'];
if (this.chances === 0) ids.push('voice/chances_out'); // 机会归 0 提示
if (this.chances === 1) ids.push('voice/gd_chances_left1'); // 余 1:还剩一次
else if (this.chances === 0) ids.push('voice/gd_chances_left0'); // 余 0:用完了
const s = this._session;
this.phase = 'roundend'; // 重听播报期间暂停接收输入
this._speakGuarded(ids, () => { if (s === this._session) this._playSequence(); });