344 lines
12 KiB
JavaScript
344 lines
12 KiB
JavaScript
class SoundManager {
|
|
constructor(scene) {
|
|
this.scene = scene;
|
|
this.musicVolume = 0.3;
|
|
this.sfxVolume = 0.5;
|
|
this.isMuted = false;
|
|
this.currentMusic = null;
|
|
this.currentAmbient = null;
|
|
console.log('🎵 SoundManager: Initialized');
|
|
}
|
|
|
|
playSFX(key) {
|
|
if (this.isMuted) return;
|
|
|
|
if (this.scene.sound.get(key)) {
|
|
this.scene.sound.play(key, { volume: this.sfxVolume });
|
|
} else {
|
|
// Enhanced placeholder beeps
|
|
if (key === 'chop') {
|
|
this.beepChop();
|
|
} else if (key === 'pickup') {
|
|
this.beepPickup();
|
|
} else if (key === 'plant') {
|
|
this.beepPlant();
|
|
} else if (key === 'harvest') {
|
|
this.beepHarvest();
|
|
} else if (key === 'build') {
|
|
this.beepBuild();
|
|
} else if (key === 'dig') {
|
|
this.beepDig();
|
|
}
|
|
}
|
|
}
|
|
|
|
beepChop() {
|
|
if (!this.scene.sound.context) return;
|
|
const ctx = this.scene.sound.context;
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.frequency.value = 150;
|
|
osc.type = 'sawtooth';
|
|
gain.gain.setValueAtTime(0.15, ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.15);
|
|
osc.start();
|
|
osc.stop(ctx.currentTime + 0.15);
|
|
}
|
|
|
|
beepPickup() {
|
|
if (!this.scene.sound.context) return;
|
|
const ctx = this.scene.sound.context;
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.frequency.setValueAtTime(600, ctx.currentTime);
|
|
osc.frequency.linearRampToValueAtTime(1200, ctx.currentTime + 0.1);
|
|
osc.type = 'sine';
|
|
gain.gain.setValueAtTime(0.12, ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.1);
|
|
osc.start();
|
|
osc.stop(ctx.currentTime + 0.1);
|
|
}
|
|
|
|
beepPlant() {
|
|
if (!this.scene.sound.context) return;
|
|
const ctx = this.scene.sound.context;
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.frequency.value = 300;
|
|
osc.type = 'triangle';
|
|
gain.gain.setValueAtTime(0.1, ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.12);
|
|
osc.start();
|
|
osc.stop(ctx.currentTime + 0.12);
|
|
}
|
|
|
|
beepHarvest() {
|
|
if (!this.scene.sound.context) return;
|
|
const ctx = this.scene.sound.context;
|
|
const osc1 = ctx.createOscillator();
|
|
const gain1 = ctx.createGain();
|
|
osc1.connect(gain1);
|
|
gain1.connect(ctx.destination);
|
|
osc1.frequency.value = 523;
|
|
osc1.type = 'sine';
|
|
gain1.gain.setValueAtTime(0.1, ctx.currentTime);
|
|
gain1.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.08);
|
|
osc1.start();
|
|
osc1.stop(ctx.currentTime + 0.08);
|
|
|
|
const osc2 = ctx.createOscillator();
|
|
const gain2 = ctx.createGain();
|
|
osc2.connect(gain2);
|
|
gain2.connect(ctx.destination);
|
|
osc2.frequency.value = 659;
|
|
osc2.type = 'sine';
|
|
gain2.gain.setValueAtTime(0.1, ctx.currentTime + 0.08);
|
|
gain2.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.16);
|
|
osc2.start(ctx.currentTime + 0.08);
|
|
osc2.stop(ctx.currentTime + 0.16);
|
|
}
|
|
|
|
beepBuild() {
|
|
if (!this.scene.sound.context) return;
|
|
const ctx = this.scene.sound.context;
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.frequency.value = 80;
|
|
osc.type = 'square';
|
|
gain.gain.setValueAtTime(0.2, ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.2);
|
|
osc.start();
|
|
osc.stop(ctx.currentTime + 0.2);
|
|
}
|
|
|
|
beepAttack() {
|
|
if (!this.scene.sound.context) return;
|
|
const ctx = this.scene.sound.context;
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.frequency.setValueAtTime(400, ctx.currentTime);
|
|
osc.frequency.exponentialRampToValueAtTime(100, ctx.currentTime + 0.08);
|
|
osc.type = 'sawtooth';
|
|
gain.gain.setValueAtTime(0.2, ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.08);
|
|
osc.start();
|
|
osc.stop(ctx.currentTime + 0.08);
|
|
}
|
|
|
|
beepHit() {
|
|
if (!this.scene.sound.context) return;
|
|
const ctx = this.scene.sound.context;
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.frequency.value = 80;
|
|
osc.type = 'square';
|
|
gain.gain.setValueAtTime(0.25, ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.05);
|
|
osc.start();
|
|
osc.stop(ctx.currentTime + 0.05);
|
|
}
|
|
|
|
beepFootstep() {
|
|
if (!this.scene.sound.context) return;
|
|
const ctx = this.scene.sound.context;
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.frequency.value = 120 + Math.random() * 20;
|
|
osc.type = 'sine';
|
|
gain.gain.setValueAtTime(0.05, ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.03);
|
|
osc.start();
|
|
osc.stop(ctx.currentTime + 0.03);
|
|
}
|
|
|
|
beepDeath() {
|
|
if (!this.scene.sound.context) return;
|
|
const ctx = this.scene.sound.context;
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.frequency.setValueAtTime(300, ctx.currentTime);
|
|
osc.frequency.exponentialRampToValueAtTime(50, ctx.currentTime + 0.5);
|
|
osc.type = 'sawtooth';
|
|
gain.gain.setValueAtTime(0.2, ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.5);
|
|
osc.start();
|
|
osc.stop(ctx.currentTime + 0.5);
|
|
}
|
|
|
|
startRainNoise() {
|
|
if (!this.scene.sound.context || this.rainNode) return;
|
|
const ctx = this.scene.sound.context;
|
|
|
|
// Create noise buffer
|
|
const bufferSize = 2 * ctx.sampleRate;
|
|
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
|
|
const output = buffer.getChannelData(0);
|
|
for (let i = 0; i < bufferSize; i++) {
|
|
output[i] = Math.random() * 2 - 1;
|
|
}
|
|
|
|
this.rainNode = ctx.createBufferSource();
|
|
this.rainNode.buffer = buffer;
|
|
this.rainNode.loop = true;
|
|
|
|
// Lowpass filter for rain sound
|
|
const filter = ctx.createBiquadFilter();
|
|
filter.type = 'lowpass';
|
|
filter.frequency.value = 800;
|
|
|
|
this.rainGain = ctx.createGain();
|
|
this.rainGain.gain.value = 0.05 * this.sfxVolume;
|
|
|
|
this.rainNode.connect(filter);
|
|
filter.connect(this.rainGain);
|
|
this.rainGain.connect(ctx.destination);
|
|
|
|
this.rainNode.start();
|
|
}
|
|
|
|
stopRainNoise() {
|
|
if (this.rainNode) {
|
|
this.rainNode.stop();
|
|
this.rainNode.disconnect();
|
|
this.rainNode = null;
|
|
}
|
|
if (this.rainGain) {
|
|
this.rainGain.disconnect();
|
|
this.rainGain = null;
|
|
}
|
|
}
|
|
|
|
playAmbient(key, loop = true) {
|
|
if (this.isMuted) return;
|
|
if (this.currentAmbient) this.currentAmbient.stop();
|
|
if (!this.scene.sound.get(key)) return;
|
|
this.currentAmbient = this.scene.sound.add(key, { volume: this.sfxVolume * 0.5, loop: loop });
|
|
this.currentAmbient.play();
|
|
}
|
|
|
|
stopAmbient() {
|
|
if (this.currentAmbient) {
|
|
this.currentAmbient.stop();
|
|
this.currentAmbient = null;
|
|
}
|
|
}
|
|
|
|
startMusic() {
|
|
if (!this.scene.sound.context || this.musicInterval) return;
|
|
|
|
console.log('🎵 Starting Ambient Music...');
|
|
// Simple C Minor Pentatonic: C3, Eb3, F3, G3, Bb3
|
|
const scale = [130.81, 155.56, 174.61, 196.00, 233.08, 261.63];
|
|
|
|
// Loop every 3-5 seconds play a note
|
|
this.musicInterval = setInterval(() => {
|
|
if (this.isMuted) return;
|
|
// 40% chance to play a note
|
|
if (Math.random() > 0.6) {
|
|
const freq = scale[Math.floor(Math.random() * scale.length)];
|
|
this.playProceduralNote(freq);
|
|
}
|
|
}, 2000);
|
|
}
|
|
|
|
stopMusic() {
|
|
if (this.musicInterval) {
|
|
clearInterval(this.musicInterval);
|
|
this.musicInterval = null;
|
|
}
|
|
}
|
|
|
|
playProceduralNote(freq) {
|
|
if (!this.scene.sound.context) return;
|
|
const ctx = this.scene.sound.context;
|
|
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
|
|
osc.frequency.value = freq;
|
|
osc.type = 'sine'; // Soft tone
|
|
|
|
const now = ctx.currentTime;
|
|
const duration = 2.0; // Long decay like reverb/pad
|
|
|
|
gain.gain.setValueAtTime(0, now);
|
|
gain.gain.linearRampToValueAtTime(0.05 * this.musicVolume, now + 0.5); // Slow attack
|
|
gain.gain.exponentialRampToValueAtTime(0.001, now + duration); // Long release
|
|
|
|
osc.start(now);
|
|
osc.stop(now + duration);
|
|
}
|
|
|
|
toggleMute() {
|
|
this.isMuted = !this.isMuted;
|
|
this.scene.sound.mute = this.isMuted;
|
|
console.log(this.isMuted ? '🔇 Muted' : '🔊 Unmuted');
|
|
}
|
|
|
|
playChop() { this.playSFX('chop'); }
|
|
playPlant() { this.playSFX('plant'); }
|
|
playHarvest() { this.playSFX('harvest'); }
|
|
playBuild() { this.playSFX('build'); }
|
|
playPickup() { this.playSFX('pickup'); }
|
|
playDig() { this.playSFX('dig'); }
|
|
playUIClick() { this.beepUIClick(); } // UI click sound
|
|
playAttack() { this.beepAttack(); }
|
|
playHit() { this.beepHit(); }
|
|
playFootstep() { this.beepFootstep(); }
|
|
playDeath() { this.beepDeath(); }
|
|
playRainSound() { this.startRainNoise(); }
|
|
stopRainSound() { this.stopRainNoise(); }
|
|
|
|
beepUIClick() {
|
|
if (!this.scene.sound.context) return;
|
|
const ctx = this.scene.sound.context;
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
// Quick, pleasant click sound
|
|
osc.frequency.value = 800;
|
|
osc.type = 'sine';
|
|
gain.gain.setValueAtTime(0.08, ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.05);
|
|
osc.start();
|
|
osc.stop(ctx.currentTime + 0.05);
|
|
}
|
|
|
|
beepDig() {
|
|
if (!this.scene.sound.context) return;
|
|
const ctx = this.scene.sound.context;
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
// Low "thud" for dirt interaction
|
|
osc.frequency.setValueAtTime(80, ctx.currentTime);
|
|
osc.frequency.exponentialRampToValueAtTime(10, ctx.currentTime + 0.15);
|
|
osc.type = 'triangle';
|
|
gain.gain.setValueAtTime(0.2, ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.15);
|
|
osc.start();
|
|
osc.stop(ctx.currentTime + 0.15);
|
|
}
|
|
}
|