// Audio Manager - handles all game sounds and music export type SoundEffect = 'jump' | 'coin' | 'stomp' | 'death' | 'levelup' | 'gameover'; export class AudioManager { private sounds: Map = new Map(); private bgm: HTMLAudioElement | null = null; private isMuted: boolean = false; private volume: number = 0.5; private initialized: boolean = false; constructor() { if (typeof window !== 'undefined') { this.initSounds(); } } private initSounds(): void { // Create audio elements with Web Audio API fallback const soundEffects: SoundEffect[] = ['jump', 'coin', 'stomp', 'death', 'levelup', 'gameover']; soundEffects.forEach(sound => { const audio = new Audio(); audio.volume = this.volume; // Use data URLs for simple 8-bit sounds audio.src = this.getDataUrl(sound); audio.preload = 'auto'; this.sounds.set(sound, audio); }); // Background music this.bgm = new Audio(); this.bgm.volume = this.volume * 0.3; this.bgm.loop = true; this.initialized = true; } private getDataUrl(sound: SoundEffect): string { // Generate simple 8-bit style sounds using oscillator tones encoded as data URLs // These are placeholder audio - in production, use actual audio files const ctx = new (window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)(); const frequencies: Record = { jump: { freq: 400, duration: 0.15, type: 'square' }, coin: { freq: 800, duration: 0.1, type: 'square' }, stomp: { freq: 200, duration: 0.1, type: 'square' }, death: { freq: 150, duration: 0.5, type: 'sawtooth' }, levelup: { freq: 600, duration: 0.3, type: 'square' }, gameover: { freq: 100, duration: 0.8, type: 'sawtooth' }, }; // Store context for later use this.audioContext = ctx; this.soundConfigs = frequencies; // Return empty data URL - we'll use Web Audio API directly return 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA='; } private audioContext: AudioContext | null = null; private soundConfigs: Record | null = null; play(sound: SoundEffect): void { if (this.isMuted || !this.initialized) return; // Use Web Audio API for dynamic sound generation if (this.audioContext && this.soundConfigs) { try { const config = this.soundConfigs[sound]; const oscillator = this.audioContext.createOscillator(); const gainNode = this.audioContext.createGain(); oscillator.type = config.type; oscillator.frequency.setValueAtTime(config.freq, this.audioContext.currentTime); // Frequency slide for jump sound if (sound === 'jump') { oscillator.frequency.exponentialRampToValueAtTime( config.freq * 2, this.audioContext.currentTime + config.duration ); } // Frequency drop for death sound if (sound === 'death' || sound === 'gameover') { oscillator.frequency.exponentialRampToValueAtTime( config.freq * 0.5, this.audioContext.currentTime + config.duration ); } // Coin sound - quick double beep if (sound === 'coin') { oscillator.frequency.setValueAtTime(config.freq, this.audioContext.currentTime); oscillator.frequency.setValueAtTime(config.freq * 1.5, this.audioContext.currentTime + 0.05); } gainNode.gain.setValueAtTime(this.volume * 0.3, this.audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + config.duration); oscillator.connect(gainNode); gainNode.connect(this.audioContext.destination); oscillator.start(this.audioContext.currentTime); oscillator.stop(this.audioContext.currentTime + config.duration); } catch (e) { console.warn('Audio playback failed:', e); } } } playBGM(): void { if (this.isMuted || !this.bgm) return; // Create a simple looping melody using Web Audio API if (this.audioContext) { this.createBGMLoop(); } } private bgmInterval: ReturnType | null = null; private createBGMLoop(): void { if (!this.audioContext || this.bgmInterval) return; const notes = [262, 294, 330, 349, 392, 440, 494, 523]; // C major scale let noteIndex = 0; this.bgmInterval = setInterval(() => { if (this.isMuted || !this.audioContext) { if (this.bgmInterval) clearInterval(this.bgmInterval); return; } const oscillator = this.audioContext.createOscillator(); const gainNode = this.audioContext.createGain(); oscillator.type = 'square'; oscillator.frequency.setValueAtTime(notes[noteIndex % notes.length], this.audioContext.currentTime); gainNode.gain.setValueAtTime(this.volume * 0.1, this.audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.15); oscillator.connect(gainNode); gainNode.connect(this.audioContext.destination); oscillator.start(this.audioContext.currentTime); oscillator.stop(this.audioContext.currentTime + 0.15); noteIndex++; }, 200); } stopBGM(): void { if (this.bgmInterval) { clearInterval(this.bgmInterval); this.bgmInterval = null; } if (this.bgm) { this.bgm.pause(); this.bgm.currentTime = 0; } } setVolume(volume: number): void { this.volume = Math.max(0, Math.min(1, volume)); this.sounds.forEach(audio => { audio.volume = this.volume; }); if (this.bgm) { this.bgm.volume = this.volume * 0.3; } } getVolume(): number { return this.volume; } toggleMute(): boolean { this.isMuted = !this.isMuted; if (this.isMuted) { this.stopBGM(); } return this.isMuted; } isSoundMuted(): boolean { return this.isMuted; } setMuted(muted: boolean): void { this.isMuted = muted; if (muted) { this.stopBGM(); } } // Resume audio context (needed after user interaction) resume(): void { if (this.audioContext && this.audioContext.state === 'suspended') { this.audioContext.resume(); } } } // Singleton instance let audioManagerInstance: AudioManager | null = null; export function getAudioManager(): AudioManager { if (!audioManagerInstance) { audioManagerInstance = new AudioManager(); } return audioManagerInstance; }