15 · 《不眠频率》(radio) — 深夜电台热线叙事 RPG 设计文档
给开发的实现要点摘要
- 叙事模式(声命线范式):节点链 onComplete 串行 + seqToken 打断作废 + pendingChoice 缓存;复制
web/storyline/ 结构起步,状态机用 6 宏状态(INTRO/RING/ONAIR/BRIDGE/ENDING + MENU 覆盖层)。
- 手势零豁免:全部走全局默认映射(单击=继续/接听/跳句、双击=重听、左右滑=抉择、上滑=状态、下滑=模态菜单 §5.2 约定、长按=引擎快进)。
- 台词依据 15-radio-voice-ids.md(132 句:双性别 103 + 平铺 NPC 29);NPC 人声电话滤波在资产侧 ffmpeg 带通(storyline n03_msg 先例),引擎零改动。
- 向导层:storyline 同款看门狗节奏 + RING 专用两级新句提示;
gd_ring1/gd_ring2 为本游戏新增 gd_* 资产。
- 章程任务 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/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 全局骨架(来电共享主干,分歧局部化)
分支纪律:每通电话内部分支、通话末尾收敛到导播收尾句 → BRIDGE;跨来电只传三个数据(rep 数值、bond 好感、heardArgument 布尔),不生成组合节点。最坏路径与最好路径节点数相同。
3.2 分通结构(◆=抉择点,〔〕=条件差分)
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'})
- 写入时机:每通电话 RING 进入、每个抉择结算后、ENDING 进入、菜单「保存并回大厅」。
- 断点续玩:登录/本地读到存档 →
sys_continue_ask(单击=继续上次 / 右滑=新夜班);继续 = 恢复 phase+node 并重播当前节点链 = 一句话局面复述(L5:恢复成本游戏承担);重进 ≤5s 可玩。
- summary 字符串格式:进行中
第N通·<来电者> · 口碑±X · 结局K/4;已收播 已收播·<结局名> · 结局K/4(大厅卡片直接显示,人话)。
9. playtest 断言清单(登记 13 号文档 radio 节,格式同 §1)
关键路径(E1 全程,≤60 次手势):
- 启动遮罩点按 → INTRO 链(b_open 起,voice 层有播放记录)→ 链完自动进 RING(c1),turn 事件与铃声断言。
- RING 单击接听 → ONAIR(c1):p1 左 → p2 左 → z1_ok1 → BRIDGE(rep 断言 +1+2=3)→ RING(c2)。
- 依序走 p3 左 / p4 右 / p5 左 / p6 左 / p7 左 / p8 左(断言每步后 rep 与 §4.2 一致;p4 右后
heardArgument === true 且 C4 播 w1_say 组)。
- 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)
- 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 看门狗。
- longpress 误报无效:gesture.js 把 longpress/longpressend/tapcancel 派发进 onGesture,RING/
BRIDGE/ENDING/MENU/ASK 的 default 分支把它们当无效手势报 gd_invalid/error 震——§2.3 长按行是
「引擎接管快进」,全状态合法。修复:仅 left/right(ASK 另含 up/down/doubletap)报无效。
- 菜单单击循环的引擎队列竞态:
audio.play(item, {queue:true}) 在引擎 _finishVoice 中先跑
onComplete(游戏链播下一句)后 shift 队列,排队报名会把 sys_menu→sys_menu_resume 链播句顶掉。
修复:菜单报名改游戏侧 _menuQueue + _drainMenuQueue() 排水,当前报名播完接播、零截断。
- RING 双击首通不报无效:INTRO 链节点完成会写
lastCloser,首通双击重听了 b_goal 而未按开发
注记「首通前无上一通 → 无效手势零静默」报错。修复:callN <= 1 时报无效。
- ENDING 上滑缺口碑:§2.3 ENDING 上滑=「帮助+状态(结局名+口碑)」,原实现只播 sys_help+结局
主句。修复:链尾补
TIER_VOICES[tierOf(rep)]。
- stop() 后中断链复活:被 stopVoice 掐断的
_playSeq 链可经 30s 单句看门狗「资产缺失」路径续链,
已拆除实例会走到 enterRing 留下无人接听的永久铃声定时器。修复:stop() 先 seqToken++ 作废在途链。
- newNight 不清抉择武装:菜单重开/结算重开到新夜班首链起播前,choiceArmed/pendingChoice 残留,
窗口期手势会在旧节点上误抉择。修复:newNight() 显式清零。
- _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 款全绿。