# 26-apply · 《暗码》重构施工单(26 号文档落地) > **设计单正文见 [19-cipher.md](19-cipher.md) 附录 C**(一句话核心动作 / 状态 / > 手势表与解锁 / 状态×手势矩阵 / 等待点 / 首 3 分钟脚本 / 失败表 / 台词账目 / > 断点复述),本施工单不重复,只写:实现接线(§1)、台词类别映射(§2)、 > 偏差记录(§3)、测试记录(§4)。交互规则权威来源: > [26-audio-interaction-playbook.md](26-audio-interaction-playbook.md)、 > [00-global-ux.md](00-global-ux.md)。台词登记 [19-cipher-voice-ids.md](19-cipher-voice-ids.md) > R4 节;清单 `tools/voice_lines_26/cipher.md`。 > 范式对齐 [26-apply-storyline.md](26-apply-storyline.md) / [26-apply-radio.md](26-apply-radio.md) > (叙事型主参考;OP/WIND 双音色组与 radio 的 HOST/PROD 同构)。 > 本次只动交互层:电文生成(4 封固定字符)、摩尔斯运行时合成(morse.js)、暴露度 > 隐藏账本、4 结局、OP/WIND 双音色一概不动。 ## 1. 实现接线(引擎六模块,对齐 storyline/radio 参考) - **装配**(game.js 构造器 + index.html):`this.receipts / this.floor / this.confusion / this.invalidHints(= new InvalidHints().bind(floor))/ this.ladder` 全部暴露; `audio.setRateBoostEnabled(false)`(长按不是玩法、也不触发旁白快进;摩尔斯 u 恒定 不受 settings.speed)。index.html 照 storyline/radio:`makeInputPipeline({ floor, confusion, receipts, invalidHints, game, ladder, opts: game.PIPELINE_OPTS || {} })` + `attachGestures(root, pipe, { doubleTapWindow: 0 })`——叙事+解码型无核心玩法手势 opt(doubletapCore/swipeCore/longpressCore 均 false),双击未解锁折算单击由管道 新手门承担。 - **onGesture 收口**:返回 `'ok' | 'hold' | 'invalid' | 'silent-ok'`(摩尔斯即内容音 的操作给 hold 附 ok 回执;无 silent-ok 档位用法——全部操作都有回执或内容音)。 `stateName()` → menu / ask / decode / confirm / settle / story / choice / night; `invalidHint(state, g)` → locked:up=inv_up_locked、locked:down=inv_down_locked、 locked 其余=gd_invalid;left/right:menu=null(切项合法)、settle=sys_hint_again_full、 其余=inv_nosel;默认 gd_invalid。 - **floor 状态流**:叙事/提示链 `floor.speak`(critical:sys_first_tap、dv1_teach、 揭晓/收尾链首句 dv1_right / d9_send / d9_send_e / d9_caught_e / d9_sleep / d9_sleepe ——判定出口 800ms 手势死窗的 26 号等价物:抢拍 tap 给 busy 回执不吞揭晓)。 摩尔斯排程 = BUSY(手势 busy 回执,G4 首播保护的等价物):RX 首播 `_rxGuard` 保护窗 (floor BUSY + 作废令牌)、T4 danger 拍前置 2s(step_halt 渐强 + danger 签名/震同窗, 直置 floor.state,见偏差 7)、两段确认电文重播(confirm 等待点在挂但摩尔斯可先起)。 等待点 8 个全配 full/short/silenceMs/expected/onDemo(19 号附录 C §5 表 + ask/menu)。 - **morseGate(19 号 §3.4 R1-R5)**:`_busyVoice() = floor.speaking || audio.isVoicePlaying()`(整条链含句间空隙都算语音在播),在播则挂队 150ms 轮询补发; 候选预听链 = 候选摩尔斯(620Hz / u=110 / 字母隙 5u 对照档)→ 静默一拍 ≥500ms → cand_* 词义句(给耳朵留对照时间);转写账本 `morseLog`(kind/word/preset/逐元素 at/endMs,被掐断时收敛 endMs,零重叠断言口径)。 - **BGM dip(R3 + §6.1)**:引擎 duck 独占 _bgmBus,游戏层在 _bgmBus → _bgmPrefBus 之间插私有增益节点:摩尔斯排程期间 ×0.5(−6dB,排程到 endAt 自动恢复)、MENU 同曲 低响度 ×0.4,复合相乘。 - **confusion**:信号由管道喂(含锁定手势,2026-10 修复后锁定门在 record 之前); `onConfused → 当前等待点 takeover(lvl)`(引擎 takeover(1/2) 已带 30s 台账去重, 游戏侧零自研路径);双击重听/再说一遍喂 recordReplay;L3 `onDemo` 分档:intro/ continue 替玩家走一步、choice 只复述不替选、decode = 慢放电文 0.5× 一遍(u=160、 字母隙 6u)+ T1 附免费教学页(hint_t1,不扣页数,Prime→Teach 兑现)再交还浏览。 - **novice 阶梯**:T1 译对 = `ladder.onGoal()`(阶段 1),返回的 teachLine **已带 voice/ 前缀**(2026-10 novice.js 修复,直接进 floor.speak,不要再加前缀)挂 dv1_right 揭晓链尾;一局结束 = `ladder.onGameDone()`(阶段 2/3),教学句挂结局链尾, 一局只报一次(_gameDoneCounted);第二夜起 `goalMet = ladder.stage() >= 1` 跨局 记住不重教(做对过的事不教第二遍,原则 11)。 - **存档(§8.1)**:schema 1、summary 人话必带(`第{N}封·{名} · 暴露:{档} · 结局{k}/4` / 收台 `已收台·{结局名} · 结局{k}/4`);写入时机 = 每封 RX 进入 / NIGHT 进入 / 每个抉择结算后 / ENDING 进入 / 菜单「保存并回大厅」。断点回来 5s 内 sys_continue_ask → 单击恢复(DECODE 恢复链 prog_t → book 档 → 重挂浏览 + 自动 预听当前候选,30s 台账内不重复;gd_tune2 已停用)/ 右滑新夜班。 - **环境音**:静电底噪 static_bed(RX/DECODE)与值班室 amb_room 为游戏层 buffer loop(sfx 层无 loop API,持句柄起停;无头测试 ambLoopEnabled=false 防周期定时器 不排水)。 ## 2. 台词类别映射(瘦版;全量登记见 19-cipher-voice-ids.md R4 节与 `tools/voice_lines_26/cipher.md`) - `critical`:sys_first_tap(开场 ≤3s 唯一一次指令)、dv1_teach(改写版「向右滑, 听下一个词。」,19 号附录 C §6)、揭晓/收尾链首句(dv1_right、d9_send、d9_send_e、 d9_caught_e、d9_sleep、d9_sleepe——判定出口死窗等价物,首句 <3s) - `content`:b_open/b_goal/b_chief(移驻 NIGHT(1) 链首)、n1..n3、rx1..4_in、rev_*、 r*/m*_chief_c/g9_*/p2..p9/r1a..r4b、hint_t1..t4、cf_confirm、sys_turn_teach、 sys_unlock_*(阶梯教学)、sys_menu、sys_menu_resume/book/restart/lobby、 sys_new_night、prog_t1..t4、book_3..0、exp_calm/low/mid/hot、end_c1..c4、 e1..e4 主句 + ep_*(OP/WIND/处长按 19b 原组) - `hint`:sys_hint_continue_full/short、sys_hint_choice_short、sys_hint_again_full、 gd_tune1 / gd_tune1_short、cf_hint_full/short、sys_only_choice、sys_demo_continue、 sys_demo_choice、sys_continue_ask、sys_skip - `hint 前缀`:sys_say_again「再说一遍:」(主动重听,全项目一句) - `feedback`:inv_up_locked、inv_down_locked、inv_nosel、gd_invalid(改写版补 「单击,继续。」)、dv1_wrong(T1 译错即时归因真话,摩尔斯写死句内) - 音色组:系统/交互层句子全部走 **WIND 北风组**(北风电台=上线联络+系统音,radio PROD 同型);剧情句沿用原组(OP 报务员 / 处长 NPC 平铺)。 - 账目:新增 20 / 修改 3 / 删除 10,主句 132 → **142**;`_k` 变体 11 → 24 对 (`web/harness/voice_text.json` cipher 主表已同步 142 条)。 ## 3. 偏差记录(与 26 号/19 号附录 C 的落地差异及理由) 1. **DECODE 左右滑切候选返回 hold(附 ok 回执)而非设计单的 silent-ok**:管道无 「silent-hold」档——silent-ok 会被视为完成当前等待点(摘掉浏览点,三档提示计数 清零),而切候选没有完成这个点;取 hold 保住等待点,回执由摩尔斯预听的内容音 + ok 双保险(§4.2「已有明显内容音」的反向适用)。 2. **上滑状态链三句(prog_t → book 档 → exp 档),无「动作」尾句**:19 号附录 C §3 写「第几封。密码本余页。暴露档。左右滑听词。」——落地与 radio 同型(动作由等待 声/等待点提示承担,原则 3「一句话」);DECODE 态要听词时的动作指令由 decode 等待点三档提示(gd_tune1 / gd_tune1_short)就近给。 3. **无 sys_praise「对。」**:19 号附录 C §6 ≈25s「『对。』dv1_right」——首正反馈由 dv1_right 本句(「货船,译对了。」)+ success 震承担,不再单设肯定句(避免 「对」与「货船,译对了」连播两拍;radio 的 sys_praise 是其主播线无归因句所致)。 4. **开场叙事移驻 NIGHT(1) 链首**:§4.10 首夜第一句只剩 critical 指令 + 状态教学, b_open/b_goal/b_chief 移驻 NIGHT(1) 链首补全叙事、sys_skip 每夜一次挂链尾; b_rule(规矩念白=重听承诺,随双击教学废止)、gd_brief、dv1_teach2、dv1_ask 停用 (dv1_ask 仍留在 TELS 数据源但 T1 走 dv1_teach 改写版,不可达)。 5. **阶段 3 双击语义分态**:19 号附录 C 只写「重听电文原速;再请求=慢放」——落地: DECODE = 重听电文(dtCount 连击:第 2 次起 0.5×,其他手势重置);叙事/菜单态 = 等待点 requestReplay(「再说一遍:」+完整句)或无等待点时重播当前节点(NIGHT = 重播夜链、ENDING = 重听结局链);RX 态 invalid(引入链一句在播,等待点不存在)。 6. **翻密码本入口 = 菜单项(阶段 2)+ 迷茫 L3**:长按捷径废止(长按 → invalid + gd_invalid 解释一次);19 号 §7「页数用尽再翻 = paper_page + 无语音轰炸」照旧; 非破译态选「翻密码本」= gd_invalid(feedback 通道,话轮保住)。 7. **danger 拍前置 2s 直置 `floor.state = 'BUSY'`**:引擎无「演出 BUSY 定时」公开 API(floor 只有 speak/yieldTurn/enterAction),游戏层直置 + seqToken 作废令牌, 2s 后 _speakPayoff 接管(代码注释已标;dungeon 同款直置先例)。**引擎建议**: floor 补 `busyFor(ms)` 公开演出窗 API。 8. **摩尔斯首播保护窗**:`_rxGuard` + floor BUSY(管道路径 busy 回执)双保险; 会话/直调(transcribe 的 onGesture 直调不走管道)由 _rxGuard 返回 silent-ok 兜底不推进状态(G4 轻震等价物移到回执层)。 9. **intro 等待点 full=null**:指令句刚说过(§4.10 找猫示例同型),静默第一档直接 short(sys_first_tap_short);第二夜起 newNight 只播 sys_new_night 不再重播 sys_first_tap。 10. **menu 等待点 short 复用 gd_tune1_short「左右滑。」**:菜单三档提示省一句资产 (full=sys_menu 报幕句刚播过,30s 台账自动降档;两处「左右滑」语义一致)。 11. **confirm 等待点 onDemo = 替确认(_settleDecode)**:迷茫兜底要能推进(19 号 附录 C §5「替确认」);choice 的 onDemo 不替玩家做有后果的抉择(sys_demo_choice 复述两向后交回);settle 的 onDemo 只给提示音(不替玩家开新夜班)。 12. **m4_chief_a/b 登记未接线**:19b M4 组的处长试探 ①/② 在 26 号改造前(HEAD)即 未进节点表(rev4 → @p8 直连),本轮维持现状不动玩法;台词资产维持登记不删。 13. **乱点机器人白名单口径(§6.2 被动原句重复=0 的「主动查询」判定)**:a)「再说 一遍:」前缀后的完整句;b) 玩家 8s 内主动操作索引的句子(候选词/状态句/ dv1_wrong/dv1_right/cf_confirm/hint_t/rev_ 归因与确认句)。 ## 4. 测试记录 - 无头 playtest:`tools/node/bin/node web/cipher/test/playtest.mjs`,**16 段、断言 191 条全绿、退出码 0**(2026-10-02;`game.floor.bedEnabled = false` 关等待声周期 定时器,mock 口径 2s/句)。覆盖: 1. 开场 §4.10 + RX(1):第一句 critical 指令(抢拍 tap=busy)、3s 内动手、 gd_brief/b_rule/gd_tune2 零播出、首候选预听 ≤12s、BAY 编码/时值逐元素 (§2.2 表)、R3 摩尔斯 dip 0.5、首播保护窗 tap/left=busy; 2. DECODE 浏览 / 两段确认 / T1 译对:右滑 ×3 循环候选、确认取消回浏览、揭晓首句 critical、success 震、sys_unlock_up 挂链尾、抉择点「再说一遍:」+ 完整句; 3. T1 译错即时归因 + 无限重选零惩罚(expose 不动、t1 恒收敛); 4. 剧本 A(E1 全对全右 p9 左,guide=full):NIGHT(1) 开场叙事移驻各一次、四封 编码逐元素断言(TOP/HAND/SPY)、danger 拍(step_halt+签名+震同窗)、ep 逐封 点名 flags 单源、end_c1 分档计数、summary、手势数 ≤170(实际 24)、第二夜 不重播 sys_first_tap; 5. 剧本 B(E2)/ 剧本 D(E4,全译错全左 endingPre=8)playNight 驱动; 6. 剧本 C(E3):暴露度逐 NIGHT 验算(0→2→5→8,§5.2 单源表)、跨档播报 exp_low/exp_hot 各一次、warning 震、同档零重复; 7. 手势阶梯:locked 首次解释一次(原则 7)、未解锁双击折算单击、上滑状态链 G6 顺序、sys_help 零播出; 8. 三档提示:decode 10s/25s、continue 13s/30s、choice 25s(台账降档直接 short) /50s(>30s 给完整句)、两级用尽即停; 9. 迷茫接管:L1 短句 → L2 只放行 expected(L3 白盒 takeover(3):慢放 u=160/ 字母隙 6u + T1 免费教学页不扣页数,演示后重挂浏览); 10. 菜单(seed 阶段 3):左右滑循环换项、上滑重听说明+设置提示、下滑关闭回 DECODE(局面复述 + 30s 台账内自动预听不重复)、「翻密码本」= 回 DECODE 提示 流程(T1 免费页 + paper_page)、「重新开始今晚」= 新夜班、「保存并回大厅」= leaving + 存档;MENU 同曲低响度 dip 复合断言; 11. 阶段 3 双击:重听电文原速 → 再请求慢放 0.5×(字母隙 960ms 时值断言)、 切候选重置连击; 12. 断点复述:预置存档 phase=DECODE(tel=3/hints=1/cand=1)→ sys_continue_ask → 单击恢复(数值全还原、恢复链 prog_t3 → book_1、gd_tune2 零播出、自动预听 SAND);右滑 = 新夜班标量重置; 13. §6.2 乱点机器人(120 步、seed 42):无声操作 0/114、第一次操作 2120ms ≤10s、 第一次成功 2120ms ≤20s、打断失败 0、被动原句重复 0(白名单口径见偏差 13); 14. morseGate:全剧本摩尔斯起振区间与 voice 零重叠、候选词义句全部 ≥500ms 接播; 15. 资产缺失:屏蔽 rev_top 仅 console.error/warn,Floor 单句哨兵续链不卡死仍到 e1、 gd_tune2/gd_prog1 零播出; 16. 键盘变体(input=key:gd_tune1 走 key/ 目录)与语速边界(speed=2.2 首播 u 仍 80ms)。 - 玩家感知转写:`web/harness/sessions/cipher.mjs` 已同步 26 号口径(开场第一下 tap、 主动重听、上滑状态链、走神听三档、结局后下滑逛菜单、`floor.state` 链级门控, radio 范式),`tools/node/bin/node web/harness/transcribe.mjs cipher`(含 `--session sessionStress`)两剧本零死锁跑通(主线虚拟 174s 到 E1 并开第二夜 / 乱按 416s:解码 12 步、5 抉择、乱试 66 次、逛菜单 11 次仍到结局)。 `web/harness/voice_text.json` cipher 主表同步 142 条。 - 待生成资产:新增 20 + 重录 3(`tools/voice_lines_26/cipher.md`,主会话统一生成; playtest 对缺失 voice 资产按可用 mock,与 TTS 生成解耦)。 - 真人冷启动:待测(26 §6.3,3~5 人)。