class SoundManager { constructor() { this.ctx = null; this.master = null; this.muted = false; this.musicNodes = null; const raw = localStorage.getItem('naddie_muted'); this.muted = raw === '1'; } init() { if (this.ctx) return; const AC = window.AudioContext || window.webkitAudioContext; if (!AC) return; this.ctx = new AC(); this.master = this.ctx.createGain(); this.master.gain.value = 0.4; this.master.connect(this.ctx.destination); } resume() { if (this.ctx && this.ctx.state === 'suspended') { this.ctx.resume(); } } setMuted(value) { this.muted = value; localStorage.setItem('naddie_muted', value ? '1' : '0'); if (this.master) { this.master.gain.value = value ? 0 : 0.4; } } isMuted() { return this.muted; } _beep(freq, duration, type = 'square', volume = 0.3) { if (!this.ctx || this.muted) return; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = type; osc.frequency.setValueAtTime(freq, this.ctx.currentTime); gain.gain.setValueAtTime(volume, this.ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration); osc.connect(gain); gain.connect(this.master); osc.start(); osc.stop(this.ctx.currentTime + duration); } _sweep(freqStart, freqEnd, duration, type = 'square', volume = 0.3) { if (!this.ctx || this.muted) return; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = type; osc.frequency.setValueAtTime(freqStart, this.ctx.currentTime); osc.frequency.exponentialRampToValueAtTime(Math.max(1, freqEnd), this.ctx.currentTime + duration); gain.gain.setValueAtTime(volume, this.ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration); osc.connect(gain); gain.connect(this.master); osc.start(); osc.stop(this.ctx.currentTime + duration); } _noise(duration, volume = 0.15) { if (!this.ctx || this.muted) return; const bufferSize = this.ctx.sampleRate * duration; const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) { data[i] = (Math.random() * 2 - 1) * (1 - i / bufferSize); } const src = this.ctx.createBufferSource(); src.buffer = buffer; const gain = this.ctx.createGain(); gain.gain.value = volume; src.connect(gain); gain.connect(this.master); src.start(); } jump() { this._sweep(320, 540, 0.1, 'square', 0.14); } land() { this._beep(180, 0.06, 'triangle', 0.10); } spring() { this._sweep(280, 1000, 0.18, 'square', 0.22); setTimeout(() => this._sweep(800, 1300, 0.12, 'triangle', 0.15), 50); } powerup() { [440, 554, 659, 880].forEach((f, i) => setTimeout(() => this._beep(f, 0.13, 'triangle', 0.20), i * 55)); } stomp() { this._sweep(420, 100, 0.16, 'sawtooth', 0.22); this._noise(0.08, 0.10); } break() { this._noise(0.15, 0.12); this._sweep(200, 60, 0.2, 'sawtooth', 0.10); } death() { this._sweep(280, 50, 0.5, 'sawtooth', 0.28); setTimeout(() => this._noise(0.3, 0.15), 100); } milestone() { [523, 659, 783, 1046].forEach((f, i) => setTimeout(() => this._beep(f, 0.22, 'triangle', 0.25), i * 90)); } click() { this._beep(660, 0.05, 'square', 0.12); } newBest() { [523, 659, 783, 1046, 1318].forEach((f, i) => setTimeout(() => this._beep(f, 0.18, 'triangle', 0.25), i * 80)); } } export const sound = new SoundManager();