Files
Cozypaw-Hospital/docs/superpowers/plans/2026-05-10-sprint-22-character-ambient-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

11 KiB

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

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:


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



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

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

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:

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:

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:

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

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