Compare commits

2 Commits

Author SHA1 Message Date
Steven Wroblewski 52ebb78862 chore(audio): add download script, audio credits, and sprint 21/22 docs
- 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>
2026-05-11 14:57:27 +02:00
Steven Wroblewski ad9a406775 assets(audio): replace all placeholder SFX and music with real CC0 audio
All 22 placeholder 0-byte OGG files replaced with freesound.org previews
(128 kbps HQ OGG). All tracks are CC0. Includes Godot import sidecar files
(.ogg.import) for 11 files that had none previously.

Sprints 19, 21, 22 audio coverage complete.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 14:57:21 +02:00
41 changed files with 1970 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"env": {
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
}
}
Binary file not shown.
+18
View File
@@ -0,0 +1,18 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://dgcrrb572igsv"
valid=false
[deps]
source_file="res://assets/audio/music/floor_0.ogg"
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
+18
View File
@@ -0,0 +1,18 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://5e2h06ahgper"
valid=false
[deps]
source_file="res://assets/audio/music/floor_1.ogg"
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
+18
View File
@@ -0,0 +1,18 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://ddia1ses0471i"
valid=false
[deps]
source_file="res://assets/audio/music/floor_2.ogg"
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
+18
View File
@@ -0,0 +1,18 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://hswbjuc6exdq"
valid=false
[deps]
source_file="res://assets/audio/music/floor_3.ogg"
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+18
View File
@@ -0,0 +1,18 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://clhj71pir50qn"
valid=false
[deps]
source_file="res://assets/audio/sfx/chest_tap.ogg"
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,18 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://c8ejoka50o3yr"
valid=false
[deps]
source_file="res://assets/audio/sfx/item_drag_start.ogg"
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
@@ -0,0 +1,18 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://cv8mj3nk04dov"
valid=false
[deps]
source_file="res://assets/audio/sfx/item_drop_floor.ogg"
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
@@ -0,0 +1,18 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://c3hooek70n7dq"
valid=false
[deps]
source_file="res://assets/audio/sfx/item_drop_hand.ogg"
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
@@ -0,0 +1,18 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://db5cgjn6svke4"
valid=false
[deps]
source_file="res://assets/audio/sfx/item_drop_outfit.ogg"
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
@@ -0,0 +1,18 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://c7t2tdceav7ms"
valid=false
[deps]
source_file="res://assets/audio/sfx/item_return_chest.ogg"
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
+18
View File
@@ -0,0 +1,18 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://bu26y0klq2pn5"
valid=false
[deps]
source_file="res://assets/audio/sfx/item_spawn.ogg"
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
# Audio Credits
CC-BY files require attribution.
| File | Title | Author | License | URL |
|---|---|---|---|---|
| `assets/audio/sfx/item_drop_outfit.ogg` | cape-swoosh | CosmicEmbers | CC-BY 3.0 | https://freesound.org/s/161415/ |
+1 -1
View File
@@ -28,7 +28,7 @@ import requests
from pathlib import Path
# ── Fill in your API key here ──────────────────────────────────────────────────
API_KEY = "XLXzH6xQJbt5HQjLx7kQwfDSB9MTFawMTsAFhRFG" # e.g. "aB3dEfGhIjKlMnOpQrStUvWx"
API_KEY = "" # get your free key at freesound.org → API credentials
# ──────────────────────────────────────────────────────────────────────────────
REPO_ROOT = Path(__file__).parent.parent
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,291 @@
# Sprint 21 — Interactive Object SFX Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add `AudioManager.play_sfx()` calls to all 6 tappable interactive objects, add 7 new SFX keys to `AudioManager._SFX_MAP`, and create placeholder audio files for each.
**Architecture:** Single `AudioManager.play_sfx("key")` call per trigger method, before the tween. New keys added to the existing `_SFX_MAP` constant. Placeholder 0-byte `.ogg` files committed so `ResourceLoader.exists()` returns true; `AudioServer.get_driver_name() == "Dummy"` guard in `play_sfx()` ensures headless tests don't crash on empty files.
**Tech Stack:** GDScript 4 (static types), GUT v9.6.0.
---
### Task 1: Create 7 placeholder SFX files + update audio asset docs
**Files:**
- Create: `assets/audio/sfx/xray_scan.ogg`
- Create: `assets/audio/sfx/tea_pour.ogg`
- Create: `assets/audio/sfx/cradle_rock.ogg`
- Create: `assets/audio/sfx/gift_open.ogg`
- Create: `assets/audio/sfx/ambulance_siren.ogg`
- Create: `assets/audio/sfx/delivery_cheer.ogg`
- Create: `assets/audio/sfx/object_tap.ogg`
- Modify: `docs/audio-assets-sprint19.md`
- [ ] **Step 1: Create 7 empty placeholder files**
```bash
New-Item -ItemType File "assets/audio/sfx/xray_scan.ogg" -Force
New-Item -ItemType File "assets/audio/sfx/tea_pour.ogg" -Force
New-Item -ItemType File "assets/audio/sfx/cradle_rock.ogg" -Force
New-Item -ItemType File "assets/audio/sfx/gift_open.ogg" -Force
New-Item -ItemType File "assets/audio/sfx/ambulance_siren.ogg" -Force
New-Item -ItemType File "assets/audio/sfx/delivery_cheer.ogg" -Force
New-Item -ItemType File "assets/audio/sfx/object_tap.ogg" -Force
```
All 0-byte. `ResourceLoader.exists()` returns true for these; `load()` is never called in headless mode due to the AudioServer Dummy guard already present in `play_sfx()`.
- [ ] **Step 2: Append Sprint 21 entries to `docs/audio-assets-sprint19.md`**
Append to the end of the existing file:
```markdown
## Sprint 21 — Interactive Object SFX
All CC0 or CC-BY from freesound.org. Replace placeholder 0-byte files with the downloads below.
| File | Description | Freesound suggestion |
|---|---|---|
| `assets/audio/sfx/xray_scan.ogg` | electrical hum / machine beep | search "xray machine beep" or "electrical hum short" |
| `assets/audio/sfx/tea_pour.ogg` | liquid pouring | search "liquid pour short" or "tea pouring" |
| `assets/audio/sfx/cradle_rock.ogg` | gentle creak / lullaby chime | search "gentle creak wood" or "lullaby chime" |
| `assets/audio/sfx/gift_open.ogg` | unwrapping / pop | search "gift unwrap" or "pop sound soft" |
| `assets/audio/sfx/ambulance_siren.ogg` | short siren sting <1.5s child-friendly | search "toy siren short" or "ambulance beep" |
| `assets/audio/sfx/delivery_cheer.ogg` | happy chime / fanfare | search "happy chime short" or "fanfare child" |
| `assets/audio/sfx/object_tap.ogg` | soft tap / click | search "soft tap" or "gentle click" |
All files must be <1.5 s, child-friendly (no harsh/loud sounds), mono or stereo, 44100 Hz, OGG Vorbis.
```
- [ ] **Step 3: Commit**
```bash
git add assets/audio/sfx/xray_scan.ogg assets/audio/sfx/tea_pour.ogg assets/audio/sfx/cradle_rock.ogg assets/audio/sfx/gift_open.ogg assets/audio/sfx/ambulance_siren.ogg assets/audio/sfx/delivery_cheer.ogg assets/audio/sfx/object_tap.ogg docs/audio-assets-sprint19.md
git commit -m "assets(sfx): add sprint-21 interactive object SFX placeholders"
```
---
### Task 2: Add 7 new keys to AudioManager._SFX_MAP + test
**Files:**
- Modify: `scripts/autoload/AudioManager.gd`
- Modify: `test/unit/test_audio_manager.gd`
- [ ] **Step 1: Write failing test**
Append to `test/unit/test_audio_manager.gd`:
```gdscript
func test_sfx_map_has_all_interactive_object_keys() -> void:
assert_true(AudioManager._SFX_MAP.has("xray_scan"))
assert_true(AudioManager._SFX_MAP.has("tea_pour"))
assert_true(AudioManager._SFX_MAP.has("cradle_rock"))
assert_true(AudioManager._SFX_MAP.has("gift_open"))
assert_true(AudioManager._SFX_MAP.has("ambulance_siren"))
assert_true(AudioManager._SFX_MAP.has("delivery_cheer"))
assert_true(AudioManager._SFX_MAP.has("object_tap"))
```
- [ ] **Step 2: Run → verify FAIL**
```bash
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit --path "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-21-object-sfx"
```
Expected: `test_sfx_map_has_all_interactive_object_keys` fails — keys not in `_SFX_MAP`.
- [ ] **Step 3: Add 7 new keys to AudioManager._SFX_MAP**
In `scripts/autoload/AudioManager.gd`, replace the `_SFX_MAP` constant:
```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",
"xray_scan": "res://assets/audio/sfx/xray_scan.ogg",
"tea_pour": "res://assets/audio/sfx/tea_pour.ogg",
"cradle_rock": "res://assets/audio/sfx/cradle_rock.ogg",
"gift_open": "res://assets/audio/sfx/gift_open.ogg",
"ambulance_siren": "res://assets/audio/sfx/ambulance_siren.ogg",
"delivery_cheer": "res://assets/audio/sfx/delivery_cheer.ogg",
"object_tap": "res://assets/audio/sfx/object_tap.ogg",
}
```
- [ ] **Step 4: Run → verify PASS**
```bash
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit --path "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-21-object-sfx"
```
Expected: all previous tests plus `test_sfx_map_has_all_interactive_object_keys` pass. Total ≥ 221.
- [ ] **Step 5: Commit**
```bash
git add scripts/autoload/AudioManager.gd test/unit/test_audio_manager.gd
git commit -m "feat(sfx): add interactive object SFX keys to AudioManager._SFX_MAP"
```
---
### Task 3: Wire play_sfx() into 6 interactive object scripts + base class
**Files:**
- Modify: `scripts/objects/interactive_object.gd`
- Modify: `scripts/objects/xray_machine.gd`
- Modify: `scripts/objects/tea_pot.gd`
- Modify: `scripts/objects/cradle.gd`
- Modify: `scripts/objects/gift_box.gd`
- Modify: `scripts/objects/ambulance.gd`
- Modify: `scripts/objects/delivery_bed.gd`
No new tests — these are single-line wiring calls. The AudioManager Dummy-driver guard makes headless tests safe (no crash on empty .ogg files). The existing test suite verifies nothing regresses.
- [ ] **Step 1: Add object_tap to interactive_object.gd**
In `scripts/objects/interactive_object.gd`, in `_trigger_interaction()`, add `AudioManager.play_sfx("object_tap")` as the first line:
```gdscript
func _trigger_interaction() -> void:
AudioManager.play_sfx("object_tap")
_set_state(State.ACTIVE)
object_interacted.emit(self)
GameState.set_object_state(object_id, "active")
_play_bounce_animation()
```
- [ ] **Step 2: Add xray_scan to xray_machine.gd**
In `scripts/objects/xray_machine.gd`, in `_start_scan()`, add `AudioManager.play_sfx("xray_scan")` as the first line:
```gdscript
func _start_scan() -> void:
AudioManager.play_sfx("xray_scan")
_state = State.SLIDING_IN
var plate: Node2D = get_node_or_null("Plate") as Node2D
if plate == null:
_state = State.IDLE
return
var tween: Tween = create_tween()
tween.set_ease(Tween.EASE_IN_OUT)
tween.set_trans(Tween.TRANS_QUAD)
tween.tween_property(plate, "position:x", PLATE_ACTIVE_X, SLIDE_DURATION)
tween.finished.connect(_on_plate_in)
```
- [ ] **Step 3: Add tea_pour to tea_pot.gd**
In `scripts/objects/tea_pot.gd`, in `_start_pouring()`, add `AudioManager.play_sfx("tea_pour")` as the first line:
```gdscript
func _start_pouring() -> void:
AudioManager.play_sfx("tea_pour")
_state = State.POURING
var tween: Tween = create_tween()
tween.set_ease(Tween.EASE_IN_OUT)
tween.set_trans(Tween.TRANS_SINE)
tween.tween_property(self, "rotation_degrees", TILT_ANGLE, TILT_DURATION)
tween.tween_interval(POUR_HOLD)
tween.tween_property(self, "rotation_degrees", 0.0, RETURN_DURATION)
tween.finished.connect(func() -> void: _state = State.IDLE)
```
- [ ] **Step 4: Add cradle_rock to cradle.gd**
In `scripts/objects/cradle.gd`, in `_start_rocking()`, add `AudioManager.play_sfx("cradle_rock")` as the first line:
```gdscript
func _start_rocking() -> void:
AudioManager.play_sfx("cradle_rock")
_state = State.ROCKING
var tween: Tween = create_tween()
tween.set_ease(Tween.EASE_IN_OUT)
tween.set_trans(Tween.TRANS_SINE)
tween.tween_property(self, "rotation_degrees", ROCK_ANGLE, ROCK_DURATION)
tween.tween_property(self, "rotation_degrees", -ROCK_ANGLE, ROCK_DURATION)
tween.tween_property(self, "rotation_degrees", 0.0, ROCK_DURATION * 0.5)
tween.finished.connect(func() -> void: _state = State.IDLE)
```
- [ ] **Step 5: Add gift_open to gift_box.gd**
In `scripts/objects/gift_box.gd`, in `_start_opening()`, add `AudioManager.play_sfx("gift_open")` as the first line:
```gdscript
func _start_opening() -> void:
AudioManager.play_sfx("gift_open")
_state = State.OPENING
var lid: Node2D = get_node_or_null("Lid") as Node2D
if lid == null:
_state = State.OPEN
return
var tween: Tween = create_tween()
tween.set_ease(Tween.EASE_OUT)
tween.set_trans(Tween.TRANS_BACK)
tween.tween_property(lid, "position:y", LID_OPEN_Y, OPEN_DURATION)
tween.parallel().tween_property(lid, "modulate:a", 0.0, OPEN_DURATION)
tween.finished.connect(_on_lid_opened)
```
- [ ] **Step 6: Add ambulance_siren to ambulance.gd**
In `scripts/objects/ambulance.gd`, in `_drive_in()`, add `AudioManager.play_sfx("ambulance_siren")` as the first line:
```gdscript
func _drive_in() -> void:
AudioManager.play_sfx("ambulance_siren")
_is_animating = true
_is_parked = false
var tween: Tween = create_tween()
tween.set_ease(Tween.EASE_OUT)
tween.set_trans(Tween.TRANS_QUAD)
tween.tween_property(self, "position:x", _parked_x, DRIVE_DURATION)
tween.finished.connect(func() -> void:
_is_parked = true
_is_animating = false
_play_stop_bounce()
)
```
- [ ] **Step 7: Add delivery_cheer to delivery_bed.gd**
In `scripts/objects/delivery_bed.gd`, in `_start_arrival()`, add `AudioManager.play_sfx("delivery_cheer")` as the first line:
```gdscript
func _start_arrival() -> void:
AudioManager.play_sfx("delivery_cheer")
_state = State.MAMA_ARRIVING
var mama: Node2D = get_node_or_null("Mama") as Node2D
if mama == null:
_state = State.IDLE
return
var tween: Tween = create_tween()
tween.set_ease(Tween.EASE_OUT)
tween.set_trans(Tween.TRANS_QUAD)
tween.tween_property(mama, "position:x", MAMA_PARKED_X, ARRIVE_DURATION)
tween.finished.connect(_on_mama_arrived)
```
- [ ] **Step 8: Run full test suite → verify no regressions**
```bash
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit --path "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-21-object-sfx"
```
Expected: all tests pass. Total ≥ 221.
- [ ] **Step 9: Commit**
```bash
git add scripts/objects/interactive_object.gd scripts/objects/xray_machine.gd scripts/objects/tea_pot.gd scripts/objects/cradle.gd scripts/objects/gift_box.gd scripts/objects/ambulance.gd scripts/objects/delivery_bed.gd
git commit -m "feat(sfx): wire interactive object SFX to AudioManager.play_sfx"
```
@@ -0,0 +1,299 @@
# Sprint 22 — Character & Ambient SFX Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add looping ambient heartbeat SFX to UltrasoundMachine (room-driven start/stop) and wire three character interaction sounds (pickup, tap, place) into `character.gd`.
**Architecture:** UltrasoundMachine owns its own `AudioStreamPlayer` with `stream.loop = true`, connected to `RoomNavigator.room_changed` — mirrors the Ambulance pattern. Character sounds use the existing `AudioManager.play_sfx()` one-shot path with 3 new `_SFX_MAP` keys.
**Tech Stack:** GDScript 4 (static types), GUT v9.6.0.
---
### Task 1: Create 4 placeholder SFX files + update audio asset docs
**Files:**
- Create: `assets/audio/sfx/ultrasound_heartbeat.ogg`
- Create: `assets/audio/sfx/character_pickup.ogg`
- Create: `assets/audio/sfx/character_place.ogg`
- Create: `assets/audio/sfx/character_tap.ogg`
- Modify: `docs/audio-assets-sprint19.md`
- [ ] **Step 1: Create 4 empty placeholder files**
```powershell
New-Item -ItemType File -Force "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-22-character-ambient-sfx/assets/audio/sfx/ultrasound_heartbeat.ogg"
New-Item -ItemType File -Force "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-22-character-ambient-sfx/assets/audio/sfx/character_pickup.ogg"
New-Item -ItemType File -Force "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-22-character-ambient-sfx/assets/audio/sfx/character_place.ogg"
New-Item -ItemType File -Force "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-22-character-ambient-sfx/assets/audio/sfx/character_tap.ogg"
```
- [ ] **Step 2: Append Sprint 22 entries to `docs/audio-assets-sprint19.md`**
Append to the end of the file:
```markdown
## Sprint 22 — Character & Ambient SFX
All CC0 or CC-BY from freesound.org. Replace placeholder 0-byte files with the downloads below.
| File | Description | Freesound suggestion |
|---|---|---|
| `assets/audio/sfx/ultrasound_heartbeat.ogg` | soft beep/blip ~1s, loops seamlessly | search "heartbeat beep soft" or "medical monitor beep" |
| `assets/audio/sfx/character_pickup.ogg` | happy soft squeak / whoosh | search "cartoon pickup soft" or "whoosh gentle" |
| `assets/audio/sfx/character_place.ogg` | gentle thud / landing | search "soft thud" or "gentle landing" |
| `assets/audio/sfx/character_tap.ogg` | short happy chime / pop | search "happy chime short" or "cartoon pop soft" |
All files must be child-friendly (no harsh/loud sounds), mono or stereo, 44100 Hz, OGG Vorbis.
`ultrasound_heartbeat.ogg` must loop seamlessly (start and end points match).
```
- [ ] **Step 3: Commit**
```bash
git -C "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-22-character-ambient-sfx" add assets/audio/sfx/ultrasound_heartbeat.ogg assets/audio/sfx/character_pickup.ogg assets/audio/sfx/character_place.ogg assets/audio/sfx/character_tap.ogg docs/audio-assets-sprint19.md
git -C "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-22-character-ambient-sfx" commit -m "assets(sfx): add sprint-22 character and ambient SFX placeholders"
```
---
### Task 2: Add 3 character SFX keys to AudioManager._SFX_MAP + test
**Files:**
- Modify: `scripts/autoload/AudioManager.gd`
- Modify: `test/unit/test_audio_manager.gd`
- [ ] **Step 1: Write failing test**
Append to `test/unit/test_audio_manager.gd`:
```gdscript
func test_sfx_map_has_all_character_keys() -> void:
assert_true(AudioManager._SFX_MAP.has("character_pickup"))
assert_true(AudioManager._SFX_MAP.has("character_place"))
assert_true(AudioManager._SFX_MAP.has("character_tap"))
```
- [ ] **Step 2: Run → verify FAIL**
```bash
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit --path "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-22-character-ambient-sfx"
```
Expected: `test_sfx_map_has_all_character_keys` fails — 3 keys missing from `_SFX_MAP`.
- [ ] **Step 3: Add 3 keys to AudioManager._SFX_MAP**
In `scripts/autoload/AudioManager.gd`, replace the `_SFX_MAP` constant with:
```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",
"xray_scan": "res://assets/audio/sfx/xray_scan.ogg",
"tea_pour": "res://assets/audio/sfx/tea_pour.ogg",
"cradle_rock": "res://assets/audio/sfx/cradle_rock.ogg",
"gift_open": "res://assets/audio/sfx/gift_open.ogg",
"ambulance_siren": "res://assets/audio/sfx/ambulance_siren.ogg",
"delivery_cheer": "res://assets/audio/sfx/delivery_cheer.ogg",
"object_tap": "res://assets/audio/sfx/object_tap.ogg",
"character_pickup": "res://assets/audio/sfx/character_pickup.ogg",
"character_place": "res://assets/audio/sfx/character_place.ogg",
"character_tap": "res://assets/audio/sfx/character_tap.ogg",
}
```
- [ ] **Step 4: Run → verify PASS**
```bash
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit --path "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-22-character-ambient-sfx"
```
Expected: all tests pass. Total Passing Tests ≥ 221.
- [ ] **Step 5: Commit**
```bash
git -C "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-22-character-ambient-sfx" add scripts/autoload/AudioManager.gd test/unit/test_audio_manager.gd
git -C "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-22-character-ambient-sfx" commit -m "feat(sfx): add character SFX keys to AudioManager._SFX_MAP"
```
---
### Task 3: Wire character SFX into character.gd
**Files:**
- Modify: `scripts/characters/character.gd`
No new tests — single-line additions, AudioManager Dummy guard covers headless safety.
- [ ] **Step 1: Edit `_on_drag_picked_up()`**
Current:
```gdscript
func _on_drag_picked_up(pos: Vector2) -> void:
_is_held = true
_drag_start_position = pos
set_animation_state("held")
character_picked_up.emit(self)
```
Replace with:
```gdscript
func _on_drag_picked_up(pos: Vector2) -> void:
AudioManager.play_sfx("character_pickup")
_is_held = true
_drag_start_position = pos
set_animation_state("held")
character_picked_up.emit(self)
```
- [ ] **Step 2: Edit `_on_drag_released()` — tap branch and place branch**
Current:
```gdscript
func _on_drag_released(pos: Vector2) -> void:
_is_held = false
var drag_distance: float = pos.distance_to(_drag_start_position)
if drag_distance < _TAP_THRESHOLD:
set_animation_state("idle")
_handle_outfit_tap()
return
set_animation_state("idle")
if data == null or data.id.is_empty():
return
GameState.set_character_position(character_id, global_position)
character_placed.emit(self, global_position)
```
Replace with:
```gdscript
func _on_drag_released(pos: Vector2) -> void:
_is_held = false
var drag_distance: float = pos.distance_to(_drag_start_position)
if drag_distance < _TAP_THRESHOLD:
AudioManager.play_sfx("character_tap")
set_animation_state("idle")
_handle_outfit_tap()
return
AudioManager.play_sfx("character_place")
set_animation_state("idle")
if data == null or data.id.is_empty():
return
GameState.set_character_position(character_id, global_position)
character_placed.emit(self, global_position)
```
- [ ] **Step 3: Run full test suite → verify no regressions**
```bash
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit --path "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-22-character-ambient-sfx"
```
Expected: all tests pass. Total ≥ 221.
- [ ] **Step 4: Commit**
```bash
git -C "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-22-character-ambient-sfx" add scripts/characters/character.gd
git -C "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-22-character-ambient-sfx" commit -m "feat(sfx): wire character pickup/tap/place SFX to AudioManager"
```
---
### Task 4: UltrasoundMachine ambient heartbeat audio
**Files:**
- Modify: `scripts/objects/ultrasound_machine.gd`
No new tests — room-event-driven audio, mirrors Ambulance pattern (no per-SFX unit test for Ambulance either).
- [ ] **Step 1: Replace `ultrasound_machine.gd`**
Full replacement:
```gdscript
## UltrasoundMachine — displays a continuous heartbeat pulse on the screen.
## Plays looping ambient heartbeat audio when the ultrasound room is active.
class_name UltrasoundMachine extends Node2D
const BEAT_RISE_DURATION: float = 0.12
const BEAT_FALL_DURATION: float = 0.12
const BEAT_INTERVAL: float = 0.60
const BEAT_SCALE_PEAK: Vector2 = Vector2(1.5, 1.5)
const BEAT_SCALE_REST: Vector2 = Vector2(1.0, 1.0)
const _HEARTBEAT_PATH: String = "res://assets/audio/sfx/ultrasound_heartbeat.ogg"
@export var trigger_floor: int = 2
@export var trigger_room: int = 0
var _audio: AudioStreamPlayer
func _ready() -> void:
_start_heartbeat_loop()
_setup_audio()
RoomNavigator.room_changed.connect(_on_room_changed)
func _exit_tree() -> void:
if RoomNavigator.room_changed.is_connected(_on_room_changed):
RoomNavigator.room_changed.disconnect(_on_room_changed)
func _setup_audio() -> void:
_audio = AudioStreamPlayer.new()
add_child(_audio)
if AudioServer.get_driver_name() == "Dummy":
return
if not ResourceLoader.exists(_HEARTBEAT_PATH):
return
var stream: AudioStreamOggVorbis = load(_HEARTBEAT_PATH) as AudioStreamOggVorbis
if stream == null:
return
stream.loop = true
_audio.stream = stream
func _on_room_changed(floor_index: int, room_index: int) -> void:
if _audio == null or _audio.stream == null:
return
if floor_index == trigger_floor and room_index == trigger_room:
_audio.play()
else:
_audio.stop()
func _start_heartbeat_loop() -> void:
var dot: Node2D = get_node_or_null("HeartbeatDot") as Node2D
if dot == null:
return
var tween: Tween = create_tween()
tween.set_loops()
tween.tween_property(dot, "scale", BEAT_SCALE_PEAK, BEAT_RISE_DURATION)
tween.tween_property(dot, "scale", BEAT_SCALE_REST, BEAT_FALL_DURATION)
tween.tween_interval(BEAT_INTERVAL)
```
- [ ] **Step 2: Run full test suite → verify no regressions**
```bash
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit --path "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-22-character-ambient-sfx"
```
Expected: all tests pass. Total ≥ 221.
- [ ] **Step 3: Commit**
```bash
git -C "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-22-character-ambient-sfx" add scripts/objects/ultrasound_machine.gd
git -C "F:/Development/_gameDev/Cozypaw-Hospital/.worktrees/sprint-22-character-ambient-sfx" commit -m "feat(sfx): add looping ambient heartbeat to UltrasoundMachine"
```
@@ -0,0 +1,61 @@
# Sprint 21 — Interactive Object SFX Design Spec
## Goal
Add SFX to all 6 tappable interactive objects. Each object makes a sound when its animation starts — XRay scan, TeaPot pour, Cradle rock, GiftBox open, Ambulance drive-in, DeliveryBed mama arrival. UltrasoundMachine is excluded (continuous auto-loop, not tap-triggered).
## New SFX Events
7 new events added to `AudioManager._SFX_MAP`:
| Event key | Object | Trigger |
|---|---|---|
| `xray_scan` | XRayMachine | `_start_scan()` |
| `tea_pour` | TeaPot | `_start_pouring()` |
| `cradle_rock` | Cradle | `_start_rocking()` |
| `gift_open` | GiftBox | `_start_opening()` |
| `ambulance_siren` | Ambulance | `_drive_in()` |
| `delivery_cheer` | DeliveryBed | mama arrives (`_start_mama_arriving()`) |
| `object_tap` | Generic fallback | any tap on InteractiveObject base |
## Asset Specification
New files:
```
assets/audio/sfx/xray_scan.ogg — electrical hum / machine beep
assets/audio/sfx/tea_pour.ogg — liquid pouring
assets/audio/sfx/cradle_rock.ogg — gentle creak / lullaby chime
assets/audio/sfx/gift_open.ogg — unwrapping / pop
assets/audio/sfx/ambulance_siren.ogg — short siren sting (<1.5s, child-friendly)
assets/audio/sfx/delivery_cheer.ogg — happy chime / fanfare
assets/audio/sfx/object_tap.ogg — soft tap / click
```
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
Each is a single `AudioManager.play_sfx("key")` call at the start of the action:
- `xray_machine.gd` → in `_start_scan()`, before tween
- `tea_pot.gd` → in `_start_pouring()`, before tween
- `cradle.gd` → in `_start_rocking()`, before tween
- `gift_box.gd` → in `_start_opening()` (the method that starts the animation)
- `ambulance.gd` → in the drive-in animation method, before tween
- `delivery_bed.gd` → when mama starts arriving, before tween
- `interactive_object.gd` → in the base tap handler if one exists (object_tap)
## Testing
Append to `test/unit/test_audio_manager.gd`:
- `test_sfx_map_has_all_interactive_object_keys` — verifies all 7 new keys exist in `_SFX_MAP`
No per-object unit tests for SFX wiring — the calls are single-line, and the AudioManager Dummy-driver guard makes headless tests safe.
## Out of Scope
- UltrasoundMachine heartbeat sound (continuous loop, separate sprint)
- Character reaction sounds (Häschen/Kätzchen — separate sprint)
- Per-state-transition sounds (e.g., XRay completion sound)
@@ -0,0 +1,70 @@
# 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)