# Sprint 22 — Character & Ambient SFX Design Spec

## Goal

Two deferred SFX items from Sprint 21:

1. **UltrasoundMachine ambient heartbeat** — continuous looping audio that starts when the player enters the ultrasound room and stops when they leave.
2. **Character SFX** — pickup, place, and tap sounds wired into `character.gd`.

## New SFX Events

### AudioManager._SFX_MAP additions (3 new keys)

| Event key | Trigger |
|---|---|
| `character_pickup` | `Character._on_drag_picked_up()` |
| `character_place` | `Character._on_drag_released()` — drag distance ≥ tap threshold |
| `character_tap` | `Character._on_drag_released()` — drag distance < tap threshold |

### UltrasoundMachine (self-managed, not via _SFX_MAP)

The heartbeat is a looping ambient sound owned by the `UltrasoundMachine` node itself. It does not go through `AudioManager.play_sfx()` — that path is for one-shot SFX. Instead, `UltrasoundMachine` creates its own `AudioStreamPlayer` child, sets `stream.loop = true` at runtime, and starts/stops it in response to `RoomNavigator.room_changed`.

## Asset Specification

New files:

```
assets/audio/sfx/ultrasound_heartbeat.ogg  — soft beep/blip, ~1s, loops seamlessly
assets/audio/sfx/character_pickup.ogg      — happy soft squeak / whoosh
assets/audio/sfx/character_place.ogg       — gentle thud / landing
assets/audio/sfx/character_tap.ogg         — short happy chime / pop
```

All CC0 or CC-BY from freesound.org. Placeholder 0-byte files committed; real downloads documented in `docs/audio-assets-sprint19.md` (extend existing file).

## Integration Points

### UltrasoundMachine (`scripts/objects/ultrasound_machine.gd`)

Full replacement. New behaviour:

- `@export var trigger_floor: int = 2` and `@export var trigger_room: int = 0` (matches ultrasound room position in `_ROOM_NAMES`)
- In `_ready()`: create `AudioStreamPlayer` child, load stream with `loop = true`, connect to `RoomNavigator.room_changed`
- In `_on_room_changed(floor_index, room_index)`: if matches trigger position → `_audio.play()`, else → `_audio.stop()`
- `AudioServer.get_driver_name() == "Dummy"` guard wraps all audio operations
- `_exit_tree()`: disconnect signal (mirrors Ambulance pattern)
- Volume is inherited from the bus (no explicit volume set — ambient heartbeat is soft by design)

### Character (`scripts/characters/character.gd`)

Three one-liner additions:

- `_on_drag_picked_up()` → `AudioManager.play_sfx("character_pickup")` as first line
- `_on_drag_released()` tap branch → `AudioManager.play_sfx("character_tap")` before `_handle_outfit_tap()`
- `_on_drag_released()` place branch → `AudioManager.play_sfx("character_place")` before `character_placed.emit()`

## Testing

Append to `test/unit/test_audio_manager.gd`:

- `test_sfx_map_has_all_character_keys` — verifies `character_pickup`, `character_place`, `character_tap` exist in `_SFX_MAP`

No unit test for UltrasoundMachine audio start/stop — the trigger is room-navigation-driven and mirrors the Ambulance pattern (which also has no per-SFX unit test).

## Out of Scope

- Per-state-transition character sounds (e.g., happy sound when healed — separate sprint)
- Room-specific ambient audio for other rooms
- UltrasoundMachine volume linked to `GameState.sfx_volume` (ambient bus handles this via AudioServer)
