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