extends Node ## A bounded pool and wall-clock spacing keep 2x battles from flooding audio. const CLIPS: Array[String] = ["place", "error", "stone", "bolt", "mortar", "ember", "ability", "wave", "boss", "settle", "leak", "victory", "defeat"] var viewed_team: int = 0 var enabled: bool = true var played: int = 0 var _streams: Dictionary = {} var _players: Array[AudioStreamPlayer] = [] var _last: Dictionary = {} var _last_combat: int = -1000 func _ready() -> void: enabled = DisplayServer.get_name() != "headless" for id: String in CLIPS: _streams[id] = load("res://assets/audio/" + id + ".wav") for index: int in range(12): var player := AudioStreamPlayer.new() add_child(player) _players.append(player) GameState.phase_changed.connect(_phase_changed) func play(id: String, lane: int = -1, combat: bool = false, pitch: float = 1.0) -> bool: if not enabled or Preferences.muted or Preferences.volume <= 0.0 or not _streams.has(id): return false if lane >= 0 and lane != viewed_team: return false if combat and (GameState.paused or GameState.phase != GameState.Phase.COMBAT): return false var now: int = Time.get_ticks_msec() if now - int(_last.get(id, -2000)) < (110 if combat else 80): return false if combat and now - _last_combat < 65: return false # Reserve two channels for wave/result/UI cues. for index: int in range(10 if combat else 12): var player: AudioStreamPlayer = _players[index] if player.playing: continue player.stream = _streams[id] player.volume_db = -13.0 if combat else -5.0 player.pitch_scale = clampf(pitch, .85, 1.15) player.play() _last[id] = now if combat: _last_combat = now played += 1 return true return false func stop_combat() -> void: for player: AudioStreamPlayer in _players: player.stop() func quit_game(code: int = 0) -> void: enabled = false stop_combat() # Let the audio mixer release playback references before the tree is freed. await get_tree().create_timer(.12).timeout get_tree().quit(code) func _phase_changed(phase: int) -> void: stop_combat() match phase: GameState.Phase.COMBAT: play("boss" if GameState.wave in [5, 8] else "wave") GameState.Phase.RESULTS: play("settle") GameState.Phase.GAME_OVER: play("victory" if GameState.outcome == 0 else ("settle" if GameState.outcome == GameState.DRAW else "defeat"))