From f037bcaa81edbf1f39f2e3c772d54e70991f4afd Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 5 Apr 2026 11:01:28 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20HUD=20visibility=20groups=20?= =?UTF-8?q?=E2=80=94=20gameplay=20vs=20implant=20layers=20(D-170)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New HudGroups autoload manages show/hide of related HUD elements as groups. Exclusive groups (gameplay, implant) are mutually exclusive — opening the star map hides stance indicator, minimap, interaction prompts, etc. Closing it restores them. Gameplay nodes registered in main.gd _ready(). Star map registers as implant group and uses toggle_group() instead of direct visibility. Co-Authored-By: Claude Opus 4.6 (1M context) --- client/project.godot | 1 + client/scripts/autoloads/hud_groups.gd | 88 ++++++++++++++++++++++++++ client/scripts/main.gd | 8 +++ client/ui/star_map.gd | 8 ++- decisions/architecture.md | 17 +++++ 5 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 client/scripts/autoloads/hud_groups.gd diff --git a/client/project.godot b/client/project.godot index f04e59ef1..95f636a33 100644 --- a/client/project.godot +++ b/client/project.godot @@ -28,6 +28,7 @@ UIStrings="*res://scripts/autoloads/ui_strings.gd" FogState="*res://scripts/autoloads/fog_state.gd" AudioManager="*res://scripts/autoloads/audio_manager.gd" SessionManager="*res://scripts/autoloads/session_manager.gd" +HudGroups="*res://scripts/autoloads/hud_groups.gd" HardwareDetector="*res://ui/hardware_detector.gd" [audio] diff --git a/client/scripts/autoloads/hud_groups.gd b/client/scripts/autoloads/hud_groups.gd new file mode 100644 index 000000000..782f8231e --- /dev/null +++ b/client/scripts/autoloads/hud_groups.gd @@ -0,0 +1,88 @@ +extends Node +## HUD visibility group manager (D-170). +## +## Nodes register themselves into groups. When a group's visibility changes, +## all registered nodes show/hide together. This replaces per-panel manual +## hide/show of individual HUD elements. +## +## Groups: +## "gameplay" — stance indicator, minimap, health/time, interaction prompts, +## cursor renderer, inventory, news ticker, examine display +## "implant" — star map, travel planner, GTTR reader, station profile +## "modal" — settings, bug report, loading screen, debug console +## "debug" — debug overlay, gauntlet HUD, checklist +## "always" — nodes that never hide (reserved for future use) +## +## Usage from any node: +## HudGroups.register(self, "gameplay") +## HudGroups.show_group("implant") # shows implant, hides gameplay +## HudGroups.show_group("gameplay") # shows gameplay, hides implant +## HudGroups.toggle_group("implant") # toggle between gameplay and implant + +## Which groups are mutually exclusive — showing one hides the others. +## Modal and debug are independent (they overlay on top of anything). +const EXCLUSIVE_GROUPS: Array[String] = ["gameplay", "implant"] + +var _groups: Dictionary = {} # group_name -> Array[Control] +var _active_exclusive: String = "gameplay" + + +func register(node: Control, group: String) -> void: + if not _groups.has(group): + _groups[group] = [] + if node not in _groups[group]: + _groups[group].append(node) + # Apply current visibility state + if group in EXCLUSIVE_GROUPS: + node.visible = (group == _active_exclusive) + + +func unregister(node: Control, group: String) -> void: + if _groups.has(group): + _groups[group].erase(node) + + +## Show a group. If it's exclusive, hide the other exclusive groups. +func show_group(group: String) -> void: + if group in EXCLUSIVE_GROUPS: + _active_exclusive = group + for g: String in EXCLUSIVE_GROUPS: + _set_group_visible(g, g == group) + else: + _set_group_visible(group, true) + + +## Hide a group. If exclusive, falls back to "gameplay". +func hide_group(group: String) -> void: + if group in EXCLUSIVE_GROUPS: + show_group("gameplay") + else: + _set_group_visible(group, false) + + +## Toggle between gameplay and the specified group. +func toggle_group(group: String) -> void: + if _active_exclusive == group: + show_group("gameplay") + else: + show_group(group) + + +## Check if a group is currently active. +func is_group_active(group: String) -> bool: + if group in EXCLUSIVE_GROUPS: + return _active_exclusive == group + # Non-exclusive: check if any member is visible + if _groups.has(group): + for node: Control in _groups[group]: + if is_instance_valid(node) and node.visible: + return true + return false + + +func _set_group_visible(group: String, vis: bool) -> void: + if not _groups.has(group): + return + for node: Control in _groups[group]: + if is_instance_valid(node): + node.visible = vis diff --git a/client/scripts/main.gd b/client/scripts/main.gd index 05c3d8fd9..df1edceda 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -62,6 +62,14 @@ func _ready() -> void: camera.global_position = GameState.player_position * Constants.TILE_SIZE _camera_anchored = true + # D-170: Register HUD nodes into visibility groups + # Gameplay group — hidden when implant panels are open + for node: Control in [hud, minimap, stance_indicator, cursor_renderer, + interaction_prompt, interaction_list, inventory_grid, news_ticker, + examine_display, world_radial]: + if node: + HudGroups.register(node, "gameplay") + # #775: Initialize extracted components _consumers = SnapshotConsumers.new().init({ "monologue_display": monologue_display, diff --git a/client/ui/star_map.gd b/client/ui/star_map.gd index 94aad9aa0..53d594920 100644 --- a/client/ui/star_map.gd +++ b/client/ui/star_map.gd @@ -118,6 +118,9 @@ func _ready() -> void: _info_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(_info_panel) + # D-170: Register with HUD visibility groups + HudGroups.register(self, "implant") + _load_data() if _data_loaded: _compute_layout() @@ -140,9 +143,10 @@ func set_insert_active(active: bool) -> void: visible = false -## Toggle visibility (e.g., from a keybind or button). +## Toggle visibility via HUD group system (D-170). +## Shows implant group (hides gameplay) or vice versa. func toggle_visible() -> void: - visible = not visible + HudGroups.toggle_group("implant") if visible: _dirty = true diff --git a/decisions/architecture.md b/decisions/architecture.md index 569871de7..43e6d3fe4 100644 --- a/decisions/architecture.md +++ b/decisions/architecture.md @@ -622,4 +622,21 @@ Technical foundation decisions that constrain implementation: engine, client-ser --- +### D-170: HUD visibility groups — gameplay vs implant layers +- **Date:** 2026-04-05 +- **Decision:** HUD elements register into named visibility groups via a `HudGroups` autoload. Groups control show/hide of related elements as a unit. Exclusive groups (gameplay, implant) are mutually exclusive — showing one hides the other. Non-exclusive groups (modal, debug) overlay independently. +- **Rationale:** Full-screen implant panels (star map, travel planner, GTTR reader) need to hide gameplay HUD elements (stance, minimap, health, interaction prompts). Without groups, each panel manually hides/shows individual nodes — error-prone and unsustainable as the panel count grows. +- **Groups:** + - `gameplay` — stance indicator, minimap, HUD status, interaction prompts, cursor, inventory, news ticker, examine display. Visible during normal gameplay. + - `implant` — star map, travel planner, GTTR reader, station profile. Fullscreen implant overlays. Showing this hides gameplay. + - `modal` — settings, bug report, loading screen, debug console. Independent — overlays on top of anything. + - `debug` — debug overlay, gauntlet HUD, checklist. Independent. + - `always` — reserved for elements that never hide. +- **API:** `HudGroups.register(node, "gameplay")`, `HudGroups.show_group("implant")`, `HudGroups.toggle_group("implant")`, `HudGroups.is_group_active("implant")` +- **Implementation:** `client/scripts/autoloads/hud_groups.gd` — lightweight autoload, no scene tree manipulation beyond `node.visible`. +- **Raised by:** Jeroen, 2026-04-05 — "Walk" badge visible over fullscreen star map. +- **Dissent:** None. + +--- + *50 decisions. Last updated: 2026-03-24 (D-101 partial supersession noted; D-166 development cascade added)*