Files
SoundGame/docs/design/14-guide-layer.md
T

187 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 14 · 向导交互层设计裁定(v1 基线)
> 依据:research/02-methodology/methodology.md §3(L3 状态层 / L4 引导层)→
> research/03-schemes/schemes.md §1/§2(实施蓝图,唯一事实源)。
> 本文存档 v1 已落地的裁定与新增语音资产台词表,供后续轮次回溯。
> 实现载体:`web/engine/guide.js`(引擎向导模块)+ 各游戏 game.js 接线。
## 1. 裁定摘要
| # | 裁定 | 内容 |
|---|---|---|
| 1 | 分期 | v1 只做「全局基线」= 引擎向导层 + 逐游戏接线;场景参数包(A 通勤/B 摸鱼/C 睡前/D 下班)与人设旋钮在试玩验证后分期(schemes §3)。 |
| 2 | 等级 | `settings.guide`:`'full'`(默认,earcon+语音+震动)/ `'standard'`(earcon+震动,无语音)/ `'off'`(全静 no-op,**不挂任何定时器**)。大厅设置面板「向导帮助:详细/只听提示音/关闭」。 |
| 3 | earcon | 8 个签名(6 状态 menu/explore/battle/choice/settle/danger + 2 功能 turn/nudge)**全项目统一**,全部运行时 WebAudio 合成(零资产、可复现);总长 0.25~0.4s、单音 ≥0.08s、125Hz~5kHz、总增益 ≈0.12;家族=波形音色、子类=节奏轮廓(schemes §1.4)。 |
| 4 | 语音纪律 | 向导插播语音一律 `queue:true`(等当前对白播完),**绝不顶掉正在播的内容**;抉择提示(storyline p*)播完即可选的等待点不插任何语音。 |
| 5 | 看门狗 | 统一模板:静默 N 秒 → nudge 轻 earcon(+ full 档语音)→ 二次升级播上下文提示 → 级数用尽自动停止;任何手势/向导动作复位。escape 的三级卡关骨架保留,仅加「连续撞墙 ≥2 次 → full 档阈值提前一级」。 |
| 6 | 引擎配套 | `audio.isVoicePlaying()` 公开只读 API(收编 5 处私有 `_voice` 读);`audio.setRateBoostEnabled(bool)` 长按快进 per-game 开关(rhythm/space 对局期豁免);`settings.speed` 增加 2.2 档(视障档,250+ WPM)。 |
| 7 | 语义分工铁律 | 语音说「你要做什么」、earcon 说「哪类事/哪个状态」、BGM 说「宏观状态」、震动说「刚才那下对不对」——向导层不越界(L2 铁律:earcon 只做粗标记,语义靠语音)。 |
## 2. 逐游戏接线裁定(v1 落地范围)
| 游戏 | 接线 |
|---|---|
| storyline | W1/W3 静默看门狗 [8s]=nudge+`gd_continue`、[20s]=复用 `playHelpAndResume`;选择节点外左右滑的静默 return 处加 error 震+`gd_invalid`;抉择态(p* 播完)不插语音不挂看门狗;状态签名 explore/menu 与状态 BGM 同点。 |
| simon | input 看门狗 [10s]=nudge+`gd_relisten`、[25s]=免扣机会自动重听(每局 2 次);`your_turn` 泛化为 turn earcon(每轮,第 1 轮保留原语音);状态签名 settle/menu。 |
| escape | REOPEN 提示播完与引导步解锁给 turn cue;连续撞墙 ≥2 次 → full 档卡关三级阈值提前一级;其余(wrong/blunt/stuck/nav)保持既有完备体系。 |
| rhythm | 每曲首次领奏→应答切换推 `gd_answer`(谱面排程处入 `_timed`);连续 2 Miss 播 `gd_miss`(queue 插播,同曲一次);结算链换 `_speakGuarded`;main 演奏期 `setRateBoostEnabled(false)`;状态签名 menu/battle/settle。 |
| space | 对局第 1 次受击(保护期外)追加 `gd_hit_hint`;结算链换 `_speakGuarded`;对局期 `setRateBoostEnabled(false)`;状态签名 battle/menu/settle。 |
| rpg | 格挡窗口内做错手势 → error 震+`gd_parry_miss`(每局一次);PLAYER_SELECT 挂看门狗 [60s]=nudge+`gd_stall`;状态签名 explore/battle/menu/settle 与四状态 BGM 同点补齐。 |
## 3. 新增语音资产台词表(8 句 ×2 性别 = 16 文件)
定稿依据 schemes.md §1.5,台词全文一字不改;speaker/instruct 沿用各游戏旁白常量
(见 `tools/gen_voice_dual.py` SPEAKERS 与各 `_XXX` pair):
| 游戏 | id | 全文 | 触发 | female speaker / instruct | male speaker / instruct |
|---|---|---|---|---|---|
| storyline | `gd_continue` | 轻触屏幕,继续。 | W1/W3 叙事节点播完静默 8s(看门狗第 1 级) | Vivian / _SL_NARR[1] | Uncle_Fu / _SL_NARR[0] |
| storyline | `gd_invalid` | 这个操作在这里用不上。 | 无效手势(选择节点外左右滑静默 return,同局限次) | Vivian / _SL_NARR[1] | Uncle_Fu / _SL_NARR[0] |
| simon | `gd_relisten` | 需要的话,双击可以重新听一遍序列。 | input 阶段 10s 无输入(不改变重听扣机会规则) | Serena / _SIMON | Uncle_Fu / _SIMON |
| rhythm | `gd_answer` | 现在跟着我打。 | 每曲首次领奏→应答切换 | Vivian / _RHY | Uncle_Fu / _RHY |
| rhythm | `gd_miss` | 慢了半拍,下一小节跟上我就行。 | 连续 2 次 Miss(同曲限 1 次,queue 插播) | Vivian / _RHY | Uncle_Fu / _RHY |
| space | `gd_hit_hint` | 被撞了。听方位,同向滑动就能拨开。 | 对局第 1 次受击(保护期外,同局 1 次) | Serena / _SPC | Uncle_Fu / _SPC |
| rpg | `gd_parry_miss` | 前摇声起的时候双击,才能格挡。 | 格挡窗口内做错手势(每局限次) | Serena / _RPG | Uncle_Fu / _RPG |
| rpg | `gd_stall` | 想好了吗?左右滑动选择,单击出手。 | PLAYER_SELECT 60s 无输入 | Serena / _RPG | Uncle_Fu / _RPG |
资产落位:`web/<game>/audio/voice/{female,male}/gd_*.{ogg,m4a}`(管线
`tools/gen_voice_dual.py --game <g> --only gd_xxx` 生成 + `--convert` 落盘,
双格式配对由 `tools/check_all_assets.py` 终检保证)。
## 4. 已知边界与后续候选
- `onceKey` 语义为「同局(游戏会话)一次」;schemes §1.5 中 storyline `gd_invalid`
「同局限 2 次」、rpg `gd_parry_miss`「每场战斗 1 次」为更细粒度限次,v1 统一收敛到
onceKey(一次教学提示足矣),如试玩反馈不足再引入 per-run 限次 API。
- 教学义务(schemes §1.4「full 档开场教学带教签名 earcon」):v1 未在各游戏教学链中
逐个口播签名音,列为 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_<dir>`+`hit_last` 链播完后发 `gd_last_life`(见 F1.4 接线点说明)。 |
| F2.3 | escape 撞墙报方位 | 撞墙在既有 `voice/wall` 播完后接既有 `guide_blunt_<dir>` 方向句(资产 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 跳过未播」。
---
# 14 · 向导交互层设计裁定(round3 v1.2 增补)
> 依据:research/04-playtests/round2/synthesis.md(G1~G8 + §3 新语音表,唯一事实源)。
> 本文只做**追加**,上文 v1 / round2 裁定不回改;实现载体同前(`web/engine/guide.js`
> + 各游戏 game.js 接线),全部改动已落地。G8(不做项)照裁定书记录,不再列。
## 8. round3 裁定落地摘要(G1~G7)
| # | 裁定 | 落地 |
|---|---|---|
| G1 | rhythm「轮到你」迟到 bug | **根因诊断**:round2 F3 把 gd_answer 排在 `leadBarT0 + 4*spb`——那是首个应答窗口**起点本身**(领奏小节的末尾),不是窗口前;P4 转写(窗口 t+79~95、gd_answer t+93.7)即窗口开后才响,起不到预警作用。**修复**:排程点改到 `leadBarT0`(窗口前整整 1 小节 = 领奏小节头);`_armAnswerCue`/`_fireAnswerCue` 承接——菜单恢复也是一次「进 main」,旧标记已消费时对恢复后首个未判定窗口重排同款标记(已迫近则 0.15s 内尽早补上);上一句结束后的下一句不适用(裁定原文)。gd_answer 语音 full 档经曲谱定时队列直达(不走 guide queue),turn earcon **standard 也发**(off 全静不变)。guide.test 增转写级断言:真实跑 90BPM 一曲,gd_answer 时刻 < 首窗口起点,且窗口起点 − gd_answer ≈ 4×spb。 |
| G2 | rhythm 教学降难 DDA | 同一步连续 2 次失败 → 该步领奏**降速 25% 重演**(`_tutRate=0.75`:该步领奏与应答网格间隔 ×1/0.75,领奏小节头仍对齐 BGM 小节,BGM 不变速;排程照走 `_sched` 绝对时间 + 40ms 泵,泵与判定零改动)+ full 档先播 `gd_slow`「我放慢一些,跟着试试。」(经 `_speakGuarded` 串行,播完才重演);每教学局 1 次封顶(`_tutSlowUsed`);该步做对或离开(跳步)恢复正常速度;降速重演后该步重新给两次机会。**off 档维持 round2 的 retry_skip 跳步原路径**(6 playtest 断言零改动红线)。 |
| G3 | rhythm 恢复 count-in 可感知化 | **根因诊断**:round2 F3.2 的 count-in 排了但**从未按时响过**——底鼓 push 进 `_sched` 后没有重排,队头是数秒外的领奏音符,40ms 泵的 `LOOKAHEAD(0.6s)` 前瞻窗口永远轮不到队尾的 count-in(复现实测:恢复后 4 记底鼓与 beat 震全部迟到聚堆),P3 听到的正是「恢复后 5 秒死寂」。**修复**:push 后 `_sortSched()`;底鼓音量 0.7→**1.0**(sfx 层独立于 BGM,每拍 beat 震保持);full 档恢复时先经 `_speakGuarded` 播 `gd_count`「四拍之后,继续。」(每局恢复限 1 次,`_gdCountUsed`;链完/缺失兜底后才开始数拍);恢复用令牌 `_resumeSeq` 防 count-in 语音期间再开菜单的旧恢复串场。**off 档保持 v1 即时续播**(playtest 断言零改动红线)。 |
| G4 | escape 保险丝→新目标断链 | 拾取保险丝的 `unlock_cabinet`(关键获得不可跳)播完后接 `gd_next_fuse`「保险丝到手。接下来,去找电箱。」——游戏反馈层**全档位**(走游戏语音链不走向导通道,gd_economy 先例)。 |
| G5 | simon 双击双义根治 | **改全局规则**:input 阶段**双击永远=作答(第 6 音色)**——期望恰为 doubletap 判对,否则照常判错,不再承担重听;主动重听改绑**上滑**(期望输入恰为 up 时上滑=作答,六手势全是答题输入的 00 §5.3 豁免同款);非输入阶段行为不变。`gd_economy` 按裁定书新文案**重录**(记不住就向上滑…);教学第 6 音色步资产 `t6_double`「双击,是这个声音。」不含重听语义、无需改动,教学步与新文案不矛盾。**超出裁定书 §3 清单的同口径修正(2 句,见 §9 注)**:`gd_relisten`(input 看门狗提示)与 `t7_retry`(教学收尾链)旧文案都在教「双击重听」,不清掉等于保留 P0,随 G5 一并重录为上滑口径,台词改动仅「双击→向上滑」机械替换。 |
| G6 | simon 局尾反馈轻量化 | 每轮成功在既有轮结算链(`_roundClear` 的 correct/chance_plus 链)尾追加 `gd_clear`「漂亮,过了。」(每局 1 次);本轮完成数超过历史最佳且确有旧纪录(`best>0`)时追加 `gd_record`「破纪录了。」(每局 1 次;结算另有 new_record,此处是局中破纪录的即时一刻)。游戏反馈层全档位。 |
| G7 | space 方向呼喊第二通道 | loud 警报档(撞击前 500ms)回调里同窗直达播 `voice/dir_<dir>`「左!/右!/上!/下!」——与 sfx/alarm 分属 voice/sfx 两通道天然同时不互顶(引擎对白 duck 压 BGM 保证可闻);**直达式播放不排队**(呼喊必须赶在撞击前,宁可顶掉里程/充能类短句也不排到链尾迟到);游戏反馈层全档位。 |
## 9. round3 新增语音资产台词表(11 句 ×2 性别 = 22 文件;其中 G5 为重录 3 句)
全文与 synthesis §3 逐字一致(speaker/instruct 沿用各游戏旁白 pair 常量,dir_* 按表注
用 space 旁白 pair);另 2 句为 G5 手势映射的同口径机械修正(裁定书 §3 未列,落地
原因见 §8/G5——不清掉「双击重听」旧文案,input 看门狗与教学收尾仍在教已废除的手势):
| 游戏 | id | 全文 | 触发 |
|---|---|---|---|
| rhythm | `gd_slow` | 我放慢一些,跟着试试。 | 教学连 2 失败降速重演前(G2,full 档,每教学局 1 次) |
| rhythm | `gd_count` | 四拍之后,继续。 | 菜单恢复 count-in 前(G3,full 档,每局恢复限 1 次) |
| escape | `gd_next_fuse` | 保险丝到手。接下来,去找电箱。 | 拾取保险丝链尾(G4,全档位) |
| simon | `gd_economy`(重录) | 记不住就向上滑,重新听一遍。免费机会有两次,用完要扣一次机会。 | 开局教学末尾(G5,全档位) |
| simon | `gd_clear` | 漂亮,过了。 | 每轮成功链尾(G6,全档位,每局 1 次) |
| simon | `gd_record` | 破纪录了。 | 局中破个人纪录(G6,全档位,每局 1 次) |
| space | `dir_left` / `dir_right` / `dir_up` / `dir_down` | 左!/ 右!/ 上!/ 下! | loud 警报档方向呼喊(G7,全档位) |
| simon | `gd_relisten`(同口径修正) | 需要的话,向上滑可以重新听一遍序列。 | input 10s 看门狗(原「双击可以重新听」,G5) |
| simon | `t7_retry`(同口径修正) | 答错可以消耗机会重听。向上滑,可以随时重听。 | 教学收尾链(原「双击,可以随时重听」,G5) |
资产落位与管线同前:`web/<game>/audio/voice/{female,male}/<id>.{ogg,m4a}`
(`tools/gen_voice_dual.py --game <g> --only <id>` 生成 → `--convert` 落盘 →
`tools/check_all_assets.py` 终检)。
## 10. round3 验证记录
- 6 playtest(guide=off):除 simon 按 13 号文档 round3 变更记录改口 3 处断言
(input 双击=作答、上滑=重听;108 条总数与 round2 基线一致)外,**断言零改动
全绿**:storyline 54 / escape 103 / simon 108 / rhythm 81 / space 113 / rpg 139。
- `web/harness/guide.test.mjs` **93 条断言全绿**(round2 基线 52 条既有断言零改动,
其余为新增):G1(gd_answer 时间窗=窗口前整整 1 小节、恢复后重排仍在窗前)、
G2(连 2 失败降速 ×1/0.75 + gd_slow + 做对恢复 + 每局封顶后回退 retry_skip)、
G3(恢复先 gd_count 后 count-in、底鼓重排入队头、beat 震、每局限 1 次)、
G5(双击作答/上滑重听/期望恰为 up 时上滑=作答,full 与 off 双档)、
G6(gd_clear 每局 1 次 + 破纪录 gd_record)、G7(dir_* 与 loud 警报同窗 ≤50ms)。
为此给 harness mock 补了 setInterval/虚拟 ctx.currentTime/用例间 localStorage 隔离。
- 转写 harness 修复(`web/harness/mock.mjs`,工具层非玩法):decodeAudioData 原来
按「最近一次 fetch 的 url」估时长,prewarm 并发下 voice 会随机继承 bgm 的 90s
时长,语音结束定时器形同虚设(链全靠兜底看门狗推进、教学 tap 被滞留语音当跳过
吃掉)——改为按 ArrayBuffer 自身 `__url` 估算,与各 playtest mock 对齐。
- `tools/check_all_assets.py` 全绿(双格式配对、无 0 字节、全可解码、双性别集合
一致);`tools/extract_voice_text.py` 已刷新 `web/harness/voice_text.json`
(437 句)。`gen_voice_dual.py --check` 中 rpg/tut_b0 等 4 句「缺 staging」为
R5 既有已知项(资产已先行落盘、staging 无 wav),非本轮引入。
- 转写抽查(research/04-playtests/transcripts/,6 份全部重跑、零资产缺失):
rhythm gd_answer 落在首窗口前整整 1 小节(t+67.0 vs 窗口 t+69.7)、教学第 2 句
连 2 失败触发「我放慢一些」+ 慢速领奏(拍距 ×1/0.75)+ 做对恢复、菜单恢复
「四拍之后,继续」+ 4 记底鼓 count-in 在;escape 保险丝后紧接 gd_next_fuse、
其后再走两步;simon 看门狗提示上滑重听 + 期望恰为 up 时上滑作答 + 双击音
双击作答,全程无双击歧义时刻;space 每次撞击前 500ms 警报与「左!/右!/上!/
下!」同窗到达。