Source: models/sound-manager.class.js

// sound-manager.class.js

/**
 * @class SoundManager
 *
 * Static utility class for managing all game sounds.
 * Handles sound playback, volume settings, and cooldowns to avoid audio spam.
 */
class SoundManager {
  /**
   * Maps sound objects to their individual cooldown timers.
   * Prevents the same sound from being played repeatedly in quick succession.
   * @type {Map<Audio, number>}
   */
  static cooldowns = new Map();

  /**
   * Stores individual volume levels for each sound object.
   * Allows for fine-grained volume control.
   * @type {Map<Audio, number>}
   */
  static volumes = new Map();

  /**
   * Sound when the character dies
   * @type {HTMLAudioElement}
   *
   * */
  static CHARACTER_DEAD = new Audio("assets/audio/character/dead_1.mp3");

  /**
   * Sound when the character gets hurt
   * @type {HTMLAudioElement}
   *
   * */
  static CHARACTER_HURT = new Audio("assets/audio/character/hurt_2.mp3");

  /**
   * Sound when the character jumps
   * @type {HTMLAudioElement}
   *
   * */
  static CHARACTER_JUMP = new Audio("assets/audio/character/jump_2.mp3");

  /**
   * Sound for walking steps
   * @type {HTMLAudioElement}
   *
   * */
  static CHARACTER_WALK = new Audio("assets/audio/character/walk_2.mp3");

  /**
   * Snoring sound played during a long idle animation
   * @type {HTMLAudioElement}
   *
   * */
  static CHARACTER_LONG_IDLE = new Audio("assets/audio/character/long_idle_1.mp3");

  /**
   * Sound played when the character throws a bottle
   * @type {HTMLAudioElement}
   *
   * */
  static CHARACTER_THROW = new Audio("assets/audio/character/throw_1.mp3");

  /**
   * Sound for low bounce effect
   * @type {HTMLAudioElement}
   *
   * */
  static CHARACTER_BOUNCE_LOW = new Audio("assets/audio/character/bounce_1.mp3");

  /**
   * Sound for high bounce effect
   * @type {HTMLAudioElement}
   *
   * */
  static CHARACTER_BOUNCE_HIGH = new Audio("assets/audio/character/bounce_2.mp3");

  /**
   * Random chicken clucking sound
   * @type {HTMLAudioElement}
   *
   * */
  static CHICKEN_NOISE = new Audio("assets/audio/chicken/noise_1.mp3");

  /**
   * Chicken spawn sound
   * @type {HTMLAudioElement}
   *
   * */
  static CHICKEN_SPAWN_1 = new Audio("assets/audio/chicken/spawn-1.mp3");

  /**
   * Frying sound played when the boss dies
   * @type {HTMLAudioElement}
   *
   * */
  static BOSS_DEAD = new Audio("assets/audio/endboss/dead_3.mp3");

  /**
   * Sound played when the boss takes damage
   * @type {HTMLAudioElement}
   *
   * */
  static BOSS_HURT = new Audio("assets/audio/endboss/hurt_1.mp3");

  /**
   * Sound played when the boss takes damage
   * @type {HTMLAudioElement}
   *
   * */
  static BOSS_HURT_2 = new Audio("assets/audio/endboss/dead_2.mp3");

  /**
   * Sound played when the boss performs an attack
   * @type {HTMLAudioElement}
   *
   * */
  static BOSS_ATTACK = new Audio("assets/audio/endboss/attack_1.mp3");

  /**
   * Intro sound played when the boss is triggered
   * @type {HTMLAudioElement}
   *
   * */
  static BOSS_INTRO = new Audio("assets/audio/endboss/intro_1.mp3");

  /**
   * Glass shattering sound played when the character throws a bottle
   * @type {HTMLAudioElement}
   *
   * */
  static BOTTLE_BREAK = new Audio("assets/audio/salsa_bottle/break_1.mp3");

  /**
   * Sound played when the player collects a bottle
   * @type {HTMLAudioElement}
   *
   * */
  static BOTTLE_COLLECT = new Audio("assets/audio/salsa_bottle/collect_1.mp3");

  /**
   * Sound played when a bottle drops into sand
   * @type {HTMLAudioElement}
   *
   * */
  static BOTTLE_DROP = new Audio("assets/audio/salsa_bottle/drop-into-sand-1.mp3");

  /**
   * Sound played when the player collects a coin
   * @type {HTMLAudioElement}
   *
   * */
  static COIN_COLLECT = new Audio("assets/audio/coin/collect_1.mp3");

  /**
   * Sound played when the coin bar is full and a reward is triggered
   * @type {HTMLAudioElement}
   *
   * */
  static COIN_BAR_FILLED_UP = new Audio("assets/audio/coin/bar_filled_up_1.mp3");

  /**
   * Looping background music for regular gameplay
   * @type {HTMLAudioElement}
   *
   * */
  static MUSIC_BACKGROUND = new Audio("assets/audio/music/background_loop_1.mp3");

  /**
   * Intro music played before the boss fight begins
   * @type {HTMLAudioElement}
   *
   * */
  static MUSIC_BOSS_INTRO = new Audio("assets/audio/music/boss_intro_1.mp3");

  /**
   * Music played during the boss fight
   * @type {HTMLAudioElement}
   *
   * */
  static MUSIC_BOSS_FIGHT = new Audio("assets/audio/music/boss_7.mp3");

  /**
   * Music played on game over screen
   * @type {HTMLAudioElement}
   *
   * */
  static MUSIC_GAME_OVER = new Audio("assets/audio/music/game_over_1.mp3");

