'use strict'; let audioCtx = null; function getAudio() { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); return audioCtx; } function beep(freq, freq2, dur, type = 'sine', vol = 0.3) { try { const ac = getAudio(); const o = ac.createOscillator(); const g = ac.createGain(); o.type = type; o.connect(g); g.connect(ac.destination); o.frequency.setValueAtTime(freq, ac.currentTime); if (freq2 && freq2 !== freq) o.frequency.exponentialRampToValueAtTime(freq2, ac.currentTime + dur); g.gain.setValueAtTime(vol, ac.currentTime); g.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + dur); o.start(ac.currentTime); o.stop(ac.currentTime + dur + 0.01); } catch(e) {} } const SFX = { jump: () => beep(280, 560, 0.18, 'sine', 0.25), attack: () => beep(140, 70, 0.12, 'sawtooth', 0.30), stomp: () => { beep(200, 80, 0.1, 'square', 0.35); beep(400, 400, 0.08, 'sine', 0.2); }, hurt: () => beep(180, 90, 0.28, 'square', 0.40), die: () => beep(380, 50, 0.80, 'square', 0.40), collect: () => beep(550, 880, 0.10, 'sine', 0.20), heart: () => { beep(660, 660, 0.06, 'sine', 0.2); setTimeout(() => beep(880, 880, 0.12, 'sine', 0.2), 70); }, killEnemy: () => beep(260, 100, 0.25, 'sawtooth', 0.28), combo: (n) => beep(440 + n * 40, 550 + n * 40, 0.08, 'sine', 0.18), levelUp: () => { [523, 659, 784, 1047].forEach((f, i) => setTimeout(() => beep(f, f, i === 3 ? 0.25 : 0.08, 'sine', 0.28), i * 100) ); }, }; // ─── Background Music ───────────────────────────────────────────────────────── let _musicTimer = null; let _musicStep = 0; const MUSIC = { menu: { bpm: 112, notes: [392,0,523,0,659,523,392,330, 392,0,523,659,784,659,523,0] }, grass: { bpm: 132, notes: [523,0,659,0,784,659,523,0, 440,0,523,0,659,523,440,0] }, cave: { bpm: 96, notes: [294,0,330,0,262,0,294,0, 220,0,247,0,262,0,220,0] }, ice: { bpm: 108, notes: [440,0,494,523,587,0,523,494, 440,0,392,0,440,494,523,0] }, }; function startMusic(theme) { stopMusic(); try { getAudio(); } catch(e) { return; } const pat = MUSIC[theme] || MUSIC.menu; const beatMs = (60000 / pat.bpm) / 2; // eighth-note duration _musicStep = 0; const tick = () => { const freq = pat.notes[_musicStep % pat.notes.length]; if (freq > 0) { beep(freq / 2, freq / 2, beatMs / 1000 * 0.75, 'square', 0.04); if (_musicStep % 2 === 0) beep(freq, freq, beatMs / 1000 * 0.55, 'triangle', 0.05); } _musicStep++; _musicTimer = setTimeout(tick, beatMs); }; _musicTimer = setTimeout(tick, 0); } function stopMusic() { if (_musicTimer) { clearTimeout(_musicTimer); _musicTimer = null; } _musicStep = 0; }