Files
settled-reach/client/ui/gauntlet_hud.gd
T
2026-04-05 11:09:10 +02:00

209 lines
5.6 KiB
GDScript

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)