saumwacht-godot/scripts/presentation/soundscape.gd
Martin Stickelbroeck c2b447610c Baseline: Saumwacht 0.15.0 (Phase 20A) vor dem Umbau zu Champions of Luneraria
Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern
unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore
unversioniert; letztere sind unter ../backups gesichert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:00:36 +02:00

70 lines
2.3 KiB
GDScript

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"))