feat(ui): implant app hierarchy in HudGroups (D-170)

Implant apps use hierarchical groups: implant/map, implant/wiki,
implant/journal, etc. Apps are mutually exclusive with each other
and with gameplay. Opening implant/map hides gameplay + any other
implant app. Closing returns to gameplay.

API: open_app(), close_app(), toggle_app(), is_app_active().
Star map uses implant/map. Future apps register their own group.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-05 11:04:19 +02:00
co-authored by Claude Opus 4.6
parent f037bcaa81
commit 5ad2452213
3 changed files with 84 additions and 59 deletions
+72 -52
View File
@@ -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:
+3 -3
View File
@@ -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
+9 -4
View File
@@ -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.