diff --git a/CHANGELOG.md b/CHANGELOG.md index adcbaaf3e..a562f0cd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). - Workshop archives: #796 (economics design, 3 rounds) and #801 (commodity catalog, 3 rounds + validation) ### Changed +- HUD status panel: merged TimeDisplay into ImplantPanel — time, health, perception in one themed panel (#786) +- HUD moved from UILayer (20) to InsertOverlay (10) for bloom treatment per D-051 +- Interaction prompt: converted from PanelContainer+StyleBoxFlat to ImplantPanel+ImplantDataRow (#788) +- Minimap: added ImplantTheme-styled container frame behind circular display (#787) - Amended 7 stale D-records with supersession notes (D-012, D-035, D-044, D-061, D-086, D-090, D-119) - Closed 4 resolved scope questions (Q-002, Q-004, Q-005, Q-007) - Fixed questions.md index (was 61 questions, actually 96 — 33 architecture Qs missing from index) @@ -29,6 +33,7 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). - `worktree-update` skill template from whatsinagame kit ### Fixed +- HUD layout overlaps: DebugOverlay positioned below merged status panel, GauntletHUD moved below StanceIndicator, StanceIndicator sized for ImplantPanel content (#792) - D-061 dialogue max-width: corrected from 640px to 1200px (per D-076) - DEVOPS.md and stig briefing: gdlint no longer "pending setup" - `{team_scope_dirs}` dangling placeholder in sprint-start resolved diff --git a/client/scenes/main.tscn b/client/scenes/main.tscn index 5c597f99f..f7e9ec716 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 per D-049 (z-layer 6). +[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) per D-049 (insert z-layer 6). [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: y:150 — below merged HUD status panel (y:16, ~80px tall), with 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..6d2162045 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 InsertOverlay/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 InsertOverlay/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 InsertOverlay/HUD (time row merged in, TimeDisplay removed #786) # ------------------------------------------------------------------------- -func test_insert_clock_exists_in_ui_layer() -> void: +func test_hud_panel_exists_in_insert_overlay() -> 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("InsertOverlay/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("InsertOverlay/HUD") + assert_that(hud).is_not_null() + assert_that(hud.get_time_text()).is_equal("12:00 · Afternoon · D1") # ------------------------------------------------------------------------- diff --git a/client/ui/debug_overlay.gd b/client/ui/debug_overlay.gd index bd78d9119..accde3632 100644 --- a/client/ui/debug_overlay.gd +++ b/client/ui/debug_overlay.gd @@ -76,7 +76,7 @@ var _tick_deltas: Array = [] # ms between consecutive snapshots func _ready() -> void: _dev_mode = OS.is_debug_build() visible = false - position.y = 150.0 # Below TimeDisplay (16-70) + HUD status panel (80-140) + # Position set in main.tscn: below merged HUD status panel (starts at y:16) _cached_font = ThemeDB.fallback_font # Clear NPC path history on session change to prevent entity ID collisions GameState.connect("game_id_changed", _on_game_id_changed) diff --git a/client/ui/gauntlet_hud.tscn b/client/ui/gauntlet_hud.tscn index bd90467e8..da3e169da 100644 --- a/client/ui/gauntlet_hud.tscn +++ b/client/ui/gauntlet_hud.tscn @@ -2,16 +2,16 @@ [ext_resource type="Script" path="res://ui/gauntlet_hud.gd" id="1_gauntlet"] -; #496: Gauntlet HUD — room timer + personal bests, top-right below StanceIndicator +; #496: Gauntlet HUD — room timer + personal bests, top-right below StanceIndicator (bottom at y:244) [node name="GauntletHUD" type="Control"] layout_mode = 3 anchors_preset = 1 anchor_left = 1.0 anchor_right = 1.0 offset_left = -160.0 -offset_top = 48.0 +offset_top = 252.0 offset_right = -16.0 -offset_bottom = 74.0 +offset_bottom = 278.0 grow_horizontal = 0 mouse_filter = 2 script = ExtResource("1_gauntlet") diff --git a/client/ui/hud.gd b/client/ui/hud.gd index f065ddaf5..076be411f 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 @@ -20,22 +21,22 @@ func _ready() -> void: _panel.name = "StatusPanel" _panel.theme_resource = theme_res _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) - print("HUD: Initialized") - func update_from_hud_data(data: Dictionary) -> void: + if not _perception_row: + return if data.has("perception_mode"): var prefix := UIStrings.get_text("hud.perception_mode_prefix") if prefix.is_empty(): @@ -45,4 +46,23 @@ func update_from_hud_data(data: Dictionary) -> void: func update_health(health: int) -> void: + if not _health_row: + return _health_row.text = UIStrings.get_text("hud.health") + ": " + str(health) + + +func update_from_state() -> void: + if not _time_row: + return + 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 = "" diff --git a/client/ui/minimap.gd b/client/ui/minimap.gd index 35f45f7c6..21e40bab7 100644 --- a/client/ui/minimap.gd +++ b/client/ui/minimap.gd @@ -35,15 +35,43 @@ const COLOR_CARDINAL: Color = Color(0.784, 0.816, 0.878, 0.4) # Dimmer E/S/W ti const COLOR_PLAYER: Color = Constants.ENTITY_COLOR_PLAYER var _insert_active: bool = true +# D-169: StyleBoxFlat matching ImplantPanel aesthetic — drawn as container frame behind the circle. +var _container_style: StyleBoxFlat = null + +# Dirty flag — only queue_redraw() when player position or POI list changes. +var _dirty: bool = true +var _last_player_pos: Vector2 = Vector2(INF, INF) +var _last_poi_count: int = -1 func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE set_process(true) + _init_container_style() + + +func _init_container_style() -> void: + var t := load("res://ui/implant/default_implant.tres") as ImplantTheme + if not t: + return + _container_style = StyleBoxFlat.new() + _container_style.bg_color = t.panel_bg + _container_style.border_color = t.separator + _container_style.set_border_width_all(int(t.border_width)) + _container_style.set_content_margin_all(0) func _process(_delta: float) -> void: - if _insert_active: + if not _insert_active: + return + var pos := GameState.player_position + var poi_count := GameState.discovered_pois.size() + if pos != _last_player_pos or poi_count != _last_poi_count: + _last_player_pos = pos + _last_poi_count = poi_count + _dirty = true + if _dirty: + _dirty = false queue_redraw() @@ -52,9 +80,16 @@ func _process(_delta: float) -> void: func set_insert_active(active: bool) -> void: _insert_active = active visible = active + if active: + _dirty = true func _draw() -> void: + # D-169: ImplantPanel-style container frame behind the circular minimap. + # Gives the minimap the same panel border aesthetic as other implant components. + if _container_style: + _container_style.draw(get_canvas_item(), Rect2(Vector2.ZERO, get_rect().size)) + var sz: Vector2 = get_rect().size var center := sz / 2.0 # Outer radius: fill control with 1px edge padding diff --git a/client/ui/stance_indicator.tscn b/client/ui/stance_indicator.tscn index 2b3ddf454..bc14f6abb 100644 --- a/client/ui/stance_indicator.tscn +++ b/client/ui/stance_indicator.tscn @@ -2,16 +2,17 @@ [ext_resource type="Script" path="res://ui/stance_indicator.gd" id="1_stance"] -; D-053: Stance indicator — below minimap, right-aligned +; D-053: Stance indicator — below minimap (bottom at y:176), 24px gap. +; Control height 44px to contain ImplantPanel (padding 12+12 + row 18 = 42px minimum). [node name="StanceIndicator" type="Control"] layout_mode = 3 anchors_preset = 1 anchor_left = 1.0 anchor_right = 1.0 offset_left = -100.0 -offset_top = 192.0 +offset_top = 200.0 offset_right = -16.0 -offset_bottom = 218.0 +offset_bottom = 244.0 grow_horizontal = 0 mouse_filter = 2 script = ExtResource("1_stance")