Files
xsl e943227136 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 回归通过。
2026-10-01 21:09:26 +08:00

63 lines
3.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* receipts.js — 输入回执三件套(26 号文档 §4.2,P0 第 2 项)。
*
* 每次玩家操作 100ms 内出声,告知「这下算没算」。三种回执(合成参数为文档起步值):
* | kind | 用在哪里 | 听感 |
* |----------|------------------------|----------------------|
* | ok | 操作被接受并生效 | 清脆的「嗒」 |
* | invalid | 收到了,但现在用不上 | 低沉柔和的「咚」 |
* | busy | 关键句在说 / 正在处理 | 很轻的「嗯」 |
*
* 规则(文档 §4.2 / 原则 4):
* - 回执由引擎统一播,游戏只告诉管道这次操作的结果是哪一种(见 pipeline.js)。
* - 游戏已为这次操作播了明显内容音(脚步、开门)时可以省 ok,invalid/busy 永远要播。
* - 音色避开 guide.js 的 turn(1318Hz 风铃)/ nudge(880Hz 木鱼);系统音一律合成音
* (原则 10),接 master 总线吃限幅、不被 BGM duck 压低(§4.11 优先级 1)。
* - 任何设备都出声,不依赖震动;震动由 gesture/haptics 照常叠加。
*/
/** 合成参数(26 号文档 §4.2 表:合成参数(起步值))。 */
const SPECS = {
// 三角波 1600Hz,35ms,起音 2ms,快速衰减 —— 清脆的「嗒」
// (增益 0.08→0.11:真人首测反馈笔记本外放偏轻,26 号 §3「参数是起步值,真人测试后再调」)
ok: { wave: 'triangle', from: 1600, to: 1600, dur: 0.035, attack: 0.002, gain: 0.11 },
// 正弦波 260Hz 滑到 200Hz,120ms,起音 10ms —— 低沉柔和的「咚」(0.10→0.13)
invalid: { wave: 'sine', from: 260, to: 200, dur: 0.12, attack: 0.01, gain: 0.13 },
// 正弦波 520Hz,40ms —— 很轻的「嗯」(0.04→0.06)
busy: { wave: 'sine', from: 520, to: 520, dur: 0.04, attack: 0.005, gain: 0.06 },
};
export class Receipts {
/** @param {import('./audio.js').WebAudioEngine} audio */
constructor(audio) {
this.audio = audio;
}
/**
* 播一种回执。未解锁 / 无 ctx(playtest mock)时静默跳过。
* @param {'ok'|'invalid'|'busy'} kind
*/
play(kind) {
const spec = SPECS[kind];
const ctx = this.audio && this.audio.ctx;
if (!spec || !ctx || !this.audio._master || !this.audio.unlocked) return;
const t0 = ctx.currentTime + 0.005;
const osc = ctx.createOscillator();
osc.type = spec.wave;
osc.frequency.setValueAtTime(spec.from, t0);
if (spec.to !== spec.from) osc.frequency.linearRampToValueAtTime(spec.to, t0 + spec.dur);
const g = ctx.createGain();
g.gain.setValueAtTime(0.0001, t0);
g.gain.linearRampToValueAtTime(spec.gain, t0 + spec.attack);
// 快速衰减:attack 后线性落到接近 0(听感接近指数衰减)
g.gain.linearRampToValueAtTime(spec.gain * 0.35, t0 + spec.attack + (spec.dur - spec.attack) * 0.45);
g.gain.linearRampToValueAtTime(0.0001, t0 + spec.dur);
osc.connect(g);
g.connect(this.audio._master);
osc.start(t0);
osc.stop(t0 + spec.dur + 0.02);
}
}
export default Receipts;