// Bombardment sound. // // Two effects share the same clip. A standing ground or naval barrage plays // spaced impacts at random intervals while its target is in view. An aircraft // bomb run is different: the plane flies to its target and only then drops its // bombs, so it gets one large burst when the run actually resolves, not an // ambience during the flight. Both rise in loudness as the camera zooms in, and // all the timing is frame-driven, so closing the map stops them without leaving // a timer behind. import { ZOOM_MIN, ZOOM_MAX } from "../map_view/constants.js"; export const BOMB_BURST = { min: 1, max: 3 }; // Time between the impacts inside one burst. export const BOMB_GAP_MS = { min: 160, max: 380 }; // Time between bursts. export const BOMB_IDLE_MS = { min: 900, max: 2400 }; // How much of the clip each hit plays. export const BOMB_CLIP_S = { min: 0.3, max: 0.7 }; export const BOMB_VOLUME = { min: 0.06, max: 0.6 }; // An aircraft's bomb run arrives all at once: a large, fast burst. export const AIR_BOMB_BURST = { min: 7, max: 10 }; export const AIR_BOMB_GAP_MS = { min: 55, max: 150 }; export const AIR_BOMB_CLIP_S = { min: 0.25, max: 0.5 }; // Ceiling on a burst so several squadrons bombing together cannot swamp the mix. const AIR_BOMB_LIMIT = AIR_BOMB_BURST.max * 3; function between(range, random) { return range.min + random() * (range.max - range.min); } // The impact volume for a camera zoom: quiet when zoomed far out, louder the // closer the camera is to the ground. export function zoomVolume(zoom) { const t = Math.min(1, Math.max(0, (zoom - ZOOM_MIN) / (ZOOM_MAX - ZOOM_MIN))); return BOMB_VOLUME.min + t * (BOMB_VOLUME.max - BOMB_VOLUME.min); } export class BombardmentSfx { // `play(volume, durationSeconds)` is called once per impact. `random` is // injectable so a test can pin the intervals. constructor(play, random = Math.random) { this.play = play; this.random = random; this.reset(); } reset() { this._idle = 0; this._left = 0; this._gap = 0; } // Advances one frame. `dtMs` is the elapsed time, `active` whether a barrage // is falling in view, and `volume` the zoom-scaled loudness of each hit. update(dtMs, active, volume) { if (!active) { this.reset(); return; } if (this._left > 0) { this._gap -= dtMs; while (this._left > 0 && this._gap <= 0) { this.play(volume, between(BOMB_CLIP_S, this.random)); this._left -= 1; // Reset rather than carry the leftover, so a large frame step can never // bunch the impacts together. this._gap = between(BOMB_GAP_MS, this.random); } if (this._left <= 0) this._idle = between(BOMB_IDLE_MS, this.random); return; } this._idle -= dtMs; if (this._idle > 0) return; this._left = Math.min( BOMB_BURST.max, BOMB_BURST.min + Math.floor(this.random() * (BOMB_BURST.max - BOMB_BURST.min + 1)) ); this._gap = 0; } } export class AirStrikeSfx { constructor(play, random = Math.random) { this.play = play; this.random = random; this.reset(); } reset() { this._left = 0; this._gap = 0; this._volume = 0; } // Starts (or extends) a bomb-run burst. Each triggered strike adds at least // AIR_BOMB_BURST.min explosions, so planes bombing together sound heavier. trigger(volume) { const count = AIR_BOMB_BURST.min + Math.floor(this.random() * (AIR_BOMB_BURST.max - AIR_BOMB_BURST.min + 1)); this._left = Math.min(AIR_BOMB_LIMIT, this._left + count); this._gap = 0; this._volume = volume; } // Advances the burst. It runs to completion even if the camera moves away. update(dtMs) { if (this._left <= 0) return; this._gap -= dtMs; while (this._left > 0 && this._gap <= 0) { this.play(this._volume, between(AIR_BOMB_CLIP_S, this.random)); this._left -= 1; this._gap = between(AIR_BOMB_GAP_MS, this.random); } } }