diff --git a/client/scenes/main.tscn b/client/scenes/main.tscn index 5c597f99f..27bc6951a 100644 --- a/client/scenes/main.tscn +++ b/client/scenes/main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=29 format=3 uid="uid://bswrmh7w8dbgm"] +[gd_scene load_steps=28 format=3 uid="uid://bswrmh7w8dbgm"] [ext_resource type="Script" path="res://scripts/main.gd" id="1_main"] [ext_resource type="Script" path="res://scripts/rendering/world_renderer.gd" id="2_world"] @@ -22,7 +22,6 @@ [ext_resource type="PackedScene" path="res://ui/bug_report_dialog.tscn" id="19_bugreport"] [ext_resource type="PackedScene" path="res://ui/settings_dialog.tscn" id="21_settings"] [ext_resource type="Script" path="res://ui/debug_overlay.gd" id="22_debug"] -[ext_resource type="PackedScene" path="res://ui/time_display.tscn" id="23_tdisplay"] [ext_resource type="PackedScene" path="res://ui/examine_display.tscn" id="24_examine"] [ext_resource type="PackedScene" path="res://ui/journal_panel.tscn" id="25_journal"] [ext_resource type="PackedScene" path="res://ui/loading_screen.tscn" id="26_loading"] @@ -124,9 +123,6 @@ zoom = Vector2(2, 2) [node name="InsertOverlay" type="CanvasLayer" parent="."] layer = 10 -; #263: Time display — diegetic insert clock, top-left placeholder (D-013, D-031) -[node name="TimeDisplay" parent="InsertOverlay" instance=ExtResource("23_tdisplay")] - ; InteractionPrompt — v0.1 fallback single-line "E - Talk" display [node name="InteractionPrompt" parent="InsertOverlay" instance=ExtResource("9_prompt")] @@ -148,13 +144,15 @@ layer = 10 ; #264: Journal panel — knowledge graph review, toggle J key, read-only (D-041) [node name="JournalPanel" parent="InsertOverlay" instance=ExtResource("25_journal")] +; HUD — implant-styled status panel (D-169, D-170). On InsertOverlay for bloom per D-051. +[node name="HUD" parent="InsertOverlay" instance=ExtResource("6_hud")] + ; --- UI layer (CanvasLayer 20) --- -; HUD, monologue, cursor — always visible, not affected by fog or camera. +; Monologue, cursor, stance — always visible, not affected by fog or camera. +; HUD moved to InsertOverlay (layer 10) for bloom treatment per D-051. [node name="UILayer" type="CanvasLayer" parent="."] layer = 20 -[node name="HUD" parent="UILayer" instance=ExtResource("6_hud")] - [node name="MonologueDisplay" parent="UILayer" instance=ExtResource("8_monologue")] ; D-053: Stance indicator — top-right, color-coded @@ -170,12 +168,13 @@ layer = 20 [node name="InventoryGrid" parent="UILayer" instance=ExtResource("12_inv")] ; #511: F3 debug overlay — real-time game state, toggled by F3 +; Position: below merged HUD status panel (starts at y:16, ~80px), 10px gap [node name="DebugOverlay" type="Control" parent="UILayer"] anchors_preset = 0 offset_left = 16 -offset_top = 120 +offset_top = 150 offset_right = 400 -offset_bottom = 400 +offset_bottom = 430 mouse_filter = 2 script = ExtResource("22_debug") diff --git a/client/scripts/main.gd b/client/scripts/main.gd index 97dadffce..ee0aaf4b7 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -13,7 +13,7 @@ var _dialogue: DialogueCoordinator # #775: dialogue consumers + signal handlers @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 hud = $InsertOverlay/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 @@ -24,7 +24,6 @@ var _dialogue: DialogueCoordinator # #775: dialogue consumers + signal handlers @onready var cursor_renderer = $UILayer/CursorRenderer # D-056: z-layer 7 @onready var gauntlet_hud = $UILayer/GauntletHUD # #496: room timer + personal bests @onready var checklist_overlay = $UILayer/ChecklistOverlay # #503: auto-checklist progress -@onready var time_display = $InsertOverlay/TimeDisplay # #263: diegetic time display (D-013, D-031) @onready var minimap = $InsertOverlay/Minimap # #151: diegetic minimap overlay (D-013, D-049) @onready var examine_display = $InsertOverlay/ExamineDisplay # #174: examine result overlay @onready var journal_panel = $InsertOverlay/JournalPanel # #264: knowledge journal (D-041) @@ -34,7 +33,7 @@ var _dialogue: DialogueCoordinator # #775: dialogue consumers + signal handlers @onready var loading_screen = $ModalLayer/LoadingScreen # #257: blocking overlay during load @onready var debug_console = $ModalLayer/DebugConsole # #581: tilde debug console @onready var news_ticker = $UILayer/NewsTicker # #592: scrolling headline bar (D-049 z-7) -@onready var star_map = $UILayer/HUD/StarMap # #674: star map insert module (hop-ring view) +@onready var star_map = $InsertOverlay/HUD/StarMap # #674: star map insert module (hop-ring view) func _ready() -> void: @@ -135,8 +134,8 @@ func _ready() -> void: _router.register_always(gauntlet_hud.update_from_state) if checklist_overlay: _router.register_always(checklist_overlay.update_from_state) - if time_display: - _router.register_always(time_display.update_from_state) + if hud: + _router.register_always(hud.update_from_state) if news_ticker: _router.register_always(news_ticker.update_from_state) if journal_panel: diff --git a/client/tests/test_interaction_prompt.gd b/client/tests/test_interaction_prompt.gd index b2d79190e..7e3557b3d 100644 --- a/client/tests/test_interaction_prompt.gd +++ b/client/tests/test_interaction_prompt.gd @@ -222,15 +222,9 @@ func test_interact_with_data_encodes_as_data_variant() -> void: # -- Helpers -- -func _make_prompt() -> PanelContainer: +func _make_prompt() -> Control: var PromptScript = load("res://ui/interaction_prompt.gd") - var panel = PanelContainer.new() - panel.set_script(PromptScript) - var margin = MarginContainer.new() - margin.name = "MarginContainer" - panel.add_child(margin) - var label = Label.new() - label.name = "PromptLabel" - margin.add_child(label) - add_child(panel) - return panel + var ctrl = Control.new() + ctrl.set_script(PromptScript) + add_child(ctrl) # triggers _ready() which creates ImplantPanel + ImplantDataRow + return ctrl diff --git a/client/tests/test_time_display_sprint17.gd b/client/tests/test_time_display_sprint17.gd index bd583cb05..fbd8d1b2b 100644 --- a/client/tests/test_time_display_sprint17.gd +++ b/client/tests/test_time_display_sprint17.gd @@ -5,15 +5,15 @@ ## D-031 (game time: 10 ticks = 1 game-minute, 1440 min/day, HH:MM display) ## D-051 (diegetic insert display) ## -## Implementation: client/ui/time_display.gd — draw-based Control at InsertOverlay/TimeDisplay. +## Implementation: client/ui/time_display.gd — draw-based Control (removed from main scene #786; +## time display merged into HUD ImplantPanel at UILayer/HUD, see test_hud_* tests below). ## Format function: Constants.format_game_time(time_of_day: int) -> String (extracted for -## testability from time_display.gd:46 inline Constants.format_game_time(tod)). +## testability from time_display.gd). ## ## Private state access: Tests read _time_str, _phase_str, _day_str, _has_data directly ## because time_display.gd is draw-based (no Label nodes to inspect). This is an accepted ## test pattern for draw-based UI — the private vars ARE the rendered output contract. -## If the rendering approach changes (e.g. to Label nodes), these tests should switch to -## reading Label.text via public node paths instead. +## Scene integration tests now check UILayer/HUD.get_time_text() instead. class_name TestTimeDisplaySprint17 extends GdUnitTestSuite @@ -273,18 +273,18 @@ func test_sim_bridge_day_phase_is_valid() -> void: # ------------------------------------------------------------------------- -# Scene: InsertClock node at InsertOverlay/TimeDisplay +# Scene: HUD panel at UILayer/HUD (time row merged in, TimeDisplay removed #786) # ------------------------------------------------------------------------- -func test_insert_clock_exists_in_ui_layer() -> void: +func test_hud_panel_exists_in_ui_layer() -> void: var scene := MAIN_SCENE var instance = scene.instantiate() auto_free(instance) add_child(instance) - assert_that(instance.get_node_or_null("InsertOverlay/TimeDisplay")).is_not_null() + assert_that(instance.get_node_or_null("UILayer/HUD")).is_not_null() -func test_insert_clock_time_str_updates_after_process() -> void: +func test_hud_time_row_updates_after_process() -> void: var scene := MAIN_SCENE var instance = scene.instantiate() auto_free(instance) @@ -296,9 +296,9 @@ func test_insert_clock_time_str_updates_after_process() -> void: }) instance._process(0.016) - var clock = instance.get_node_or_null("InsertOverlay/TimeDisplay") - assert_that(clock).is_not_null() - assert_that(clock._time_str).is_equal("12:00") + var hud = instance.get_node_or_null("UILayer/HUD") + assert_that(hud).is_not_null() + assert_that(hud.get_time_text()).is_equal("12:00 · Afternoon · D1") # ------------------------------------------------------------------------- diff --git a/client/ui/hud.gd b/client/ui/hud.gd index f065ddaf5..049caafb9 100644 --- a/client/ui/hud.gd +++ b/client/ui/hud.gd @@ -1,9 +1,10 @@ extends Control ## HUD — implant-styled status panel (D-169, D-170). -## Displays health and perception mode. Time is handled by TimeDisplay. +## Merged panel: Time, Health, Perception mode. Time row replaces standalone TimeDisplay (#786). var _panel: ImplantPanel +var _time_row: ImplantDataRow var _health_row: ImplantDataRow var _perception_row: ImplantDataRow @@ -22,13 +23,14 @@ func _ready() -> void: _panel.custom_minimum_size.x = 200.0 _panel.size.x = 200.0 _panel.mouse_filter = Control.MOUSE_FILTER_IGNORE - _panel.position = Vector2(16, 80) # below TimeDisplay (occupies 16-70) + _panel.position = Vector2(16, 16) # top-left; replaces standalone TimeDisplay add_child(_panel) - _panel.move_child(_panel.get_node("Content"), 0) + _time_row = ImplantDataRow.new("--:--") _health_row = ImplantDataRow.new("Health: 100") _perception_row = ImplantDataRow.new("Mode: Baseline") + _panel.add_component(_time_row) _panel.add_component(_health_row) _panel.add_component(_perception_row) @@ -46,3 +48,18 @@ func update_from_hud_data(data: Dictionary) -> void: func update_health(health: int) -> void: _health_row.text = UIStrings.get_text("hud.health") + ": " + str(health) + + +func update_from_state() -> void: + var gt: Dictionary = GameState.game_time + if gt.is_empty(): + return + var tod: int = int(gt.get("time_of_day", 0)) + var day: int = int(gt.get("day", 0)) + var phase: String = str(gt.get("day_phase", "")) + _time_row.text = "%s · %s · D%d" % [Constants.format_game_time(tod), phase, day + 1] + + +## Returns the time row text. Used by tests after standalone TimeDisplay was removed. +func get_time_text() -> String: + return _time_row.text if _time_row else "--:--" diff --git a/client/ui/hud.tscn b/client/ui/hud.tscn index f0be6d347..76a892348 100644 --- a/client/ui/hud.tscn +++ b/client/ui/hud.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=3 format=3 uid="uid://cq1y5w3hmxr8b"] +[gd_scene load_steps=2 format=3 uid="uid://cq1y5w3hmxr8b"] [ext_resource type="Script" path="res://ui/hud.gd" id="1_hud"] [ext_resource type="PackedScene" path="res://ui/star_map.tscn" id="2_starmap"] @@ -13,32 +13,9 @@ grow_vertical = 2 mouse_filter = 2 script = ExtResource("1_hud") -[node name="MarginContainer" type="MarginContainer" parent="."] -layout_mode = 1 -anchors_preset = 10 -anchor_right = 1.0 -offset_bottom = 100.0 -grow_horizontal = 2 -theme_override_constants/margin_left = 16 -theme_override_constants/margin_top = 16 -theme_override_constants/margin_right = 16 - -[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] -layout_mode = 2 - -[node name="HealthLabel" type="Label" parent="MarginContainer/VBoxContainer"] -layout_mode = 2 -text = "Health: 100" - -[node name="PerceptionLabel" type="Label" parent="MarginContainer/VBoxContainer"] -layout_mode = 2 -text = "Mode: Baseline" - -[node name="TimeLabel" type="Label" parent="MarginContainer/VBoxContainer"] -layout_mode = 2 -text = "Time: 08:00" - -; #674: Star map insert — D-170 z-index layer managed, not visibility toggled +; #674: Star map — full-screen insert overlay, hidden until player activates (M key / insert UI). +; Toggled via star_map.toggle_visible() from main.gd. +; TODO: migrate to HudGroups.open_app("implant/map/starchart") per D-170. [node name="StarMap" parent="." instance=ExtResource("2_starmap")] visible = false diff --git a/client/ui/interaction_prompt.gd b/client/ui/interaction_prompt.gd index 7ef4fdf21..dcecf9470 100644 --- a/client/ui/interaction_prompt.gd +++ b/client/ui/interaction_prompt.gd @@ -1,4 +1,4 @@ -extends PanelContainer +extends Control # Interaction prompt — displays available interaction verb for nearby entity. # Server-driven: shows when GameState.nearby_interactions is non-empty, @@ -17,24 +17,28 @@ var _current_target_id: int = -1 # OQ-07 (#522): when false, prompt is suppressed (z-layer 6 insert overlay only) var _insert_active: bool = true -@onready var prompt_label: Label = $MarginContainer/PromptLabel +var _panel: ImplantPanel +var _row: ImplantDataRow func _ready() -> void: modulate.a = 0.0 visible = false _is_showing = false - # D-169: Apply implant theme - var t := load("res://ui/implant/default_implant.tres") as ImplantTheme - if t: - var style := StyleBoxFlat.new() - style.bg_color = t.panel_bg - style.border_color = t.separator - style.set_border_width_all(int(t.border_width)) - style.set_content_margin_all(t.padding) - add_theme_stylebox_override("panel", style) - prompt_label.add_theme_color_override("font_color", t.text_primary) - prompt_label.add_theme_font_size_override("font_size", t.font_body) + mouse_filter = Control.MOUSE_FILTER_IGNORE + + var theme_res := load("res://ui/implant/default_implant.tres") as ImplantTheme + + _panel = ImplantPanel.new() + _panel.name = "PromptPanel" + _panel.theme_resource = theme_res + _panel.mouse_filter = Control.MOUSE_FILTER_IGNORE + _panel.set_anchors_preset(Control.PRESET_FULL_RECT) + add_child(_panel) + + _row = ImplantDataRow.new("", theme_res.text_primary if theme_res else Color.WHITE) + _row.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _panel.add_component(_row) func _process(_delta: float) -> void: @@ -63,8 +67,8 @@ func _show_prompt(interaction: Dictionary) -> void: var verb_label: String = verbs[0].get("label", "") var display_text: String = "E - %s" % verb_label - if _current_target_id != target_id or prompt_label.text != display_text: - prompt_label.text = display_text + if _current_target_id != target_id or _row.text != display_text: + _row.set_content(display_text) _current_target_id = target_id if not _is_showing: diff --git a/client/ui/interaction_prompt.tscn b/client/ui/interaction_prompt.tscn index 6f540c9d6..9883a168b 100644 --- a/client/ui/interaction_prompt.tscn +++ b/client/ui/interaction_prompt.tscn @@ -2,28 +2,17 @@ [ext_resource type="Script" path="res://ui/interaction_prompt.gd" id="1_prompt"] -[node name="InteractionPrompt" type="PanelContainer"] +; Interaction prompt — implant-styled (D-169), positioned above DialogueBox, 16px gap +[node name="InteractionPrompt" type="Control"] anchors_preset = 7 anchor_top = 1.0 anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 200.0 -offset_top = -240.0 +offset_top = -264.0 offset_right = -200.0 -offset_bottom = -210.0 +offset_bottom = -222.0 grow_horizontal = 2 grow_vertical = 0 mouse_filter = 2 script = ExtResource("1_prompt") - -[node name="MarginContainer" type="MarginContainer" parent="."] -layout_mode = 2 -theme_override_constants/margin_left = 16 -theme_override_constants/margin_top = 8 -theme_override_constants/margin_right = 16 -theme_override_constants/margin_bottom = 8 - -[node name="PromptLabel" type="Label" parent="MarginContainer"] -layout_mode = 2 -horizontal_alignment = 1 -text = ""