Files
settled-reach/client/scripts/main.gd
T
jpmschweitzerandClaude Opus 4.6 6d6b59c17b 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>
2026-02-16 01:30:15 +01:00

147 lines
5.4 KiB
GDScript

extends Node2D
@onready var world_renderer = $World
@onready var fog_entities = $World/FogEntities # D-059/D-060: cognitive delay fog visualization
@onready var camera = $Camera2D
@onready var hud = $UILayer/HUD
@onready var monologue_display = $UILayer/MonologueDisplay
@onready var interaction_prompt = $InsertOverlay/InteractionPrompt # v0.1 single-line fallback
@onready var interaction_list = $InsertOverlay/InteractionList # D-057: z-layer 6
@onready var world_radial = $InsertOverlay/WorldRadial # D-058: z-layer 6
@onready var dialogue_box = $InsertOverlay/DialogueBox # D-061: z-layer 6
@onready var inventory_grid = $UILayer/InventoryGrid # D-065: z-layer 7
@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()
if snapshot != null:
GameState.apply_snapshot(snapshot)
# Update renderers with new state
if world_renderer and world_renderer.has_method("update_from_state"):
world_renderer.update_from_state()
# D-057: Update interaction list from game state
if interaction_list and interaction_list.has_method("update_from_state"):
interaction_list.update_from_state()
# D-065: Update inventory grid
if inventory_grid and inventory_grid.has_method("update_from_state"):
inventory_grid.update_from_state()
# D-053: Update stance indicator
if stance_indicator and stance_indicator.has_method("update_from_state"):
stance_indicator.update_from_state()
# D-059/D-060: Update fog entity visualization (#431)
if fog_entities and fog_entities.has_method("update_from_state"):
fog_entities.update_from_state()
# Show monologue if server sent one this tick (#414)
_consume_monologue()
# D-061: Show dialogue if server sent one this tick (#434)
_consume_dialogue()
# Track camera to player position every frame (D-015: locked, no panning)
# Camera2D smoothing handles interpolation — we just set the target
camera.global_position = GameState.player_position * Constants.TILE_SIZE
# Send queued input to simulation
var inputs = InputMapper.flush_queue()
for input in inputs:
if input.action == InputMapper.Action.INTERACT:
# D-057: prefer interaction list (multi-verb), fall back to prompt (v0.1)
var target_id: int = -1
var verb: String = ""
if interaction_list and interaction_list.has_method("get_interaction_target"):
target_id = interaction_list.get_interaction_target()
verb = interaction_list.get_selected_verb()
if target_id < 0 and interaction_prompt:
target_id = interaction_prompt.get_interaction_target()
verb = interaction_prompt.get_selected_verb()
# Always send struct form for Interact (#415) — server expects named fields
if target_id >= 0:
input["action_data"] = {
"target_entity_id": target_id,
"verb": verb,
}
else:
input["action_data"] = {
"target_entity_id": null,
"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",
},
})