diff --git a/public/audio/thunder-distant.mp3 b/public/audio/thunder-distant.mp3 new file mode 100644 index 0000000..9e2d419 Binary files /dev/null and b/public/audio/thunder-distant.mp3 differ diff --git a/src/OceanScene.js b/src/OceanScene.js index 46562f3..cdbcc83 100644 --- a/src/OceanScene.js +++ b/src/OceanScene.js @@ -10,6 +10,7 @@ import { TerrainGenerator } from './TerrainGenerator.js'; import { VegetationSystem } from './VegetationSystem.js'; const RAIN_AUDIO_URL = '/audio/rain-calming.mp3'; +const THUNDER_AUDIO_URL = '/audio/thunder-distant.mp3'; export class OceanScene { constructor(container) { @@ -41,6 +42,9 @@ export class OceanScene { this.horizonFog = null; this.skyHazeBand = null; this.rainAudio = null; + this.thunderAudioPool = []; + this.thunderAudioIndex = 0; + this.scheduledThunder = []; this.params = { elevation: 2, @@ -192,6 +196,13 @@ export class OceanScene { this.rainAudio.preload = 'auto'; this.rainAudio.volume = this.params.rainAudioVolume; this.rainAudio.crossOrigin = 'anonymous'; + + this.thunderAudioPool = Array.from({ length: 3 }, () => { + const audio = new Audio(THUNDER_AUDIO_URL); + audio.preload = 'auto'; + audio.crossOrigin = 'anonymous'; + return audio; + }); } createRainShader() { @@ -1108,7 +1119,10 @@ export class OceanScene { this.lightningFlash = 0; this.lightningBurstEnd = 0; this.nextLightningAt = 0; + this.lightningPulseSchedule = []; + this.scheduledThunder = []; this.applyLightningState(0); + this.stopThunderAudio(); } } @@ -1152,6 +1166,59 @@ export class OceanScene { const size = THREE.MathUtils.randFloat(720, 1480); this.lightningCloudGlow.scale.set(size, size * THREE.MathUtils.randFloat(0.72, 1.08), 1); } + + this.scheduleThunderBurst(flashX, flashY, flashZ); + } + + scheduleThunderBurst(flashX, flashY, flashZ) { + const distanceNorm = THREE.MathUtils.clamp( + (Math.abs(flashX) / 1600) * 0.35 + (Math.abs(flashZ) / 1800) * 0.65, + 0, + 1 + ); + const delay = THREE.MathUtils.lerp(0.65, 2.4, distanceNorm) + Math.random() * 0.45; + const volume = this.params.lightningIntensity * THREE.MathUtils.lerp(0.9, 0.42, distanceNorm); + this.scheduledThunder.push({ + playAt: this.lightningBurstEnd + delay, + volume, + playbackRate: THREE.MathUtils.randFloat(0.94, 1.03) + }); + } + + stopThunderAudio() { + for (const audio of this.thunderAudioPool) { + audio.pause(); + audio.currentTime = 0; + } + } + + playThunder(volume, playbackRate) { + if (!this.params.lightningEnabled || this.thunderAudioPool.length === 0) return; + + const audio = this.thunderAudioPool[this.thunderAudioIndex % this.thunderAudioPool.length]; + this.thunderAudioIndex += 1; + audio.pause(); + audio.currentTime = 0; + audio.volume = THREE.MathUtils.clamp(volume, 0, 1); + audio.playbackRate = playbackRate; + const playPromise = audio.play(); + if (playPromise?.catch) { + playPromise.catch(() => {}); + } + } + + updateThunder(time) { + if (!this.params.lightningEnabled || this.scheduledThunder.length === 0) return; + + const pending = []; + for (const thunder of this.scheduledThunder) { + if (time >= thunder.playAt) { + this.playThunder(thunder.volume, thunder.playbackRate); + } else { + pending.push(thunder); + } + } + this.scheduledThunder = pending; } updateLightning(time) { @@ -1293,6 +1360,7 @@ export class OceanScene { const time = this.clock.getElapsedTime(); this.updateLightning(time); + this.updateThunder(time); if (this.water) { this.water.material.uniforms['time'].value += 1.0 / 60.0;