- 玩法一 数字一笔:规则库、求解器、26 关、竖屏触摸 Web 原型、测试 - 玩法二 连连气球:连消畅玩版核心、Web 原型、Cocos 球槽组件与导出脚本 - 玩法三 六边一笔:规则库、求解器、10 关 + 转墙实验包、Web 原型、测试 - 产品文档、UI 设计规范、各玩法 GDD / HANDOFF - 文档移除全部外部参考作引用;删除玩法二 v0 旧稿
69 lines
2.1 KiB
TypeScript
69 lines
2.1 KiB
TypeScript
// 音效:WebAudio 合成,不需要资源文件。落子短音(音高随步号上升,12 后循环)、撞墙闷响、冲突、过关和弦。
|
|
// Cocos 版换成 AudioSource 播放几段短音即可,接口保持 step/wall/conflict/cleared/reject。
|
|
|
|
export interface Sfx {
|
|
unlock(): void;
|
|
step(n: number): void;
|
|
wall(): 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, slideTo?: number): 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);
|
|
if (slideTo) osc.frequency.exponentialRampToValueAtTime(slideTo, t0 + dur);
|
|
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);
|
|
}
|
|
|
|
/** 撞墙:短促的低频“咚” */
|
|
wall(): void {
|
|
this.tone(180, 0.09, 'square', 0.07, 0, 90);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|