extends Control ## Main launcher scene script - orchestrates all launcher functionality const Config = preload("config.gd") const VersionChecker = preload("version_checker.gd") const DownloadManager = preload("download_manager.gd") const UpdateApplier = preload("update_applier.gd") const NewsFetcher = preload("news_fetcher.gd") # Child nodes (will be set up in _ready) @onready var version_checker := $VersionChecker as VersionChecker @onready var download_manager := $DownloadManager as DownloadManager @onready var update_applier := $UpdateApplier as UpdateApplier @onready var news_fetcher := $NewsFetcher as NewsFetcher # UI Elements @onready var play_button := $MainPanel/ContentContainer/BottomBar/PlayButton as Button @onready var progress_bar := $MainPanel/ContentContainer/BottomBar/ProgressContainer/ProgressBar as ProgressBar @onready var progress_label := $MainPanel/ContentContainer/BottomBar/ProgressContainer/ProgressLabel as Label @onready var progress_container := $MainPanel/ContentContainer/BottomBar/ProgressContainer as Control @onready var status_label := $MainPanel/ContentContainer/BottomBar/StatusLabel as Label @onready var version_label := $MainPanel/TitleBar/VersionLabel as Label @onready var news_container := $MainPanel/ContentContainer/TabContainer/News/ScrollContainer/NewsVBox as VBoxContainer @onready var changelog_container := $MainPanel/ContentContainer/TabContainer/Changelog/ScrollContainer/ChangelogVBox as VBoxContainer enum LauncherState { CHECKING, UP_TO_DATE, UPDATE_AVAILABLE, DOWNLOADING, INSTALLING, READY, ERROR } var current_state: LauncherState = LauncherState.CHECKING var pending_changelog: Array = [] func _ready() -> void: _setup_signals() _apply_theme() _set_state(LauncherState.CHECKING) # Start checking for updates await get_tree().create_timer(0.5).timeout # Small delay for UI to initialize version_checker.check_for_updates() news_fetcher.fetch_news() func _setup_signals() -> void: # Version checker signals version_checker.version_check_completed.connect(_on_version_check_completed) version_checker.version_check_failed.connect(_on_version_check_failed) # Download manager signals download_manager.download_started.connect(_on_download_started) download_manager.download_progress.connect(_on_download_progress) download_manager.download_completed.connect(_on_download_completed) download_manager.download_failed.connect(_on_download_failed) # Update applier signals update_applier.update_started.connect(_on_update_started) update_applier.update_progress.connect(_on_update_progress) update_applier.update_completed.connect(_on_update_completed) update_applier.update_failed.connect(_on_update_failed) # News fetcher signals news_fetcher.news_fetched.connect(_on_news_fetched) # Play button play_button.pressed.connect(_on_play_pressed) func _apply_theme() -> void: # Apply theme colors from config var bg_style := StyleBoxFlat.new() bg_style.bg_color = Config.COLOR_BACKGROUND add_theme_stylebox_override("panel", bg_style) func _set_state(new_state: LauncherState) -> void: current_state = new_state match new_state: LauncherState.CHECKING: status_label.text = "Checking for updates..." play_button.disabled = true play_button.text = "CHECKING..." progress_container.visible = false LauncherState.UP_TO_DATE: status_label.text = "Game is up to date!" play_button.disabled = false play_button.text = "► PLAY" progress_container.visible = false version_label.text = "v" + version_checker.current_version LauncherState.UPDATE_AVAILABLE: status_label.text = "Update available: v" + version_checker.latest_version play_button.disabled = false play_button.text = "⬇ UPDATE" progress_container.visible = false LauncherState.DOWNLOADING: status_label.text = "Downloading update..." play_button.disabled = true play_button.text = "DOWNLOADING..." progress_container.visible = true progress_bar.value = 0 LauncherState.INSTALLING: status_label.text = "Installing update..." play_button.disabled = true play_button.text = "INSTALLING..." progress_container.visible = true LauncherState.READY: status_label.text = "Ready to play!" play_button.disabled = false play_button.text = "► PLAY" progress_container.visible = false version_label.text = "v" + version_checker.current_version LauncherState.ERROR: play_button.disabled = true play_button.text = "ERROR" progress_container.visible = false # --- Version Check Callbacks --- func _on_version_check_completed(has_update: bool, latest_version: String, changelog: Array) -> void: pending_changelog = changelog _populate_changelog(changelog) if has_update: _set_state(LauncherState.UPDATE_AVAILABLE) else: # Check if game is installed if _is_game_installed(): _set_state(LauncherState.UP_TO_DATE) else: # Fresh install needed status_label.text = "Game not installed - click UPDATE to download" _set_state(LauncherState.UPDATE_AVAILABLE) func _on_version_check_failed(error: String) -> void: status_label.text = "Failed to check for updates: " + error # Still allow playing if game is installed if _is_game_installed(): _set_state(LauncherState.READY) status_label.text = "Offline mode - " + error else: _set_state(LauncherState.ERROR) # --- Download Callbacks --- func _on_download_started(_file_name: String, _total_size: int) -> void: _set_state(LauncherState.DOWNLOADING) func _on_download_progress(_downloaded: int, _total: int, percentage: float) -> void: progress_bar.value = percentage progress_label.text = "%.1f%%" % percentage func _on_download_completed(file_path: String) -> void: _set_state(LauncherState.INSTALLING) update_applier.apply_update(file_path, version_checker.latest_version) func _on_download_failed(error: String) -> void: status_label.text = "Download failed: " + error _set_state(LauncherState.ERROR) # --- Update Callbacks --- func _on_update_started() -> void: progress_bar.value = 0 func _on_update_progress(step: String, percentage: float) -> void: progress_bar.value = percentage progress_label.text = step func _on_update_completed() -> void: version_checker.save_version(version_checker.latest_version) _set_state(LauncherState.READY) status_label.text = "Update complete! Ready to play." func _on_update_failed(error: String) -> void: status_label.text = "Update failed: " + error _set_state(LauncherState.ERROR) # --- News Callbacks --- func _on_news_fetched(news_items: Array) -> void: _populate_news(news_items) # --- UI Helpers --- func _populate_changelog(changelog: Array) -> void: # Clear existing for child in changelog_container.get_children(): child.queue_free() if changelog.is_empty(): var label := Label.new() label.text = "No changelog available" label.add_theme_color_override("font_color", Config.COLOR_TEXT_DIM) changelog_container.add_child(label) return for entry in changelog: var version_label_item := Label.new() version_label_item.text = "Version " + entry.get("version", "?") + " - " + entry.get("date", "") version_label_item.add_theme_color_override("font_color", Config.COLOR_PRIMARY) version_label_item.add_theme_font_size_override("font_size", 16) changelog_container.add_child(version_label_item) var changes: Array = entry.get("changes", []) for change in changes: var change_label := Label.new() change_label.text = " • " + str(change) change_label.add_theme_color_override("font_color", Config.COLOR_TEXT) changelog_container.add_child(change_label) # Spacer var spacer := Control.new() spacer.custom_minimum_size = Vector2(0, 10) changelog_container.add_child(spacer) func _populate_news(news_items: Array) -> void: # Clear existing for child in news_container.get_children(): child.queue_free() if news_items.is_empty(): var label := Label.new() label.text = "No news available" label.add_theme_color_override("font_color", Config.COLOR_TEXT_DIM) news_container.add_child(label) return for item in news_items: # News card panel var card := PanelContainer.new() var card_style := StyleBoxFlat.new() card_style.bg_color = Config.COLOR_SURFACE card_style.set_corner_radius_all(8) card_style.set_content_margin_all(12) card.add_theme_stylebox_override("panel", card_style) var vbox := VBoxContainer.new() card.add_child(vbox) # Title var title := Label.new() title.text = item.get("title", "Untitled") title.add_theme_color_override("font_color", Config.COLOR_PRIMARY) title.add_theme_font_size_override("font_size", 16) vbox.add_child(title) # Date var date := Label.new() date.text = item.get("date", "") date.add_theme_color_override("font_color", Config.COLOR_TEXT_DIM) date.add_theme_font_size_override("font_size", 12) vbox.add_child(date) # Content var content := Label.new() content.text = item.get("content", "") content.add_theme_color_override("font_color", Config.COLOR_TEXT) content.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART vbox.add_child(content) news_container.add_child(card) # Spacer var spacer := Control.new() spacer.custom_minimum_size = Vector2(0, 8) news_container.add_child(spacer) func _is_game_installed() -> bool: var pck_path := Config.GAME_DIRECTORY + Config.get_game_pck() return FileAccess.file_exists(pck_path) func _on_play_pressed() -> void: match current_state: LauncherState.UPDATE_AVAILABLE: # Start download var download_info := version_checker.get_download_info() if download_info.get("url", "") != "": download_manager.download_file( download_info.url, Config.get_game_pck(), download_info.get("size", 0), download_info.get("checksum_md5", ""), download_info.get("checksum_sha256", "") ) else: status_label.text = "Error: No download URL available" _set_state(LauncherState.ERROR) LauncherState.UP_TO_DATE, LauncherState.READY: _launch_game() func _launch_game() -> void: var game_executable := Config.get_game_executable() var game_path := ProjectSettings.globalize_path(Config.GAME_DIRECTORY + game_executable) if not FileAccess.file_exists(Config.GAME_DIRECTORY + game_executable): # Try to find the executable in the same directory as launcher var launcher_dir := OS.get_executable_path().get_base_dir() game_path = launcher_dir + "/" + game_executable print("[Launcher] Platform: ", Config.get_platform_name()) print("[Launcher] Launching game: ", game_path) var args: PackedStringArray = [] var pid: int = -1 # Platform-specific launch handling match Config.get_current_platform(): Config.Platform.MACOS: # macOS: Use 'open' command for .app bundles if game_executable.ends_with(".app"): pid = OS.create_process("open", PackedStringArray(["-a", game_path])) else: pid = OS.create_process(game_path, args) Config.Platform.LINUX: # Linux: May need to set executable permission OS.execute("chmod", PackedStringArray(["+x", game_path])) pid = OS.create_process(game_path, args) _: # Windows and others pid = OS.create_process(game_path, args) if pid > 0: # Successfully launched, close launcher get_tree().quit() else: status_label.text = "Failed to launch game" _set_state(LauncherState.ERROR)