## AudioManager — floor music with cross-fade and SFX for player interactions. ## Autoload registered in project.godot. No class_name (Godot 4 autoload conflict). extends Node const CROSSFADE_DURATION: float = 0.8 const _MUSIC_MAP: Dictionary = { 0: "res://assets/audio/music/floor_0.ogg", 1: "res://assets/audio/music/floor_1.ogg", 2: "res://assets/audio/music/floor_2.ogg", 3: "res://assets/audio/music/floor_3.ogg", } const _SFX_MAP: Dictionary = { "chest_tap": "res://assets/audio/sfx/chest_tap.ogg", "item_spawn": "res://assets/audio/sfx/item_spawn.ogg", "item_drag_start": "res://assets/audio/sfx/item_drag_start.ogg", "item_drop_hand": "res://assets/audio/sfx/item_drop_hand.ogg", "item_drop_outfit": "res://assets/audio/sfx/item_drop_outfit.ogg", "item_return_chest": "res://assets/audio/sfx/item_return_chest.ogg", "item_drop_floor": "res://assets/audio/sfx/item_drop_floor.ogg", } var _current_floor: int = -1 var _is_crossfading: bool = false var _active_player: AudioStreamPlayer var _last_room: String = "" var _music_a: AudioStreamPlayer var _music_b: AudioStreamPlayer var _sfx_player: AudioStreamPlayer func _ready() -> void: _music_a = AudioStreamPlayer.new() _music_b = AudioStreamPlayer.new() _sfx_player = AudioStreamPlayer.new() add_child(_music_a) add_child(_music_b) add_child(_sfx_player) _active_player = _music_a _music_a.volume_db = linear_to_db(GameState.music_volume) _music_b.volume_db = linear_to_db(0.0) _sfx_player.volume_db = linear_to_db(GameState.sfx_volume) GameState.state_changed.connect(_on_game_state_changed) _last_room = GameState.current_room var initial_floor: int = _derive_floor_from_room(GameState.current_room) if initial_floor != -1: play_floor_music(initial_floor) func play_floor_music(floor: int) -> void: if floor == _current_floor: return if not _MUSIC_MAP.has(floor): return if _is_crossfading: return _is_crossfading = true _current_floor = floor var inactive: AudioStreamPlayer = _music_b if _active_player == _music_a else _music_a var path: String = _MUSIC_MAP[floor] if not ResourceLoader.exists(path): _is_crossfading = false return var stream: AudioStream = load(path) as AudioStream if stream == null: _is_crossfading = false return inactive.stream = stream inactive.volume_db = linear_to_db(0.0) inactive.play() var tween: Tween = create_tween().set_parallel(true) tween.tween_property(inactive, "volume_db", linear_to_db(GameState.music_volume), CROSSFADE_DURATION) tween.tween_property(_active_player, "volume_db", linear_to_db(0.0), CROSSFADE_DURATION) await tween.finished _active_player.stop() _active_player = inactive _is_crossfading = false func play_sfx(event: String) -> void: if not _SFX_MAP.has(event): return var path: String = _SFX_MAP[event] if not ResourceLoader.exists(path): return var stream: AudioStream = load(path) as AudioStream if stream == null: return _sfx_player.stream = stream _sfx_player.play() func set_music_volume(vol: float) -> void: GameState.music_volume = vol _active_player.volume_db = linear_to_db(vol) func set_sfx_volume(vol: float) -> void: GameState.sfx_volume = vol _sfx_player.volume_db = linear_to_db(vol) func get_current_floor() -> int: return _current_floor func _on_game_state_changed() -> void: if GameState.current_room == _last_room: return _last_room = GameState.current_room var floor: int = _derive_floor_from_room(GameState.current_room) if floor != -1: play_floor_music(floor) func _derive_floor_from_room(room: String) -> int: match room: "reception", "giftshop", "restaurant", "emergency": return 0 "xray", "pharmacy", "lab", "patient_rooms": return 1 "ultrasound", "delivery_room", "nursery": return 2 "garden_party": return 3 return -1