feat(playbook): 26 号 P0 推广至全部 15 款游戏——兼容输入管道 + 可闻度调优

真人首测反馈「交互还是很迷惑」:大厅 15 款里只有 3 款新交互,其余 12 款
仍是旧版(无声操作、连点连跳),整体体验割裂。本次:

- engine/pipeline.js 新增 makeCompatPipeline(纯增量,游戏内部逻辑零改动):
  · 手势后 100ms 无任何音频调度 → invalid 回执(消灭无声操作,原则 4)
  · 语音中的手势 → 立即 ok 回执(§4.1 打断即收到)
  · 语音中 tap 400ms 防连点(§4.2,点两下不连跳两句)
  · longpressend/tapcancel 收尾噪声直通
- engine/audio.js 加 playCount 探针(回执启发式用)
- 12 款游戏 index.html 接入兼容管道(rhythm 保留 immediateTap;
  双击判定窗口不动——多款游戏双击是核心玩法/菜单确认,重映射会破坏)
- 可闻度调优(真人首测:笔记本外放偏轻,26 号 §3「起步值,真人测试后再调」):
  回执增益 ok 0.08→0.11 / invalid 0.10→0.13 / busy 0.04→0.06;
  等待声 0.03→0.055
- 大厅:三款深度重构游戏(声命线/暗室/音序回响)置顶 + 「新交互」徽标,
  卡片文案直接写玩法手势,测试不再误入旧交互游戏

测试:新增 compat-pipeline.test(9 断言);全部 15 款 playtest 回归通过
(共 3900+ 断言,0 失败);引擎 input/voice/guide 回归通过。
This commit is contained in:
xsl
2026-10-01 21:09:26 +08:00
parent ee9d7efe57
commit e943227136
18 changed files with 322 additions and 37 deletions
+50
View File
@@ -122,3 +122,53 @@ export function makeInputPipeline({ floor, confusion, receipts, invalidHints, ga
}
export default makeInputPipeline;
/**
* 兼容管道 —— 未做深度改造的游戏(P0 层,26 号 §5「先把通用部分抽进引擎,推广到
* 其他游戏」)。纯增量:游戏内部逻辑、手势语义、双击判定窗口一概不动,只在外围
* 补三件事:
*
* 1. 消灭无声操作(原则 4):手势后 100ms 内游戏没有调度任何音频 → invalid 回执
* (旧版「点了没反应」是真人测试的头号迷惑源)。
* 2. 语音中的手势 → 立即 ok 回执(§4.1 打断即收到);tap 额外进 400ms 防连点窗口
* (点两下不连跳两句,§4.2)。
* 3. longpressend / tapcancel 收尾噪声直通,不吃回执。
*
* 游戏自带的内容音(脚步、音色、语音)即反馈,不叠回执(§4.2「已有明显内容音可
* 省 ok」)。深度改造(发言权/等待点/接管)仍按 26-apply-* 设计单逐款推进。
*
* @param {{ audio: import('./audio.js').WebAudioEngine,
* receipts: import('./receipts.js').Receipts,
* game: { onGesture: Function } }} deps
*/
export function makeCompatPipeline({ audio, receipts, game }) {
let lastSkipAt = -Infinity;
return (g, info) => {
if (g === 'longpressend' || g === 'tapcancel') return game.onGesture(g, info);
const voicePlaying = audio.isVoicePlaying();
// 防连点:语音中 tap=跳过/继续(各游戏既有语义),400ms 内的第二下只给 busy
if (g === 'tap' && voicePlaying && performance.now() - lastSkipAt < 400) {
receipts.play('busy');
return;
}
const before = audio.playCount;
const r = game.onGesture(g, info);
if (voicePlaying) {
// 语音中的任何手势 = 打断/跳过语义:立即 ok(§4.1 打断即收到)
receipts.play('ok');
if (g === 'tap') lastSkipAt = performance.now();
return r;
}
// 100ms 内无任何音频调度 → invalid(消灭无声操作)
setTimeout(() => {
if (audio.playCount === before) receipts.play('invalid');
}, 100);
return r;
};
}