main_menu now connects SimBridge before character_creation opens, gating the transition on first ObserverSnapshot carrying a bookmark_catalog. Loading screen is shown during the connect; on cancel the SimBridge subprocess is torn down and the player returns to main_menu. Catalog is read straight from GameState.bookmark_catalog in W6. Flow (Option A): 1. New Game → SessionManager.new_game() creates save dir 2. main_menu pushes loading_screen via MetaStack with "Connecting to simulation..." message 3. SimBridge.connect_to_sim() spawned; main_menu listens on connection_state_changed, then on snapshot_received for the catalog 4. On catalog arrival: loading_screen closed, scene-transition to character_creation 5. character_creation Cancel → SimBridge.disconnect_from_sim() + scene transition back to main_menu (Tyre's recommendation: clean state per session over warm-start savings) 6. character_creation Start → ConfirmBookmark sent (stubbed for W4 with first catalog entry; real bookmark + location from W6's UI) ESC priority chain in main.gd OPEN_MENU handler: - MetaStack.handle_escape() first — closes the topmost meta overlay - HudGroups.is_implant_active() / close_app() — closes active implant - Fallback: toggle settings_dialog (existing W2 behavior) Files: - sim_bridge.gd: send_named_action(action_name, action_data) helper. Bridges named tag-enum PlayerActions (RequestBookmarkCatalog, ConfirmBookmark) into the existing outbound buffer, parallel to send_input's InputMapper.Action handling. - loading_screen.gd: set_message(text) for the connecting/loading label. - main_menu.gd: full Option A flow rewrite. Tracks _waiting_for_catalog so re-clicking New Game during connect is a no-op. - character_creation.gd: _on_back disconnect path + _on_start ConfirmBookmark stub. MAIN_MENU_SCENE / GAME_SCENE constants. - main.gd: connect_to_sim guard (don't reconnect when Option A leaves it CONNECTED). ESC chain wiring. Verification: - gdlint clean - godot --headless --path client --quit — no SCRIPT ERROR - test_protocol 62/62, test_implant_nav_stack 52/52, test_client_p3 24/24, test_ui_framework_sprint15 54/54 Pre-existing failing suites unchanged: test_sprint2_proof, test_dialogue_sprint18, test_client_p2 (camera-smoothing assertions that pre-date W4 — main.gd has disabled position_smoothing_enabled since #117 / #501 / #117 manual-lerp; tests were stale). Workstream 5 (3-tab restructure of character_creation: Bookmark / Appearance with sub-nav / Skills / Debug) lands next. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
184 lines
5.9 KiB
GDScript
184 lines
5.9 KiB
GDScript
extends MetaScreen
|
|
## #258: Main menu — New Game / Continue / Load Game / Quit.
|
|
## New Game: opens character creation screen, then starts game.
|
|
## Continue: loads most recent save directory.
|
|
## Load Game: shows sorted save list for manual selection (#257).
|
|
|
|
const GAME_SCENE := "res://scenes/main.tscn"
|
|
const CHARACTER_CREATION_SCENE := "res://scenes/character_creation.tscn"
|
|
const LOADING_SCREEN_SCENE := "res://ui/loading_screen.tscn"
|
|
|
|
const BG_COLOR := Color(0.05, 0.05, 0.08, 1.0)
|
|
const TITLE_COLOR := Color("#c8d0e0")
|
|
const SUBTITLE_COLOR := Color("#8890a0")
|
|
const BTN_NORMAL_COLOR := Color("#e8c547")
|
|
const BTN_DISABLED_COLOR := Color("#4a5060")
|
|
const FONT_SIZE_TITLE := 36
|
|
const FONT_SIZE_SUBTITLE := 14
|
|
const FONT_SIZE_BTN := 15
|
|
|
|
var _list_built: bool = false
|
|
var _loading_screen = null # LoadingScreen — instantiated on demand
|
|
var _waiting_for_catalog: bool = false
|
|
|
|
@onready var _new_game_btn: Button = $VBox/NewGameBtn
|
|
@onready var _continue_btn: Button = $VBox/ContinueBtn
|
|
@onready var _load_game_btn: Button = $VBox/LoadGameBtn
|
|
@onready var _quit_btn: Button = $VBox/QuitBtn
|
|
@onready var _load_panel: Control = $LoadGamePanel
|
|
@onready var _saves_list: VBoxContainer = $LoadGamePanel/VBox/SavesScroll/SavesList
|
|
@onready var _load_back_btn: Button = $LoadGamePanel/VBox/BackBtn
|
|
|
|
|
|
func _ready() -> void:
|
|
_new_game_btn.pressed.connect(_on_new_game)
|
|
_continue_btn.pressed.connect(_on_continue)
|
|
_load_game_btn.pressed.connect(_on_load_game_browse)
|
|
_quit_btn.pressed.connect(_on_quit)
|
|
_load_back_btn.pressed.connect(_on_load_back)
|
|
_load_panel.visible = false
|
|
_refresh_continue_state()
|
|
|
|
|
|
func _refresh_continue_state() -> void:
|
|
var saves := SessionManager.list_game_dirs()
|
|
_continue_btn.disabled = saves.is_empty()
|
|
_load_game_btn.disabled = saves.is_empty()
|
|
|
|
|
|
func _on_new_game() -> void:
|
|
if _waiting_for_catalog:
|
|
return
|
|
var game_id := SessionManager.new_game()
|
|
if game_id.is_empty():
|
|
push_error("MainMenu: new_game() failed to create save directory — cannot start")
|
|
return
|
|
GameState.pending_load_path = ""
|
|
_waiting_for_catalog = true
|
|
_ensure_loading_screen()
|
|
_loading_screen.set_message("Connecting to simulation...")
|
|
_loading_screen.show_loading()
|
|
SimBridge.connection_state_changed.connect(_on_sim_state_changed_for_new_game)
|
|
SimBridge.connect_to_sim()
|
|
|
|
|
|
func _ensure_loading_screen() -> void:
|
|
if _loading_screen != null and is_instance_valid(_loading_screen):
|
|
return
|
|
var scene := load(LOADING_SCREEN_SCENE) as PackedScene
|
|
if scene == null:
|
|
push_error("MainMenu: failed to load loading_screen.tscn")
|
|
return
|
|
_loading_screen = scene.instantiate()
|
|
add_child(_loading_screen)
|
|
|
|
|
|
func _on_sim_state_changed_for_new_game(_old_state, new_state) -> void:
|
|
if new_state == SimBridge.ConnectionState.CONNECTED:
|
|
SimBridge.connection_state_changed.disconnect(_on_sim_state_changed_for_new_game)
|
|
SimBridge.send_named_action("RequestBookmarkCatalog")
|
|
SimBridge.snapshot_received.connect(_on_snapshot_received_for_catalog)
|
|
elif new_state == SimBridge.ConnectionState.ERROR:
|
|
SimBridge.connection_state_changed.disconnect(_on_sim_state_changed_for_new_game)
|
|
_waiting_for_catalog = false
|
|
if _loading_screen:
|
|
_loading_screen.set_message("Connection failed. Try again.")
|
|
|
|
|
|
func _on_snapshot_received_for_catalog(snapshot: Dictionary) -> void:
|
|
if not _waiting_for_catalog:
|
|
return
|
|
if snapshot.get("bookmark_catalog") == null:
|
|
return
|
|
_waiting_for_catalog = false
|
|
SimBridge.snapshot_received.disconnect(_on_snapshot_received_for_catalog)
|
|
GameState.apply_snapshot(snapshot)
|
|
if _loading_screen:
|
|
_loading_screen.hide_loading()
|
|
get_tree().change_scene_to_file(CHARACTER_CREATION_SCENE)
|
|
|
|
|
|
func _on_continue() -> void:
|
|
GameState.pending_load_path = ""
|
|
var saves := SessionManager.list_game_dirs()
|
|
if saves.is_empty():
|
|
return
|
|
SessionManager.resume_game(saves[0].game_id)
|
|
get_tree().change_scene_to_file(GAME_SCENE)
|
|
|
|
|
|
func _on_load_game_browse() -> void:
|
|
if not _list_built:
|
|
_build_saves_list()
|
|
_list_built = true
|
|
_load_panel.visible = true
|
|
|
|
|
|
func _on_load_back() -> void:
|
|
_load_panel.visible = false
|
|
_list_built = false
|
|
|
|
|
|
func _on_quit() -> void:
|
|
get_tree().quit()
|
|
|
|
|
|
## Build the saves list panel from existing save directories, sorted by date (newest first).
|
|
func _build_saves_list() -> void:
|
|
for child in _saves_list.get_children():
|
|
child.queue_free()
|
|
|
|
var saves := SessionManager.list_game_dirs()
|
|
if saves.is_empty():
|
|
var lbl := Label.new()
|
|
lbl.text = UIStrings.get_text("menu.load_game_empty")
|
|
lbl.add_theme_color_override("font_color", Color(BTN_DISABLED_COLOR))
|
|
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
_saves_list.add_child(lbl)
|
|
return
|
|
|
|
for save in saves:
|
|
var btn := Button.new()
|
|
btn.text = _format_save_entry(save)
|
|
btn.add_theme_font_size_override("font_size", FONT_SIZE_BTN)
|
|
var has_save_file: bool = not save.get("newest_save", "").is_empty()
|
|
if has_save_file:
|
|
btn.add_theme_color_override("font_color", BTN_NORMAL_COLOR)
|
|
btn.pressed.connect(_on_save_selected.bind(save))
|
|
else:
|
|
btn.add_theme_color_override("font_color", BTN_DISABLED_COLOR)
|
|
btn.disabled = true
|
|
_saves_list.add_child(btn)
|
|
|
|
|
|
func _on_save_selected(save: Dictionary) -> void:
|
|
var game_id: String = save.get("game_id", "")
|
|
var save_file: String = save.get("newest_save", "")
|
|
if save_file.is_empty():
|
|
push_error("MainMenu: save entry '%s' has no newest_save — load cancelled" % game_id)
|
|
return
|
|
SessionManager.resume_game(game_id)
|
|
GameState.pending_load_path = "user://saves/" + game_id + "/" + save_file
|
|
get_tree().change_scene_to_file(GAME_SCENE)
|
|
|
|
|
|
## Format a save entry for display. game_id format: YYYYMMDD-HHMMSS-hex6.
|
|
static func _format_save_entry(save: Dictionary) -> String:
|
|
var game_id: String = save.get("game_id", "")
|
|
var parts := game_id.split("-")
|
|
if parts.size() >= 2 and parts[0].length() == 8 and parts[1].length() == 6:
|
|
var d := parts[0]
|
|
var t := parts[1]
|
|
return (
|
|
"%s-%s-%s %s:%s:%s"
|
|
% [
|
|
d.substr(0, 4),
|
|
d.substr(4, 2),
|
|
d.substr(6, 2),
|
|
t.substr(0, 2),
|
|
t.substr(2, 2),
|
|
t.substr(4, 2),
|
|
]
|
|
)
|
|
return game_id
|