feat(client): add gauntlet room timer + personal bests (#496)

GauntletHUD in UILayer shows TIMER: MM:SS (PB: MM:SS). Timer starts on
room entry, resets on room change, records personal bests to
user://gauntlet-stats.json. Session summary printed on disconnect.
Hidden in non-gauntlet mode (anti-tedium guard).

Adds room_id and gauntlet_mode fields to GameState, parsed from
ObserverSnapshot. Main.gd wires update_from_state() and finalize().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 10:53:05 +01:00
co-authored by Claude Opus 4.6
parent 318a1e8b07
commit bc073c891f
5 changed files with 241 additions and 2 deletions
+14
View File
@@ -36,6 +36,10 @@ var current_dialogue: Variant = null # {npc_name, npc_entity_id, speech, option
# InputMapper suppresses movement when this is true.
var dialogue_active: bool = false
# v8 fields (#496): Gauntlet mode — room timer + personal bests
var room_id: Variant = null # String room_id from snapshot, null in non-gauntlet mode
var gauntlet_mode: bool = false # true when snapshot includes gauntlet_mode flag
# v7 fields (#431, D-059/D-060)
var pending_recognitions: Array = [] # [{entity_id, x, y, z, remaining_ticks, total_delay_ticks}]
@@ -118,6 +122,16 @@ func apply_snapshot(snapshot: Dictionary) -> void:
else:
pending_recognitions = []
# v8: gauntlet mode (#496) — room_id and gauntlet_mode
if snapshot.has("gauntlet_mode") and snapshot.gauntlet_mode == true:
gauntlet_mode = true
else:
gauntlet_mode = false
if snapshot.has("room_id") and snapshot.room_id is String:
room_id = snapshot.room_id
else:
room_id = null
# 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:
+21
View File
@@ -12,6 +12,8 @@ extends Node2D
@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
@@ -49,6 +51,10 @@ func _ready() -> void:
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
@@ -88,6 +94,10 @@ func _process(_delta: float) -> void:
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()
@@ -109,6 +119,11 @@ func _process(_delta: float) -> void:
# 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
@@ -197,3 +212,9 @@ func _on_dialogue_dismissed() -> void:
"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()