extends "res://ui/meta/meta_screen.gd" ## #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). ## ## LoadingScreen lifecycle: this scene instantiates its own LoadingScreen child ## (see `_ensure_loading_screen`). main.tscn has a separate `$MetaLayer/LoadingScreen`. ## Safe today because main_menu.tscn and main.tscn never co-exist — the scene ## transition in `_start_game` replaces the tree wholesale. If that invariant ## ever changes (e.g. embedding the menu as an overlay), promote LoadingScreen ## to an autoload to enforce single-instance across the MetaStack. 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 _process(_delta: float) -> void: if _waiting_for_catalog: SimBridge.poll_snapshot() 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