Files
NumberSwipe_New/modes/01-number-stroke/web/audio.ts
T
cabinw 8026e4628d 初始提交:三玩法 Web 原型与设计文档
- 玩法一 数字一笔:规则库、求解器、26 关、竖屏触摸 Web 原型、测试
- 玩法二 连连气球:连消畅玩版核心、Web 原型、Cocos 球槽组件与导出脚本
- 玩法三 六边一笔:规则库、求解器、10 关 + 转墙实验包、Web 原型、测试
- 产品文档、UI 设计规范、各玩法 GDD / HANDOFF
- 文档移除全部外部参考作引用;删除玩法二 v0 旧稿
2026-09-16 14:30:59 +08:00

62 lines
1.9 KiB
TypeScript

// 音效:WebAudio 合成,不需要资源文件。GDD §6:落子短音(音高随步号上升,12 后循环)、冲突闷音、过关和弦。
// Cocos 版换成 AudioSource 播放几段短音即可,接口保持 step/conflict/cleared/reject。
export interface Sfx {
unlock(): void;
step(n: number): void;
conflict(): void;
cleared(): void;
reject(): void;
enabled: boolean;
}
export class WebAudioSfx implements Sfx {
enabled = true;
private ctx: AudioContext | null = null;
/** 必须在用户手势里调用一次,iOS 才放行 */
unlock(): void {
if (!this.ctx) {
const AC = window.AudioContext ?? (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
if (!AC) return;
this.ctx = new AC();
}
if (this.ctx.state === 'suspended') void this.ctx.resume();
}
private tone(freq: number, dur: number, type: OscillatorType, gain: number, when = 0): void {
if (!this.enabled || !this.ctx) return;
const ctx = this.ctx;
const t0 = ctx.currentTime + when;
const osc = ctx.createOscillator();
const g = ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, t0);
g.gain.setValueAtTime(0.0001, t0);
g.gain.exponentialRampToValueAtTime(gain, t0 + 0.008);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
osc.connect(g).connect(ctx.destination);
osc.start(t0);
osc.stop(t0 + dur + 0.02);
}
step(n: number): void {
const semi = (n - 1) % 12;
const f = 523.25 * Math.pow(2, semi / 12);
this.tone(f, 0.09, 'sine', 0.18);
}
conflict(): void {
this.tone(130, 0.16, 'square', 0.08);
}
cleared(): void {
const base = 523.25;
[0, 4, 7, 12].forEach((semi, i) => this.tone(base * Math.pow(2, semi / 12), 0.22, 'triangle', 0.16, i * 0.07));
}
reject(): void {
this.tone(220, 0.06, 'triangle', 0.06);
}
}