Files
SoundGame/docs/design/15-radio.md
T
xsl 24c526af68 feat(assets): SA3 音效补齐 radio 11+1 与 dungeon 13源+15pan(token 走 hf-mirror 实测可用)
- 模型 17/17 文件经镜像全量缓存(gated+token,直连本机不通;sfx.md/setup.md/asset-pipeline.md 镜像结论同步更新)
- 修复迁移遗留:stable_audio_3/models/ 源码子包被 .gitignore models/ 规则误伤丢失,从上游恢复并加白名单
- radio:arg_muffled/brake_screech 等叙事件落地(接线原有 L111/L154 零改动)+ phone_ring SYN→SA3 升级;dungeon 13 源 A/B 对齐(RMS 偏差 ≤0.3dB)全量同名替换,SYN 原版备份 tools/asset_fix_backup/sa3_ab_20260902/
- 关键玩法语义达标:echo 距离档 0.83<1.43<2.18<2.98s 严格递增;one-shot 真峰值全 ≤-3dBFS(解码回读法)
- check_all_assets 8 游戏全绿(radio 254/254、dungeon 303/303);playtest radio 221/dungeon 1610 全绿
2026-09-02 19:35:38 +08:00

43 KiB
Raw Blame History

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(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/`
导播/系统/向导 PROD 双性别(female=Vivian / male=Dylan + PROD instruct 常量) `voice/female male/`
来电者 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'})

{
  "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 款全绿。