- docs/download_audio.py: freesound batch downloader with all 22 confirmed IDs (API key removed — fill in locally from freesound.org) - docs/credits-audio.md: generated CC-BY attribution table - docs/superpowers/plans+specs: sprint 15, 21, 22 implementation plan/spec docs - .claude/settings.json: enable experimental agent teams env var Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3.3 KiB
Sprint 22 — Character & Ambient SFX Design Spec
Goal
Two deferred SFX items from Sprint 21:
- UltrasoundMachine ambient heartbeat — continuous looping audio that starts when the player enters the ultrasound room and stops when they leave.
- 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 = 2and@export var trigger_room: int = 0(matches ultrasound room position in_ROOM_NAMES)- In
_ready(): createAudioStreamPlayerchild, load stream withloop = true, connect toRoomNavigator.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")beforecharacter_placed.emit()
Testing
Append to test/unit/test_audio_manager.gd:
test_sfx_map_has_all_character_keys— verifiescharacter_pickup,character_place,character_tapexist 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)