Files
Cozypaw-Hospital/docs/superpowers/plans/2026-05-10-sprint-21-object-sfx.md
T
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

12 KiB

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

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:


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

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
"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:

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
"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
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:

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:

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:

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:

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:

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:

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:

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