feat(playbook): 26 号交互手册全量落地——剩余 12 款深度重构 + 语音全量重制

storyline/escape/simon 首批之后的推广收官:15 款全部按 26 号模式运行
(发言权/回执/三档提示/迷茫接管/手势阶梯/断点复述),旧「双击重听/格挡/
菜单确认」等反模式口径全线废除,开场零规则念白(首句即动手)。

游戏层(12 款):rpg(格挡=前摇窗口内再单击)/ rhythm / space / radio /
dungeon / abyss / detective / cipher / teahouse / keeper / bridge /
hunt(联机切片):
- 每款 game.js 接引擎六模块 + makeInputPipeline(doubleTapWindow:0);
  四向玩法类 swipeCore + 长按帮助;双击是玩法的(space 护盾 / hunt
  旋风斩)按手势阶梯阶段 3 分档恢复双击窗口(doubletapCore)
- 每款 playtest 重写:玩法断言 + §6.2 乱点机器人(无声操作 / 被动
  原句重复 / 打断失败 = 0);15 款 3900+ 断言全绿
- 12 份 docs/design/26-apply-*.md 施工单 + tools/voice_lines_26/
  台词清单 + voice-ids 文档同步 + 转写会话剧本按新口径重写

引擎层(web/engine/,6 处修复):pipeline 增 doubletapCore;novice
teachLine 带 voice/ 前缀(此前教学句被静默丢弃);confusion.record
前置到新手门(锁定手势的乱点也计信号);InvalidHints 播完恢复解释前
状态(根治孤儿 BUSY);WaitPoint takeover(2) 台账去重;台账只在
speak 真正播出时登记。遗留引擎债登记 roadmap §3.1。

语音资产:台词表合并(新增 ~250 系统句 / 改 ~50 旧口径 / 删 ~110
废止句;合并器 tools/merge_voice_lines_26.py + 引用校验器
tools/validate_voice_refs.py),GPU 重录 1536 wav → 4204 ogg/m4a
落地,check_all_assets + validate_voice_refs 双绿;键盘变体同步扩表。

