7.0 KiB
Sprint 19 — AudioManager & SFX Design Spec
Goal
Implement an AudioManager autoload that provides background music (per floor, with cross-fade) and SFX feedback for all player interactions. Real audio assets sourced from freesound.org (CC0/CC-BY). All 7 SFX events and 4 music tracks wired up by end of sprint.
Architecture
AudioManager (Autoload)
scripts/autoload/AudioManager.gd — Node with three child AudioStreamPlayers:
AudioManager (Node)
├── MusicA (AudioStreamPlayer) # Cross-fade ping-pong player A
├── MusicB (AudioStreamPlayer) # Cross-fade ping-pong player B
└── SfxPlayer (AudioStreamPlayer) # Single SFX player (no polyphony this sprint)
Startup: reads GameState.music_volume and GameState.sfx_volume, applies to players. Connects to GameState.state_changed.
_on_game_state_changed(): reads GameState.current_room, calls _derive_floor_from_room(), compares to _current_floor. If floor changed → play_floor_music(new_floor).
play_floor_music(floor: int) -> void: public API. If floor == _current_floor → no-op. Otherwise: inactive player loads new stream, tweens volume from 0 → GameState.music_volume over 0.8 s; active player tweens volume → 0 over 0.8 s, then stops. Players swap active/inactive roles.
play_sfx(event: String) -> void: looks up event key in _SFX_MAP: Dictionary, loads stream, plays on SfxPlayer. Volume = GameState.sfx_volume.
set_music_volume(vol: float) -> void and set_sfx_volume(vol: float) -> void: update GameState and apply immediately to active players.
get_current_floor() -> int: returns _current_floor (-1 if none set).
Internal State
var _current_floor: int = -1
var _active_player: AudioStreamPlayer # points to MusicA or MusicB
Floor Derivation
_derive_floor_from_room(room: String) -> int — pure function, no side effects:
| Room strings | Floor |
|---|---|
"reception", "giftshop", "restaurant", "emergency" |
0 |
"xray", "pharmacy", "lab", "patient_rooms" |
1 |
"ultrasound", "delivery_room", "nursery" |
2 |
"garden_party" |
3 |
| anything else | -1 |
SFX Event Mapping
| Event key | Trigger location | When |
|---|---|---|
chest_tap |
room_chest.gd |
Chest tapped, before spawn |
item_spawn |
room_chest.gd |
Item tween starts (fly-out) |
item_drag_start |
holdable_item.gd |
_on_drag_picked_up |
item_drop_hand |
holdable_item.gd |
Item attached to hand slot |
item_drop_outfit |
outfit_item.gd |
Outfit applied to character |
item_return_chest |
holdable_item.gd |
_try_return_to_chest succeeds |
item_drop_floor |
holdable_item.gd |
_on_drag_released — no target found |
All calls: AudioManager.play_sfx("event_key") (autoload, one line, no wiring).
Asset Specification
Directory Layout
assets/audio/music/
floor_0.ogg
floor_1.ogg
floor_2.ogg
floor_3.ogg
assets/audio/sfx/
chest_tap.ogg
item_spawn.ogg
item_drag_start.ogg
item_drop_hand.ogg
item_drop_outfit.ogg
item_return_chest.ogg
item_drop_floor.ogg
Music — freesound.org Search Terms
| File | Search terms | Character |
|---|---|---|
floor_0.ogg |
"children hospital happy loop" |
Heiter, belebte Lobby |
floor_1.ogg |
"calm medical ambient loop" |
Ruhig, klinisch |
floor_2.ogg |
"nursery lullaby gentle loop" |
Sanft, Wiegenlied |
floor_3.ogg |
"garden birds outdoor loop" |
Draußen, Vogelgezwitscher |
Requirements: CC0 or CC-BY, loopable (seamless start/end), 30–60 s duration, .ogg or convertible.
SFX — freesound.org Search Terms
| File | Search terms | Target duration |
|---|---|---|
chest_tap.ogg |
"wooden box tap" |
< 0.3 s |
item_spawn.ogg |
"soft whoosh pop" |
< 0.5 s |
item_drag_start.ogg |
"soft pickup" |
< 0.3 s |
item_drop_hand.ogg |
"light click" |
< 0.2 s |
item_drop_outfit.ogg |
"fabric swoosh" |
< 0.5 s |
item_return_chest.ogg |
"soft click" |
< 0.2 s |
item_drop_floor.ogg |
"light thud" |
< 0.3 s |
Requirements: CC0 or CC-BY, child-appropriate, no startling sounds.
SFX Map in Code
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",
}
Music Map in Code
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",
}
Integration with Existing Scripts
room_chest.gd
Add two AudioManager.play_sfx() calls:
- In the chest tap handler (before spawn guard):
AudioManager.play_sfx("chest_tap") - In
spawn_items(), at the start of each item's tween:AudioManager.play_sfx("item_spawn")
holdable_item.gd
_on_drag_picked_up: addAudioManager.play_sfx("item_drag_start")_try_return_to_chest(on success, before tween): addAudioManager.play_sfx("item_return_chest")_on_drag_released(after hand slot attach): addAudioManager.play_sfx("item_drop_hand")_on_drag_released(no target branch): addAudioManager.play_sfx("item_drop_floor")
outfit_item.gd
_on_drag_released(afterapply_outfit_itemcall): addAudioManager.play_sfx("item_drop_outfit")
project.godot
Add AudioManager to the autoload list:
[autoload]
AudioManager="*res://scripts/autoload/AudioManager.gd"
GameState Integration
AudioManager observes GameState.state_changed. No changes to GameState are required — music_volume and sfx_volume are already persisted in v3.
set_music_volume / set_sfx_volume write back to GameState so values persist on save.
Testing
File: test/unit/test_audio_manager.gd
Testable (pure logic, no actual audio):
_derive_floor_from_room()— all 12 room strings → correct floor int_derive_floor_from_room("unknown")→-1play_floor_music()with same floor →_current_floorunchanged (no-op)get_current_floor()→ returns-1before any music playedplay_sfx()with unknown key → no crash (guard required)
Not testable (manual on device):
- Actual audio playback
- Cross-fade quality and timing
- Volume balance SFX vs. music
- Loop seamlessness
Out of Scope
- Polyphonic SFX (multiple simultaneous sounds)
- Per-room music (12 tracks) — floor-level is sufficient for this sprint
- Character voice sounds (Häschen-Schnuffeln, Kätzchen-Miau) — separate sprint
- Settings UI for volume sliders — volume API exists, UI in a later sprint
- AudioBus setup (Master/Music/SFX buses) — single bus is sufficient for now