游戏重构
This commit is contained in:
+120
-62
@@ -1,15 +1,14 @@
|
||||
// audio.js — 音频管理器(HTML Audio + Web Audio API 混用)
|
||||
// audio.js — 音频管理器 + 合成 UI 音效
|
||||
|
||||
class AudioManager {
|
||||
constructor() {
|
||||
this._ctx = null;
|
||||
this._master = null;
|
||||
this._bgmNode = null;
|
||||
this._ambNode = null;
|
||||
this._voiceQueue = [];
|
||||
this._voiceEl = null;
|
||||
this._ringEl = null;
|
||||
this._ringTimer = null;
|
||||
this._bgmEl = null;
|
||||
this._ambEl = null;
|
||||
this._preloaded = {};
|
||||
}
|
||||
|
||||
@@ -22,19 +21,44 @@ class AudioManager {
|
||||
this._master.connect(this._ctx.destination);
|
||||
}
|
||||
|
||||
// ── 预加载短音效(铃声、UI音)──
|
||||
// ── 合成短音(UI 反馈音效)──
|
||||
synth({ freq = 440, freq2 = null, dur = 100, type = 'sine', vol = 0.3 } = {}) {
|
||||
if (!this._ctx) return;
|
||||
const t = this._ctx.currentTime;
|
||||
const d = dur / 1000;
|
||||
|
||||
const osc = this._ctx.createOscillator();
|
||||
const gain = this._ctx.createGain();
|
||||
|
||||
osc.type = type;
|
||||
osc.frequency.setValueAtTime(freq, t);
|
||||
if (freq2 != null) {
|
||||
osc.frequency.linearRampToValueAtTime(freq2, t + d);
|
||||
}
|
||||
|
||||
gain.gain.setValueAtTime(vol, t);
|
||||
gain.gain.setValueAtTime(vol, t + d * 0.75);
|
||||
gain.gain.linearRampToValueAtTime(0, t + d);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this._master);
|
||||
osc.start(t);
|
||||
osc.stop(t + d + 0.05);
|
||||
}
|
||||
|
||||
// ── 预加载短音效 ──
|
||||
async preload(key, url) {
|
||||
if (this._preloaded[key]) return;
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
const buf = await res.arrayBuffer();
|
||||
this._preloaded[key] = await this._ctx.decodeAudioData(buf);
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
console.warn('[Audio] preload failed:', key, e);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 播放预加载的短音效 ──
|
||||
// ── 播放预加载音效 ──
|
||||
playFX(key, { vol = 1.0, loop = false } = {}) {
|
||||
const buf = this._preloaded[key];
|
||||
if (!buf) return null;
|
||||
@@ -46,17 +70,16 @@ class AudioManager {
|
||||
src.connect(gain);
|
||||
gain.connect(this._master);
|
||||
src.start();
|
||||
return { src, gain, stop: () => { try { src.stop(); } catch(_){} } };
|
||||
return { src, gain, stop: () => { try { src.stop(); } catch (_) {} } };
|
||||
}
|
||||
|
||||
// ── 铃声(循环直到 stopRingtone)──
|
||||
// ── 铃声(循环)──
|
||||
async startRingtone(url) {
|
||||
this.stopRingtone();
|
||||
// 使用 HTMLAudio 以支持更大文件
|
||||
this._ringEl = new Audio(url);
|
||||
this._ringEl.loop = true;
|
||||
this._ringEl.volume = 1.0;
|
||||
try { await this._ringEl.play(); } catch(e) { console.warn('[Audio] ring play error', e); }
|
||||
try { await this._ringEl.play(); } catch (e) { console.warn('[Audio] ring error', e); }
|
||||
}
|
||||
|
||||
stopRingtone() {
|
||||
@@ -65,17 +88,16 @@ class AudioManager {
|
||||
this._ringEl.src = '';
|
||||
this._ringEl = null;
|
||||
}
|
||||
if (this._ringTimer) { clearTimeout(this._ringTimer); this._ringTimer = null; }
|
||||
}
|
||||
|
||||
// ── 语音消息播放(支持多段顺序播放)──
|
||||
playVoice(urls, { vol = 1.0, onEnd = null, onSegEnd = null } = {}) {
|
||||
// ── 语音播放(多段顺序)──
|
||||
playVoice(urls, { vol = 1.0, onEnd = null } = {}) {
|
||||
this.stopVoice();
|
||||
this._voiceQueue = [...urls];
|
||||
this._playNextSegment(vol, onEnd, onSegEnd);
|
||||
this._playNext(vol, onEnd);
|
||||
}
|
||||
|
||||
_playNextSegment(vol, onEnd, onSegEnd) {
|
||||
_playNext(vol, onEnd) {
|
||||
if (this._voiceQueue.length === 0) {
|
||||
this._voiceEl = null;
|
||||
if (onEnd) onEnd();
|
||||
@@ -85,22 +107,19 @@ class AudioManager {
|
||||
const el = new Audio(url);
|
||||
el.volume = vol;
|
||||
this._voiceEl = el;
|
||||
el.onended = () => {
|
||||
if (onSegEnd) onSegEnd();
|
||||
this._playNextSegment(vol, onEnd, onSegEnd);
|
||||
};
|
||||
el.onended = () => this._playNext(vol, onEnd);
|
||||
el.onerror = () => {
|
||||
console.warn('[Audio] voice error:', url);
|
||||
this._playNextSegment(vol, onEnd, onSegEnd);
|
||||
this._playNext(vol, onEnd);
|
||||
};
|
||||
el.play().catch(e => console.warn('[Audio] voice play error', e));
|
||||
el.play().catch(e => console.warn('[Audio] play error', e));
|
||||
}
|
||||
|
||||
stopVoice() {
|
||||
this._voiceQueue = [];
|
||||
if (this._voiceEl) {
|
||||
this._voiceEl.pause();
|
||||
this._voiceEl.onended = null;
|
||||
this._voiceEl.pause();
|
||||
this._voiceEl = null;
|
||||
}
|
||||
}
|
||||
@@ -111,7 +130,6 @@ class AudioManager {
|
||||
|
||||
// ── BGM(淡入淡出切换)──
|
||||
async startBGM(url, { vol = 0.35, fade = 2000 } = {}) {
|
||||
// 先淡出现有
|
||||
if (this._bgmEl) {
|
||||
const old = this._bgmEl;
|
||||
this._fadeOut(old, fade / 2).then(() => { old.pause(); old.src = ''; });
|
||||
@@ -123,7 +141,7 @@ class AudioManager {
|
||||
try {
|
||||
await el.play();
|
||||
this._fadeTo(el, vol, fade);
|
||||
} catch(e) { console.warn('[Audio] BGM error', e); }
|
||||
} catch (e) { console.warn('[Audio] BGM error', e); }
|
||||
}
|
||||
|
||||
stopBGM(fade = 2000) {
|
||||
@@ -134,21 +152,18 @@ class AudioManager {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 环境音(底噪循环)──
|
||||
// ── 环境音 ──
|
||||
async startAmbience(url, { vol = 0.25 } = {}) {
|
||||
if (this._ambEl) { this._ambEl.pause(); this._ambEl.src = ''; }
|
||||
const el = new Audio(url);
|
||||
el.loop = true;
|
||||
el.volume = vol;
|
||||
this._ambEl = el;
|
||||
try { await el.play(); } catch(e) { console.warn('[Audio] ambience error', e); }
|
||||
try { await el.play(); } catch (e) { console.warn('[Audio] ambience error', e); }
|
||||
}
|
||||
|
||||
stopAmbience() {
|
||||
if (this._ambEl) {
|
||||
this._ambEl.pause(); this._ambEl.src = '';
|
||||
this._ambEl = null;
|
||||
}
|
||||
if (this._ambEl) { this._ambEl.pause(); this._ambEl.src = ''; this._ambEl = null; }
|
||||
}
|
||||
|
||||
// ── 音量渐变 ──
|
||||
@@ -160,8 +175,7 @@ class AudioManager {
|
||||
let i = 0;
|
||||
const t = setInterval(() => {
|
||||
el.volume = Math.max(0, Math.min(1, start + dv * i));
|
||||
i++;
|
||||
if (i >= steps) clearInterval(t);
|
||||
if (++i >= steps) clearInterval(t);
|
||||
}, dt);
|
||||
}
|
||||
|
||||
@@ -174,38 +188,82 @@ class AudioManager {
|
||||
let i = 0;
|
||||
const t = setInterval(() => {
|
||||
el.volume = Math.max(0, start - dv * i);
|
||||
i++;
|
||||
if (i >= steps) { clearInterval(t); resolve(); }
|
||||
if (++i >= steps) { clearInterval(t); resolve(); }
|
||||
}, dt);
|
||||
});
|
||||
}
|
||||
|
||||
// ── 键盘声(空间音频:左耳)──
|
||||
startTyping() {
|
||||
if (this._typingNode) return;
|
||||
const buf = this._preloaded['sfx_keyboard_slow'];
|
||||
if (!buf) return;
|
||||
const src = this._ctx.createBufferSource();
|
||||
src.buffer = buf;
|
||||
src.loop = true;
|
||||
const gain = this._ctx.createGain();
|
||||
gain.gain.value = 0.15;
|
||||
// 偏左声道
|
||||
const pan = this._ctx.createStereoPanner();
|
||||
pan.pan.value = -0.8;
|
||||
src.connect(gain);
|
||||
gain.connect(pan);
|
||||
pan.connect(this._master);
|
||||
src.start();
|
||||
this._typingNode = { src, gain, pan };
|
||||
}
|
||||
|
||||
stopTyping() {
|
||||
if (this._typingNode) {
|
||||
try { this._typingNode.src.stop(); } catch(_) {}
|
||||
this._typingNode = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Snd = new AudioManager();
|
||||
|
||||
// ══════════════════════════════════════════════════
|
||||
// SFX_UI — 合成 UI 音效(不依赖音频文件)
|
||||
// ══════════════════════════════════════════════════
|
||||
|
||||
const SFX_UI = {
|
||||
// 确认操作(单击)
|
||||
tap() {
|
||||
Snd.synth({ freq: 880, dur: 45, vol: 0.2 });
|
||||
},
|
||||
|
||||
// 滑动确认
|
||||
swipe() {
|
||||
Snd.synth({ freq: 660, dur: 60, vol: 0.18 });
|
||||
},
|
||||
|
||||
// 无效操作
|
||||
error() {
|
||||
Snd.synth({ freq: 200, dur: 100, type: 'square', vol: 0.1 });
|
||||
},
|
||||
|
||||
// 新消息到达
|
||||
notify() {
|
||||
Snd.synth({ freq: 523, dur: 100, vol: 0.25 });
|
||||
setTimeout(() => Snd.synth({ freq: 659, dur: 130, vol: 0.2 }), 130);
|
||||
},
|
||||
|
||||
// 进入选择模式(三音上行)
|
||||
choiceReady() {
|
||||
Snd.synth({ freq: 440, dur: 80, type: 'triangle', vol: 0.18 });
|
||||
setTimeout(() => Snd.synth({ freq: 523, dur: 80, type: 'triangle', vol: 0.18 }), 100);
|
||||
setTimeout(() => Snd.synth({ freq: 659, dur: 120, type: 'triangle', vol: 0.18 }), 200);
|
||||
},
|
||||
|
||||
// 选择:温暖/接受(右滑)— 上行音
|
||||
choiceWarm() {
|
||||
Snd.synth({ freq: 440, freq2: 659, dur: 140, vol: 0.22 });
|
||||
},
|
||||
|
||||
// 选择:冷淡/拒绝(左滑)— 下行音
|
||||
choiceCold() {
|
||||
Snd.synth({ freq: 523, freq2: 349, dur: 140, vol: 0.22 });
|
||||
},
|
||||
|
||||
// 选择:好奇/追问(上滑)— 快速上行
|
||||
choiceCurious() {
|
||||
Snd.synth({ freq: 523, freq2: 784, dur: 120, vol: 0.22 });
|
||||
},
|
||||
|
||||
// 选择:沉默/退缩(下滑)— 极轻低音
|
||||
choiceSilent() {
|
||||
Snd.synth({ freq: 330, dur: 200, vol: 0.08 });
|
||||
},
|
||||
|
||||
// 跳过 / 忽略
|
||||
skip() {
|
||||
Snd.synth({ freq: 587, freq2: 392, dur: 80, vol: 0.12 });
|
||||
},
|
||||
|
||||
// 下一条消息即将到来
|
||||
next() {
|
||||
Snd.synth({ freq: 440, dur: 80, vol: 0.1 });
|
||||
setTimeout(() => Snd.synth({ freq: 523, dur: 100, vol: 0.13 }), 120);
|
||||
},
|
||||
|
||||
// 结局音效
|
||||
ending() {
|
||||
Snd.synth({ freq: 523, dur: 400, vol: 0.25 });
|
||||
setTimeout(() => Snd.synth({ freq: 659, dur: 400, vol: 0.2 }), 450);
|
||||
setTimeout(() => Snd.synth({ freq: 784, dur: 600, vol: 0.15 }), 900);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user