213 lines
9.6 KiB
JavaScript
213 lines
9.6 KiB
JavaScript
/* ═══════════════════════════════════════════════════════════════
|
|
AUDIO SYSTEM - Web Audio API Sound Effects
|
|
═══════════════════════════════════════════════════════════════ */
|
|
|
|
const AudioSystem = {
|
|
ctx: null,
|
|
masterVolume: 0.3,
|
|
sounds: {},
|
|
muted: false,
|
|
|
|
init() {
|
|
try {
|
|
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
|
|
this.createSounds();
|
|
} catch (e) {
|
|
console.log('Audio not supported');
|
|
}
|
|
},
|
|
|
|
createSounds() {
|
|
// Jump sound - rising pitch
|
|
this.sounds.jump = () => {
|
|
if (!this.ctx || this.muted) return;
|
|
const osc = this.ctx.createOscillator();
|
|
const gain = this.ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(this.ctx.destination);
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(200, this.ctx.currentTime);
|
|
osc.frequency.exponentialRampToValueAtTime(600, this.ctx.currentTime + 0.1);
|
|
gain.gain.setValueAtTime(this.masterVolume * 0.5, this.ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.15);
|
|
osc.start();
|
|
osc.stop(this.ctx.currentTime + 0.15);
|
|
};
|
|
|
|
// Double jump sound - higher pitch
|
|
this.sounds.doubleJump = () => {
|
|
if (!this.ctx || this.muted) return;
|
|
const osc = this.ctx.createOscillator();
|
|
const gain = this.ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(this.ctx.destination);
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(400, this.ctx.currentTime);
|
|
osc.frequency.exponentialRampToValueAtTime(800, this.ctx.currentTime + 0.1);
|
|
gain.gain.setValueAtTime(this.masterVolume * 0.4, this.ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.12);
|
|
osc.start();
|
|
osc.stop(this.ctx.currentTime + 0.12);
|
|
};
|
|
|
|
// Coin collect - bright chime
|
|
this.sounds.coin = () => {
|
|
if (!this.ctx || this.muted) return;
|
|
const frequencies = [880, 1100, 1320];
|
|
frequencies.forEach((freq, i) => {
|
|
const osc = this.ctx.createOscillator();
|
|
const gain = this.ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(this.ctx.destination);
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(freq, this.ctx.currentTime + i * 0.05);
|
|
gain.gain.setValueAtTime(0, this.ctx.currentTime);
|
|
gain.gain.linearRampToValueAtTime(this.masterVolume * 0.3, this.ctx.currentTime + i * 0.05);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.2 + i * 0.05);
|
|
osc.start(this.ctx.currentTime + i * 0.05);
|
|
osc.stop(this.ctx.currentTime + 0.25 + i * 0.05);
|
|
});
|
|
};
|
|
|
|
// Damage/hurt - low thud
|
|
this.sounds.hurt = () => {
|
|
if (!this.ctx || this.muted) return;
|
|
const osc = this.ctx.createOscillator();
|
|
const gain = this.ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(this.ctx.destination);
|
|
osc.type = 'sawtooth';
|
|
osc.frequency.setValueAtTime(150, this.ctx.currentTime);
|
|
osc.frequency.exponentialRampToValueAtTime(50, this.ctx.currentTime + 0.2);
|
|
gain.gain.setValueAtTime(this.masterVolume * 0.4, this.ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.2);
|
|
osc.start();
|
|
osc.stop(this.ctx.currentTime + 0.2);
|
|
};
|
|
|
|
// Power-up collected - magical chime
|
|
this.sounds.powerUp = () => {
|
|
if (!this.ctx || this.muted) return;
|
|
const frequencies = [523, 659, 784, 1047];
|
|
frequencies.forEach((freq, i) => {
|
|
const osc = this.ctx.createOscillator();
|
|
const gain = this.ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(this.ctx.destination);
|
|
osc.type = 'triangle';
|
|
osc.frequency.setValueAtTime(freq, this.ctx.currentTime + i * 0.08);
|
|
gain.gain.setValueAtTime(0, this.ctx.currentTime);
|
|
gain.gain.linearRampToValueAtTime(this.masterVolume * 0.25, this.ctx.currentTime + i * 0.08);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.3 + i * 0.08);
|
|
osc.start(this.ctx.currentTime + i * 0.08);
|
|
osc.stop(this.ctx.currentTime + 0.4 + i * 0.08);
|
|
});
|
|
};
|
|
|
|
// Checkpoint - celebratory sound
|
|
this.sounds.checkpoint = () => {
|
|
if (!this.ctx || this.muted) return;
|
|
const frequencies = [392, 523, 659, 784];
|
|
frequencies.forEach((freq, i) => {
|
|
const osc = this.ctx.createOscillator();
|
|
const gain = this.ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(this.ctx.destination);
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(freq, this.ctx.currentTime + i * 0.1);
|
|
gain.gain.setValueAtTime(0, this.ctx.currentTime);
|
|
gain.gain.linearRampToValueAtTime(this.masterVolume * 0.3, this.ctx.currentTime + i * 0.1);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.3 + i * 0.1);
|
|
osc.start(this.ctx.currentTime + i * 0.1);
|
|
osc.stop(this.ctx.currentTime + 0.5 + i * 0.1);
|
|
});
|
|
};
|
|
|
|
// Level complete - victory fanfare
|
|
this.sounds.levelComplete = () => {
|
|
if (!this.ctx || this.muted) return;
|
|
const notes = [523, 587, 659, 698, 784, 880, 988, 1047];
|
|
notes.forEach((freq, i) => {
|
|
const osc = this.ctx.createOscillator();
|
|
const gain = this.ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(this.ctx.destination);
|
|
osc.type = 'square';
|
|
osc.frequency.setValueAtTime(freq, this.ctx.currentTime + i * 0.12);
|
|
gain.gain.setValueAtTime(0, this.ctx.currentTime);
|
|
gain.gain.linearRampToValueAtTime(this.masterVolume * 0.15, this.ctx.currentTime + i * 0.12 + 0.02);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.4 + i * 0.12);
|
|
osc.start(this.ctx.currentTime + i * 0.12);
|
|
osc.stop(this.ctx.currentTime + 0.5 + i * 0.12);
|
|
});
|
|
};
|
|
|
|
// Game over - sad descending
|
|
this.sounds.gameOver = () => {
|
|
if (!this.ctx || this.muted) return;
|
|
const frequencies = [440, 415, 392, 370, 349, 330, 311, 294];
|
|
frequencies.forEach((freq, i) => {
|
|
const osc = this.ctx.createOscillator();
|
|
const gain = this.ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(this.ctx.destination);
|
|
osc.type = 'sawtooth';
|
|
osc.frequency.setValueAtTime(freq, this.ctx.currentTime + i * 0.15);
|
|
gain.gain.setValueAtTime(0, this.ctx.currentTime);
|
|
gain.gain.linearRampToValueAtTime(this.masterVolume * 0.2, this.ctx.currentTime + i * 0.15);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.3 + i * 0.15);
|
|
osc.start(this.ctx.currentTime + i * 0.15);
|
|
osc.stop(this.ctx.currentTime + 0.4 + i * 0.15);
|
|
});
|
|
};
|
|
|
|
// Step/land sound
|
|
this.sounds.land = () => {
|
|
if (!this.ctx || this.muted) return;
|
|
const osc = this.ctx.createOscillator();
|
|
const gain = this.ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(this.ctx.destination);
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(80, this.ctx.currentTime);
|
|
osc.frequency.exponentialRampToValueAtTime(40, this.ctx.currentTime + 0.05);
|
|
gain.gain.setValueAtTime(this.masterVolume * 0.2, this.ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.05);
|
|
osc.start();
|
|
osc.stop(this.ctx.currentTime + 0.05);
|
|
};
|
|
},
|
|
|
|
play(soundName) {
|
|
if (this.sounds[soundName]) {
|
|
// Resume audio context if suspended (browser autoplay policy)
|
|
if (this.ctx && this.ctx.state === 'suspended') {
|
|
this.ctx.resume();
|
|
}
|
|
this.sounds[soundName]();
|
|
}
|
|
},
|
|
|
|
toggleMute() {
|
|
this.muted = !this.muted;
|
|
return this.muted;
|
|
},
|
|
|
|
setVolume(vol) {
|
|
this.masterVolume = Math.max(0, Math.min(1, vol));
|
|
}
|
|
};
|
|
|
|
// Auto-initialize on first user interaction
|
|
document.addEventListener('click', () => {
|
|
if (!AudioSystem.ctx) {
|
|
AudioSystem.init();
|
|
}
|
|
}, { once: true });
|
|
|
|
document.addEventListener('keydown', () => {
|
|
if (!AudioSystem.ctx) {
|
|
AudioSystem.init();
|
|
}
|
|
}, { once: true });
|