# 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

```gdscript
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

```gdscript
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

```gdscript
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:

1. In the chest tap handler (before spawn guard): `AudioManager.play_sfx("chest_tap")`
2. In `spawn_items()`, at the start of each item's tween: `AudioManager.play_sfx("item_spawn")`

### holdable_item.gd

- `_on_drag_picked_up`: add `AudioManager.play_sfx("item_drag_start")`
- `_try_return_to_chest` (on success, before tween): add `AudioManager.play_sfx("item_return_chest")`
- `_on_drag_released` (after hand slot attach): add `AudioManager.play_sfx("item_drop_hand")`
- `_on_drag_released` (no target branch): add `AudioManager.play_sfx("item_drop_floor")`

### outfit_item.gd

- `_on_drag_released` (after `apply_outfit_item` call): add `AudioManager.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")` → `-1`
- `play_floor_music()` with same floor → `_current_floor` unchanged (no-op)
- `get_current_floor()` → returns `-1` before any music played
- `play_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
