Compare commits
2 Commits
faed0951d3
...
52ebb78862
| Author | SHA1 | Date | |
|---|---|---|---|
| 52ebb78862 | |||
| ad9a406775 |
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"env": {
|
||||
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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/ |
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user