diff --git a/client/scripts/autoloads/hud_groups.gd b/client/scripts/autoloads/hud_groups.gd index 782f8231e..2eb6da5ff 100644 --- a/client/scripts/autoloads/hud_groups.gd +++ b/client/scripts/autoloads/hud_groups.gd @@ -1,30 +1,35 @@ 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. +## Nodes register into hierarchical groups. Groups control show/hide as a unit. ## -## 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) +## Hierarchy: +## "gameplay" — stance, minimap, health, prompts, cursor, inventory +## "implant/map" — star map +## "implant/wiki" — GTTR reader +## "implant/journal" — knowledge journal +## "implant/travel" — travel planner +## "implant/station" — station profile +## "implant/cargo" — cargo manifest +## "modal" — settings, bug report, loading (independent layer) +## "debug" — debug overlay, gauntlet HUD (independent layer) ## -## Usage from any node: +## Rules: +## - "gameplay" and any "implant/*" are mutually exclusive. +## Opening an implant app hides gameplay. +## - "implant/*" apps are mutually exclusive with each other. +## Opening implant/map hides implant/wiki. +## - "modal" and "debug" are independent — overlay on anything. +## +## Usage: ## 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 +## HudGroups.register(self, "implant/map") +## HudGroups.open_app("implant/map") # hides gameplay + other implant apps +## HudGroups.close_app() # returns to gameplay +## HudGroups.toggle_app("implant/map") # open if closed, close if open -## 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" +var _groups: Dictionary = {} # group_name -> Array[Control] +var _active_app: String = "" # currently open implant/* app ("" = none) func register(node: Control, group: String) -> void: @@ -32,9 +37,11 @@ func register(node: Control, group: String) -> void: _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) + # Apply current visibility + if group == "gameplay": + node.visible = _active_app.is_empty() + elif group.begins_with("implant/"): + node.visible = (group == _active_app) func unregister(node: Control, group: String) -> void: @@ -42,42 +49,55 @@ func unregister(node: Control, group: String) -> void: _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) +## Open an implant app. Hides gameplay and any other implant app. +func open_app(app: String) -> void: + if not app.begins_with("implant/"): + push_warning("HudGroups.open_app: expected implant/* group, got: " + app) + return + + # Hide previous app if different + if not _active_app.is_empty() and _active_app != app: + _set_group_visible(_active_app, false) + + _active_app = app + _set_group_visible("gameplay", false) + _set_group_visible(app, true) + + +## Close the current implant app. Returns to gameplay. +func close_app() -> void: + if not _active_app.is_empty(): + _set_group_visible(_active_app, false) + _active_app = "" + _set_group_visible("gameplay", true) + + +## Toggle an implant app. If it's open, close it. If another is open, switch. +func toggle_app(app: String) -> void: + if _active_app == app: + close_app() else: - _set_group_visible(group, true) + open_app(app) -## 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) +## Check if a specific app is currently open. +func is_app_active(app: String) -> bool: + return _active_app == app -## 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 any implant app is open. +func is_implant_active() -> bool: + return not _active_app.is_empty() -## 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 +## Get the currently active app name, or "" if none. +func get_active_app() -> String: + return _active_app + + +## Show/hide an independent group (modal, debug). Does not affect gameplay/implant. +func show_group(group: String, vis: bool = true) -> void: + _set_group_visible(group, vis) func _set_group_visible(group: String, vis: bool) -> void: diff --git a/client/ui/star_map.gd b/client/ui/star_map.gd index 53d594920..e7abc2fd1 100644 --- a/client/ui/star_map.gd +++ b/client/ui/star_map.gd @@ -119,7 +119,7 @@ func _ready() -> void: add_child(_info_panel) # D-170: Register with HUD visibility groups - HudGroups.register(self, "implant") + HudGroups.register(self, "implant/map") _load_data() if _data_loaded: @@ -144,9 +144,9 @@ func set_insert_active(active: bool) -> void: ## Toggle visibility via HUD group system (D-170). -## Shows implant group (hides gameplay) or vice versa. +## Opens implant/map (hides gameplay) or closes it (returns to gameplay). func toggle_visible() -> void: - HudGroups.toggle_group("implant") + HudGroups.toggle_app("implant/map") if visible: _dirty = true diff --git a/decisions/architecture.md b/decisions/architecture.md index 43e6d3fe4..67cc0ce74 100644 --- a/decisions/architecture.md +++ b/decisions/architecture.md @@ -626,13 +626,18 @@ Technical foundation decisions that constrain implementation: engine, client-ser - **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:** +- **Groups (hierarchical):** - `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. + - `implant/map` — star map navigator. Mutually exclusive with gameplay and other implant apps. + - `implant/wiki` — GTTR reader. Mutually exclusive with gameplay and other implant apps. + - `implant/journal` — knowledge journal. Mutually exclusive with gameplay and other implant apps. + - `implant/travel` — travel planner. Mutually exclusive with gameplay and other implant apps. + - `implant/station` — station profile. Mutually exclusive with gameplay and other implant apps. + - `implant/cargo` — cargo manifest. Mutually exclusive with gameplay and other implant apps. - `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")` +- **Exclusivity rules:** Opening any `implant/*` app hides gameplay and any other implant app. Closing an app returns to gameplay. Modal and debug are independent layers. +- **API:** `HudGroups.register(node, "implant/map")`, `HudGroups.open_app("implant/map")`, `HudGroups.close_app()`, `HudGroups.toggle_app("implant/map")`, `HudGroups.is_app_active("implant/map")`, `HudGroups.is_implant_active()` - **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.