fix(client): address PR #25 review — 5 critical bugs, 4 warnings, 3 suggestions

Critical fixes:
- hide_dialogue() sent PAUSE instead of UNPAUSE, permanently freezing
  simulation after every dialogue (both reviewers)
- Confrontation monologue hardcoded in GDScript constant, violating
  D-042/D-020 — moved to ui-strings.yaml as dialogue.confrontation_beat
- Walk-away WASD didn't call set_input_as_handled(), letting movement
  event propagate and potentially stepping on the same frame
- is_dialogue_active() returned _is_showing only — interaction list
  could flash during 300ms fade gap. Now includes dialogue_active state
- Removed dead _last_dialogue_id / get_dialogue_id() state (never read)

Warnings addressed:
- Audio registry now scans res://audio/ recursively (subdirs registered)
- add_bus_effect guarded against duplicate calls in tests
- int64 encoder dead code tagged KNOWN-DEFECT, filed as ticket #516
- D-073 zone crossfade stub comment clarifies Sprint 9+ deferral
- listening_focus dip documents caller tick-gate responsibility (D-069/D-071)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 17:33:56 +01:00
co-authored by Claude Opus 4.6
parent 4ccb73d364
commit 2ab41a0f04
5 changed files with 37 additions and 27 deletions
+19 -7
View File
@@ -15,6 +15,8 @@ const BUSES := [BUS_MUSIC, BUS_AMBIENT, BUS_WORLD_SFX, BUS_PLAYER_ACTIONS, BUS_U
# D-069: Audio dip profiles — dB offsets relative to player slider setting.
# Mid-range of spec values used (e.g. -6 to -8 → -7).
# NOTE: listening_focus requires 30+ stationary ticks before activation (D-069/D-071).
# The tick gate is the caller's responsibility — AudioManager only manages bus volumes.
const DIP_SPECS := {
"dialogue": {
"ease_in": 0.3, "ease_out": 0.5,
@@ -70,8 +72,9 @@ func _setup_buses() -> void:
_bus_volumes[bus_name] = 0.0
# Low-pass filter on Ambient bus — cutoff starts high (bypassed).
# Confrontation dip sweeps it down to 800 Hz (D-069).
# Guard: skip if filter already exists (e.g. duplicate _ready in tests).
var amb_idx := AudioServer.get_bus_index(BUS_AMBIENT)
if amb_idx >= 0:
if amb_idx >= 0 and _ambient_filter == null:
_ambient_filter = AudioEffectLowPassFilter.new()
_ambient_filter.cutoff_hz = FILTER_CUTOFF_DEFAULT
AudioServer.add_bus_effect(amb_idx, _ambient_filter)
@@ -80,20 +83,28 @@ func _setup_buses() -> void:
# --- Asset registry (D-068 directory-scan pattern) ---
func _scan_registry() -> void:
var dir := DirAccess.open("res://audio/")
_scan_dir("res://audio/")
print("AudioManager: %d assets registered" % _registry.size())
func _scan_dir(path: String) -> void:
var dir := DirAccess.open(path)
if dir == null:
print("AudioManager: res://audio/ not found — all play methods no-op")
if path == "res://audio/":
print("AudioManager: res://audio/ not found — all play methods no-op")
return
dir.list_dir_begin()
var file_name := dir.get_next()
while file_name != "":
if not dir.current_is_dir() and not file_name.ends_with(".import"):
var full_path := path.path_join(file_name)
if dir.current_is_dir():
_scan_dir(full_path)
elif not file_name.ends_with(".import"):
if file_name.get_extension().to_lower() in ["ogg", "wav", "mp3"]:
var stream := load("res://audio/" + file_name) as AudioStream
var stream := load(full_path) as AudioStream
if stream:
_registry[file_name.get_basename()] = stream
file_name = dir.get_next()
print("AudioManager: %d assets registered" % _registry.size())
func has_asset(asset_key: String) -> bool:
@@ -268,7 +279,8 @@ func get_volume(bus: String) -> float:
# --- Zone crossfade (D-073 stub) ---
## Handle zone transition. Server sends zone_id per tile in ObserverSnapshot.
## Full crossfade implementation deferred — stub for integration point.
## Full crossfade implementation deferred to Sprint 9+ (D-073).
## Stub exists so server integration can call it without conditional checks.
func set_zone(_zone_id: String) -> void:
pass
-5
View File
@@ -13,10 +13,6 @@ extends Node2D
@onready var stance_indicator = $UILayer/StanceIndicator # D-053: z-layer 7
@onready var cursor_renderer = $UILayer/CursorRenderer # D-056: z-layer 7
# Tracks the dialogue_id from dialogue_box to prevent consume-once race during fade-in.
# If a new snapshot arrives with null dialogue while fade-in is still running,
# we don't re-trigger show_dialogue because _last_dialogue_id still matches.
var _last_dialogue_id: int = 0
var _last_dialogue_npc_id: int = -1 # D-064: NPC entity_id for WalkAway input
var _camera_anchored: bool = false
var _last_monologue_tick: int = -1 # Prevent re-consuming monologue when same tick polled twice
@@ -169,7 +165,6 @@ func _consume_dialogue() -> void:
dlg.get("speech", ""),
dlg.get("options", [])
)
_last_dialogue_id = dialogue_box.get_dialogue_id()
GameState.current_dialogue = null