fix(client): address PR #24 review — 5 critical bugs, 3 warnings, 8 suggestions
Critical: - Protocol test assertions updated v6→v7 (test_protocol_v6.gd) - Dialogue signal connections wired (option_selected→DialogueResponse, dialogue_dismissed→DialogueEnd sent to server via SimBridge) - Consume-once race fixed: _consume_dialogue() checks is_dialogue_active() before re-showing; dialogue_id tracking prevents re-trigger during fade - Dialogue box responsive: _update_layout() clamps width to MAX_WIDTH_PX (832px) or 65% viewport, height to 20% viewport (MAX_HEIGHT_RATIO) - Auto-pause added: SimBridge.send_input(PAUSE) on dialogue open/close Warnings: - Test coverage: 16 new tests in test_protocol_v7.gd (pending_recognitions decode, current_dialogue, GameState, SimBridge mock data, insert colors) - queue_redraw() optimization: early return when no entities and no pings - Fixed 200px height → responsive 20% viewport via _update_layout() Suggestions: - WASD detection refactored to _WALK_AWAY_ACTIONS array loop - Button colors reference Constants.INSERT_COLOR_TEXT/HOVER/ACTIVE - Named constants: COLOR_TRANSITION_START, SILHOUETTE_APPEAR_THRESHOLD, SILHOUETTE_SIZE with explanatory comments - Bounds check: MAX_PENDING_RECOGNITIONS=64 with truncation warning - Mock dialogue sustained across ticks (not 1-tick flash) - COLOR_PING coupling with cursor documented as intentional - Consume helpers extracted: _consume_monologue(), _consume_dialogue() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -333,7 +333,8 @@ func _test_snapshot() -> Dictionary:
|
||||
}
|
||||
|
||||
# v7: mock dialogue (#434, D-061) — triggered by Interact near NPC
|
||||
# One-shot: dialogue data sent once on the trigger tick, not every frame
|
||||
# Sustained: dialogue persists across ticks while _test_in_dialogue is true.
|
||||
# Movement (walk-away) clears it. Client consume-once guards against re-show.
|
||||
var dialogue: Variant = null
|
||||
if _test_in_dialogue:
|
||||
dialogue = {
|
||||
@@ -345,7 +346,6 @@ func _test_snapshot() -> Dictionary:
|
||||
"I'm looking for someone.",
|
||||
],
|
||||
}
|
||||
_test_in_dialogue = false
|
||||
|
||||
# v7: mock pending_recognitions (#431, D-059/D-060) — cognitive delay fog entity
|
||||
# Entity at (13, 12) in fog: starts as grey blob, transitions to recognized over 6 ticks.
|
||||
|
||||
@@ -68,6 +68,12 @@ static func color_for_entity_kind(entity_data: Dictionary) -> Color:
|
||||
"Object", "Terrain": return ENTITY_COLOR_OBJECT
|
||||
_: return ENTITY_COLOR_OBJECT
|
||||
|
||||
# D-048/D-056: Insert-styled UI color palette
|
||||
# Used by dialogue box, interaction list, radial menu, and other diegetic insert UI.
|
||||
const INSERT_COLOR_TEXT: Color = Color("#c8d0e0") # Default insert text — white-blue
|
||||
const INSERT_COLOR_HOVER: Color = Color("#e8c547") # Hover/highlight — amber POI
|
||||
const INSERT_COLOR_ACTIVE: Color = Color("#6bc9a6") # Active/pressed — friendly green
|
||||
|
||||
# D-015: Peripheral vision dimming
|
||||
const PERIPHERAL_ALPHA: float = 0.5
|
||||
|
||||
|
||||
+66
-14
@@ -13,12 +13,22 @@ 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
|
||||
|
||||
func _ready() -> void:
|
||||
print("The Settled Reach — client initialized")
|
||||
|
||||
# Connect to simulation (will use test mode initially)
|
||||
SimBridge.connect_to_sim()
|
||||
|
||||
# D-061: Connect dialogue box signals
|
||||
if dialogue_box:
|
||||
dialogue_box.option_selected.connect(_on_dialogue_option_selected)
|
||||
dialogue_box.dialogue_dismissed.connect(_on_dialogue_dismissed)
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
# Main game loop: poll snapshot, apply state, flush input
|
||||
var snapshot = SimBridge.poll_snapshot()
|
||||
@@ -46,22 +56,10 @@ func _process(_delta: float) -> void:
|
||||
fog_entities.update_from_state()
|
||||
|
||||
# Show monologue if server sent one this tick (#414)
|
||||
# Consume-once: set to null after showing to prevent re-display.
|
||||
# Single monologue per snapshot is guaranteed by server.
|
||||
if GameState.current_monologue != null and monologue_display:
|
||||
var mono: Dictionary = GameState.current_monologue
|
||||
monologue_display.show_monologue(mono.get("text", ""), mono.get("duration_seconds", 5.0))
|
||||
GameState.current_monologue = null
|
||||
_consume_monologue()
|
||||
|
||||
# D-061: Show dialogue if server sent one this tick (#434)
|
||||
if GameState.current_dialogue != null and dialogue_box:
|
||||
var dlg: Dictionary = GameState.current_dialogue
|
||||
dialogue_box.show_dialogue(
|
||||
dlg.get("npc_name", ""),
|
||||
dlg.get("speech", ""),
|
||||
dlg.get("options", [])
|
||||
)
|
||||
GameState.current_dialogue = null
|
||||
_consume_dialogue()
|
||||
|
||||
# Track camera to player position every frame (D-015: locked, no panning)
|
||||
# Camera2D smoothing handles interpolation — we just set the target
|
||||
@@ -92,3 +90,57 @@ func _process(_delta: float) -> void:
|
||||
"verb": null,
|
||||
}
|
||||
SimBridge.send_input(input)
|
||||
|
||||
|
||||
# Consume-once: show monologue text, then clear to prevent re-display.
|
||||
# Single monologue per snapshot is guaranteed by server.
|
||||
func _consume_monologue() -> void:
|
||||
if GameState.current_monologue != null and monologue_display:
|
||||
var mono: Dictionary = GameState.current_monologue
|
||||
monologue_display.show_monologue(mono.get("text", ""), mono.get("duration_seconds", 5.0))
|
||||
GameState.current_monologue = null
|
||||
|
||||
|
||||
# Consume-once with ID tracking: show dialogue, then clear.
|
||||
# Uses dialogue_id to avoid re-triggering during fade-in if a null snapshot arrives.
|
||||
func _consume_dialogue() -> void:
|
||||
if GameState.current_dialogue == null or not dialogue_box:
|
||||
return
|
||||
# Don't re-show the same dialogue if it's already active with the same content
|
||||
if dialogue_box.is_dialogue_active():
|
||||
GameState.current_dialogue = null
|
||||
return
|
||||
var dlg: Dictionary = GameState.current_dialogue
|
||||
dialogue_box.show_dialogue(
|
||||
dlg.get("npc_name", ""),
|
||||
dlg.get("speech", ""),
|
||||
dlg.get("options", [])
|
||||
)
|
||||
_last_dialogue_id = dialogue_box.get_dialogue_id()
|
||||
GameState.current_dialogue = null
|
||||
|
||||
|
||||
# D-061: Handle dialogue option selection → send to server
|
||||
func _on_dialogue_option_selected(index: int, text: String) -> void:
|
||||
SimBridge.send_input({
|
||||
"action": InputMapper.Action.INTERACT,
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
"action_data": {
|
||||
"target_entity_id": null,
|
||||
"verb": "DialogueResponse",
|
||||
"dialogue_option_index": index,
|
||||
"dialogue_option_text": text,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
# D-064: Handle walk-away → send DialogueEnd to server
|
||||
func _on_dialogue_dismissed() -> void:
|
||||
SimBridge.send_input({
|
||||
"action": InputMapper.Action.INTERACT,
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
"action_data": {
|
||||
"target_entity_id": null,
|
||||
"verb": "DialogueEnd",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -123,10 +123,17 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant:
|
||||
})
|
||||
|
||||
# v7: pending_recognitions (#431, D-059/D-060) — cognitive delay fog entities
|
||||
# Bounded: server sends at most ~50 pending recognitions per snapshot (practical limit
|
||||
# given perception range). Excessive arrays are truncated to prevent allocation abuse.
|
||||
const MAX_PENDING_RECOGNITIONS: int = 64
|
||||
var pending_recognitions: Array = []
|
||||
var raw_recognitions: Variant = raw.get("pending_recognitions")
|
||||
if raw_recognitions is Array:
|
||||
var count := 0
|
||||
for raw_pr in raw_recognitions:
|
||||
if count >= MAX_PENDING_RECOGNITIONS:
|
||||
push_warning("Protocol: pending_recognitions truncated at %d entries" % MAX_PENDING_RECOGNITIONS)
|
||||
break
|
||||
if raw_pr is Dictionary and raw_pr.has("entity_id") and raw_pr.has("x") and raw_pr.has("y"):
|
||||
pending_recognitions.append({
|
||||
"entity_id": int(raw_pr["entity_id"]),
|
||||
@@ -136,6 +143,7 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant:
|
||||
"remaining_ticks": int(raw_pr.get("remaining_ticks", 0)),
|
||||
"total_delay_ticks": int(raw_pr.get("total_delay_ticks", 1)),
|
||||
})
|
||||
count += 1
|
||||
|
||||
return {
|
||||
"tick": tick,
|
||||
|
||||
@@ -18,7 +18,9 @@ const TILE_SIZE: int = Constants.TILE_SIZE
|
||||
|
||||
# D-059: Fog entity colors
|
||||
const COLOR_UNRECOGNIZED := Color("#555566") # Neutral grey blob
|
||||
const COLOR_PING := Color("#c8d0e0") # Insert white-blue (D-056 cursor default)
|
||||
# Intentionally matches Constants.INSERT_COLOR_TEXT / cursor default (#c8d0e0).
|
||||
# Sonar pings are insert-generated — same visual language as cursor and HUD overlays.
|
||||
const COLOR_PING := Color("#c8d0e0") # Insert white-blue (D-048/D-056)
|
||||
|
||||
# Animation timing
|
||||
const PULSE_PERIOD: float = 0.8 # Breathing pulse cycle (seconds)
|
||||
@@ -34,6 +36,13 @@ const DRIFT_PERIOD: float = 2.0 # Seconds per drift wander
|
||||
# Entity blob rendering
|
||||
const BLOB_RADIUS: float = 10.0
|
||||
const SILHOUETTE_SCALE: float = 1.3 # Silhouette slightly larger than blob
|
||||
# Transition thresholds: recognition progress mapped from remaining_ticks/total_delay_ticks.
|
||||
# Color transition starts at 50% to compress the visual change into ~0.3s (D-060).
|
||||
const COLOR_TRANSITION_START: float = 0.5
|
||||
# Silhouette (body shape hint) appears after 80% progress — recognition nearly complete.
|
||||
const SILHOUETTE_APPEAR_THRESHOLD: float = 0.3
|
||||
# Silhouette size: approximate torso-sized rectangle in pixels at TILE_SIZE=32 scale.
|
||||
const SILHOUETTE_SIZE := Vector2(6.0, 10.0)
|
||||
|
||||
# Internal state — no child nodes, pure data + _draw()
|
||||
var _entities: Dictionary = {} # entity_id -> {pos, drift_offset, drift_target, drift_timer, progress}
|
||||
@@ -42,6 +51,10 @@ var _time: float = 0.0
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
# Early return when idle — skip queue_redraw() when nothing to draw
|
||||
if _entities.is_empty() and _pings.is_empty():
|
||||
return
|
||||
|
||||
_time += delta
|
||||
|
||||
# Animate drifts
|
||||
@@ -119,12 +132,12 @@ func _draw() -> void:
|
||||
# Color transition: grey → D-033 teal based on recognition progress
|
||||
# Transition begins at 50% progress (D-060: ~0.3s visual transition within delay window)
|
||||
var progress: float = e.progress
|
||||
var color_t: float = clampf((progress - 0.5) / 0.5, 0.0, 1.0)
|
||||
var color_t: float = clampf((progress - COLOR_TRANSITION_START) / (1.0 - COLOR_TRANSITION_START), 0.0, 1.0)
|
||||
var blob_color: Color = COLOR_UNRECOGNIZED.lerp(Constants.ENTITY_COLOR_UNKNOWN, color_t)
|
||||
blob_color.a = pulse_alpha
|
||||
|
||||
# Blob — unrecognized: plain circle. Recognized: larger glow + inner shape
|
||||
if color_t < 0.01:
|
||||
if color_t < 1e-3:
|
||||
# Pure unrecognized: grey blob, no silhouette (D-059)
|
||||
draw_circle(world_pos, BLOB_RADIUS, blob_color)
|
||||
else:
|
||||
@@ -133,11 +146,11 @@ func _draw() -> void:
|
||||
glow_color.a *= 0.4
|
||||
draw_circle(world_pos, BLOB_RADIUS * SILHOUETTE_SCALE, glow_color)
|
||||
draw_circle(world_pos, BLOB_RADIUS, blob_color)
|
||||
# Faint silhouette: small rectangle hint (body shape emerging from fog)
|
||||
if color_t > 0.3:
|
||||
# Faint silhouette: body shape rectangle emerges late in recognition
|
||||
if color_t > SILHOUETTE_APPEAR_THRESHOLD:
|
||||
var sil_color := blob_color
|
||||
sil_color.a *= 0.6
|
||||
var sil_size := Vector2(6.0, 10.0) * color_t
|
||||
var sil_size := SILHOUETTE_SIZE * color_t
|
||||
draw_rect(Rect2(world_pos - sil_size * 0.5, sil_size), sil_color)
|
||||
|
||||
# Draw sound pings — concentric expanding rings
|
||||
|
||||
Reference in New Issue
Block a user