diff --git a/client/scripts/autoloads/hud_groups.gd b/client/scripts/autoloads/hud_groups.gd index 87d548b81..9144c02f5 100644 --- a/client/scripts/autoloads/hud_groups.gd +++ b/client/scripts/autoloads/hud_groups.gd @@ -1,83 +1,110 @@ extends Node -## HUD visibility group manager (D-170). +## HUD layer manager (D-170). ## -## Nodes register into hierarchical groups. Groups control show/hide as a unit. +## Controls z-ordering and layout mode for HUD elements. Nothing gets hidden — +## fullscreen apps render on top of gameplay, insert-mode apps share the screen. ## -## 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) +## Modes: +## MODE_GAMEPLAY — default z-level, normal layout +## MODE_INSERT — small overlay alongside gameplay (e.g. minimap-sized star map) +## MODE_FULLSCREEN — takes over the screen, gameplay renders behind ## -## 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. +## Groups use hierarchical paths: +## "gameplay" — HUD status, minimap, prompts, stance +## "implant/map/starchart" — star map navigator +## "implant/wiki/gttr" — Drifter's Guide reader +## "implant/journal" — knowledge journal ## ## Usage: -## HudGroups.register(self, "gameplay") -## 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 +## HudGroups.register(self, "implant/map/starchart") +## HudGroups.open_app("implant/map/starchart") # fullscreen by default +## HudGroups.open_app("implant/map/starchart", HudGroups.MODE_INSERT) +## HudGroups.close_app() +## HudGroups.toggle_app("implant/map/starchart") -var _groups: Dictionary = {} # group_name -> Array[Control] -var _active_app: String = "" # currently open implant/* app ("" = none) +enum Mode { GAMEPLAY, INSERT, FULLSCREEN } + +## Z-index values for each mode. Gameplay is the base layer. +const Z_GAMEPLAY: int = 0 +const Z_INSERT: int = 10 +const Z_FULLSCREEN: int = 20 +const Z_MODAL: int = 30 + +## Emitted when an app opens, closes, or changes mode. +## Listeners can use this to adapt their layout (e.g. resize to fullscreen). +signal app_changed(app_path: String, mode: Mode) + +## Emitted when gameplay rendering should pause or resume. +## Fullscreen implant apps occlude gameplay — expensive renderers (world, +## fog, entities) should skip draw calls while this is true. +## Connect from any renderer: HudGroups.gameplay_occluded.connect(_on_occluded) +signal gameplay_occluded(occluded: bool) + +var _groups: Dictionary = {} # group_path -> Array[CanvasItem] +var _active_app: String = "" # currently focused implant app ("" = none) +var _active_mode: Mode = Mode.GAMEPLAY -func register(node: Control, group: String) -> void: +func register(node: CanvasItem, group: String) -> void: if not _groups.has(group): _groups[group] = [] if node not in _groups[group]: _groups[group].append(node) - # Apply current visibility - if group == "gameplay": - node.visible = _active_app.is_empty() - elif group.begins_with("implant/"): - node.visible = (group == _active_app) + # Apply current z-level + _apply_z(node, group) -func unregister(node: Control, group: String) -> void: +func unregister(node: CanvasItem, group: String) -> void: if _groups.has(group): _groups[group].erase(node) -## 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 +## Open an implant app in fullscreen (default) or insert mode. +func open_app(app: String, mode: Mode = Mode.FULLSCREEN) -> void: + # Close previous app if different if not _active_app.is_empty() and _active_app != app: - _set_group_visible(_active_app, false) + _set_group_z(_active_app, Z_GAMEPLAY) + app_changed.emit(_active_app, Mode.GAMEPLAY) _active_app = app - _set_group_visible("gameplay", false) - _set_group_visible(app, true) + _active_mode = mode + + # Set z-levels based on mode + var was_occluded := _active_mode == Mode.FULLSCREEN and not _active_app.is_empty() + if mode == Mode.FULLSCREEN: + _set_group_z("gameplay", Z_GAMEPLAY) + _set_group_z(app, Z_FULLSCREEN) + elif mode == Mode.INSERT: + _set_group_z("gameplay", Z_GAMEPLAY) + _set_group_z(app, Z_INSERT) + + app_changed.emit(app, mode) + + # Notify renderers about occlusion state change + var now_occluded := mode == Mode.FULLSCREEN + if now_occluded != was_occluded: + gameplay_occluded.emit(now_occluded) -## Close the current implant app. Returns to gameplay. +## Close the current app. Returns everything to gameplay z-level. func close_app() -> void: + var was_fullscreen := _active_mode == Mode.FULLSCREEN and not _active_app.is_empty() if not _active_app.is_empty(): - _set_group_visible(_active_app, false) + _set_group_z(_active_app, Z_GAMEPLAY) + app_changed.emit(_active_app, Mode.GAMEPLAY) _active_app = "" - _set_group_visible("gameplay", true) + _active_mode = Mode.GAMEPLAY + _set_group_z("gameplay", Z_GAMEPLAY) + if was_fullscreen: + gameplay_occluded.emit(false) -## Toggle an implant app. If it's open, close it. If another is open, switch. -func toggle_app(app: String) -> void: +## Toggle an app. If open, close it. If closed or different app, open it. +func toggle_app(app: String, mode: Mode = Mode.FULLSCREEN) -> void: if _active_app == app: close_app() else: - open_app(app) + open_app(app, mode) ## Check if a specific app is currently open. @@ -90,19 +117,31 @@ func is_implant_active() -> bool: return not _active_app.is_empty() -## Get the currently active app name, or "" if none. +## Get the currently active app path, 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) +## Get the current mode of the active app. +func get_active_mode() -> Mode: + return _active_mode -func _set_group_visible(group: String, vis: bool) -> void: +func _apply_z(node: CanvasItem, group: String) -> void: + if group == "gameplay": + node.z_index = Z_GAMEPLAY + elif group.begins_with("implant/"): + if group == _active_app: + node.z_index = Z_FULLSCREEN if _active_mode == Mode.FULLSCREEN else Z_INSERT + else: + node.z_index = Z_GAMEPLAY + elif group == "modal": + node.z_index = Z_MODAL + + +func _set_group_z(group: String, z: int) -> void: if not _groups.has(group): return - for node: Control in _groups[group]: + for node: CanvasItem in _groups[group]: if is_instance_valid(node): - node.visible = vis + node.z_index = z diff --git a/client/scripts/main.gd b/client/scripts/main.gd index 968fcd2ab..4aa4c5f85 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -62,20 +62,13 @@ 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 + # D-170: Register HUD nodes into layer groups for node in [ - hud, - minimap, - stance_indicator, - interaction_prompt, - interaction_list, - inventory_grid, - news_ticker, - examine_display, - world_radial + hud, minimap, stance_indicator, interaction_prompt, + interaction_list, inventory_grid, news_ticker, + examine_display, world_radial, cursor_renderer, ]: - if node and node is Control: + if node and node is CanvasItem: HudGroups.register(node, "gameplay") # #775: Initialize extracted components diff --git a/client/scripts/rendering/world_renderer.gd b/client/scripts/rendering/world_renderer.gd index ddeaeac20..c84969776 100644 --- a/client/scripts/rendering/world_renderer.gd +++ b/client/scripts/rendering/world_renderer.gd @@ -21,13 +21,23 @@ var _last_tick: int = -1 @onready var sound_indicator_renderer = $SoundIndicators # #126 D-018 medium-range indicators +var _occluded: bool = false # D-170: skip rendering when fullscreen implant app is covering us + + func _ready() -> void: + HudGroups.gameplay_occluded.connect(_on_gameplay_occluded) print("WorldRenderer: Initialized (D-049 z-stack)") +func _on_gameplay_occluded(occluded: bool) -> void: + _occluded = occluded + + # Called each frame to update visuals from game state. # Uses tick-based invalidation — re-renders all layers when a new snapshot arrives. func update_from_state() -> void: + if _occluded: + return # D-170: skip rendering while fullscreen implant app is active var tick := GameState.current_tick if tick == _last_tick: return diff --git a/client/ui/star_map.gd b/client/ui/star_map.gd index e61eb79c1..dc279f92a 100644 --- a/client/ui/star_map.gd +++ b/client/ui/star_map.gd @@ -118,8 +118,9 @@ func _ready() -> void: _info_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(_info_panel) - # D-170: Register with HUD visibility groups + # D-170: Register with HUD layer groups HudGroups.register(self, "implant/map/starchart") + HudGroups.app_changed.connect(_on_app_changed) _load_data() if _data_loaded: @@ -135,20 +136,27 @@ func _process(_delta: float) -> void: ## Called from main.gd when insert state changes. -## Only force-hides when insert is inactive. Does NOT auto-show — star map is -## modal (player opens via toggle_visible()), not always-on like the minimap. +## Called from main.gd when insert state changes. func set_insert_active(active: bool) -> void: _insert_active = active - if not active: - visible = false + if not active and HudGroups.is_app_active("implant/map/starchart"): + HudGroups.close_app() -## Toggle visibility via HUD group system (D-170). -## Opens implant/map (hides gameplay) or closes it (returns to gameplay). +## Toggle via HUD layer system (D-170). func toggle_visible() -> void: HudGroups.toggle_app("implant/map/starchart") - if visible: + + +## Respond to app layer changes (D-170). +func _on_app_changed(app_path: String, mode: HudGroups.Mode) -> void: + if app_path != "implant/map/starchart": + return + if mode == HudGroups.Mode.FULLSCREEN or mode == HudGroups.Mode.INSERT: + visible = true _dirty = true + else: + visible = false ## Return the currently selected system data, or empty dict.