let audioCtx = null; function getCtx() { if (audioCtx === null || audioCtx === undefined) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); return audioCtx; } function applySoundUpgrade(osc, now) { osc.frequency.setValueAtTime(659, now); osc.frequency.setValueAtTime(784, now + 0.06); } function applySoundHatch(osc, now) { osc.type = "sine"; osc.frequency.setValueAtTime(392, now); osc.frequency.setValueAtTime(523, now + 0.08); osc.frequency.setValueAtTime(659, now + 0.16); } function applySoundPlace(osc, now) { osc.type = "sine"; osc.frequency.setValueAtTime(659, now); osc.frequency.setValueAtTime(523, now + 0.05); osc.frequency.setValueAtTime(784, now + 0.1); } function applySoundBuy(osc, now) { osc.type = "sine"; osc.frequency.setValueAtTime(523, now); osc.frequency.setValueAtTime(659, now + 0.05); osc.frequency.setValueAtTime(784, now + 0.1); } function applySoundSell(osc, now) { osc.type = "triangle"; osc.frequency.setValueAtTime(440, now); osc.frequency.setValueAtTime(349, now + 0.06); osc.frequency.setValueAtTime(262, now + 0.12); } function applySoundPlotUpgrade(osc, now) { osc.type = "sine"; osc.frequency.setValueAtTime(330, now); osc.frequency.setValueAtTime(440, now + 0.06); osc.frequency.setValueAtTime(554, now + 0.12); } function applySoundTruckUpgrade(osc, now) { osc.type = "square"; osc.frequency.setValueAtTime(220, now); osc.frequency.setValueAtTime(277, now + 0.05); osc.frequency.setValueAtTime(330, now + 0.1); } function applySoundSchoolUpgrade(osc, now) { osc.type = "sine"; osc.frequency.setValueAtTime(523, now); osc.frequency.setValueAtTime(659, now + 0.05); osc.frequency.setValueAtTime(784, now + 0.1); osc.frequency.setValueAtTime(1047, now + 0.15); } function applySoundWorldMapUpgrade(osc, now) { osc.type = "sine"; osc.frequency.setValueAtTime(440, now); osc.frequency.setValueAtTime(554, now + 0.06); osc.frequency.setValueAtTime(698, now + 0.12); } function applySoundQuest(osc, now) { osc.frequency.setValueAtTime(659, now); osc.frequency.setValueAtTime(784, now + 0.06); } function applySoundError(osc, now) { osc.frequency.setValueAtTime(200, now); osc.frequency.setValueAtTime(180, now + 0.08); } function applySoundDefault(osc, now) { osc.frequency.setValueAtTime(440, now); } const SOUND_APPLIERS = { upgrade: applySoundUpgrade, hatch: applySoundHatch, place: applySoundPlace, buy: applySoundBuy, sell: applySoundSell, plotUpgrade: applySoundPlotUpgrade, truckUpgrade: applySoundTruckUpgrade, schoolUpgrade: applySoundSchoolUpgrade, worldMapUpgrade: applySoundWorldMapUpgrade, quest: applySoundQuest, error: applySoundError, }; /** * @param {string} type One of: hatch, place, buy, sell, plotUpgrade, truckUpgrade, schoolUpgrade, worldMapUpgrade, upgrade, quest, error * @returns {void} */ export function playSound(type) { try { const ctx = getCtx(); const now = ctx.currentTime; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); gain.gain.setValueAtTime(0.15, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.15); osc.start(now); osc.stop(now + 0.15); const applier = SOUND_APPLIERS[type] || applySoundDefault; applier(osc, now); } catch (e) { console.warn("playSound failed", e); } } let musicEnabled = false; let musicIntervalId = null; let musicGetState = () => null; const MUSIC_BEAT_MS = 400; /** * Register state getter so music can switch by day/night/truck. * @param {() => { timeOfDay?: number, truckSale?: { startAt: number } }} getState */ export function setMusicGetState(getState) { musicGetState = getState || (() => null); } function isNight(timeOfDay) { const t = (timeOfDay ?? 6) % 24; return t < 6 || t >= 20; } function isTruckMoving(state) { const sale = state.truckSale; if (!sale || !sale.startAt) return false; const truckMs = 2500; return (Date.now() - sale.startAt) < truckMs; } function playMusicVoliere() { if (!musicEnabled || (audioCtx === null || audioCtx === undefined)) return; try { const ctx = getCtx(); const now = ctx.currentTime; const gain = ctx.createGain(); gain.connect(ctx.destination); gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(0.04, now + 0.02); gain.gain.exponentialRampToValueAtTime(0.004, now + 0.25); const freq = 600 + Math.random() * 800; const osc = ctx.createOscillator(); osc.type = "sine"; osc.frequency.setValueAtTime(freq, now); osc.connect(gain); osc.start(now); osc.stop(now + 0.25); } catch (e) { console.warn("playMusicVoliere failed", e); } } function playMusicBrahms() { if (!musicEnabled || (audioCtx === null || audioCtx === undefined)) return; try { const ctx = getCtx(); const now = ctx.currentTime; const gain = ctx.createGain(); gain.connect(ctx.destination); gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(0.05, now + 0.03); gain.gain.exponentialRampToValueAtTime(0.005, now + 0.5); const waltz = [415.3, 523.25, 622.25]; waltz.forEach((freq, i) => { const osc = ctx.createOscillator(); osc.type = "sine"; osc.frequency.setValueAtTime(freq, now + i * 0.15); osc.connect(gain); osc.start(now + i * 0.15); osc.stop(now + 0.5); }); } catch (e) { console.warn("playMusicBrahms failed", e); } } function playMusicTrepak() { if (!musicEnabled || (audioCtx === null || audioCtx === undefined)) return; try { const ctx = getCtx(); const now = ctx.currentTime; const gain = ctx.createGain(); gain.connect(ctx.destination); gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(0.08, now + 0.02); gain.gain.exponentialRampToValueAtTime(0.006, now + 0.12); const osc = ctx.createOscillator(); osc.type = "triangle"; osc.frequency.setValueAtTime(280, now); osc.frequency.setValueAtTime(350, now + 0.06); osc.connect(gain); osc.start(now); osc.stop(now + 0.12); } catch (e) { console.warn("playMusicTrepak failed", e); } } function playMusicTick() { if (!musicEnabled || (audioCtx === null || audioCtx === undefined)) return; const state = musicGetState(); if (state && isTruckMoving(state)) { playMusicTrepak(); return; } const timeOfDay = state && state.timeOfDay !== null && state.timeOfDay !== undefined ? state.timeOfDay : 6; if (isNight(timeOfDay)) { playMusicBrahms(); } else { playMusicVoliere(); } } /** Call to enable/disable background music (procedural: Volière / Brahms / Trepak). */ export function setMusicEnabled(enabled) { if (musicIntervalId !== null && musicIntervalId !== undefined) { clearInterval(musicIntervalId); musicIntervalId = null; } musicEnabled = Boolean(enabled); if (musicEnabled) { getCtx(); playMusicTick(); musicIntervalId = setInterval(playMusicTick, MUSIC_BEAT_MS); } } export function isMusicEnabled() { return musicEnabled; }