From ef3f4a70fa77efce9783774bfe73981cf26bd391 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Fri, 20 Feb 2026 20:10:30 +0100 Subject: [PATCH] =?UTF-8?q?feat(ui):=20F3=20debug=20overlay=20=E2=80=94=20?= =?UTF-8?q?real-time=20game=20state=20display=20(#511)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toggle with F3. Shows tick, fps, position, facing, stance, zone, entity/tile counts, interaction/recognition counts, dialogue status, stationary ticks, insert state, time/tick_rate, mode, gauntlet. Two-column layout, click-through, hidden by default. Co-Authored-By: Claude Opus 4.6 --- client/project.godot | 5 ++ client/scripts/ui/debug_overlay.gd | 115 +++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 client/scripts/ui/debug_overlay.gd diff --git a/client/project.godot b/client/project.godot index d87dd633c..8aa99cf2f 100644 --- a/client/project.godot +++ b/client/project.godot @@ -120,6 +120,11 @@ bug_report={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194343,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } +debug_overlay={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194334,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} teleport_hub={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194317,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) diff --git a/client/scripts/ui/debug_overlay.gd b/client/scripts/ui/debug_overlay.gd new file mode 100644 index 000000000..0586a6399 --- /dev/null +++ b/client/scripts/ui/debug_overlay.gd @@ -0,0 +1,115 @@ +extends Control +# #511: F3 debug overlay — real-time game state display for dev use. + +const HEADER_COLOR := Color("#e8c547") +const LABEL_COLOR := Color("#8890a0") +const VALUE_COLOR := Color("#c8d0e0") +const BG_COLOR := Color(0.08, 0.08, 0.12, 0.85) +const FONT_SIZE := 12 +const LINE_HEIGHT := 16 +const PADDING := Vector2(10, 8) +const COL_GAP := 16 # gap between left and right columns + +var _cached_font: Font = null + +func _ready() -> void: + visible = false + _cached_font = ThemeDB.fallback_font + +func _unhandled_input(event: InputEvent) -> void: + if event.is_action_pressed("debug_overlay"): + visible = not visible + if visible: + queue_redraw() + +func update_from_state() -> void: + if not visible: + return + queue_redraw() + +func _draw() -> void: + if not visible: + return + var font: Font = _cached_font if _cached_font else ThemeDB.fallback_font + + # Build lines as [label, value, label, value] pairs (two columns) + var left_lines: Array = [] + var right_lines: Array = [] + + left_lines.append(["tick", str(GameState.current_tick)]) + right_lines.append(["fps", str(Engine.get_frames_per_second())]) + + var pos := GameState.player_position + left_lines.append(["pos", "(%d, %d)" % [int(pos.x), int(pos.y)]]) + right_lines.append(["facing", GameState.player_facing]) + + left_lines.append(["stance", GameState.player_stance]) + right_lines.append(["zone", GameState.current_zone_id if GameState.current_zone_id != "" else "-"]) + + left_lines.append(["entities", str(GameState.visible_entities.size())]) + right_lines.append(["tiles", str(GameState.visible_tiles.size())]) + + left_lines.append(["interactions", str(GameState.nearby_interactions.size())]) + right_lines.append(["recognitions", str(GameState.pending_recognitions.size())]) + + var mono_status := "active" if GameState.current_monologue != null else "idle" + var dlg_status := "active" if GameState.dialogue_active else "idle" + left_lines.append(["monologue", mono_status]) + right_lines.append(["dialogue", dlg_status]) + + left_lines.append(["stationary", str(GameState.stationary_ticks)]) + right_lines.append(["insert", "ON" if GameState.insert_active else "OFF"]) + + var gt := GameState.game_time + var time_str := "%s d%s" % [gt.get("day_phase", "-"), str(gt.get("day", "-"))] if gt.size() > 0 else "-" + var rate_str: String = gt.get("tick_rate", "-") if gt.size() > 0 else "-" + left_lines.append(["time", time_str]) + right_lines.append(["tick_rate", rate_str]) + + var mode_str := "test" if SimBridge.test_mode else "live" + var gauntlet_str := "ON" if GameState.gauntlet_mode else "OFF" + left_lines.append(["mode", mode_str]) + right_lines.append(["gauntlet", gauntlet_str]) + + # Measure column widths + var left_label_w: float = 0.0 + var left_value_w: float = 0.0 + var right_label_w: float = 0.0 + var right_value_w: float = 0.0 + + for line in left_lines: + left_label_w = max(left_label_w, font.get_string_size(line[0] + ": ", HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x) + left_value_w = max(left_value_w, font.get_string_size(line[1], HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x) + for line in right_lines: + right_label_w = max(right_label_w, font.get_string_size(line[0] + ": ", HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x) + right_value_w = max(right_value_w, font.get_string_size(line[1], HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x) + + var header_text := "F3 DEBUG" + var header_w := font.get_string_size(header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE + 1).x + var content_w := left_label_w + left_value_w + COL_GAP + right_label_w + right_value_w + var box_w: float = max(header_w, content_w) + PADDING.x * 2 + var line_count := max(left_lines.size(), right_lines.size()) + var box_h: float = PADDING.y * 2 + LINE_HEIGHT + LINE_HEIGHT * line_count # header + data lines + + # Background + draw_rect(Rect2(Vector2.ZERO, Vector2(box_w, box_h)), BG_COLOR) + + # Header + var y: float = PADDING.y + FONT_SIZE + draw_string(font, Vector2(PADDING.x, y), header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE + 1, HEADER_COLOR) + y += LINE_HEIGHT + + # Data lines (two columns) + var right_x: float = PADDING.x + left_label_w + left_value_w + COL_GAP + for i in range(line_count): + if i < left_lines.size(): + var lbl: String = left_lines[i][0] + ": " + var val: String = left_lines[i][1] + draw_string(font, Vector2(PADDING.x, y), lbl, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, LABEL_COLOR) + draw_string(font, Vector2(PADDING.x + left_label_w, y), val, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, VALUE_COLOR) + if i < right_lines.size(): + var lbl: String = right_lines[i][0] + ": " + var val: String = right_lines[i][1] + draw_string(font, Vector2(right_x, y), lbl, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, LABEL_COLOR) + draw_string(font, Vector2(right_x + right_label_w, y), val, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, VALUE_COLOR) + y += LINE_HEIGHT