// Klang: KI-generierte Samples (ElevenLabs, assets/sounds/*.mp3) mit // WebAudio abgespielt; fehlt eine Datei, greift ein kleiner Synth-Fallback. // Dazu saisonale Atmosphäre (Sommerwiese/Winterwind) als Loop. // AudioContext entsteht erst nach der ersten Nutzer-Geste (iOS/Safari). const SAMPLES = ['click', 'build', 'demolish', 'success', 'error', 'card', 'milestone', 'win', 'amb_birds', 'amb_cow', 'amb_cowbell', 'amb_wind']; export const audio = { ctx: null, soundOn: true, musicOn: true, _buffers: {}, // Name → AudioBuffer _raw: {}, // Name → ArrayBuffer (vor dem Decodieren) _season: 'Sommer', // MP3s sofort laden (ohne AudioContext), decodiert wird bei ensure() preload() { for (const name of SAMPLES) { fetch(`assets/sounds/${name}.mp3`) .then(r => (r.ok ? r.arrayBuffer() : null)) .then(buf => { if (buf) { this._raw[name] = buf; this._decode(name); } }) .catch(() => {}); } }, _decode(name) { if (!this.ctx || !this._raw[name] || this._buffers[name]) return; this.ctx.decodeAudioData(this._raw[name].slice(0)) .then(b => { this._buffers[name] = b; }) .catch(() => {}); }, ensure() { if (!this.ctx) { const AC = window.AudioContext || window.webkitAudioContext; if (!AC) return false; this.ctx = new AC(); for (const name of Object.keys(this._raw)) this._decode(name); } if (this.ctx.state === 'suspended') this.ctx.resume(); return true; }, // ---------- Effekte ---------- play(name, gain = 0.5) { if (!this.soundOn || !this.ensure()) return; const buf = this._buffers[name]; if (buf) { const src = this.ctx.createBufferSource(); const g = this.ctx.createGain(); g.gain.value = gain; src.buffer = buf; src.connect(g).connect(this.ctx.destination); src.start(); } else { this._synthFallback(name); } }, click() { this.play('click', 0.35); }, success() { this.play('success', 0.5); }, error() { this.play('error', 0.45); }, chime() { this.play('card', 0.45); }, card() { this.chime(); }, build() { this.play('build', 0.55); }, demolish() { this.play('demolish', 0.55); }, milestone() { this.play('milestone', 0.55); }, win() { this.play('win', 0.6); }, _tone(freq, dur, type = 'sine', gain = 0.08, delay = 0) { const t0 = this.ctx.currentTime + delay; const osc = this.ctx.createOscillator(); const g = this.ctx.createGain(); osc.type = type; osc.frequency.value = freq; g.gain.setValueAtTime(0, t0); g.gain.linearRampToValueAtTime(gain, t0 + 0.02); g.gain.exponentialRampToValueAtTime(0.001, t0 + dur); osc.connect(g).connect(this.ctx.destination); osc.start(t0); osc.stop(t0 + dur + 0.05); }, _synthFallback(name) { switch (name) { case 'click': this._tone(660, 0.08, 'triangle', 0.05); break; case 'success': case 'build': case 'milestone': case 'win': this._tone(523, 0.12, 'triangle', 0.07); this._tone(784, 0.18, 'triangle', 0.07, 0.09); break; case 'error': case 'demolish': this._tone(180, 0.22, 'sawtooth', 0.05); break; default: this._tone(880, 0.3, 'sine', 0.05); } }, // ---------- Gelegentliche Naturlaute (statt Dauer-Ambience) ---------- // Alle 10–25 s ein kurzer Laut passend zur Saison: Vögel, Kuhglocken, // ein Muhen – im Winter meist nur ein Windhauch. _ambTimer: null, setSeason(season) { this._season = season; }, _pickAmbience() { const r = Math.random(); if (this._season === 'Winter') { return r < 0.6 ? 'amb_wind' : r < 0.85 ? 'amb_cowbell' : 'amb_birds'; } return r < 0.45 ? 'amb_birds' : r < 0.75 ? 'amb_cowbell' : 'amb_cow'; }, _scheduleAmbience() { this._ambTimer = setTimeout(() => { if (this.soundOn && this.ctx) { const name = this._pickAmbience(); const buf = this._buffers[name]; if (buf) { const src = this.ctx.createBufferSource(); const g = this.ctx.createGain(); g.gain.value = 0.22; src.buffer = buf; src.connect(g).connect(this.ctx.destination); src.start(); } } this._scheduleAmbience(); }, 10000 + Math.random() * 15000); }, startMusic() { if (!this.ensure()) return; clearTimeout(this._ambTimer); this._scheduleAmbience(); }, _stopAmbience() { clearTimeout(this._ambTimer); this._ambTimer = null; }, toggleMusic() { this.musicOn = !this.musicOn; if (this.musicOn) this.startMusic(); else this._stopAmbience(); return this.musicOn; }, toggleSound() { this.soundOn = !this.soundOn; return this.soundOn; }, }; audio.preload();