Files
SoundGame/docs/design/15-radio.md
T
xsl f7509ad5d8 feat(new8/R3): 双游戏收口修复轮 + 验证档案(radio G1-G6 / dungeon E0-1..E1-2)
- radio:帮助链去重(同等待点二次=beat震+earcon,全量口令38→20)/rep_low警告震/空点轻震/状态链重排(状态在前)/「首播必完整、复播可即掐」显式规则登记/G3核查为设计使然转断言;221→246断言
- dungeon:看门狗双轨制(无操作轨+无进展轨,30/60/90s三级升级到导航接管)/跳教学90s自动滚出(2403s→95s内)/踩楼梯到达即报贴身铁器/band1新词「带回来一点,不多」全管线重录/逃跑长按900ms+提示句;1610→1642断言
- R3 验证(进行中已入库部分):全量回归 9/9 全绿;R3 转写×4;P2/P3/P4 终审×6份(radio 满分档首评/三审,dungeon 4/4.5/3-4 全票愿意玩,无P0);两处独立定址同一残余=漫游型迷路刷不响无进展轨(R4 第一条)
- 8套件+guide全绿;Mureka 累计 $0.87/$2.00
2026-09-02 23:08:35 +08:00

540 lines
50 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.
# 15 · 《不眠频率》(radio) — 深夜电台热线叙事 RPG 设计文档
> **给开发的实现要点摘要**
> 1. 叙事模式(声命线范式):节点链 onComplete 串行 + seqToken 打断作废 + pendingChoice 缓存;复制 `web/storyline/` 结构起步,状态机用 6 宏状态(INTRO/RING/ONAIR/BRIDGE/ENDING + MENU 覆盖层)。
> 2. 手势零豁免:全部走全局默认映射(单击=继续/接听/跳句、双击=重听、左右滑=抉择、上滑=状态、下滑=模态菜单 §5.2 约定、长按=引擎快进)。
> 3. 台词依据 [15-radio-voice-ids.md](15-radio-voice-ids.md)(132 句:双性别 103 + 平铺 NPC 29);NPC 人声电话滤波在**资产侧** ffmpeg 带通(storyline n03_msg 先例),引擎零改动。
> 4. 向导层:storyline 同款看门狗节奏 + RING 专用两级新句提示;`gd_ring1/gd_ring2` 为本游戏新增 gd_* 资产。
> 5. 章程任务 N1(research/new8/00-program.md):单局 15-25 分钟、9 抉择点、4 结局、分支局部化。
## 1. 概述
- **一句话**:你是午夜档电台热线主播「今夜不眠」,直播间里一晚接进五个来电——你说什么话、接不接、转不转接,改变五个陌生人的夜晚,也改变你自己。
- **题材(声音即世界)**:全部叙事靠电话音质感(每位来电者一种滤波声)、电台垫乐、直播间环境音呈现——来电者永远不露面,只有声音;「声音即存在」与纯音频载体天然同构。
- **单局**:一个完整夜班(一条抉择路径)15-22 分钟;五个来电共享主干,分歧点局部化在每通电话内部;9 个抉择点(单局实走 8-9 个),4 结局。
- **核心循环**:接听 → 听 → 抉择(左=走心介入 / 右=克制程序)→ 结果与口碑结算 → 幕间喘息 → 下一通。跨来电积累「今晚状态」(听众口碑 + 各来电好感),收播前一个抉择决定你自己的心结是否解开。
## 2. 状态机
### 2.1 状态列表
| 状态 | 内容 | BGM | guide.state |
|---|---|---|---|
| `INTRO` | 开场链:台标口播 + 一句规矩 + 今晚目标 | `bgm/onair` | explore |
| `RING` | 热线接入:铃声循环,等待单击接听 | `bgm/onair`(不切) | 无新签名(turn cue 承担) |
| `ONAIR` | 通话叙事:对白链 + 抉择点 + 结果 | `bgm/onair` | explore;抉择点=choice |
| `BRIDGE` | 幕间:口碑跨档播报(如有)+ 喘息垫乐,自动链 | `bgm/bridge` | settle |
| `ENDING` | 结局链 + 尾声回响(epilogue),结算态 | `bgm/ending` | settle |
| `MENU` | 模态覆盖层(任意叙事态下滑呼出,暂停一切链) | `bgm/menu` | menu |
### 2.2 转移图(文本)
```
┌─────────────── 结算单击=新夜班 ───────────────┐
▼ │
INTRO ──链完──> RING(c1) ──单击接听──> ONAIR(c1) ──链完──> BRIDGE ──┤→ RING(c2)
│铃声循环/看门狗两级,无超时惩罚 │ …(c3,c4 同型)…
└────────────────────────────────────────────────┘
ONAIR(c5) ──链完──> BRIDGE ──链完──> SIGNOFF
│
└──> ◆p9 抉择 ──> d9 结果链 ──> ENDING(e1..e4 + ep 回响链)
任意叙事态(INTRO/RING/ONAIR/BRIDGE/ENDING):下滑 → MENU(模态)
MENU:单击=循环切换选项 → 双击=确认执行 → 上滑=帮助 → 下滑/「回到直播」项=关闭回原状态(重播当前节点)
```
- INTRO/RING/ONAIR/BRIDGE/ENDING 均为节点链驱动(storyline playNode 模式);`SIGNOFF` 是 ONAIR 的最后一个节点组(◆p9),不单设状态。
- BRIDGE 自动链 ≤12s:口碑跨档则播 rep_*(播报真实档位,账目真实),随后直接进下一通 RING。
- MENU 打开时 `guide.disarm()` + 取消一切看门狗/自动链;关闭后重播当前节点(局面复述,storyline closeMenu 先例)。
### 2.3 每状态手势映射(6+2 全覆盖,零豁免)
| 手势 | INTRO | RING | ONAIR | BRIDGE | ENDING | MENU |
|---|---|---|---|---|---|---|
| 上滑 | 帮助+状态,随后重播当前段 | 状态播报(第几通+口碑档),留在 RING | 帮助+状态,随后重播当前节点 | 帮助+状态,自动链继续 | 帮助+状态(结局名+口碑) | 帮助(sys_help) |
| 下滑 | 模态菜单 | 模态菜单 | 模态菜单 | 模态菜单 | 模态菜单 | 关闭菜单 |
| 左滑 | 无效 → error 震+gd_invalid | 无效 → gd_invalid | 抉择=左(走心) | 无效 → gd_invalid | 无效 → gd_invalid | 无效 → error 震 |
| 右滑 | 无效 → gd_invalid | 无效 → gd_invalid | 抉择=右(克制);存档询问态=新夜班 | 无效 → gd_invalid | 无效 → gd_invalid | 无效 → error 震 |
| 单击 | 播放中=跳过本句;链末=进 RING | **接听** | 播放中=跳过本句(p* 不可跳);链末=继续 | 跳过本句(自动链) | **再来一局**(新夜班) | 循环切换选项 |
| 双击 | 重听当前段 | 重听上一通话的收尾句 | 重听当前节点台词(含抉择提示) | 重听幕间链 | 重听结局链 | **确认执行**(菜单约定覆盖重听) |
| 长按 | 引擎接管:旁白临时 1.7×(12 号 §2.2,叙事类不豁免) | 同左 | 同左 | 同左 | 同左 | 同左(快进菜单语音) |
| 长按松开 | 引擎接管:恢复 settings.speed | 同左 | 同左 | 同左 | 同左 | 同左 |
- 豁免登记:**无**。六个基础手势全部符合全局宪法默认语义(单击=确认/继续/**接听**,宪法 §5.1 原文即含「接听」);菜单走 §5.2 R3 模态约定(单击循环+双击确认),不引用 rpg 探索态豁免。
- 抉择 prompt(voice/p1..p9)播放中单击不可跳、左右滑缓存 `pendingChoice` 播完即生效(storyline 盲玩公平先例)。
- 无效手势零静默:所有「无效」格均有 error 震 + gd_invalid(onceKey 同局限次,F1.4 idle-only:语音在播时跳过语音只发震)。
## 3. 抉择树
### 3.1 全局骨架(来电共享主干,分歧局部化)
```
INTRO: b_open → b_rain → b_rule → b_goal ──> 铃①
【C1 老周·走丢的老人】教学通 · 2 抉择 ──> BRIDGE ──> 铃②
【C2 小鹿·想退票的年轻人】单抉择 · 呼吸口 ──> BRIDGE ──> 铃③
【C3 糖糖·失眠的孩子】2 抉择 · 埋 heardArgument ──> BRIDGE ──> 铃④
【C4 陈姐·争吵的夫妻】2 抉择 · 消费 heardArgument ──> BRIDGE ──> 铃⑤
【C5 老韩·雨夜司机】高潮 · 1 抉择 + 危机脚本拍 ──> 签收 ◆p9(心结)──> ENDING
```
分支纪律:**每通电话内部分支、通话末尾收敛**到导播收尾句 → BRIDGE;跨来电只传三个数据(rep 数值、bond 好感、heardArgument 布尔),不生成组合节点。最坏路径与最好路径节点数相同。
### 3.2 分通结构(◆=抉择点,〔〕=条件差分)
```
【C1 老周】
铃① → z1_ring_in → z1_a → z1_b → ◆p1
├左 哼老歌: z1_song → z1_songo → z1_sdone ─┐
└右 问路牌: z1_ask → z1_asko → z1_adone ───┤
▼
◆p2
├左 陪他等: z1_stay → z1_stayo → z1_staye ─┐
└右 转接: z1_pol → z1_polo → z1_pole ────┤ (仅 p1=右 时 p2=右 改走 z1_bad → z1_bade 归因)
▼
z1_ok1(首通确认,全局一次)→ BRIDGE
【C2 小鹿】
铃② → l1_in → l1_a → l1_b → ◆p3
├左 劝上车: l1_go → l1_goo → l1_goe ──┐
└右 还自由: l1_free → l1_freeo → l1_freee ─┴→ BRIDGE
【C3 糖糖】
铃③ → k1_in → k1_a → k1_b(垫 arg_muffled 争吵声)→ ◆p4
├左 哄睡: k1_lull → k1_lullo → k1_lulle ──────────────┐(错过线索,本通结束)
└右 问她: k1_ask → k1_asko → ◆p5 │
├左 真话: k1_truth → k1_trutho → k1_truthe ────────┤(heardArgument=true)
└右 谎言: k1_lie → k1_lieo → k1_liee ──────────────┴→ BRIDGE
【C4 陈姐】
铃④ → w1_in → w1_a → w1_b → ◆p6
├左 替孩子带话:〔heardArgument ? w1_say 组 : w1_say2 组〕→ ◆p7
│ ├左 留在直播: w1_air → w1_airo → w1_aire ─┐
│ └右 换垫乐私聊: w1_priv → w1_privq → w1_prive ─┤
└右 劝她回家: w1_home → w1_homeo → w1_homee ──────────┴→ BRIDGE(右路本通结束,无 p7)
【C5 老韩】高潮
铃⑤ → h1_in → h1_a → h1_b → ◆p8
├左 陪聊闺女: h1_talk → h1_talko → 【危机拍: danger 签名+brake_screech(前置2s)】
│ → h1_danger → h1_dango → h1_talke ─┐
└右 服务区: h1_stop → h1_stopo → h1_stope ────────┤
〔bond_zhou=true 时插播 h1_echo 回响句〕 ▼
BRIDGE → g9_in → ◆p9
├左 直面(播出老主播的晚安): d9_face → d9_facee → knot=解
└右 回避(照常收播): d9_avoid → d9_avoide
▼
ENDING: e*_a → e*_b(按 rep×knot 四选一)→ ep_lead → ep_<caller>×5(按 bond 选播)
```
### 3.3 分支局部化说明
- 跨来电状态只有三个标量:`rep`(数值)、`heardArgument`(布尔)、`bond`(5 个 0/1)。没有任何「来电 i 的分支 × 来电 j 的分支」组合节点。
- 唯一的跨来电内容差分:C4 的 w1_say/w1_say2 两组(由 heardArgument 选播)与 C5 的 h1_echo 插播句(由 bond_zhou 选播)——都是**节点内选播**,不改树形。
- 树规模:132 句台词、约 60 个节点,任一路径实走 8-9 个抉择点(C3 左路/ C4 右路各少一个)。
### 3.4 四结局达成条件
| 结局 | 条件 | 主题 | 开环钩子 |
|---|---|---|---|
| e1 天亮之前 | rep ≥ 5 且 p9=左(直面) | 城市有了心跳,话筒接住了 | 「明晚零点,接着不眠」 |
| e2 热线常亮 | rep ≥ 5 且 p9=右(回避) | 满座夜班,留言机红灯未看 | 红灯「灭得很慢」 |
| e3 一个人的频道 | rep < 5 且 p9=左(直面) | 夜班难,但晚安说出去了 | 「师父,收到了吗」 |
| e4 静音 | rep < 5 且 p9=右(回避) | 谁也接不住谁的夜班 | 「明晚再说吧」 |
- 尾声回响(P2 回响感):epilogue 对 5 个来电者各播一句 bond ? 救到的后续 : 错过的后续(ep_zhou/deer/kid/wife/han_ok/no),玩家每晚的救与错过都被点名。
## 4. 数值设计
### 4.1 变量
| 变量 | 类型 | 初值 | 说明 |
|---|---|---|---|
| `rep`(听众口碑) | int | 0 | 抉择与结果积累,收播时结算档位 |
| `knot`(心结) | bool | false | 仅由 p9=左 置 true |
| `bond_zhou/deer/kid/wife/han` | 0/1 | 0 | 只影响尾声回响选播,不影响结局 |
| `heardArgument` | bool | false | p4=右 置 true,C4 消费 |
| `nights` / `endings{}` | int / map | — | 重玩计数与结局收集(存档展示用) |
### 4.2 抉择计分表(左=走心 / 右=克制)
| 抉择 | 左(走心) | 右(克制) | 备注 |
|---|---|---|---|
| p1 哼歌/问路 | +1 | 0 | 左路老周稳住(首个正反馈) |
| p2 陪等/转接 | +2(p1=左)/ +1(p1=右) | +1(p1=左)/ **-1(p1=右,砸)** | 唯一负分组合,触发归因句 |
| p3 劝上车/还自由 | +2 | +1 | 两个选项都是体面的(教学后建立信任) |
| p4 问她/哄睡 | +1,heardArgument=true | 0 | 左=倾听,右=敷衍即错过线索 |
| p5 真话/善意谎言 | +1 | 0 | p5 仅 p4=右 可达 |
| p6 替孩子带话/劝回家 | +2(heardArgument)/ +1(否则) | +0(本通结束) | |
| p7 留在直播/换垫乐 | +2 | +1 | p7 仅 p6=左 可达 |
| p8 陪聊/服务区 | +1(含危机拍) | 0 | 两路都活着(P4 零死亡) |
| p9 直面/回避 | knot=解 | knot=未解 | 不动 rep |
### 4.3 阈值与导向
- 收播结算:`rep ≥ 5` = 高口碑(e1/e2),`rep < 5` = 低口碑(e3/e4)。
- 理论上限 12、全克制路径 0;「多数走心 + 容忍 1-2 次失误」落在 5-8 档——阈值 5 把「今晚是否立住」压在玩家自己的重量上,而非单点失误。
- 口碑跨档被动播报(BRIDGE):进入 ≥5 区间播 rep_high、跌回 1-4 播 rep_mid、≤0 播 rep_low——分档整句、账目真实(A4 资源账目律)。
- 归因真话律落地:每个负向结果节点(z1_bade、k1_lulle、k1_liee、e4 链)的导播收尾句必须指向玩家的**实际选择**(「地址没报清就转接」「你没听见她在怕什么」),禁止「运气不好」类假归因;成功路径不播任何补救教学(A5)。
## 5. 音频三层映射
### 5.1 BGM(状态→曲目,Mureka instrumental,4 首 ≈ $0.12)
| 状态 | 曲目 | 切换 | Mureka prompt 草稿(英文,seamless loop) |
|---|---|---|---|
| INTRO/RING/ONAIR | `bgm/onair` | 开局淡入 800ms,全夜班不切;语音 duck 引擎自理 | "Midnight radio broadcast music bed, warm lo-fi neo-soul chords, soft vinyl crackle, gentle night-drive groove with brushed drums, intimate late-night host mood, no vocals, seamless loop, 60-90 seconds" |
| BRIDGE | `bgm/bridge` | crossfade 800ms 进;进下一通 RING 淡回 onair 600ms | "Radio interlude pad, sparse felt piano notes over warm analog drone, distant rain mood, lonely but safe, no drums, no vocals, seamless loop, 60-90 seconds" |
| MENU | `bgm/menu` | crossfade 500ms 进;关菜单淡回原状态曲 800ms | "Late-night radio studio menu pad, warm minor pad with faint tape hiss, static harmony, very sparse, no vocals, seamless loop, 60-90 seconds" + 共性后缀(10 号 §8) |
| ENDING | `bgm/ending` | p9 结果链播完后淡入 1500ms | "Dawn sign-off theme, soft piano and warm strings, hopeful sunrise after a long night shift, gentle resolution, no vocals, seamless loop, 60-90 seconds" |
- `bgm=0` 时全部静默(引擎约定);menu pad 与 onair 同调性优先。
### 5.2 SFX 清单(Stable Audio 3,one-shot 真峰值 ≤-3dBFS;amb 循环由游戏层 buffer.loop 实现,escape amb 先例)
| id | 用途 | Stable Audio prompt(英文) |
|---|---|---|
| `sfx/onair_sting` | 红灯亮起台标 sting(INTRO/重开) | "Short broadcast on-air light sting, warm chime with vintage radio click, 1 second" |
| `sfx/phone_ring` | 热线铃(RING 循环体,每遍 ≥4s 间隔) | "Old studio telephone bell, two-strike bright metallic ring, clean" |
| `sfx/line_connect` | 接听咔哒+线路接通音 | "Telephone line connect click and short dial tone fade, subtle" |
| `sfx/line_drop` | 挂断/转接忙音 | "Telephone hang-up click with short busy tone, soft" |
| `sfx/amb_studio` | 直播间底噪 loop(调音台嗡+空调) | "Quiet radio studio room tone, console hum and soft air conditioning, seamless loop" |
| `sfx/amb_rain_city` | C1 老人侧:街道雨夜 loop | "Heavy rain on city street at night, distant traffic hiss, seamless loop" |
| `sfx/amb_station` | C2 小鹿侧:深夜车站 loop | "Empty train station hall at night, distant echo and rolling suitcase, seamless loop" |
| `sfx/amb_kid_room` | C3 糖糖侧:儿童房 loop | "Quiet child bedroom at night, soft lamp buzz, muffled house silence, seamless loop" |
| `sfx/arg_muffled` | C3 线索音:闷住的争吵+摔门(信息承载,k1_b 时起) | "Muffled argument behind a door, dull thud of door slam, indistinct voices, 4 seconds" |
| `sfx/amb_street_rain` | C4 陈姐侧:公车站雨 loop | "Rain at an empty bus stop at night, distant wet road traffic, seamless loop" |
| `sfx/amb_cab` | C5 老韩侧:车内 loop(引擎+雨刷节奏) | "Inside a moving truck cab at night, steady engine hum, rhythmic windshield wipers, rain outside, seamless loop" |
| `sfx/brake_screech` | C5 危机拍:急刹+护栏摩擦 | "Sudden brake screech on wet asphalt, brief tire friction, 2 seconds" |
| `sfx/tape_static` | p9 直面:留言机电流声 | "Old cassette tape static and tape hiss with a click stop, 3 seconds" |
| `sfx/ui_confirm` | 菜单/接听确认(程序合成兜底) | —(程序合成,同全局先例) |
| `sfx/ui_menu` | 菜单呼出(程序合成兜底) | —(程序合成) |
- C3/C4 的 amb 是**信息层**:arg_muffled 在 k1_b 起播 2-3 次,玩家听没听见由 p4 抉择兑现(声音即世界)。
- 预警窗纪律:h1_danger 前 2s 先起 brake 渐强(L2 前置预警),danger 签名与 brake 同窗。
### 5.3 Voice 划分(详见 15-radio-voice-ids.md)
| 组 | 声部 | 落位 | 数量 |
|---|---|---|---|
| 主播(你)HOST | 双性别(female=Serena / male=Uncle_Fu + HOST instruct 常量) | `voice/female|male/` | 21 句 |
| 导播/系统/向导 PROD | 双性别(female=Vivian / male=Dylan + PROD instruct 常量) | `voice/female|male/` | 82 句 |
| 来电者 NPC ×5 | 平铺单音色(老周=Uncle_Fu / 小鹿=Serena / 糖糖=Vivian / 陈姐=Vivian / 老韩=Eric),**不做双性别** | `voice/` 根(平铺回退命中)+ 电话滤波转码 | 29 句 |
- NPC ≤5 个约束满足;玩家自己的声音跟随全局 `settings.voice`(男/女主播皆可代入),导播耳语是向导人设(L4 角色化系统声)。
- 合计 132 句(≤170 GPU 预算);音频文件 = 103×2 + 29 = 235 条(×2 格式 = 470 文件)。
## 6. 来电音质感设计(声音即世界的实现)
### 6.1 引擎能力边界(对照 web-engine-api.md)
| 能力 | 引擎现状 | 方案 |
|---|---|---|
| voice 运行时 EQ/滤波 | ❌ voice 链 = SoundTouch → gain → voiceBus,无滤波插入点 | **资产侧转码滤波**(storyline n03_msg/n05_bcast 的 convert.sh 带通先例):ffmpeg `highpass + lowpass + 压缩器` 逐来电者定档 |
| voice 逐句声像/空间定位 | ❌ voice 单声道总线 | 不使用;空间感交给 sfx amb(sfx 资产内 pan,escape amb 先例) |
| sfx 循环 | sfx 即发即弃,无 loop 选项 | 游戏层 `buffer.loop=true` 或 onEnded 重触发(escape amb_loop 先例) |
| 语音 duck 垫乐 | ✅ 内置(duck 20%,300ms) | 直接依赖 |
| 运行时混响/失真(「信号变差」差分) | ❌ | 降级:用 amb 响度强弱 + 台词停顿节奏表达,不做动态滤波 |
| 变速不变调 | ✅ SoundTouch(settings.speed + 长按 1.7×) | 直接依赖 |
### 6.2 每位来电者的声音签名(资产侧 ffmpeg 参数)
| 来电者 | 滤波档 | 质感设计 | 背景 amb(sfx 层) |
|---|---|---|---|
| 老周 | highpass 500 + lowpass 2800 + 轻压缩 | 老旧手机,窄而颤抖 | amb_rain_city |
| 小鹿 | highpass 400 + lowpass 3400 | 车站手机,干净偏冷 | amb_station |
| 糖糖 | highpass 450 + lowpass 3000 | 家用座机,小而近 | amb_kid_room + arg_muffled |
| 陈姐 | highpass 420 + lowpass 3100 | 户外手机,风噪 | amb_street_rain |
| 老韩 | highpass 300 + lowpass 3400 | 车载蓝牙,宽而轰 | amb_cab |
- **可读性底线**(L2 反模式「写实但不可读」):滤波保持在语音可懂频带内,宁窄勿脏;QC 试听不可读的句子回退**未滤波原声**——叙事不破,只损失质感(显式降级方案,不阻塞管线)。
- 主播/导播人声**不滤波**(直播间级音质);「电话里的人」与「身边的人」靠这一层音质差立住,无需任何视觉。
## 7. 向导层接入
### 7.1 状态签名 earcon / turn cue
| 时机 | 向导动作 | 说明 |
|---|---|---|
| INTRO 进入 | `guide.state('explore')` | 叙事态签名 |
| 每个抉择点 p* 起播 | `guide.state('choice')` | 选项句本身即 turn cue(播完当刻 choiceArmed 即真,W2 契约零插播) |
| RING 铃响 | `guide.turn({haptic:true})`(无语音) | 铃声+turn 签名=「轮到你接听」;首次铃前 b_goal 已口播规矩 |
| BRIDGE 进入 | `guide.state('settle')` | 喘息/结算签名 |
| ENDING 进入 | `guide.state('settle')` | 同上 |
| MENU 打开 | `guide.state('menu')` | 菜单签名 |
| C5 危机拍 | `guide.state('danger')` | 全局唯一 danger 使用点(刹车前 2s,与 brake_screech 同窗) |
| 无效手势 | `guide.error({voice:'gd_invalid', onceKey})` | error 震 + full 档语音,idle-only(F1.4) |
- earcon 词表:只用全项目统一 8 音(menu/explore/battle/choice/settle/danger/turn/nudge),**零新增**;battle 本游戏不用。开场台词自然教 3 个最高频签名(铃=接听、短促提示音=轮到你选择、上滑/下滑口令);settle/menu 首遇即学;逐签名口播教学列 v2 候选(14 号 §4 同款)。
### 7.2 看门狗参数(秒;任何手势经 guide.disarm 复位;guide=off 全不挂)
| 等待点 | full 档 | standard 档 | 级别内容 | 礼仪 |
|---|---|---|---|---|
| RING 无接听 | [10, 25] | [10, 25](onEscalate 游戏层补播) | ① gd_ring1「热线在闪。单击,接听。」② gd_ring2「灯还亮着,听众在等。」 | **两级不同句**(绝不逐字重播);两级用尽只剩铃声循环,永不惩罚 |
| ONAIR/INTRO/BRIDGE 叙事链末等待 | [25, 50] | [13, 30](同 storyline P2-3 先例) | ① gd_continue ② gd_again | storyline F1.1 同款,两级后彻底静默 |
| 抉择点 p* 播完无操作 | [25, 50] | [25, 50] | 重播**当前 p* 提示句本身**(复述选项=合法帮助,13 号 §16 N3 先例) | 两级后静默;空点单击=重挂计时 |
| ENDING 结算等待 | [25, 50] | [13, 30] | gd_continue / gd_again | 结算单击即重开,不阻拦 |
- 向导插播一律 `queue:true`,绝不顶掉在播对白;error/watchdog 类语音 idle-only(在播则跳过且过期丢弃,F1.4)。
- 单句语音看门狗(资产缺失兜底):30s 超时且 `isVoicePlaying()===false` 才自动续链,长句续约不掐(storyline N1 先例照抄)。
## 8. 存档设计
### 8.1 进度 data 结构(PUT /api/progress {game:'radio'})
```json
{
"schema": 1,
"updatedAt": "2026-09-01T12:00:00Z",
"summary": "第3通·失眠的孩子 · 口碑+2 · 结局1/4",
"phase": "ONAIR",
"node": "k1_asko",
"rep": 2,
"knot": false,
"heardArgument": true,
"bond": { "zhou": 1, "deer": 1, "kid": 0, "wife": 0, "han": 0 },
"endings": { "e2": true },
"nights": 3
}
```
- 写入时机:每通电话 RING 进入、每个抉择结算后、ENDING 进入、菜单「保存并回大厅」。
- 断点续玩:登录/本地读到存档 → `sys_continue_ask`(单击=继续上次 / 右滑=新夜班);继续 = 恢复 phase+node 并**重播当前节点链** = 一句话局面复述(L5:恢复成本游戏承担);重进 ≤5s 可玩。
- summary 字符串格式:进行中 `第N通·<来电者> · 口碑±X · 结局K/4`;已收播 `已收播·<结局名> · 结局K/4`(大厅卡片直接显示,人话)。
## 9. playtest 断言清单(登记 13 号文档 radio 节,格式同 §1)
**关键路径**(E1 全程,≤60 次手势):
1. 启动遮罩点按 → INTRO 链(b_open 起,voice 层有播放记录)→ 链完自动进 RING(c1),turn 事件与铃声断言。
2. RING 单击接听 → ONAIR(c1):p1 左 → p2 左 → z1_ok1 → BRIDGE(rep 断言 +1+2=3)→ RING(c2)。
3. 依序走 p3 左 / p4 右 / p5 左 / p6 左 / p7 左 / p8 左(断言每步后 rep 与 §4.2 一致;p4 右后 `heardArgument === true` 且 C4 播 w1_say 组)。
4. p9 左 → knot=true → ENDING(e1):结局链 + ep_lead + 5 条 ep_*_ok → 结算态单击重开回 INTRO。
**边界断言**:
- **四结局可达**:剧本 B(路径同上但 p9 右)→ e2;剧本 C(p1右 p2左 p3右 p4右 p5右 p6右 p8右 p9左,rep=3)→ e3;剧本 D(p1右 p2右 p3右 p4左 p6右 p8右 p9右,rep=0)→ e4;各局 rep 终值与设计表一致。
- **双击重听**:ONAIR/ENDING 播完态双击 → 当前节点链同 id 序列重播;重播期间单击=跳本句(nodeId 不变),播完回原等待态。
- **菜单返回不丢节点**:ONAIR 中下滑开菜单 → 上滑听帮助 → 双击「回到直播」→ phase/node 不变、当前节点重播;菜单内单击循环顺序 = [回到直播, 重新开始今晚, 保存并回大厅];确认回大厅 → sys_menu_lobby 播出且 PUT/localStorage 有最新 data。
- **中断复述**:杀进程重进(存档 node=k1_asko)→ sys_continue_ask → 单击 → 重播该节点链;上滑播 sys_help+prog_c*+rep_* 链(顺序断言)。
- **看门狗**:RING 快进 10s → gd_ring1、25s → gd_ring2(断言第二级 id ≠ 第一级 id,绝不逐字重播;两级后仅铃声);ONAIR 链末 25s/50s → gd_continue/gd_again;p* 播完 25s/50s → 当前 p* 重播两次后静默;`settings.guide='off'` 时全程零定时器零向导事件。
- **pendingChoice**:p* 播放中左右滑 → 播完立即生效(不丢、不重复)。
- **p* 不可跳**:p* 播放中单击 → 不跳句(PROMPT_RE 门)。
- **idle-only**:向导 gd_* 在对白在播时被跳过且不再补播(F1.4 契约);queue 句排在当前句后(在播句不被截断)。
- **归因真话**:剧本 D 中 z1_bade 播出(含真实归因);剧本 A 全程无任何归因/补救句(成功不教学)。
- **资产缺失**:屏蔽任意 voice 资产 → 30s 看门狗自动续链到结局,不卡死。
- **数字规范**:prog_*/rep_* 全部整句,转写审计无运行时拼接碎片。
- **坏音质降级**:滤波资产缺失时原声可播(回退路径不断链)。
**失败即不通过**:任何等待 >5s 无声(静默死局);无效手势零反馈;结局计数与 §3.4 不符。
## 10. 难度 / 节奏曲线
| 段 | 内容 | 目标时长 | 设计目标 |
|---|---|---|---|
| 开场 | 台标 + 一句规矩 + 第一声铃 | 1.5-2 min | 首输入 <60s(链 ~30s 后铃响即接听);规矩一句讲完(P4) |
| C1 老周 | 教学通:2 个二选一,条条有出路 | ~3.5 min | **首个正反馈 ≤3 min**(p1 左 → 老周声音稳住 + 导播确认);唯一 -1 组合不扣资源(学习期零惩罚) |
| C2 小鹿 | 单抉择重话术 | ~2.5 min | 节奏呼吸口;建立「两个选项都体面」的信任 |
| C3 糖糖 | 双抉择 + amb 线索埋设 | ~3.5 min | 环境音首次承载剧情(声音即世界);错过线索可被尾声点名 |
| C4 陈姐 | 最复杂:2 抉择 + 条件差分 | ~4 min | 情感高潮;选项句最长(p7 ≤40s 播完) |
| C5 老韩 | 高潮:危机拍(danger+急刹,前置 2s 预警) | ~4 min | 紧张峰值但零死亡、零倒计时(P4);h1_echo 兑现 C1 回响 |
| 签收+结局 | p9 + 结局链 + 5 句回响 | ~3 min | 心结抉择 + epilogue 回响感(P2);开环钩子拉下一局 |
- 合计 ≈ 21-22 min(15-25 min 达标);每 30-60s 一个反馈颗粒(导播短句/结果句/口碑档),无沉默尴尬期(P3)。
- 难度曲线 = 信息复杂度(1 抉择 → 条件差分 → 双方对峙 → 生死时速),操作复杂度恒定(永远二选一)——P4「规则一遍听懂」由恒定操作面保证。
## 11. 自检清单
| # | 硬约束 | 落地 | ✓ |
|---|---|---|---|
| 1 | FTUE 首输入 <60s | INTRO 链 ~30s 后第一声铃即接听(§10) | ☐ |
| 2 | FTUE 首个正反馈 ≤3min | p1 左 outcome ~2.5min(z1_songo+z1_sdone) | ☐ |
| 3 | 学习期零惩罚 | C1 全组合不扣资源;RING 不接听无超时惩罚 | ☐ |
| 4 | 失败三问归因 + 归因真话律 | z1_bade/k1_liee/e4 链指向实际选择;§4.3 禁假归因 | ☐ |
| 5 | 手势宪法(6 手势默认语义,零豁免) | §2.3 全覆盖;单击=确认/继续/接听、菜单走 §5.2 模态约定 | ☐ |
| 6 | 长按豁免显式登记 | 无豁免:长按=引擎快进(12 号叙事类不豁免),§2.3 登记在案 | ☐ |
| 7 | 向导层:状态签名/turn cue/看门狗 | §7.1/§7.2;RING 两级**不同句**,绝不逐字重播 | ☐ |
| 8 | 插播一律 queue 不顶对白;纠错 idle-only 过期丢弃 | §7.2(F1.4 契约) | ☐ |
| 9 | 提示永不逐字重复 | gd_ring1≠gd_ring2;gd_continue→gd_again;抉择点复述 p* 为 13 号 §16 先例 | ☐ |
| 10 | 成功后不灌补救教学 | 归因句只挂负向结果(§9 断言) | ☐ |
| 11 | 单次播报 ≤3 选项 | 全部抉择恒 2 选项 | ☐ |
| 12 | 并发声流 ≤2 | voice+amb 两流;无第二 voice 通道 | ☐ |
| 13 | earcon ≤7 个、显式教一次、语义靠语音 | 只用统一 8 音中的 6 个(battle 不用),零新增;选项内容全语音 | ☐ |
| 14 | 台词一句一文件整句、≤40 字、数字分档整句、无运行时拼接 | voice-ids 全表 ≤39 字;prog/rep 分档整句 | ☐ |
| 15 | 多音字规避(重新听)、同游戏同 instruct 常量、无语速词 | voice-ids 头部注记与 instruct 常量表 | ☐ |
| 16 | P1:可打断/重听、status key、菜单规整、不当白痴 | skipVoice/doubletap/上滑状态链/模态菜单;成功零重复教学 | ☐ |
| 17 | P2:头 3 分钟见效、结局回响 | C1 即时改变对方情绪;ep_* 救/错过点名 | ☐ |
| 18 | P3:单手免视、一句话复述局面、3min 反馈颗粒 | 全手势免视;重进重播当前节点;BRIDGE 口碑播报 | ☐ |
| 19 | P4:规则一遍懂、高容错、无高压倒计时、NPC 语速感 | 一句规矩;零死亡零倒计时;NPC 句 ≤18 字+逗号停顿(不用 instruct 语速词,禁令遵守) | ☐ |
| 20 | 抉择树 4 结局可走通 | §9 剧本 A-D 四条实路径 + rep 断言 | ☐ |
| 21 | 单局 15-25 min | §10 曲线 ≈21-22 min | ☐ |
| 22 | 分支不爆炸 | 跨来电仅 3 标量;树 ~60 节点固定深度(§3.3) | ☐ |
| 23 | 台词 ≤170 句、NPC ≤5 | 132 句 / 5 个来电者(voice-ids 账目) | ☐ |
| 24 | 引擎边界诚实 | §6.1 能力表:做不到的写明资产侧/降级方案 | ☐ |
---
## 实现核对(测试 agent 登记)
> 断言实现:`web/radio/test/playtest.mjs`,193 条,`tools/node/bin/node web/radio/test/playtest.mjs`
> 退出码 0 全绿。会话剧本:`web/harness/sessions/radio.mjs`(`session` 主线完整弧线 +
> `sessionStress` 乱按压力),经 `tools/node/bin/node web/harness/transcribe.mjs radio
> [--session sessionStress]` 跑通(transcribe.mjs 已登记 radio/支持 `--session` 选剧本)。
> 回归:六游戏 playtest + guide.test 全绿不受影响。
### §9 关键路径逐条
| # | 断言 | 状态 |
|---|---|---|
| 1 | 遮罩点按 → INTRO 链(b_open 起)→ 链完自动进 RING(c1),turn 事件与铃声 | ✅已断言(INTRO 四句逐句计数 + turn 事件 + phone_ring 循环) |
| 2 | 单击接听 → p1 左 → p2 左 → z1_ok1 → BRIDGE(rep=1+2=3)→ RING(c2) | ✅已断言 |
| 3 | p3 左/p4 右/p5 左/p6 左/p7 左/p8 左逐点 rep 与 §4.2 一致;p4 右 heardArgument=true 且 C4 播 w1_say 组 | ✅已断言 |
| 4 | p9 左 → knot=true → ENDING(e1) + ep_lead + 5 条 ep_*_ok → 结算单击重开回 INTRO | ✅已断言(含手势预算 55≤60) |
### §9 边界断言逐条
| 断言 | 状态 |
|---|---|
| 四结局可达(剧本 B→e2 / C→e3 / D→e4,rep 终值与设计表一致) | ✅已断言(另补剧本 E:全左但 p4 左 → heardArgument=false → C4 播 w1_say2 组、rep=9,补齐 w1_say/w1_say2 双差分覆盖;剧本 C/D 额外断言带话差分不播与 ep_deer_no) |
| 双击重听:ONAIR 播完态/ENDING 播完态同 id 序列重播,重播期间单击=跳句 | ✅已断言 |
| 菜单返回不丢节点;单击循环顺序 [回到直播, 重新开始今晚, 保存并回大厅];确认回大厅播 sys_menu_lobby 且 localStorage 有最新 data | ✅已断言(含报名 queue 零截断顺序断言) |
| 中断复述:存档 node=k1_asko → sys_continue_ask → 单击恢复 → 重播节点链;上滑 sys_help→prog_c3→rep_mid 顺序 | ✅已断言(另补 ASK 右滑=新夜班) |
| 看门狗:RING 10s/25s 两级不同句、两级后仅铃声;ONAIR 链末 25s/50s gd_continue/gd_again;p* 25s/50s 复述两级后静默;ENDING 25s/50s;guide=off 全程零定时器零向导语音 | ✅已断言(full 档事件钩子计数,choice 签名 ×9) |
| pendingChoice:p* 播放中滑动感存、播完即生效(不丢不重复) | ✅已断言 |
| p* 不可跳(PROMPT_RE 门) | ✅已断言 |
| idle-only:对白在播时纠错跳过且不占 onceKey(F1.4);queue 句不顶在播句 | ✅已断言(skipped 事件 + 补播对照 + 菜单报名顺序) |
| 归因真话:剧本 D 播 z1_bade;剧本 A/E 全程零归因/补救句 | ✅已断言(含 k1_lulle/k1_liee 挂点) |
| 资产缺失:屏蔽 voice → 30s 看门狗续链到结局不卡死 | ✅已断言 |
| 数字规范:prog_*/rep_*/gd_ring* 分档整句字面量、无运行时拼接 | ✅已断言(静态审计) |
| 坏音质降级:滤波资产缺失回退原声 | 合并(回退是资产侧转码工序,代码无分支;NPC 性别目录→平铺回退由静态审计覆盖) |
| 失败即不通过(>5s 无声死局 / 无效手势零反馈 / 结局计数与 §3.4 不符) | ✅已断言(无效手势 error 震逐次计数;四结局矩阵即计数核对) |
### 本轮修复的实现 bug(web/radio/game.js)
1. **ONAIR 叙事链末不等待(规格 vs 实现最大偏差)**:`_onNodeSeqDone` 无条件 `advance(node.next)`,
节点表的 `auto:true` 标志(INTRO 链/危机拍/签收链/p9 结果链)从未被读取——与 §2.3「链末=继续」、
§7.2 ONAIR 链末看门狗行、§9「ONAIR 链末 25s/50s」及文件头注释均不符。修复:仅 `node.auto` 或
`@x` 过场转移自动衔接,其余 ONAIR 叙事节点链末等待单击并挂 gd_continue/gd_again 看门狗。
2. **longpress 误报无效**:gesture.js 把 longpress/longpressend/tapcancel 派发进 onGesture,RING/
BRIDGE/ENDING/MENU/ASK 的 default 分支把它们当无效手势报 gd_invalid/error 震——§2.3 长按行是
「引擎接管快进」,全状态合法。修复:仅 left/right(ASK 另含 up/down/doubletap)报无效。
3. **菜单单击循环的引擎队列竞态**:`audio.play(item, {queue:true})` 在引擎 `_finishVoice` 中先跑
onComplete(游戏链播下一句)后 shift 队列,排队报名会把 sys_menu→sys_menu_resume 链播句顶掉。
修复:菜单报名改游戏侧 `_menuQueue` + `_drainMenuQueue()` 排水,当前报名播完接播、零截断。
4. **RING 双击首通不报无效**:INTRO 链节点完成会写 `lastCloser`,首通双击重听了 b_goal 而未按开发
注记「首通前无上一通 → 无效手势零静默」报错。修复:`callN <= 1` 时报无效。
5. **ENDING 上滑缺口碑**:§2.3 ENDING 上滑=「帮助+状态(结局名+口碑)」,原实现只播 sys_help+结局
主句。修复:链尾补 `TIER_VOICES[tierOf(rep)]`。
6. **stop() 后中断链复活**:被 stopVoice 掐断的 `_playSeq` 链可经 30s 单句看门狗「资产缺失」路径续链,
已拆除实例会走到 enterRing 留下无人接听的永久铃声定时器。修复:stop() 先 `seqToken++` 作废在途链。
7. **newNight 不清抉择武装**:菜单重开/结算重开到新夜班首链起播前,choiceArmed/pendingChoice 残留,
窗口期手势会在旧节点上误抉择。修复:newNight() 显式清零。
8. **_ringOnce 双链防护**:closeMenu(RING)/_resume 直接调 _ringOnce 不清旧计时器,可能双铃声链。
修复:_ringOnce 先 clearTimeout 旧 ringTimer。
### 规格歧义 / 实现取舍(登记不改设计)
- §4.2 表「p4 问她/哄睡」选项列序与 §3.2 树形(左=哄睡/右=问她)相反,实现按 §3.2 + p4 提示句原文
(game.js 已注记);断言按 §9 口径(p4 右 → heardArgument=true,+1)。
- `sys_saved`(15b G0 组,触发=存档写入后)未接线:自动存档每局 ≥7 次,逐次播报会持续打断节目流;
菜单「保存并回大厅」已有 sys_menu_lobby 确认句。列遗留(如需可挂菜单「重新开始今晚」确认后)。
- INTRO 上滑=「帮助+状态」:开播前无通号/口碑可报,实现只播 sys_help 后重播当前段(RING 起状态链完整)。
- 存档 data 在 §8.1 字段外增加 `call`/`ending`/`lastCloser`(断点续玩与 RING 双击需要,超集兼容)。
- ASK 右滑新夜班的 `nights` 按新实例计数(不继承被拒绝存档的 nights),与结算重开 +1 口径不一致,
无游戏性影响,列遗留。
### 资产生产缺口(列遗留,本轮不修)
> **→ 已闭环(2026-09-02 晚)**:12 件 SA3 条目全部补产(onair_sting / phone_ring / line_connect /
> line_drop / amb_rain_city / amb_station / amb_kid_room / arg_muffled / amb_street_rain / amb_cab /
> brake_screech / tape_static;amb_studio 文档有、未接线,产出备用),`check_all_assets` radio 254+254
> 全过、playtest 221 断言全绿。详见 `research/new8/batch1-assets-report.md` §8。
- §5.2 SFX 15 件中 **12 件 Stable Audio 3 条目未产出**(`gen/gen_sfx.py` 头注:HF token 未配置跳过):
onair_sting / phone_ring / line_connect / line_drop / amb_rain_city / amb_station / amb_kid_room /
arg_muffled / amb_street_rain / amb_cab / brake_screech / tape_static。缺失走引擎 console.error
静默降级(§6.1 降级方案),玩法与断言不受阻;playtest 静态审计锁定「缺失集合 ⊆ 该清单」,
资产补齐后断言自动收敛。已产出:ui_confirm / ui_menu(程序合成)+ 132 句 voice + 4 首 bgm
(ogg+m4a 双格式零缺失,见静态审计)。
## R2 修复回归(round1 synthesis F1-F5 + V1/V2,2026-09-02)
> 证据源:research/new8/radio/round1/{synthesis,live-player}.md。断言全部落在
> `web/radio/test/playtest.mjs`「R2 修复回归」块 + 主路径/看门狗/静态审计改断;
> **playtest 断言总数 R1=193 → R2=221(+28)**,全绿。
| 项 | 修复(文件 → 行为) | 回归断言(✅=已断言) |
|---|---|---|
| F1 (P1) | `game.js`:`onGesture` 入口 `guide.disarm()` 拆掉等待态看门狗后,无效手势/引擎接管手势分支按态立即重挂——ONAIR/INTRO 链末(`_inStoryWait()` 与 `_onNodeSeqDone` 等待分支同口径)、ASK 空闲、RING 空闲(`_rearmRingWatchdog`)、ENDING 结算态;BRIDGE 本无 guide 看门狗(restTimer 为游戏层),不涉及 | ✅ 链末等待中发无效手势,gd_continue 25s 整准点、gd_again 50s 整准点(旧实现 34.2s 零事件);✅ RING 无效手势后 gd_ring1 仍 10s 准点;✅ ASK 无效手势后看门狗仍 25s 准点 |
| F2 (P4/P2) | 台词表两处同步改词(先 `docs/design/15-radio-voice-ids.md` 后 `tools/gen_voice_dual.py`,两表一致):`rep_low`「今晚的口碑,有点砸了。」→「今晚的口碑,还没立起来。」(零起步语义,与 rep_high「在听众里立住了」同尺度;rep=0 开播前不再读成「砸了」);重生成 `voice/{male,female}/rep_low.{ogg,m4a}`(m=1.92s / f=2.32s,`--only rep_low` → `--convert` → `check_all_assets` radio 242+242 全过:双格式配对、零 0 字节、全可解码、性别目录 id 一致) | ✅ 静态审计锁定 rep_low 字面量存在;live 转写复验归 R2 协议第 2 步(重新生成转写) |
| F3 (P2) | `gen/gen_sfx.py` 增加 `phone_ring` 程序合成:440+480Hz 柔和双音一响 1.8s + 静默 3.4s = 5.2s 循环周期烘进文件(≥4s);无缝 loop 工艺 = 逐声道去直流、**不做首尾淡化**(`save_loop`)、解码回读校验真峰值 ≤-3dBFS(ogg -3.87 / m4a -3.84dBFS)+ 循环头/尾样本 ≤-60dBFS 边界静音;落 `audio/sfx/phone_ring.{ogg,m4a}`。`game.js` RING 接循环垫:引擎 sfx 层无 loop/stop → 游戏侧经 ctx 起循环 BufferSource 持句柄(`_ringSrc`,guide earcon 同款直用总线),接听/菜单/重开 `_stopRing` 即停;移除定时重触发(`ringTimer`/`ringJitterMs`/`RING_INTERVAL_MS`) | ✅ 进 RING 循环垫起播且挂起态虚拟时钟可空闲(零重触发定时器,可复现);✅ 单击接听循环垫即停不拖尾;✅ RING 无效手势不拆铃声垫;✅ 两级铃看门狗用尽后循环垫仍在(永不惩罚);✅ 静态审计 phone_ring 出待补清单(Stable Audio 待补 12→11 件) |
| F4 (P2) | `game.js`:断点续玩恢复后的首句播完再武装跳句——`_resume()` 置 `_resumeGuard`,`_playSeq` 消费为 `_skipLockId`(恢复链首句),该句播完/链完/被新链顶掉即解锁;ONAIR 与 ENDING(live 陷阱场景)两态 tap 生效 | ✅ 恢复后首句(w1_say / e4_a)播完前单击不跳句;✅ 首句播完后跳句重新武装(w1_sayo / e4_b 被正常跳过,链照常走) |
| F5 (P2) | `game.js`:链末句跳过保护——`_isTailSentence()`:当前句为本节点末句且节点把控制权交出(链末等待单击 / `@x` 过场转移 / 直入抉择提示)时屏蔽「跳过本句」;自动链中段(b_open/b_rain、危机拍)与 w1_say 组前两句跳句自由保留。与 F1 同提交复核链末状态机:`_inStoryWait` 与 `_onNodeSeqDone` 口径一致 | ✅ 交接抉择提示前的末句(w1_saye)单击不可跳、完整播出后照常交接 p7;✅ @ending 过场前末句(d9_facee,live 被吃掉的点题句)单击不可跳、完整播出后自动进 ENDING(e1) |
| V1 | 复核结论:pendingChoice 生效时序无 bug(W2 契约实现正确),按裁定补显式断言关案 | ✅ p* 播放中连点不跳句、不清缓存、不提前生效(rep 未动);✅ 播中再滑覆盖缓存(最后一次为准);✅ 生效时序 = p1 播完 → ui_confirm 结算音 → 结果句;✅ 缓存恰好生效一次(rep=1、缓存清空、不丢不重复) |
| V2 | 复核结论:菜单逻辑无缺失(三项循环 + 回绕正确),按裁定补全项循环断言关案。注:菜单为 3 项(sys_menu + sys_menu_resume/restart/lobby,15-radio.md §实现核对「菜单单击循环顺序」口径)——任务书所写「4 项/sys_menu_save」对应菜单标题句 + 3 菜单项;「保存并回大厅」确认句即 `sys_menu_lobby`(无 sys_menu_save 资产) | ✅ 单击循环两个整周期逐项可达(resume→restart→lobby→resume→restart→lobby,第三项不再缺席)、6 次回绕模 3 无漂移;✅ 双击确认「保存并回大厅」→ sys_menu_lobby 播出 + leaving 置位 + 存档落盘 |
V3(arg_muffled/brake_screech 等 11 件 SA3 资产)维持「等待 HF token」,接线代码已备,不在本轮。
**→ 已闭环(2026-09-02 晚)**:SA3 12 件补产落地(含 phone_ring 真铃声替换),接线复核零代码改动,
详见 `research/new8/batch1-assets-report.md` §8。
### 遗留(R2 收尾时需协同)
- `web/harness/voice_text.json` 的 `rep_low` 期望文本仍是旧词「有点砸了」(harness 归属协议/
转写侧,本轮改动手清单之外):R2 协议第 2 步重新生成转写前需同步为新词「还没立起来」,
否则 live 复测该句会 MISMATCH。
- dungeon 款全量回归本轮 FAIL:另一 agent 并行改动 `web/engine/{audio,gesture}.js` 与
`web/dungeon/**` 进行中(失败点为 dungeon 网格/侧墙与 tut 旁白断言,与 radio 无关);
radio与其余 6 款全绿。
## R3 修复回归(round2 synthesis G1-G6 打磨轮,2026-09-02)
> 证据源:research/new8/radio/round2/{synthesis,live-player,p1-blind,p3-office}.md。
> 断言全部落在 `web/radio/test/playtest.mjs`「R3 修复回归」块 + 主路径/中断复述两处顺序断言改写;
> **playtest 断言总数 R2=221 → R3=246(+25)**,全绿。未新增台词/资产(G1 选纯音频标记方案,
> 零 TTS 管线动作;voice_text.json / voice-ids 无改动)。
### G5 显式规则登记(P1 要求,F4/F5 隐式行为升格)
**规则语句**:radio 的跳过公平契约 = **「首播必完整、复播可即掐」**——
- **首播必完整**(公平底线,玩家没听过的内容不许被抢跑):
1. 抉择提示句 p*(voice/p1..p9)首播不可跳(PROMPT_RE 门),播中左右滑仅缓存
`pendingChoice`、句尾当刻才生效(W2 契约零插播;播中连滑取最后一次);
2. 链末关键句(等待单击 / `@x` 过场转移 / 直入抉择提示交接前的末句,如蛋糕句、点题句)
首播不可跳(F5 链末句保护);
3. **ASK 恢复首句例外**:断点续玩恢复链本属局面复述(复播),但其首句仍受 F4 跳句锁
保护——首句播完才武装跳句。锁只锁这一句、播完即释放,**不得升级为永久保护**
(live 局 B + 会话 C 双向验证:恢复链第二句 0.2s 即可跳)。
- **复播可即掐**(效率面,已完整交付过的内容不再强制重听):F4 锁只锁首播一句且播完即释放,
**锁不随复播重新武装**(关菜单局面复述 / 双击重听回放同一句时单击照常跳过);F5 只护交接前
末句本身,链中段句首播复播均可跳;复播期间播中滑动照常预缓存抉择、句尾即生效(V1 断言)。
注:p* 复播(双击重听/看门狗复述)仍不可跳、仍句尾生效——比 P1 建议的「复播即掐即生效」更保守;
R2 live 已将 4 处截断全数归因玩家 tap 且 P1/P3 均接受现状,按代码实际契约登记并断言。
### G1-G6 逐条
| 项 | 选型 / 改法(web/radio/game.js) | 回归断言(✅=已断言) |
|---|---|---|
| G1 帮助链去重 | **选型:纯音频标记(不加新台词)**。「只震+earcon+sys_help 现有句」方案被否——sys_help 为单文件 26 字整句,无「首句」子资产,重播即逐字重复,恰是待修问题。实现:`_helpSeen`(Set,实例级,不跨实例持久化,sys_skip 每实例一次同款口径)按**等待点**键控(ONAIR/INTRO 按 nodeId、BRIDGE 整段、ENDING 按结局、菜单整段);`_helpSpokenOnce`:同键第 1 次全量口播,第 2 次起 `vibrate('beat')` + nudge earcon(复用统一 8 音词表零新增;guide 无公开单发 earcon API,经 `_playEarcon` 直用,`_startRing` 直用引擎总线同款先例;guide=off 时只震)。状态句照播(复述状态=合法帮助,P1 先例);新等待点首滑仍全量口令,非全局静默 | ✅ 同一等待点第 2 次上滑不重播 sys_help(ONAIR/ENDING 两态);✅ 状态句照播(prog/rep 各 2 次);✅ beat 轻震标记;✅ nudge earcon;✅ 新等待点(z1_a)首滑仍全量口令 |
| G2 rep_low warning 震 | R1 P3-3 遗留。原实现 `enterBridge` 跨档分支 `vibrate('warning')` **实际不可达**(rep 唯一减分组合 p1右+p2右 从 0→-1 不跨档,`lastAnnouncedTier` 初值即 low)。修:`_tierCue()` 挂在**全部 rep_low 播报位**——RING/ONAIR/BRIDGE/ENDING 四处上滑复听,rep_low 整句出口同时手腕有感(BRIDGE 跨档分支保留不动) | ✅ RING 上滑复听 rep_low 附 warning 震(live 局 B t=198.5/497.5 证据位);✅ ONAIR 上滑同款;✅ ENDING 上滑同款 |
| G3 k1_a/k1_b male 回退 | **核查结论:回退正常,非静默,不修。** k1_a/k1_b 为糖糖 NPC 平铺组资产(设计即单音色 Vivian 童声、不做双性别,§5.3);male 设置下引擎候选链 `voice/male/k1_a.{ogg,m4a}`(404,符合预期)→ `voice/k1_a.ogg`(200)命中平铺版播出(web-engine-api.md voice 路径解析)。**文档注记**:live 局 B 观测的「同通电话声线跳变」实为主播声部(随 settings.voice 换声)与糖糖童声(恒定)在同通内交替,属「电话音质感」设计使然;不补双性别资产(违反 NPC ≤5 音色约束与 §5.3 设计) | ✅ male 先尝试性别目录 male/k1_a、k1_b(404 属设计);✅ 平铺版播出(voicePlays 各 1);✅ 零「资产缺失」console.error(真静默则修——实测无需修) |
| G4 空点确认轻震 | R1 P3-1 / R2 P3-4 遗留。不可跳句(p* 首播 / F5 尾句 / F4 锁)上的单击「点了但没选上」,gesture 层确认震 [20] 游戏侧无法撤销(engine/gesture.js 不在本轮改动清单)——游戏层补一记最轻 beat [8] 形成可区分签名(20→8 连击 ≠ 真确认的单记 20),消解「像点上了」误导;落点 `_onStoryGesture` / `_onEndingGesture` 两处 tap 屏蔽分支 | ✅ F4 锁内单击 beat 震;✅ F5 尾句单击 beat 震(「p9 后空点」证据位);✅ p* 不可跳句单击 beat 震(R1 P3-1 证据位) |
| G5 显式规则 | 纯登记 + 断言,无代码改动(F4/F5/V1 现有实现即规则本体);规则语句见上节。新增断言锁「锁不随复播重新武装」(复播可即掐的反向面:保护不得扩大化) | ✅ ASK 恢复首句锁内单击不跳(例外);✅ 复播(关菜单局面复述)首句单击照常跳过;✅ 链末关键句首播不可跳(F5 引用);✅ p* 首播单击不跳(引用);(F4/F4b/F5/F5b/V1 既有断言为规则其余各半) |
| G6 状态链重排 | P1 两次建议、纯排序改动 → 本轮做。`_withHelp()` 把帮助句接在状态链尾:「当前通号 → 口碑 → 帮助」。ONAIR/INTRO(有状态时 prog → rep → sys_help)、BRIDGE(下一通 prog → rep → sys_help)、ENDING(结局主句 → rep → sys_help);RING 本就状态在前且无帮助句(§2.3 RING 行),天然满足;MENU 链无通号/口碑状态句,G6 不适用(仅 G1 去重,帮助保持在前)。§2.3 表内「帮助+状态」措辞由本节登记更新(状态在前),正文不改 | ✅ ONAIR 顺序 prog_c1 → rep_low → sys_help;✅ BRIDGE 顺序 prog_c2 → rep_mid → sys_help(原断言改写);✅ ENDING 顺序 e4_a → rep_low → sys_help;✅ 中断复述 ONAIR 顺序 prog_c3 → rep_mid → sys_help(原断言改写) |
### 顺带补的 SA3 音效接线断言
主路径补 `sfx/onair_sting`(开场台标)/`sfx/line_drop`(幕间挂断)/`sfx/tape_static`(p9 直面)
接线计数断言(arg_muffled ×2 / brake_screech / phone_ring 已有);静态审计
`STABLE_AUDIO_PENDING` 注释更新为「SA3 12 件已全部补产,清单降级为守卫」。
### 观察项(按裁定不动代码)
- 复播晚点 +7.6s 一次性事件(live P3-3):一次性观测,未加断言盯防(避免为仪器怪癖加约束)。
- mid-prompt 生效掐选项句尾(live P3-6):设计备注维持,G5 规则已按现状登记。
- harness 丢 tap 仪器怪癖(live P3-5):记入 R3 协议(tap 后 listen 一次确认再重发),非游戏侧。
### 遗留
- 无新增遗留。G1 去重集合为实例级(重进游戏首滑仍是完整口令)——如 R3 复评希望跨实例
记忆(localStorage 持久化「已听过口令」),列 v2 候选,本轮不加(保持与 sys_skip 每实例
一次同口径,避免老玩家重进被静默)。