extends Control ## #496: Gauntlet HUD — room timer + personal bests. ## Shows TIMER: MM:SS (PB: MM:SS) in top-right, below StanceIndicator. ## Hidden in non-gauntlet mode. Stats persisted to user://dev/gauntlet-stats.json. const BG_COLOR := Color(0.05, 0.05, 0.08, 0.5) const TIMER_COLOR := Color("#c8d0e0") # Default insert text const PB_COLOR := Color("#6bc9a6") # Friendly green — personal best const NEW_PB_COLOR := Color("#e8c547") # Amber flash on new PB const FONT_SIZE := 13 const PADDING := Vector2(10, 6) const STATS_PATH := "user://dev/gauntlet-stats.json" var _timer_seconds: float = 0.0 var _timer_running: bool = false var _current_room_id: Variant = null var _personal_bests: Dictionary = {} # room_id -> float (seconds) var _session_rooms: Dictionary = {} # room_id -> {attempts: int, best: float} var _new_pb_flash: float = 0.0 # Countdown for PB flash effect func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE visible = false _load_stats() func _process(delta: float) -> void: if not visible: return if _timer_running: _timer_seconds += delta queue_redraw() if _new_pb_flash > 0.0: _new_pb_flash -= delta queue_redraw() func update_from_state() -> void: if not GameState.gauntlet_mode: if visible: visible = false return if not visible: visible = true queue_redraw() var new_room_id: Variant = GameState.room_id if new_room_id == null: # Gauntlet mode but no room yet — stop timer, clear tracked room # so re-entry to the same room after null triggers a restart. _timer_running = false _current_room_id = null return if new_room_id != _current_room_id: _on_room_change(str(new_room_id)) func _on_room_change(new_room_id: String) -> void: # Record completion of previous room if _current_room_id != null and _timer_running: _record_room_completion(_current_room_id, _timer_seconds) # Start timer for new room _current_room_id = new_room_id _timer_seconds = 0.0 _timer_running = true # Track session stats if not _session_rooms.has(new_room_id): _session_rooms[new_room_id] = {"attempts": 0, "best": INF} _session_rooms[new_room_id]["attempts"] += 1 queue_redraw() func _record_room_completion(completed_room_id: String, seconds: float) -> void: var old_pb: float = _personal_bests.get(completed_room_id, INF) if seconds < old_pb: _personal_bests[completed_room_id] = seconds _new_pb_flash = 2.0 # Flash for 2 seconds _save_stats() # Update session tracking if _session_rooms.has(completed_room_id): var entry: Dictionary = _session_rooms[completed_room_id] if seconds < entry["best"]: entry["best"] = seconds func _draw() -> void: var font := ThemeDB.fallback_font var timer_text := "TIMER: " + _format_time(_timer_seconds) var pb_text := "" if _current_room_id != null and _personal_bests.has(_current_room_id): pb_text = " (PB: " + _format_time(_personal_bests[_current_room_id]) + ")" var full_text := timer_text + pb_text var text_size := font.get_string_size(full_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE) var box_size := text_size + PADDING * 2 # Background draw_rect(Rect2(Vector2.ZERO, box_size), BG_COLOR) # Timer text var y_offset := PADDING.y + text_size.y var timer_size := font.get_string_size(timer_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE) draw_string( font, Vector2(PADDING.x, y_offset), timer_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, TIMER_COLOR ) # PB text (different color) if not pb_text.is_empty(): var pb_color: Color = NEW_PB_COLOR if _new_pb_flash > 0.0 else PB_COLOR draw_string( font, Vector2(PADDING.x + timer_size.x, y_offset), pb_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, pb_color ) static func _format_time(seconds: float) -> String: var total_secs := int(seconds) var mins := total_secs / 60 var secs := total_secs % 60 return "%02d:%02d" % [mins, secs] func _load_stats() -> void: if not FileAccess.file_exists(STATS_PATH): return var file := FileAccess.open(STATS_PATH, FileAccess.READ) if file == null: return var json := JSON.new() var err := json.parse(file.get_as_text()) file.close() if err == OK and json.data is Dictionary: _personal_bests = json.data func _save_stats() -> void: var file := FileAccess.open(STATS_PATH, FileAccess.WRITE) if file == null: push_warning("GauntletHUD: cannot write stats to %s" % STATS_PATH) return file.store_string(JSON.stringify(_personal_bests, "\t")) file.close() func print_session_summary() -> void: if _session_rooms.is_empty(): return print("=== Gauntlet Session Summary ===") for room_id in _session_rooms: var entry: Dictionary = _session_rooms[room_id] var best_str := _format_time(entry["best"]) if entry["best"] != INF else "--:--" var pb_str := ( _format_time(_personal_bests[room_id]) if _personal_bests.has(room_id) else "--:--" ) print( ( " Room %s: %d attempts, session best %s, all-time PB %s" % [room_id, entry["attempts"], best_str, pb_str] ) ) print("================================") # Record current room if timer is running (called on disconnect) func finalize() -> void: if _current_room_id != null and _timer_running: _record_room_completion(_current_room_id, _timer_seconds) _timer_running = false print_session_summary() # -- Public API --------------------------------------------------------------- func get_timer_seconds() -> float: return _timer_seconds func is_timer_running() -> bool: return _timer_running func get_current_room_id() -> Variant: return _current_room_id func get_personal_best(for_room_id: String) -> float: return _personal_bests.get(for_room_id, INF)