Files
SoundGame/web/engine/receipts.js
T
xsl ee9d7efe57 feat(playbook): 26 号交互手册首批落地——storyline/escape/simon 全面重构 + 引擎六模块
按 docs/design/26-audio-interaction-playbook.md 重构三款游戏(对白重写 + 双性别语音重生成 580 文件)。

引擎层(web/engine/,其余 12 款游戏零改动):
- floor.js 发言权管理器:SPEAKING/YOUR_TURN/BUSY/ACTION 四态、交接提示音、
  等待声(220/330Hz 嘟-嘟,20s 后放缓)、跨链音乐压低(audio.js holdDuck)
- receipts.js 输入回执三件套(ok/invalid/busy,100ms 内,原则 4)
- confusion.js 迷茫检测(乱点/无效/静默信号 → 三级接管,§4.5)
- hints.js 等待点 + 三档提示(full→short→提示音,30s 台账跨点去重,原则 9)
- novice.js 手势解锁阶梯(up/down/doubletap 跨局持久,§4.3)
- pipeline.js 输入管道(回执收口、防连点 400ms、双击折算、explainOnce)

游戏层:
- storyline R3:开场 71.7s 规则念白 → 3 秒内动手;双击=重播(反模式5)废除;
  抉择点三档提示+主动重听;断点存档复述(§4.12)
- escape R3:单击=检查+使用合并(双击用法废除);长按=我在哪(四向玩法裁定);
  撞墙归因每方向一次(原则 7);迷茫 L2 缩小/L3 演示;坐标存档续玩
- simon R3:新手三音色,按实力渐进解锁(+up/down/dt);失败表替代机会账
  (归因 2s 内→免费重听→放慢→结算,原则 15);播放期手势=busy 回执

测试:三款 playtest 重写(含 §6.2 乱点机器人:无声操作/被动重复/打断失败全 0,
首操作 ~2s);guide.test 改为 Guide 层单测+rhythm/space 段。全部通过
(38+33+22+72+19+35 断言)。设计单见 docs/design/26-apply-*.md。
2026-10-01 08:11:12 +08:00

62 lines
2.9 KiB
JavaScript
Raw 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 —— 清脆的「嗒」
ok: { wave: 'triangle', from: 1600, to: 1600, dur: 0.035, attack: 0.002, gain: 0.08 },
// 正弦波 260Hz 滑到 200Hz,120ms,起音 10ms,增益 0.10 —— 低沉柔和的「咚」
invalid: { wave: 'sine', from: 260, to: 200, dur: 0.12, attack: 0.01, gain: 0.10 },
// 正弦波 520Hz,40ms,增益 0.04 —— 很轻的「嗯」
busy: { wave: 'sine', from: 520, to: 520, dur: 0.04, attack: 0.005, gain: 0.04 },
};
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;