52ebb78862
- 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>
71 lines
3.3 KiB
Markdown
71 lines
3.3 KiB
Markdown
# 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)
|