fix(client): address PR #47 review — murmur deferral, zone extraction, 6 tests

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>
This commit is contained in:
2026-02-19 18:37:23 +01:00
co-authored by Claude Opus 4.6
parent 513682522f
commit 8985340bdd
5 changed files with 103 additions and 17 deletions
+7 -5
View File
@@ -45,7 +45,12 @@ const FILTER_CUTOFF_DEFAULT := 20500.0
const CROSSFADE_DURATION := 1.8 # D-073: 1.5-2s spec, mid-range
# Maps server zone_id strings to ambient asset keys (filenames in res://audio/).
# Hub and Workplace share the same ambient layer — same location type.
# Hub and Workplace intentionally share the same ambient layer (amb_hub_layer) —
# they are the same location type, so hub→workplace transition is a same-asset no-op
# (old_asset != new_asset guard skips the fade-out). Sprint brief consolidates
# D-038's "amb_workplace_layer" to "amb_hub_layer" for v0.1.
# Note: amb_station_base (D-038 global base hum) plays globally via play_loop()
# at startup — it is not zone-dependent and has no ZONE_ASSETS entry.
const ZONE_ASSETS: Dictionary = {
"hub": "amb_hub_layer",
"workplace": "amb_hub_layer",
@@ -129,6 +134,7 @@ func _scan_dir(path: String) -> void:
if stream:
_registry[file_name.get_basename()] = stream
file_name = dir.get_next()
dir.list_dir_end()
func has_asset(asset_key: String) -> bool:
@@ -201,10 +207,6 @@ const SOUND_EVENT_ASSETS: Dictionary = {
"FootstepCrouch": "sfx_footstep_metal_walk", # D-053: same asset until stance-differentiated audio lands
"FootstepSprint": "sfx_footstep_metal_run",
"FootstepRun": "sfx_footstep_metal_run",
# D-072 (#533): NPC conversation murmur — single universal asset, World SFX bus.
# Zone ambient provides natural conspicuousness (bar buries it, corridor exposes it).
"Voice": "sfx_npc_murmur",
"VoiceConversation": "sfx_npc_murmur",
}
+16
View File
@@ -68,6 +68,11 @@ var close_sound_events: Array = []
var stationary_ticks: int = 0
var _prev_player_position: Vector2 = Vector2(-1e9, -1e9) # sentinel: no previous position
# D-073 (#529): Server-authoritative zone_id from the player's current tile.
# Extracted in apply_snapshot() — avoids O(N) tile scan in main.gd per Tyre review.
# Empty string when zone_id field absent (server hasn't shipped OQ-09 yet).
var current_zone_id: String = ""
func apply_snapshot(snapshot: Dictionary) -> void:
current_snapshot = snapshot
@@ -197,6 +202,17 @@ func apply_snapshot(snapshot: Dictionary) -> void:
medium_sound_events = []
close_sound_events = []
# D-073 (#529): Extract zone_id from the player's current tile (server-authoritative).
# O(1) via visible_positions dict would be ideal, but tiles are arrays without
# positional indexing — use the same tile iteration below instead.
current_zone_id = ""
var _px := int(player_position.x)
var _py := int(player_position.y)
for _ztile in visible_tiles:
if _ztile is Dictionary and _ztile.get("x") == _px and _ztile.get("y") == _py:
current_zone_id = _ztile.get("zone_id", "")
break
# v2: visible_tiles with visibility sectors
# Derives visible_positions when not explicitly provided (real server mode)
if snapshot.has("visible_tiles") and snapshot.visible_tiles is Array and snapshot.visible_tiles.size() > 0:
+3 -11
View File
@@ -254,19 +254,11 @@ func _play_recognition_chimes() -> void:
_known_recognition_ids.erase(eid)
# D-073 (#529): Zone ambient crossfade — read zone_id from player's current tile.
# D-073 (#529): Zone ambient crossfade — reads zone_id from GameState.current_zone_id
# (extracted in apply_snapshot(), server-authoritative per D-020).
# Calls AudioManager.set_zone() when zone changes (AudioManager handles crossfade).
# zone_id field is server-authoritative; missing field treated as empty (no zone).
func _update_zone() -> void:
var px := int(GameState.player_position.x)
var py := int(GameState.player_position.y)
var zone: String = ""
for tile in GameState.visible_tiles:
if not tile is Dictionary:
continue
if tile.get("x") == px and tile.get("y") == py:
zone = tile.get("zone_id", "")
break
var zone := GameState.current_zone_id
if zone != _current_zone:
_current_zone = zone
AudioManager.set_zone(zone)