// ============ audio.js — procedural WebAudio SFX & ambient music ============ let AC = null; let masterGain = null, musicGain = null, sfxGain = null; let musicTimer = null; let enabled = true; const settings = { master: 0.7, music: 0.5, sfx: 0.8 }; export function initAudio() { if (AC) return; try { AC = new (window.AudioContext || window.webkitAudioContext)(); masterGain = AC.createGain(); musicGain = AC.createGain(); sfxGain = AC.createGain(); musicGain.connect(masterGain); sfxGain.connect(masterGain); masterGain.connect(AC.destination); applyVolumes(); } catch (e) { console.warn('Audio unavailable', e); enabled = false; } } export function audioSupported() { return !!AC; } export function setVolumes(v) { Object.assign(settings, v); try { localStorage.setItem('at_audio', JSON.stringify(settings)); } catch { } applyVolumes(); } export function getVolumes() { return { ...settings }; } function applyVolumes() { if (!AC) return; masterGain.gain.value = settings.master; musicGain.gain.value = settings.music * 0.5; sfxGain.gain.value = settings.sfx; } // resume on first user gesture export function unlockAudio() { initAudio(); if (AC && AC.state === 'suspended') AC.resume(); } function tone(freq, dur, type = 'sine', vol = 0.3, dest, slideTo) { if (!AC || !enabled) return; const o = AC.createOscillator(); const g = AC.createGain(); o.type = type; o.frequency.value = freq; if (slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(20, slideTo), AC.currentTime + dur); g.gain.setValueAtTime(vol, AC.currentTime); g.gain.exponentialRampToValueAtTime(0.0001, AC.currentTime + dur); o.connect(g); g.connect(dest || sfxGain); o.start(); o.stop(AC.currentTime + dur + 0.02); } function noise(dur, vol = 0.2, filterFreq = 800) { if (!AC || !enabled) return; const len = AC.sampleRate * dur; const buf = AC.createBuffer(1, len, AC.sampleRate); const d = buf.getChannelData(0); for (let i = 0; i < len; i++) d[i] = Math.random() * 2 - 1; const src = AC.createBufferSource(); src.buffer = buf; const f = AC.createBiquadFilter(); f.type = 'lowpass'; f.frequency.value = filterFreq; const g = AC.createGain(); g.gain.setValueAtTime(vol, AC.currentTime); g.gain.exponentialRampToValueAtTime(0.0001, AC.currentTime + dur); src.connect(f); f.connect(g); g.connect(sfxGain); src.start(); } export const sfx = { click() { tone(660, .06, 'square', .12); }, place() { noise(.08, .25, 500); tone(220, .1, 'triangle', .2); }, demolish() { noise(.22, .3, 300); }, cash() { tone(880, .09, 'sine', .18); setTimeout(() => tone(1320, .12, 'sine', .16), 70); }, error() { tone(180, .18, 'sawtooth', .15, null, 120); }, openRide() { [440, 554, 659, 880].forEach((f, i) => setTimeout(() => tone(f, .14, 'triangle', .15), i * 90)); }, whoosh() { noise(.5, .18, 900); }, spell() { if (!AC) return; [660, 830, 990, 1245].forEach((f, i) => setTimeout(() => tone(f, .3, 'sine', .12), i * 60)); }, hit() { noise(.07, .28, 700); tone(140, .08, 'square', .14); }, monsterRoar() { tone(90, .5, 'sawtooth', .25, null, 50); noise(.4, .2, 250); }, victory() { [523, 659, 784, 1046].forEach((f, i) => setTimeout(() => tone(f, .35, 'triangle', .2), i * 160)); }, defeat() { [400, 340, 280, 200].forEach((f, i) => setTimeout(() => tone(f, .4, 'sawtooth', .15), i * 200)); }, levelup() { [600, 750, 900].forEach((f, i) => setTimeout(() => tone(f, .12, 'square', .1), i * 80)); }, }; // ------- generative ambient music: gentle fantasy pad loop ------- const SCALE = [261.63, 293.66, 329.63, 392.00, 440.00, 523.25]; // C pentatonic-ish let musicOn = false; export function startMusic() { if (!AC || musicOn) return; musicOn = true; const stepFn = () => { if (!musicOn) return; // soft chord every ~2s const root = SCALE[Math.floor(Math.random() * 3)]; const third = SCALE[Math.floor(Math.random() * SCALE.length)]; tone(root / 2, 2.4, 'sine', .10, musicGain); tone(third, 2.2, 'triangle', .05, musicGain); if (Math.random() < 0.4) tone(root * 2, 1.8, 'sine', .04, musicGain); musicTimer = setTimeout(stepFn, 1800 + Math.random() * 900); }; stepFn(); } export function stopMusic() { musicOn = false; if (musicTimer) clearTimeout(musicTimer); } export function isMusicOn() { return musicOn; }