# 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" ```