#529 Zone crossfade: Defensive zone_id read from snapshot tiles, 1.5-2s ambient tween. Auto-activates when server ships OQ-09 (#523). No client changes needed (D-073).
#530 Audio dip profiles: Dialogue dip in show/hide_dialogue(), confrontation override, ListeningFocus 30-tick gate via stationary_ticks in game_state.gd (D-069, D-070, D-071).
#531 Recognition chime: Verified already complete from prior sprint — fires at ONSET per D-067.
#533 NPC murmur: Client plumbing for event-driven World SFX playback, no-ops until audio asset arrives (#532).
Test plan
57 new tests in test_audio_sprint13.gd — bus setup, dip profiles, zone crossfade, recognition chime, murmur wiring
3 lambda capture fixes in test_audio_bus_routing.gd (String → Array wrapper for Godot 4 closure reliability)
before_test() bus volume reset for test isolation
Verify make ci passes
Manual: open settings, adjust 5 volume sliders, confirm persistence across restart
Manual: walk between zones, confirm 1.5-2s ambient crossfade (requires server zone_id)
Manual: open dialogue, confirm ambient dip; stand still 30+ ticks, confirm WorldSFX boost
External dependencies
Server OQ-09 (#523): zone_id in snapshot → zone crossfades activate
AudioManager: volume slider persistence (ConfigFile), settings UI with
5 teal-bordered sliders, default_bus_layout.tres for editor. Zone
crossfade: defensive zone_id read from snapshot tiles, 1.5-2s ambient
tween, auto-activates when server ships OQ-09. Dip profiles: dialogue
dip in show/hide_dialogue, ListeningFocus 30-tick gate via
stationary_ticks in game_state.gd. NPC murmur: client plumbing for
event-driven World SFX playback, no-ops until audio asset arrives.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
57 new tests in test_audio_sprint13.gd covering bus setup, dip profiles,
zone crossfade, recognition chime, and murmur wiring. Fix lambda capture
bug in 3 signal tests (String → Array wrapper for reliable closure
capture in Godot 4). Add bus volume reset to before_test() for test
isolation against prefs-loaded state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Solid Sprint 13 audio implementation — 5-bus architecture correctly wired, dip profiles implemented, zone crossfade functional, and comprehensive test suite (560 lines). Issues with premature murmur wiring and untested edge cases.
#
File
Severity
Issue
1
audio_manager.gd SOUND_EVENT_ASSETS
warning
Voice/VoiceConversation → sfx_npc_murmur entries added but D-072 explicitly defers NPC proximity murmur. Asset key sfx_npc_murmur is unconfirmed — if audio team delivers as sfx_npc_murmur_01.ogg, this becomes a silent permanent miss. Remove until #533 PR lands with confirmed asset
2
audio_manager.gdset_zone()
warning
Rapid back-and-forth zone crossing (D-073 spec concern) is untested. When _kill_zone_tweens() fires mid-fade, old player stays alive at intermediate volume — behavior is correct but the test suite never verifies this interruptible crossfade path
3
audio_manager.gd_load_prefs()
warning
Persistence load path is entirely untested. No test verifies _load_prefs() restores previously saved values on startup, or handles corrupted/missing config gracefully
4
main.gd_update_zone()
warning
O(N) linear scan through visible_tiles to find player tile. Existing code uses visible_positions Dictionary for O(1) lookups — zone lookup should follow the same pattern
5
audio_manager.gd_scan_dir()
suggestion
dir.list_dir_end() never called after scan completes. Godot 4 handles cleanup via refcounting but docs recommend explicit end
6
settings_dialog.gd_build_ui()
suggestion
Lambda capture of bus_name in slider loop — commit message says "lambda capture fix" but full file not verifiable from diff excerpt. Confirm bus_name is bound to a local before closure
7
test_audio_sprint13.gd Layer 4
suggestion
Dip call site tests verify AudioManager in isolation but don't test actual dialogue_box.gd wiring. A scene-level test would catch regressions if the apply_dip("dialogue") call is removed
8
audio_manager.gd ZONE_ASSETS
suggestion
"workplace" → "amb_hub_layer" diverges from D-038 which names the asset amb_workplace_layer. Intentional consolidation per sprint brief but deserves a comment. Also amb_station_base (D-038 global base hum) has no entry — unclear if it plays globally elsewhere
Tyre (Architecture): REQUEST_CHANGES
Architecture follows D-068/D-069/D-071/D-073 correctly. Dip state machine is clean, proportional volume during active dip is correct, ListeningFocus gate is caller-responsibility as spec'd. Two architectural issues need fixing.
#
File
Severity
Issue
1
audio_manager.gd SOUND_EVENT_ASSETS
warning
Voice/VoiceConversation entries violate D-072 deferral. These will fire before the eavesdropping information quality model (D-071: distance-dependent, stance-modified, ListeningFocus-boosted) is wired server-side. Remove — they belong in the sprint that implements D-072 event-driven murmur end-to-end
2
main.gd_update_zone()
warning
Runs in _process() unconditionally (60fps+), not inside if snapshot != null: block. Zone only changes on new snapshots — move inside snapshot block. Better: extract zone_id from player tile in GameState.apply_snapshot() as GameState.current_zone_id (like player_facing, player_stance), avoiding client re-derivation of server-authoritative state per D-020
3
audio_manager.gdset_zone() hub/workplace
suggestion
hub and workplace map to same asset amb_hub_layer — the old_asset != new_asset guard correctly skips fade-out on hub→workplace transition. Add a comment noting this intentional same-asset pattern
4
dialogue_box.gd_start_confrontation_beat
suggestion
Confrontation dip replaces (not nests) the dialogue dip. hide_dialogue's clear_dip restores to base volumes regardless of which profile was last active. Add a comment noting the intentional replacement semantics
Verdict: CHANGES REQUESTED
Key fixes before merge:
Remove Voice/VoiceConversation from SOUND_EVENT_ASSETS — D-072 defers NPC proximity murmur to a future sprint
Move _update_zone() inside the if snapshot != null: block — avoid per-frame tile scanning; ideally extract zone_id in GameState.apply_snapshot() as a first-class field
Add rapid zone-crossing test coverage for D-073 interruptible crossfade path
Add _load_prefs() persistence test (load after save roundtrip)
## Review: client -> main (PR #47)
### Hoshe (Code Quality): REQUEST_CHANGES
Solid Sprint 13 audio implementation — 5-bus architecture correctly wired, dip profiles implemented, zone crossfade functional, and comprehensive test suite (560 lines). Issues with premature murmur wiring and untested edge cases.
| # | File | Severity | Issue |
|---|------|----------|-------|
| 1 | `audio_manager.gd` SOUND_EVENT_ASSETS | warning | `Voice`/`VoiceConversation` → `sfx_npc_murmur` entries added but D-072 explicitly defers NPC proximity murmur. Asset key `sfx_npc_murmur` is unconfirmed — if audio team delivers as `sfx_npc_murmur_01.ogg`, this becomes a silent permanent miss. Remove until #533 PR lands with confirmed asset |
| 2 | `audio_manager.gd` `set_zone()` | warning | Rapid back-and-forth zone crossing (D-073 spec concern) is untested. When `_kill_zone_tweens()` fires mid-fade, old player stays alive at intermediate volume — behavior is correct but the test suite never verifies this interruptible crossfade path |
| 3 | `audio_manager.gd` `_load_prefs()` | warning | Persistence load path is entirely untested. No test verifies `_load_prefs()` restores previously saved values on startup, or handles corrupted/missing config gracefully |
| 4 | `main.gd` `_update_zone()` | warning | O(N) linear scan through `visible_tiles` to find player tile. Existing code uses `visible_positions` Dictionary for O(1) lookups — zone lookup should follow the same pattern |
| 5 | `audio_manager.gd` `_scan_dir()` | suggestion | `dir.list_dir_end()` never called after scan completes. Godot 4 handles cleanup via refcounting but docs recommend explicit end |
| 6 | `settings_dialog.gd` `_build_ui()` | suggestion | Lambda capture of `bus_name` in slider loop — commit message says "lambda capture fix" but full file not verifiable from diff excerpt. Confirm `bus_name` is bound to a local before closure |
| 7 | `test_audio_sprint13.gd` Layer 4 | suggestion | Dip call site tests verify AudioManager in isolation but don't test actual `dialogue_box.gd` wiring. A scene-level test would catch regressions if the `apply_dip("dialogue")` call is removed |
| 8 | `audio_manager.gd` ZONE_ASSETS | suggestion | `"workplace"` → `"amb_hub_layer"` diverges from D-038 which names the asset `amb_workplace_layer`. Intentional consolidation per sprint brief but deserves a comment. Also `amb_station_base` (D-038 global base hum) has no entry — unclear if it plays globally elsewhere |
### Tyre (Architecture): REQUEST_CHANGES
Architecture follows D-068/D-069/D-071/D-073 correctly. Dip state machine is clean, proportional volume during active dip is correct, ListeningFocus gate is caller-responsibility as spec'd. Two architectural issues need fixing.
| # | File | Severity | Issue |
|---|------|----------|-------|
| 1 | `audio_manager.gd` SOUND_EVENT_ASSETS | warning | `Voice`/`VoiceConversation` entries violate D-072 deferral. These will fire before the eavesdropping information quality model (D-071: distance-dependent, stance-modified, ListeningFocus-boosted) is wired server-side. Remove — they belong in the sprint that implements D-072 event-driven murmur end-to-end |
| 2 | `main.gd` `_update_zone()` | warning | Runs in `_process()` unconditionally (60fps+), not inside `if snapshot != null:` block. Zone only changes on new snapshots — move inside snapshot block. Better: extract `zone_id` from player tile in `GameState.apply_snapshot()` as `GameState.current_zone_id` (like `player_facing`, `player_stance`), avoiding client re-derivation of server-authoritative state per D-020 |
| 3 | `audio_manager.gd` `set_zone()` hub/workplace | suggestion | `hub` and `workplace` map to same asset `amb_hub_layer` — the `old_asset != new_asset` guard correctly skips fade-out on hub→workplace transition. Add a comment noting this intentional same-asset pattern |
| 4 | `dialogue_box.gd` `_start_confrontation_beat` | suggestion | Confrontation dip replaces (not nests) the dialogue dip. `hide_dialogue`'s `clear_dip` restores to base volumes regardless of which profile was last active. Add a comment noting the intentional replacement semantics |
### Verdict: CHANGES REQUESTED
**Key fixes before merge:**
1. Remove `Voice`/`VoiceConversation` from `SOUND_EVENT_ASSETS` — D-072 defers NPC proximity murmur to a future sprint
2. Move `_update_zone()` inside the `if snapshot != null:` block — avoid per-frame tile scanning; ideally extract `zone_id` in `GameState.apply_snapshot()` as a first-class field
3. Add rapid zone-crossing test coverage for D-073 interruptible crossfade path
4. Add `_load_prefs()` persistence test (load after save roundtrip)
Remove premature Voice/VoiceConversation from SOUND_EVENT_ASSETS (D-072
defers murmur to end-to-end sprint). Extract current_zone_id in
GameState.apply_snapshot() as first-class field, eliminating O(N) tile
scan in main.gd (D-020 server-authoritative). Add dir.list_dir_end()
after registry scan. Add rapid zone-crossing + _load_prefs() roundtrip
tests. Enhance comments on hub/workplace same-asset pattern, station
base hum, and confrontation dip replacement semantics.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Sprint 13 client team — full audio architecture implementation.
default_bus_layout.tresfor editor. 5-bus setup: Music, Ambient, WorldSFX, PlayerActions, UISounds (D-068).zone_idread from snapshot tiles, 1.5-2s ambient tween. Auto-activates when server ships OQ-09 (#523). No client changes needed (D-073).show/hide_dialogue(), confrontation override, ListeningFocus 30-tick gate viastationary_ticksingame_state.gd(D-069, D-070, D-071).Test plan
test_audio_sprint13.gd— bus setup, dip profiles, zone crossfade, recognition chime, murmur wiringtest_audio_bus_routing.gd(String → Array wrapper for Godot 4 closure reliability)before_test()bus volume reset for test isolationmake cipassesExternal dependencies
zone_idin snapshot → zone crossfades activateReview: client -> main (PR #47)
Hoshe (Code Quality): REQUEST_CHANGES
Solid Sprint 13 audio implementation — 5-bus architecture correctly wired, dip profiles implemented, zone crossfade functional, and comprehensive test suite (560 lines). Issues with premature murmur wiring and untested edge cases.
audio_manager.gdSOUND_EVENT_ASSETSVoice/VoiceConversation→sfx_npc_murmurentries added but D-072 explicitly defers NPC proximity murmur. Asset keysfx_npc_murmuris unconfirmed — if audio team delivers assfx_npc_murmur_01.ogg, this becomes a silent permanent miss. Remove until #533 PR lands with confirmed assetaudio_manager.gdset_zone()_kill_zone_tweens()fires mid-fade, old player stays alive at intermediate volume — behavior is correct but the test suite never verifies this interruptible crossfade pathaudio_manager.gd_load_prefs()_load_prefs()restores previously saved values on startup, or handles corrupted/missing config gracefullymain.gd_update_zone()visible_tilesto find player tile. Existing code usesvisible_positionsDictionary for O(1) lookups — zone lookup should follow the same patternaudio_manager.gd_scan_dir()dir.list_dir_end()never called after scan completes. Godot 4 handles cleanup via refcounting but docs recommend explicit endsettings_dialog.gd_build_ui()bus_namein slider loop — commit message says "lambda capture fix" but full file not verifiable from diff excerpt. Confirmbus_nameis bound to a local before closuretest_audio_sprint13.gdLayer 4dialogue_box.gdwiring. A scene-level test would catch regressions if theapply_dip("dialogue")call is removedaudio_manager.gdZONE_ASSETS"workplace"→"amb_hub_layer"diverges from D-038 which names the assetamb_workplace_layer. Intentional consolidation per sprint brief but deserves a comment. Alsoamb_station_base(D-038 global base hum) has no entry — unclear if it plays globally elsewhereTyre (Architecture): REQUEST_CHANGES
Architecture follows D-068/D-069/D-071/D-073 correctly. Dip state machine is clean, proportional volume during active dip is correct, ListeningFocus gate is caller-responsibility as spec'd. Two architectural issues need fixing.
audio_manager.gdSOUND_EVENT_ASSETSVoice/VoiceConversationentries violate D-072 deferral. These will fire before the eavesdropping information quality model (D-071: distance-dependent, stance-modified, ListeningFocus-boosted) is wired server-side. Remove — they belong in the sprint that implements D-072 event-driven murmur end-to-endmain.gd_update_zone()_process()unconditionally (60fps+), not insideif snapshot != null:block. Zone only changes on new snapshots — move inside snapshot block. Better: extractzone_idfrom player tile inGameState.apply_snapshot()asGameState.current_zone_id(likeplayer_facing,player_stance), avoiding client re-derivation of server-authoritative state per D-020audio_manager.gdset_zone()hub/workplacehubandworkplacemap to same assetamb_hub_layer— theold_asset != new_assetguard correctly skips fade-out on hub→workplace transition. Add a comment noting this intentional same-asset patterndialogue_box.gd_start_confrontation_beathide_dialogue'sclear_diprestores to base volumes regardless of which profile was last active. Add a comment noting the intentional replacement semanticsVerdict: CHANGES REQUESTED
Key fixes before merge:
Voice/VoiceConversationfromSOUND_EVENT_ASSETS— D-072 defers NPC proximity murmur to a future sprint_update_zone()inside theif snapshot != null:block — avoid per-frame tile scanning; ideally extractzone_idinGameState.apply_snapshot()as a first-class field_load_prefs()persistence test (load after save roundtrip)Pull request closed