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 @onready var gauntlet_hud = $UILayer/GauntletHUD # #496: room timer + personal bests @onready var bug_report_dialog = $ModalLayer/BugReportDialog # #495: F12 WRONG button 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 var _last_dialogue_tick: int = -1 func _ready() -> void: print("The Settled Reach — client initialized") # Disable camera smoothing during init. Camera2D's position_smoothing # lerps an internal smoothed_camera_pos toward global_position each frame. # That smoothed position initializes at (0,0) — the Camera2D's default in # the .tscn. Even after we set global_position to the player coords, # smoothing causes the viewport to still show (0,0) on the first rendered # frame because the lerp hasn't converged. With smoothing OFF, the viewport # uses global_position directly. Re-enabled in _process() after anchor. camera.position_smoothing_enabled = false # Connect to simulation (test mode sets CONNECTED immediately) SimBridge.connect_to_sim() # Camera anchor: snap to player position before the first frame renders. # In test mode poll_snapshot() returns synchronously — position is set # immediately. In live mode the snapshot isn't available yet — _process # handles it. No reset_smoothing() needed: smoothing is OFF. var first_snapshot: Variant = SimBridge.poll_snapshot() if first_snapshot != null: GameState.apply_snapshot(first_snapshot) camera.global_position = GameState.player_position * Constants.TILE_SIZE _camera_anchored = true # 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) dialogue_box.confrontation_monologue.connect(_on_confrontation_monologue) # #496: Print gauntlet session summary on disconnect if gauntlet_hud: SimBridge.connection_state_changed.connect(_on_connection_state_changed) func _process(_delta: float) -> void: # Main game loop: poll snapshot, apply state, flush input var snapshot: Variant = SimBridge.poll_snapshot() if snapshot != null: GameState.apply_snapshot(snapshot) # Late anchor: live mode — first snapshot arrives during _process. # Smoothing is already OFF (disabled in _ready), so setting # global_position takes effect immediately with no lerp. if not _camera_anchored: camera.global_position = GameState.player_position * Constants.TILE_SIZE _camera_anchored = true # 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 # Suppress during dialogue — player is in conversation, verb list is noise if interaction_list and interaction_list.has_method("update_from_state"): if dialogue_box and dialogue_box.is_dialogue_active(): if interaction_list.is_showing(): interaction_list._hide() else: 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() # #496: Update gauntlet HUD (room timer + personal bests) if gauntlet_hud and gauntlet_hud.has_method("update_from_state"): gauntlet_hud.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) if _camera_anchored: camera.global_position = GameState.player_position * Constants.TILE_SIZE # Re-enable smoothing after the first anchored frame. The frame that just # rendered used smoothing=OFF (correct viewport from frame one). Now we # turn smoothing back on and sync its internal state so subsequent frames # get smooth camera tracking during gameplay. if _camera_anchored and not camera.position_smoothing_enabled: camera.position_smoothing_enabled = true camera.reset_smoothing() # Send queued input to simulation var inputs = InputMapper.flush_queue() for input in inputs: # #495: F12 WRONG button — client-only, trigger bug report capture if input.action == InputMapper.Action.BUG_REPORT: if bug_report_dialog and not bug_report_dialog.is_active(): bug_report_dialog.start_capture() continue 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 per tick: show monologue text, then clear. # Tick guard prevents re-triggering when the same tick is polled multiple # times (client FPS > sim tick rate). func _consume_monologue() -> void: if GameState.current_monologue == null or not monologue_display: return if GameState.current_tick == _last_monologue_tick: return _last_monologue_tick = GameState.current_tick 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 per tick with ID tracking: show dialogue, then clear. # Tick guard + is_dialogue_active check prevent re-triggering. func _consume_dialogue() -> void: if GameState.current_dialogue == null or not dialogue_box: return if GameState.current_tick == _last_dialogue_tick: return if dialogue_box.is_dialogue_active(): GameState.current_dialogue = null return _last_dialogue_tick = GameState.current_tick var dlg: Dictionary = GameState.current_dialogue _last_dialogue_npc_id = dlg.get("npc_entity_id", -1) dialogue_box.show_dialogue( dlg.get("npc_name", ""), dlg.get("speech", ""), dlg.get("options", []) ) GameState.current_dialogue = null # D-061: Handle dialogue option selection → send to server func _on_dialogue_option_selected(response_id: String, text: String) -> void: SimBridge.send_input({ "action": InputMapper.Action.INTERACT, "timestamp_msec": Time.get_ticks_msec(), "action_data": { "target_entity_id": null, "verb": "DialogueResponse", "response_id": response_id, }, }) # D-063: Handle confrontation beat monologue → show on monologue display (layer 7) func _on_confrontation_monologue(text: String, duration: float) -> void: if monologue_display: monologue_display.show_monologue(text, duration) # D-064: Handle walk-away → send WalkAway{npc_id} 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": _last_dialogue_npc_id if _last_dialogue_npc_id >= 0 else null, "verb": "WalkAway", }, }) # #496: Finalize gauntlet stats on disconnect func _on_connection_state_changed(old_state: SimBridge.ConnectionState, new_state: SimBridge.ConnectionState) -> void: if new_state == SimBridge.ConnectionState.DISCONNECTED and gauntlet_hud: gauntlet_hud.finalize()