/** * 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;