大厅/文档:15 款统一新交互口径(「新交互」徽标退役);README /
HANDOVER / rpg README 旧口径修正;roadmap P0-P3 置完成。
This commit is contained in:
xsl
2026-10-02 12:33:20 +08:00
parent 9e4d67aa84
commit ee5d4d2544
6992 changed files with 30127 additions and 20451 deletions
+28 -22
View File
@@ -1,7 +1,9 @@
/**
* 《剑与声》新手会话剧本:听完序章进村 → 村里把选项听一圈 → 出村去小路
* (顺路调查一次捡钱)→ 岔路口转去溪流触发教学战 → 完整打一场
* (教学四步:识招→攻击→格挡→防御,再自由击杀),战后收工。
* 《剑与声》新手会话剧本(26 号重构版):critical 开场指令「点一下屏幕」→
* 序章 → 进村把选项听一圈 → 出村去小路(顺路调查一次捡钱)→ 岔路口转去溪流
* 触发教学战 → 完整打一场(四步:识招→攻击→格挡[窗口内单击]→防御,再自由
* 击杀),战后收工。26 号口径:格挡 = 前摇窗口内**再单击**(旧「双击格挡」废止);
* 新手期无双击重听(上滑=状态、菜单=通关第二章解锁,此处均不使用)。
*
* 种子 RNG:让教学狼首击必不掉(HP 6、伤害恒 5),保证四步教学完整走完。
*/
@@ -19,37 +21,42 @@ export function describe(game) {
return `战斗中:你的血量${BAND[game._hpBand(game.player.hp, game.maxHp)]},`
+ `敌方血量${BAND[game._hpBand(b.enemy.hp, b.enemy.maxHp)]},等你选招式(左右滑切换,单击出手,下滑逃跑)`;
}
if (b.state === 'ENEMY_TELEGRAPH') return '战斗中:敌方前摇声响起了——双击,格挡!';
if (b.state === 'ENEMY_TELEGRAPH') return '战斗中:敌方前摇声响起了——点一下,格挡!';
return '战斗播报中,先听着';
}
switch (game.phase) {
case 'ask': return '检测到旅程记录:单击继续,右滑开新档';
case 'intro': return '序章旁白在播,讲我怎么用耳朵走路';
case 'explore': return `【${SCENE_NAME[game.scene] ?? game.scene}】:左右滑切选项,单击确认,双击重听,上滑查状态,下滑菜单`;
case 'intro': return game._introTapped ? '序章旁白在播(单击可跳句)' : '开场指令在播:等风铃响起点一下';
case 'explore': return `【${SCENE_NAME[game.scene] ?? game.scene}】:左右滑切选项,单击确认,上滑查状态`;
case 'shop': return '商店里:左右滑看货,单击买,下滑离开';
case 'menu': return '菜单里:左右滑切选项,单击确认';
case 'menu': return '菜单里:左右滑切选项,单击确认,下滑关闭';
default: return '……';
}
}
export async function session(ctx, mock) {
const { game } = ctx;
const CMDS = ['attack', 'skill', 'defend', 'item'];
// 开场:critical 指令「点一下屏幕。」播完(风铃 + 等待声)→ 第一下单击
await ctx.waitCond(() => game.floor.state === 'YOUR_TURN');
ctx.status('开场指令听完了,点一下');
ctx.note('第一句就让我动手,还给了「轮到你了」的一声风铃');
await ctx.gesture('tap');
// 序章听完 → 进村
await ctx.waitCond(() => game.phase === 'explore');
ctx.status('进村了,先听场景描述和选项');
// 村里(选项:去小路/商店/休息/听状态):把选项听一圈,回绕到首项再出发。
// 确认(tap)前必须等选项名念完——voice 播放中单击是「跳过本句」,会把手势吃掉
// 确认(tap)前等选项名念完——报名在播时单击是「跳过这句」,会慢半拍
for (let i = 0; i < 3; i++) {
await ctx.gesture('right');
await ctx.waitVirtual(1800); // 听选项名
}
ctx.status('选项听了一圈');
await ctx.gesture('right'); // 从末项回绕到首个选项——听到「回到第一个」(round2 F7)
await ctx.gesture('right'); // 从末项回绕到首个选项——「回到第一个」
ctx.note('滑到头再滑就兜回来了,有人提醒「回到第一个」,不用自己数');
await ctx.waitCond(() => !ctx.voiceBusy()); // 选项名 + 回绕提示念完
await ctx.waitCond(() => !ctx.voiceBusy());
await ctx.gesture('tap'); // → path
// 小路(去岔路口/调查/返回):先调查一次捡钱,再左滑回「去岔路口」
@@ -59,8 +66,6 @@ export async function session(ctx, mock) {
await ctx.waitVirtual(1800);
ctx.note('翻找了一阵,捡到一点钱');
await ctx.waitCond(() => !ctx.voiceBusy());
await ctx.gesture('tap');
await ctx.waitCond(() => !ctx.voiceBusy()); // 捡钱播报念完
await ctx.gesture('left'); // 回「去岔路口」
await ctx.waitCond(() => !ctx.voiceBusy());
await ctx.gesture('tap'); // → fork
@@ -72,10 +77,11 @@ export async function session(ctx, mock) {
await ctx.waitCond(() => !ctx.voiceBusy());
await ctx.gesture('tap'); // go_stream → 溪流 → 触发教学战
// 教学战:四步(识招 → 攻击 → 格挡 → 防御)+ 自由击杀
// 教学战:四步(识招 → 攻击 → 格挡[窗口内单击] → 防御)+ 自由击杀
await ctx.waitCond(() => game.phase === 'battle' && game._battle?.state === 'PLAYER_SELECT');
ctx.status('遭遇教学狼,战斗开始');
const CMDS = ['attack', 'skill', 'defend', 'item'];
const selectCmd = async (cmd) => {
const b = game._battle;
while (b && b.cmdIdx !== CMDS.indexOf(cmd)) {
@@ -91,17 +97,17 @@ export async function session(ctx, mock) {
await ctx.waitVirtual(1300);
ctx.status('招式名听熟了');
// T1:攻击一次
// T1:选攻击,单击出剑
await ctx.waitCond(() => game._battle?.state === 'PLAYER_SELECT');
await selectCmd('attack');
ctx.note('单击出手,攻击');
await ctx.gesture('tap');
// T2:格挡(前摇窗口内双击)——挡住会先听到「挡得漂亮」(round2 F4)再进防御教学
// T2:格挡(前摇窗口内再单击)——先听「它要扑了,听前摇」「就是现在,点一下挡住」
await ctx.waitCond(() => game._battle?.state === 'ENEMY_TELEGRAPH');
ctx.status('前摇声响起了');
ctx.note('双击格挡——这回干净挡下了,听到「挡得漂亮」');
await ctx.gesture('doubletap');
ctx.note('前摇一响就点一下——这回干净挡下了,听到「挡得漂亮」');
await ctx.gesture('tap');
// T3:防御一次
await ctx.waitCond(() => game._battle?.state === 'PLAYER_SELECT');
@@ -109,14 +115,14 @@ export async function session(ctx, mock) {
ctx.note('这回合先防御,减半伤害');
await ctx.gesture('tap');
// 自由阶段:攻击到狼死(中间照常听前摇、双击格挡)
// 自由阶段:攻击到狼死(中间照常听前摇、单击格挡)
for (let i = 0; i < 8 && game.phase === 'battle'; i++) {
await ctx.waitCond(() => game._battle?.state === 'PLAYER_SELECT'
|| game._battle?.state === 'ENEMY_TELEGRAPH' || game.phase !== 'battle');
if (game.phase !== 'battle') break;
if (game._battle.state === 'ENEMY_TELEGRAPH') {
ctx.note('双击格挡');
await ctx.gesture('doubletap');
ctx.note('前摇响了,单击格挡');
await ctx.gesture('tap');
continue;
}
await selectCmd('attack');
@@ -125,5 +131,5 @@ export async function session(ctx, mock) {
}
await ctx.waitCond(() => game.phase === 'explore');
ctx.status('狼打完了,胜利播报完回到溪流边。一场完整战斗到此收工');
ctx.status('狼打完了,胜利播报(链尾还教了一句「想知道自己在哪,就向上滑」)完回到溪流边。一场完整战斗到此收工');
}