Establishes the faux-game-menu base pattern for non-diegetic UI, analogous to ImplantApp but for pre-gameplay and meta-overlay screens (main menu, character creation, settings, debug console, bug report, loading screen). Workstream 1 of the MetaScreen refactor — foundation only, no screen migrations yet. - client/ui/meta/meta_screen.gd: base class (Control) with HIDDEN/OPENING/OPEN/CLOSING phase tracking, three orthogonal policy booleans (pauses_sim, closable_by_escape, captures_input), open/close lifecycle, on_escape contract, closed + escape_pressed signals, subclass hooks (on_open, on_close). - client/ui/meta/meta_stack.gd: autoload coordinator. Overlay stack with push/pop/top/is_active; handle_escape chain; sim-pause coordination via SimBridge when pauses_sim=true; meta_active_changed signal. All class references kept inside method bodies — no top-level class_name refs, matching HudGroups / GameState autoload parse-order discipline. - client/scripts/character_profile.gd: Resource wrapping the visual descriptor with bookmark_id and start_location_id. Target of the creation_confirmed signal once the character creation flow migrates. - client/project.godot: MetaStack registered as autoload after HudGroups, before ImplantRegistry. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ FogState="*res://scripts/autoloads/fog_state.gd"
|
|||||||
AudioManager="*res://scripts/autoloads/audio_manager.gd"
|
AudioManager="*res://scripts/autoloads/audio_manager.gd"
|
||||||
SessionManager="*res://scripts/autoloads/session_manager.gd"
|
SessionManager="*res://scripts/autoloads/session_manager.gd"
|
||||||
HudGroups="*res://scripts/autoloads/hud_groups.gd"
|
HudGroups="*res://scripts/autoloads/hud_groups.gd"
|
||||||
|
MetaStack="*res://ui/meta/meta_stack.gd"
|
||||||
ImplantRegistry="*res://ui/implant/implant_registry.gd"
|
ImplantRegistry="*res://ui/implant/implant_registry.gd"
|
||||||
HardwareDetector="*res://ui/hardware_detector.gd"
|
HardwareDetector="*res://ui/hardware_detector.gd"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
class_name CharacterProfile
|
||||||
|
extends Resource
|
||||||
|
## Collects all character creation choices into a single transferable object (#618).
|
||||||
|
## Passed as the argument to character_creation's creation_confirmed signal.
|
||||||
|
|
||||||
|
@export var descriptor: CharacterVisualDescriptor
|
||||||
|
@export var bookmark_id: String = ""
|
||||||
|
@export var start_location_id: String = ""
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
class_name MetaScreen
|
||||||
|
extends Control
|
||||||
|
## Base class for all meta-UI screens (#618, #680).
|
||||||
|
## Scene-root screens (main_menu, character_creation) extend this for the
|
||||||
|
## lifecycle contract. Overlay screens (settings, debug_console, bug_report,
|
||||||
|
## loading_screen) extend this AND push onto MetaStack.
|
||||||
|
|
||||||
|
signal closed
|
||||||
|
signal escape_pressed
|
||||||
|
|
||||||
|
enum Phase { HIDDEN, OPENING, OPEN, CLOSING }
|
||||||
|
|
||||||
|
@export var pauses_sim: bool = false
|
||||||
|
@export var closable_by_escape: bool = true
|
||||||
|
@export var captures_input: bool = true
|
||||||
|
|
||||||
|
var _phase: Phase = Phase.HIDDEN
|
||||||
|
|
||||||
|
|
||||||
|
func open() -> void:
|
||||||
|
if _phase != Phase.HIDDEN:
|
||||||
|
return
|
||||||
|
_phase = Phase.OPENING
|
||||||
|
visible = true
|
||||||
|
if captures_input:
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
on_open()
|
||||||
|
_phase = Phase.OPEN
|
||||||
|
|
||||||
|
|
||||||
|
func close() -> void:
|
||||||
|
if _phase != Phase.OPEN:
|
||||||
|
return
|
||||||
|
_phase = Phase.CLOSING
|
||||||
|
on_close()
|
||||||
|
visible = false
|
||||||
|
_phase = Phase.HIDDEN
|
||||||
|
closed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func is_open() -> bool:
|
||||||
|
return _phase == Phase.OPEN
|
||||||
|
|
||||||
|
|
||||||
|
## Called by MetaStack when ESC is pressed with this screen on top.
|
||||||
|
## Return true to consume the event (prevent stack pop); false to allow pop.
|
||||||
|
func on_escape() -> bool:
|
||||||
|
escape_pressed.emit()
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
# --- Lifecycle hooks — subclasses override ---
|
||||||
|
|
||||||
|
|
||||||
|
func on_open() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
func on_close() -> void:
|
||||||
|
pass
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
extends Node
|
||||||
|
## Autoload coordinator for overlay MetaScreens (#618, #680).
|
||||||
|
## Scene-root screens (main_menu, character_creation) do NOT push onto this stack.
|
||||||
|
## Only overlay screens (settings, debug_console, bug_report, loading_screen) push.
|
||||||
|
##
|
||||||
|
## Autoload parse-order: MetaScreen is a class_name type — referenced here only
|
||||||
|
## inside method bodies called at runtime, never at the top level or in _ready().
|
||||||
|
|
||||||
|
signal meta_active_changed(active: bool)
|
||||||
|
|
||||||
|
var _stack: Array = [] # Array[MetaScreen] — untyped per parse-order rule
|
||||||
|
|
||||||
|
|
||||||
|
func push(screen) -> void: # screen: MetaScreen
|
||||||
|
if screen in _stack:
|
||||||
|
return
|
||||||
|
_stack.append(screen)
|
||||||
|
screen.closed.connect(_on_screen_closed.bind(screen), CONNECT_ONE_SHOT)
|
||||||
|
if _stack.size() == 1:
|
||||||
|
meta_active_changed.emit(true)
|
||||||
|
if screen.pauses_sim:
|
||||||
|
_request_pause(true)
|
||||||
|
|
||||||
|
|
||||||
|
func pop() -> void:
|
||||||
|
if _stack.is_empty():
|
||||||
|
return
|
||||||
|
_stack[-1].close()
|
||||||
|
|
||||||
|
|
||||||
|
func top(): # returns MetaScreen or null
|
||||||
|
return _stack[-1] if not _stack.is_empty() else null
|
||||||
|
|
||||||
|
|
||||||
|
func is_active() -> bool:
|
||||||
|
return not _stack.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
## Handle ESC key. Call from main.gd before HudGroups ESC handling.
|
||||||
|
## Returns true if the event was consumed (callers must return after).
|
||||||
|
func handle_escape() -> bool:
|
||||||
|
var t = top()
|
||||||
|
if t == null:
|
||||||
|
return false
|
||||||
|
if not t.closable_by_escape:
|
||||||
|
return t.on_escape()
|
||||||
|
if t.on_escape():
|
||||||
|
return true
|
||||||
|
t.close()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _on_screen_closed(screen) -> void: # screen: MetaScreen
|
||||||
|
_stack.erase(screen)
|
||||||
|
if screen.pauses_sim and not _any_pausing():
|
||||||
|
_request_pause(false)
|
||||||
|
if _stack.is_empty():
|
||||||
|
meta_active_changed.emit(false)
|
||||||
|
|
||||||
|
|
||||||
|
func _any_pausing() -> bool:
|
||||||
|
for s in _stack:
|
||||||
|
if s.pauses_sim:
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _request_pause(pause: bool) -> void:
|
||||||
|
if SimBridge.state != SimBridge.ConnectionState.CONNECTED:
|
||||||
|
return
|
||||||
|
var action = InputMapper.Action.PAUSE if pause else InputMapper.Action.UNPAUSE
|
||||||
|
SimBridge.send_input({"action": action, "timestamp_msec": Time.get_ticks_msec()})
|
||||||
Reference in New Issue
Block a user