350 lines
10 KiB
JavaScript
350 lines
10 KiB
JavaScript
const SoundManager = {
|
|
_ctx: null,
|
|
_master: null,
|
|
_analyser: null,
|
|
_enabled: true,
|
|
_volume: 0.3,
|
|
_ambient: null,
|
|
_ambientGain: null,
|
|
_initCalled: false,
|
|
|
|
init() {
|
|
if (this._initCalled) return;
|
|
this._initCalled = true;
|
|
try {
|
|
this._ctx = new (window.AudioContext || window.webkitAudioContext)();
|
|
this._master = this._ctx.createGain();
|
|
this._master.gain.value = this._volume;
|
|
this._master.connect(this._ctx.destination);
|
|
} catch (e) {
|
|
console.warn('Web Audio API not supported');
|
|
this._enabled = false;
|
|
}
|
|
const saved = localStorage.getItem('sound_enabled');
|
|
if (saved !== null) this._enabled = saved === 'true';
|
|
const vol = localStorage.getItem('sound_volume');
|
|
if (vol !== null) this.setVolume(parseFloat(vol));
|
|
|
|
document.addEventListener('click', () => {
|
|
if (this._ctx && this._ctx.state === 'suspended') this._ctx.resume();
|
|
}, { once: true });
|
|
},
|
|
|
|
setEnabled(on) {
|
|
this._enabled = on;
|
|
localStorage.setItem('sound_enabled', on);
|
|
if (!on && this._ambientGain) {
|
|
this._ambientGain.gain.setTargetAtTime(0, this._ctx.currentTime, 0.5);
|
|
}
|
|
},
|
|
|
|
toggle() {
|
|
this.setEnabled(!this._enabled);
|
|
return this._enabled;
|
|
},
|
|
|
|
setVolume(v) {
|
|
this._volume = Math.max(0, Math.min(1, v));
|
|
if (this._master) this._master.gain.value = this._volume;
|
|
localStorage.setItem('sound_volume', this._volume);
|
|
},
|
|
|
|
_t() {
|
|
return this._ctx ? this._ctx.currentTime : 0;
|
|
},
|
|
|
|
_osc(freq, duration, type = 'sine', vol = 1, dest) {
|
|
if (!this._enabled || !this._ctx) return;
|
|
if (this._ctx.state === 'suspended') this._ctx.resume();
|
|
const now = this._t();
|
|
const o = this._ctx.createOscillator();
|
|
const g = this._ctx.createGain();
|
|
o.type = type;
|
|
o.frequency.setValueAtTime(freq, now);
|
|
g.gain.setValueAtTime(vol, now);
|
|
g.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
|
o.connect(g);
|
|
g.connect(dest || this._master);
|
|
o.start(now);
|
|
o.stop(now + duration);
|
|
},
|
|
|
|
_noise(duration, vol = 1, filterFreq, dest) {
|
|
if (!this._enabled || !this._ctx) return;
|
|
if (this._ctx.state === 'suspended') this._ctx.resume();
|
|
const now = this._t();
|
|
const sr = this._ctx.sampleRate;
|
|
const len = sr * duration;
|
|
const buf = this._ctx.createBuffer(1, len, sr);
|
|
const data = buf.getChannelData(0);
|
|
for (let i = 0; i < len; i++) data[i] = Math.random() * 2 - 1;
|
|
const src = this._ctx.createBufferSource();
|
|
src.buffer = buf;
|
|
let node = src;
|
|
if (filterFreq) {
|
|
const f = this._ctx.createBiquadFilter();
|
|
f.type = 'lowpass';
|
|
f.frequency.value = filterFreq;
|
|
src.connect(f);
|
|
node = f;
|
|
}
|
|
const g = this._ctx.createGain();
|
|
g.gain.setValueAtTime(vol, now);
|
|
g.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
|
node.connect(g);
|
|
g.connect(dest || this._master);
|
|
src.start(now);
|
|
},
|
|
|
|
_chord(freqs, duration, type = 'sine', vol = 0.15) {
|
|
freqs.forEach(f => this._osc(f, duration, type, vol / freqs.length));
|
|
},
|
|
|
|
// ── Rarity color helpers ──
|
|
_rarityParams(rarity) {
|
|
const map = {
|
|
'Consumer Grade': { baseFreq: 300, arp: [300, 350, 400], noiseVol: 0.2, dur: 0.6 },
|
|
'Industrial Grade': { baseFreq: 400, arp: [400, 500, 600], noiseVol: 0.2, dur: 0.7 },
|
|
'Mil-Spec': { baseFreq: 500, arp: [500, 630, 750], noiseVol: 0.25, dur: 0.8 },
|
|
'Mil-Spec Grade': { baseFreq: 500, arp: [500, 630, 750], noiseVol: 0.25, dur: 0.8 },
|
|
'Restricted': { baseFreq: 600, arp: [600, 750, 900], noiseVol: 0.3, dur: 1.0 },
|
|
'Classified': { baseFreq: 700, arp: [700, 880, 1050], noiseVol: 0.35, dur: 1.2 },
|
|
'Covert': { baseFreq: 800, arp: [800, 1000, 1200], noiseVol: 0.4, dur: 1.5 },
|
|
'Rare Special Item': { baseFreq: 900, arp: [900, 1100, 1400], noiseVol: 0.45, dur: 1.8 },
|
|
'Extraordinary': { baseFreq: 900, arp: [900, 1100, 1400], noiseVol: 0.45, dur: 1.8 },
|
|
};
|
|
return map[rarity] || map['Consumer Grade'];
|
|
},
|
|
|
|
// ── Ambient ──
|
|
startAmbient() {
|
|
if (!this._ctx || this._ambient) return;
|
|
const now = this._t();
|
|
this._ambientGain = this._ctx.createGain();
|
|
this._ambientGain.gain.value = 0;
|
|
|
|
const bufLen = this._ctx.sampleRate * 2;
|
|
const buf = this._ctx.createBuffer(1, bufLen, this._ctx.sampleRate);
|
|
const data = buf.getChannelData(0);
|
|
for (let i = 0; i < bufLen; i++) {
|
|
const t = i / this._ctx.sampleRate;
|
|
data[i] = (Math.random() - 0.5) * 0.5
|
|
+ Math.sin(2 * Math.PI * 55 * t) * 0.08
|
|
+ Math.sin(2 * Math.PI * 82.5 * t) * 0.05;
|
|
}
|
|
const src = this._ctx.createBufferSource();
|
|
src.buffer = buf;
|
|
src.loop = true;
|
|
|
|
const lp = this._ctx.createBiquadFilter();
|
|
lp.type = 'lowpass';
|
|
lp.frequency.value = 300;
|
|
|
|
const hp = this._ctx.createBiquadFilter();
|
|
hp.type = 'highpass';
|
|
hp.frequency.value = 40;
|
|
|
|
src.connect(lp);
|
|
lp.connect(hp);
|
|
hp.connect(this._ambientGain);
|
|
this._ambientGain.connect(this._master);
|
|
src.start();
|
|
this._ambient = src;
|
|
this._ambientGain.gain.setTargetAtTime(0.06, now, 1);
|
|
},
|
|
|
|
stopAmbient() {
|
|
if (this._ambientGain) {
|
|
this._ambientGain.gain.setTargetAtTime(0, this._t(), 0.5);
|
|
setTimeout(() => {
|
|
if (this._ambient) { try { this._ambient.stop(); } catch (e) {} this._ambient = null; }
|
|
}, 1500);
|
|
}
|
|
},
|
|
|
|
// ── Case Opening ──
|
|
caseOpen() {
|
|
this._noise(0.15, 0.4, 2000);
|
|
this._osc(700, 0.06, 'square', 0.25);
|
|
setTimeout(() => this._osc(500, 0.08, 'square', 0.2), 50);
|
|
setTimeout(() => this._osc(350, 0.1, 'sawtooth', 0.18), 110);
|
|
setTimeout(() => this._osc(250, 0.12, 'sine', 0.12), 180);
|
|
setTimeout(() => this._noise(0.3, 0.15, 4000), 120);
|
|
},
|
|
|
|
caseReveal(rarity) {
|
|
const p = this._rarityParams(rarity);
|
|
this._noise(0.08, 0.3, 3000);
|
|
p.arp.forEach((f, i) => {
|
|
setTimeout(() => {
|
|
this._osc(f, 0.15, 'sine', 0.25);
|
|
this._osc(f * 2, 0.1, 'sine', 0.08);
|
|
}, i * 100);
|
|
});
|
|
setTimeout(() => {
|
|
this._osc(p.baseFreq * 2, 0.4, 'sine', 0.35);
|
|
this._noise(0.15, p.noiseVol, 5000);
|
|
this._osc(p.baseFreq, 0.6, 'sawtooth', 0.15);
|
|
}, p.arp.length * 100);
|
|
},
|
|
|
|
caseCovert() {
|
|
this._noise(0.1, 0.5, 4000);
|
|
[392, 523, 659, 784, 1047].forEach((f, i) => {
|
|
setTimeout(() => {
|
|
this._osc(f, 0.2, 'sawtooth', 0.3);
|
|
this._osc(f * 1.5, 0.15, 'sine', 0.1);
|
|
}, i * 120);
|
|
});
|
|
setTimeout(() => {
|
|
this._osc(1047, 0.8, 'sine', 0.6);
|
|
this._osc(784, 0.6, 'triangle', 0.25);
|
|
this._noise(0.6, 0.4, 6000);
|
|
}, 600);
|
|
},
|
|
|
|
caseRare() {
|
|
[523, 659, 784, 1047].forEach((f, i) => {
|
|
setTimeout(() => {
|
|
this._osc(f, 0.12, 'sine', 0.3);
|
|
this._osc(f * 1.5, 0.08, 'sine', 0.06);
|
|
}, i * 80);
|
|
});
|
|
setTimeout(() => this._osc(1319, 0.5, 'sine', 0.5), 320);
|
|
},
|
|
|
|
// ── Slots ──
|
|
slotSpin() {
|
|
for (let i = 0; i < 10; i++) {
|
|
setTimeout(() => {
|
|
this._osc(200 + Math.random() * 600, 0.04, 'square', 0.12);
|
|
}, i * 60);
|
|
}
|
|
},
|
|
|
|
slotMatch() {
|
|
[523, 659, 784, 1047].forEach((f, i) => {
|
|
setTimeout(() => this._osc(f, 0.15, 'sine', 0.35), i * 100);
|
|
});
|
|
setTimeout(() => this._noise(0.2, 0.3, 8000), 300);
|
|
},
|
|
|
|
// ── Contract ──
|
|
contractSubmit() {
|
|
this._osc(150, 0.25, 'sawtooth', 0.2);
|
|
setTimeout(() => this._osc(200, 0.15, 'square', 0.18), 150);
|
|
setTimeout(() => {
|
|
this._osc(250, 0.08, 'square', 0.15);
|
|
this._noise(0.05, 0.15);
|
|
}, 280);
|
|
},
|
|
|
|
contractResult() {
|
|
[400, 600, 800].forEach((f, i) => {
|
|
setTimeout(() => this._osc(f, 0.15, 'sine', 0.3), i * 100);
|
|
});
|
|
setTimeout(() => {
|
|
this._osc(1200, 0.4, 'sine', 0.45);
|
|
this._noise(0.2, 0.2, 5000);
|
|
}, 300);
|
|
},
|
|
|
|
// ── Wheel ──
|
|
wheelTick() {
|
|
this._osc(200, 0.03, 'square', 0.08);
|
|
},
|
|
|
|
wheelWin() {
|
|
[523, 659, 784, 1047].forEach((f, i) => {
|
|
setTimeout(() => this._osc(f, 0.18, 'sine', 0.35), i * 100);
|
|
});
|
|
setTimeout(() => this._noise(0.4, 0.3, 8000), 400);
|
|
},
|
|
|
|
wheelLose() {
|
|
[400, 350, 300].forEach((f, i) => {
|
|
setTimeout(() => this._osc(f, 0.25, 'sine', 0.2), i * 150);
|
|
});
|
|
},
|
|
|
|
// ── Achievements ──
|
|
achievement() {
|
|
[659, 523, 784, 659, 1047].forEach((f, i) => {
|
|
setTimeout(() => this._osc(f, 0.12, 'sine', 0.3), i * 80);
|
|
});
|
|
setTimeout(() => this._noise(0.3, 0.25, 6000), 400);
|
|
},
|
|
|
|
// ── Crash ──
|
|
crashBet() {
|
|
this._osc(300, 0.08, 'square', 0.15);
|
|
setTimeout(() => this._osc(350, 0.06, 'square', 0.1), 60);
|
|
},
|
|
|
|
crashTick(multiplier) {
|
|
const freq = 300 + Math.min(multiplier * 50, 800);
|
|
this._osc(freq, 0.03, 'sine', 0.05 + Math.min(multiplier * 0.01, 0.15));
|
|
},
|
|
|
|
crashCrashed() {
|
|
this._noise(0.5, 0.6, 2000);
|
|
this._osc(80, 0.6, 'sawtooth', 0.4);
|
|
setTimeout(() => this._osc(50, 1, 'sine', 0.3), 100);
|
|
setTimeout(() => this._osc(40, 0.8, 'triangle', 0.2), 200);
|
|
},
|
|
|
|
crashCashout(multiplier) {
|
|
const vol = 0.2 + Math.min(multiplier * 0.02, 0.5);
|
|
[600, 800, 1000, 1200].forEach((f, i) => {
|
|
setTimeout(() => this._osc(f, 0.12, 'sine', vol), i * 80);
|
|
});
|
|
setTimeout(() => this._noise(0.3, 0.3, 8000), 320);
|
|
},
|
|
|
|
// ── Upgrade ──
|
|
upgradeSubmit() {
|
|
this._osc(250, 0.1, 'square', 0.15);
|
|
this._noise(0.05, 0.2);
|
|
},
|
|
|
|
upgradeResult() {
|
|
[523, 784, 1047].forEach((f, i) => {
|
|
setTimeout(() => this._osc(f, 0.2, 'sine', 0.3), i * 120);
|
|
});
|
|
setTimeout(() => this._noise(0.25, 0.25, 6000), 360);
|
|
},
|
|
|
|
upgradeFail() {
|
|
[300, 250, 200].forEach((f, i) => {
|
|
setTimeout(() => this._osc(f, 0.2, 'sawtooth', 0.2), i * 120);
|
|
});
|
|
},
|
|
|
|
// ── UI ──
|
|
click() {
|
|
this._osc(800, 0.03, 'sine', 0.08);
|
|
},
|
|
|
|
hover() {
|
|
this._osc(1200, 0.015, 'sine', 0.03);
|
|
},
|
|
|
|
error() {
|
|
this._osc(200, 0.15, 'square', 0.2);
|
|
setTimeout(() => this._osc(180, 0.12, 'square', 0.15), 100);
|
|
},
|
|
|
|
success() {
|
|
[600, 800, 1000].forEach((f, i) => {
|
|
setTimeout(() => this._osc(f, 0.08, 'sine', 0.2), i * 60);
|
|
});
|
|
},
|
|
|
|
balanceUpdate() {
|
|
this._osc(1000, 0.04, 'sine', 0.08);
|
|
setTimeout(() => this._osc(1200, 0.04, 'sine', 0.06), 50);
|
|
},
|
|
};
|
|
|
|
document.addEventListener('DOMContentLoaded', () => SoundManager.init());
|