Spaces:
Sleeping
Sleeping
| // Audio Manager - handles all game sounds and music | |
| export type SoundEffect = 'jump' | 'coin' | 'stomp' | 'death' | 'levelup' | 'gameover'; | |
| export class AudioManager { | |
| private sounds: Map<string, HTMLAudioElement> = 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<SoundEffect, { freq: number; duration: number; type: OscillatorType }> = { | |
| 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<SoundEffect, { freq: number; duration: number; type: OscillatorType }> | 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<typeof setInterval> | 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; | |
| } | |