  /**
   * Music played during game victory screen
   * @type {HTMLAudioElement}
   *
   * */
  static MUSIC_GAME_WON = new Audio("assets/audio/music/credits_1.mp3");

  /**
   * Collection of all defined sound objects in the game.
   * Used for batch operations like muting, unmuting, or pausing all sounds.
   * @type {HTMLAudioElement[]}
   */
  static allSounds = [
    SoundManager.CHARACTER_DEAD,
    SoundManager.CHARACTER_HURT,
    SoundManager.CHARACTER_JUMP,
    SoundManager.CHARACTER_WALK,
    SoundManager.CHARACTER_LONG_IDLE,
    SoundManager.CHARACTER_THROW,
    SoundManager.CHARACTER_BOUNCE_LOW,
    SoundManager.CHARACTER_BOUNCE_HIGH,

    SoundManager.CHICKEN_NOISE,
    SoundManager.CHICKEN_SPAWN_1,
    SoundManager.BOSS_HURT,
    SoundManager.BOSS_HURT_2,
    SoundManager.BOSS_DEAD,
    SoundManager.BOSS_ATTACK,
    SoundManager.BOSS_INTRO,

    SoundManager.BOTTLE_BREAK,
    SoundManager.BOTTLE_COLLECT,
    SoundManager.BOTTLE_DROP,
    SoundManager.COIN_COLLECT,
    SoundManager.COIN_BAR_FILLED_UP,

    SoundManager.MUSIC_BACKGROUND,
    SoundManager.MUSIC_BOSS_INTRO,
    SoundManager.MUSIC_BOSS_FIGHT,
    SoundManager.MUSIC_GAME_OVER,
    SoundManager.MUSIC_GAME_WON,
  ];

  /**
   * Indicates whether global sound output is currently muted.
   * Used to toggle playback state for all sounds.
   * @type {boolean}
   */
  static isMuted = true;

  /**
   * Plays a sound once with specified settings.
   * Handles playback rate, volume, cooldown prevention, loop state, and starting time.
   * Skips playback if the sound is currently on cooldown.
   * After playback ends, automatically reloads the sound to prepare it for next use. (Fix for Firefox and Safari)
   *
   * @param {HTMLAudioElement} sound - The sound to play.
   * @param {number} [playbackRate=1] - Speed multiplier for the sound (e.g. 0.5 = slower, 2 = faster).
   * @param {number} [volume=0.2] - Initial volume (0.0 to 1.0).
   * @param {number} [cooldown=0] - Time in ms before the sound can be played again.
   * @param {boolean} [loop=false] - Whether the sound should loop continuously.
   * @param {number} [currentTime=0] - Position (in seconds) to start playback from.
   */
  static playOne(
    sound,
    playbackRate = 1,
    volume = 0.2,
    cooldown = 0,
    loop = false,
    currentTime = 0
  ) {
    if (SoundManager.cooldowns.get(sound)) return;
    if (sound.readyState == 4) {
      SoundManager.configureSound(sound, playbackRate, volume, loop, currentTime);
      SoundManager.volumes.set(sound, sound.volume);
      if (SoundManager.isMuted) {
        sound.volume = 0;
      }

      sound.play();

      SoundManager.cooldowns.set(sound, true);
      setTimeout(() => {
        SoundManager.cooldowns.set(sound, false);
      }, cooldown);

      const soundReloadInterval = setInterval(() => {
        if (sound.ended) {
          sound.load();
          clearInterval(soundReloadInterval);
        }
      }, 100);
    }
  }

  /**
   * Applies playback settings to the given sound.
   *
   * @param {HTMLAudioElement} sound - The sound to configure.
   * @param {number} playbackRate - Speed at which the sound plays.
   * @param {number} volume - Volume level (0.0 to 1.0).
   * @param {boolean} loop - Whether the sound should loop.
   * @param {number} currentTime - Start time in seconds.
   */
  static configureSound(sound, playbackRate, volume, loop, currentTime) {
    sound.playbackRate = playbackRate;
    sound.volume = volume;
    sound.loop = loop;
    sound.currentTime = currentTime;
  }

  /**
   * Stops (pauses) the given sound.
   * Playback can later be resumed with `.play()` from the current position.
   *
   * @param {HTMLAudioElement} sound - The sound to stop.
   */
  static stopOne(sound) {
    sound.pause();
  }

  /**
   * Stops (pauses) all registered sounds in the game.
   * Useful for global pause scenarios or end-of-game transitions.
   */
  static stopAll() {
    SoundManager.allSounds.forEach((sound) => {
      sound.pause();
    });
  }

  /**
   * Toggles the global mute state for all sounds.
   * Mutes or unmutes all sounds depending on the current state,
   * updates the mute button visually, and saves the state to local storage.
   */
  static toggleMuteAll() {
    blurButton(".btn");
    if (!SoundManager.isMuted) {
      SoundManager.muteAll();
    } else {
      SoundManager.unmuteAll();
    }
    saveToLocalStorage();
    updateMuteButtonState();
  }

  /**
   * Mutes all sounds by setting their volume to 0
   * and storing their current volume in a map for later restoration.
   */
  static muteAll() {
    SoundManager.allSounds.forEach((sound) => {
      SoundManager.volumes.set(sound, sound.volume);
      sound.volume = 0;
    });
    SoundManager.isMuted = true;
  }

  /**
   * Unmutes all sounds by restoring their previous volume
   * from the volume map, if available.
   */
  static unmuteAll() {
    SoundManager.allSounds.forEach((sound) => {
      const savedVolume = SoundManager.volumes.get(sound);
      if (typeof savedVolume === "number" && isFinite(savedVolume)) {
        sound.volume = savedVolume;
      }
    });
    SoundManager.isMuted = false;
  }
}