extends CanvasLayer # Game UI - Main menu and multiplayer lobby @onready var main_menu = $Control/MainMenu @onready var host_button = $Control/MainMenu/VBoxContainer/HostButton @onready var join_button = $Control/MainMenu/VBoxContainer/JoinButton @onready var network_mode_option = $Control/MainMenu/VBoxContainer/NetworkModeContainer/NetworkModeOption @onready var network_mode_container = $Control/MainMenu/VBoxContainer/NetworkModeContainer @onready var local_players_spinbox = $Control/MainMenu/VBoxContainer/LocalPlayersContainer/SpinBox @onready var address_input = $Control/MainMenu/VBoxContainer/AddressContainer/AddressInput @onready var room_fetch_status_container = $Control/MainMenu/VBoxContainer/RoomFetchStatusContainer @onready var loading_label = $Control/MainMenu/VBoxContainer/RoomFetchStatusContainer/RoomFetchLoadingContainer/LoadingLabel @onready var loading_spinner = $Control/MainMenu/VBoxContainer/RoomFetchStatusContainer/RoomFetchLoadingContainer/LoadingSpinner @onready var last_fetch_label = $Control/MainMenu/VBoxContainer/RoomFetchStatusContainer/LastFetchLabel @onready var network_manager = $"/root/NetworkManager" var connection_error_label: Label = null var connection_error_shown: bool = false # Prevent spamming error messages var is_joining_attempt: bool = false var last_join_address: String = "" var room_fetch_timer: Timer = null # Timer for retrying room fetches var is_auto_joining: bool = false # Track if we're in auto-join mode var is_hosting: bool = false # Track if we're hosting (don't fetch rooms when hosting) var room_list_container: VBoxContainer = null # Container for displaying available rooms var refresh_button: Button = null # Refresh button for manually reloading rooms var refresh_cooldown_timer: Timer = null # Timer for refresh button cooldown func _ready(): # Wait for nodes to be ready await get_tree().process_frame # Debug: Print node paths (UI category - disabled by default for network debugging) LogManager.log("GameUI _ready() called", LogManager.CATEGORY_UI) LogManager.log("Main menu node: " + str(main_menu), LogManager.CATEGORY_UI) LogManager.log("Host button node: " + str(host_button), LogManager.CATEGORY_UI) LogManager.log("Join button node: " + str(join_button), LogManager.CATEGORY_UI) # Verify nodes exist if not host_button: push_error("host_button is null! Check node path: $Control/MainMenu/VBoxContainer/HostButton") return if not join_button: push_error("join_button is null! Check node path: $Control/MainMenu/VBoxContainer/JoinButton") return # Connect buttons host_button.pressed.connect(_on_host_pressed) join_button.pressed.connect(_on_join_pressed) # Setup network mode dropdown if network_mode_option: network_mode_option.item_selected.connect(_on_network_mode_changed) # On web builds, filter out ENet option (only WebRTC and WebSocket available) if OS.get_name() == "Web": LogManager.log("GameUI: Web platform detected, filtering network mode options", LogManager.CATEGORY_UI) # Remove ENet option (index 0) for web builds network_mode_option.remove_item(0) # Adjust selected index (was 0 for ENet, now 0 is WebRTC) network_mode_option.selected = 0 # Update network manager to use WebRTC by default network_manager.set_network_mode(1) else: # On native builds, default to ENet (index 0) network_mode_option.selected = 0 network_manager.set_network_mode(0) # Update address input placeholder based on initial mode _on_network_mode_changed(network_mode_option.selected) # Connect network signals if network_manager: network_manager.connection_succeeded.connect(_on_connection_succeeded) network_manager.connection_failed.connect(_on_connection_failed) # Connect to rooms_fetched for displaying rooms (non-auto-join mode) if not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_display): network_manager.rooms_fetched.connect(_on_rooms_fetched_display) else: push_error("NetworkManager not found!") # Check for command-line arguments _check_command_line_args() # If WebRTC is selected at startup (not auto-joining and not hosting), fetch rooms if not is_auto_joining and not is_hosting: var current_mode = network_manager.network_mode if current_mode == 1: # WebRTC _start_room_fetch() func _check_command_line_args(): var args = OS.get_cmdline_args() LogManager.log("GameUI: Parsing command-line arguments: " + str(args), LogManager.CATEGORY_UI) # Parse arguments var should_host = false var should_join = false var should_debug = false var force_webrtc = false var join_address = "" # Empty by default - will fetch rooms if WebRTC/WebSocket var local_count = 1 for arg in args: if arg == "--host": should_host = true LogManager.log("GameUI: Found --host argument", LogManager.CATEGORY_UI) elif arg == "--join": should_join = true LogManager.log("GameUI: Found --join argument", LogManager.CATEGORY_UI) elif arg == "--websocket" or arg == "--webrtc": force_webrtc = true LogManager.log("GameUI: Found --websocket/--webrtc argument (forcing WebSocket mode)", LogManager.CATEGORY_UI) elif arg == "--room-debug": should_debug = true LogManager.log("GameUI: Found --room-debug argument", LogManager.CATEGORY_UI) elif arg.begins_with("--address="): join_address = arg.split("=")[1] elif arg.begins_with("--players="): local_count = int(arg.split("=")[1]) LogManager.log("GameUI: Parsed flags - should_host: " + str(should_host) + ", should_join: " + str(should_join) + ", force_webrtc: " + str(force_webrtc) + ", should_debug: " + str(should_debug), LogManager.CATEGORY_UI) # Force WebRTC mode if --webrtc flag is present if force_webrtc: network_manager.set_network_mode(1) # WebRTC if network_mode_option: if OS.get_name() == "Web": network_mode_option.selected = 0 # WebRTC is first option on web else: network_mode_option.selected = 1 # WebRTC is second option on native _on_network_mode_changed(network_mode_option.selected) LogManager.log("GameUI: WebRTC mode forced via --webrtc flag", LogManager.CATEGORY_UI) # Set debug flag only if --room-debug is used with --host or --join if should_debug and (should_host or should_join): network_manager.show_room_labels = true LogManager.log("Debug mode enabled: room labels will be shown", LogManager.CATEGORY_UI) else: LogManager.log("GameUI: Debug mode NOT enabled - should_debug: " + str(should_debug) + ", should_host: " + str(should_host) + ", should_join: " + str(should_join), LogManager.CATEGORY_UI) # Auto-start based on arguments if should_host: is_hosting = true # Set flag so we don't fetch rooms LogManager.log("Auto-hosting due to --host argument", LogManager.CATEGORY_UI) network_manager.set_local_player_count(local_count) if network_manager.host_game(): _start_game() elif should_join: # Check network mode after it's been set var current_mode = network_manager.network_mode if join_address.is_empty() and (current_mode == 1 or current_mode == 2): # No address provided, and using WebRTC or WebSocket - fetch and auto-join first available room LogManager.log("Auto-joining: No address provided, fetching available rooms (mode: " + str(current_mode) + ")...", LogManager.CATEGORY_UI) network_manager.set_local_player_count(local_count) is_auto_joining = true # Create timer for retrying room fetches room_fetch_timer = Timer.new() room_fetch_timer.name = "RoomFetchTimer" room_fetch_timer.wait_time = 2.0 # Retry every 2 seconds room_fetch_timer.timeout.connect(_retry_room_fetch) room_fetch_timer.autostart = false add_child(room_fetch_timer) # Connect to rooms_fetched signal (not one-shot, so we can keep retrying) if not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_auto_join): network_manager.rooms_fetched.connect(_on_rooms_fetched_auto_join) # Show room fetch status UI and start fetching _show_room_fetch_status() _start_room_fetch() elif not join_address.is_empty(): LogManager.log("Auto-joining to " + join_address + " due to --join argument", LogManager.CATEGORY_UI) address_input.text = join_address network_manager.set_local_player_count(local_count) if network_manager.join_game(join_address): # Connection callback will handle starting the game pass else: # ENet mode without address - need a default address var default_address = "127.0.0.1" LogManager.log("Auto-joining to " + default_address + " due to --join argument (ENet mode, no address provided)", LogManager.CATEGORY_UI) address_input.text = default_address network_manager.set_local_player_count(local_count) if network_manager.join_game(default_address): # Connection callback will handle starting the game pass func _on_rooms_fetched_display(rooms: Array): """Display available rooms when fetched (non-auto-join mode)""" # Only handle if not in auto-join mode (auto-join has its own handler) if is_auto_joining: return # Let auto-join handler take care of it # Hide loading indicator - request completed _hide_loading_indicator() # Update last fetch time _update_last_fetch_time() # Clear existing room list _clear_room_list() # Display rooms if any are found if rooms.is_empty(): LogManager.log("GameUI: No available rooms found", LogManager.CATEGORY_UI) # Keep room fetch status visible so user can see "Last fetched: ..." even with no rooms # Add a "No rooms available" message _add_room_list_header("No available rooms") else: LogManager.log("GameUI: Found " + str(rooms.size()) + " available room(s)", LogManager.CATEGORY_UI) # Add header _add_room_list_header("Available Rooms:") # Add each room to the list for room in rooms: var room_code = room.get("room", "") var players = room.get("players", 0) var level = room.get("level", 1) _add_room_item(room_code, players, level) func _on_rooms_fetched_auto_join(rooms: Array): """Auto-join the first available room when --join --webrtc is used without address""" if not is_auto_joining: return # Not in auto-join mode, ignore # Hide loading indicator - request completed _hide_loading_indicator() # Update last fetch time _update_last_fetch_time() if rooms.is_empty(): LogManager.log("No available rooms found, will retry in 2 seconds...", LogManager.CATEGORY_UI) # Start timer to retry fetching if room_fetch_timer: room_fetch_timer.start() return # Stop retrying - we found rooms! if room_fetch_timer: room_fetch_timer.stop() is_auto_joining = false # Hide room fetch status UI _hide_room_fetch_status() # Disconnect from signal since we're done if network_manager.rooms_fetched.is_connected(_on_rooms_fetched_auto_join): network_manager.rooms_fetched.disconnect(_on_rooms_fetched_auto_join) # Sort rooms by player count (prefer rooms with more players, but not full) rooms.sort_custom(func(a, b): return a.get("players", 0) < b.get("players", 0)) # Try to join the first room var room = rooms[0] var room_code = room.get("room", "") if room_code.is_empty(): LogManager.log("Room code is empty, cannot join - will retry", LogManager.CATEGORY_UI) # Restart timer to retry if room_fetch_timer: room_fetch_timer.start() is_auto_joining = true # Keep showing status UI return # Get local player count from spinbox (same as _on_join_pressed does) var local_count = int(local_players_spinbox.value) LogManager.log("Auto-joining room: " + room_code + " (players: " + str(room.get("players", 0)) + ", level: " + str(room.get("level", 1)) + ")", LogManager.CATEGORY_UI) address_input.text = room_code network_manager.set_local_player_count(local_count) if network_manager.join_game(room_code): # Connection callback will handle starting the game pass func _retry_room_fetch(): """Retry fetching available rooms""" if not is_auto_joining: if room_fetch_timer: room_fetch_timer.stop() return LogManager.log("Retrying room fetch...", LogManager.CATEGORY_UI) _start_room_fetch() func _start_room_fetch(): """Start fetching rooms and show loading indicator""" # Only fetch if WebRTC mode and not hosting if network_manager.network_mode != 1: # Not WebRTC return if is_hosting or network_manager.is_hosting: LogManager.log("GameUI: Skipping room fetch - we are hosting", LogManager.CATEGORY_UI) return # Show the status container and loading indicator _show_room_fetch_status() _show_loading_indicator() # Check if a request is already in progress before starting a new one var fetch_result = network_manager.fetch_available_rooms() if not fetch_result: # Request failed or is already in progress - hide loading indicator _hide_loading_indicator() # Start timer to retry (only in auto-join mode) if is_auto_joining and room_fetch_timer: room_fetch_timer.start() func _show_room_fetch_status(): """Show the room fetch status UI""" if room_fetch_status_container: room_fetch_status_container.visible = true if last_fetch_label: last_fetch_label.text = "Last fetched: Never" # Create room list container if it doesn't exist _create_room_list_container() # Create refresh button if it doesn't exist _create_refresh_button() func _show_loading_indicator(): """Show the loading indicator""" if loading_label: loading_label.text = "Loading rooms..." if loading_spinner: loading_spinner.text = "⏳" func _hide_loading_indicator(): """Hide the loading indicator""" if loading_label: loading_label.text = "" if loading_spinner: loading_spinner.text = "" func _hide_room_fetch_status(): """Hide the room fetch status UI""" if room_fetch_status_container: room_fetch_status_container.visible = false func _create_refresh_button(): """Create a refresh button for manually reloading the room list""" if refresh_button: return # Already exists if not room_fetch_status_container: return # Create HBoxContainer for refresh button (below last fetch label) var refresh_container = HBoxContainer.new() refresh_container.name = "RefreshButtonContainer" refresh_container.alignment = BoxContainer.ALIGNMENT_END # Create refresh button refresh_button = Button.new() refresh_button.name = "RefreshButton" refresh_button.text = "Refresh Rooms" refresh_button.pressed.connect(_on_refresh_button_pressed) refresh_container.add_child(refresh_button) # Add refresh container after LastFetchLabel (or at the end if LastFetchLabel doesn't exist) var last_fetch_node = room_fetch_status_container.get_node_or_null("LastFetchLabel") if last_fetch_node: var index = last_fetch_node.get_index() + 1 room_fetch_status_container.add_child(refresh_container) room_fetch_status_container.move_child(refresh_container, index) else: room_fetch_status_container.add_child(refresh_container) # Create cooldown timer refresh_cooldown_timer = Timer.new() refresh_cooldown_timer.name = "RefreshCooldownTimer" refresh_cooldown_timer.wait_time = 5.0 refresh_cooldown_timer.one_shot = true refresh_cooldown_timer.timeout.connect(_on_refresh_cooldown_finished) refresh_cooldown_timer.autostart = false add_child(refresh_cooldown_timer) func _on_refresh_button_pressed(): """Handle refresh button click""" # Disable button and start cooldown if refresh_button: refresh_button.disabled = true refresh_button.text = "Refreshing..." if refresh_cooldown_timer: refresh_cooldown_timer.start() # Fetch rooms _start_room_fetch() func _on_refresh_cooldown_finished(): """Re-enable refresh button after cooldown""" if refresh_button: refresh_button.disabled = false refresh_button.text = "Refresh Rooms" func _update_last_fetch_time(): """Update the last fetch time label with current datetime""" if last_fetch_label: var now = Time.get_datetime_dict_from_system() var time_str = "%02d:%02d:%02d" % [now.hour, now.minute, now.second] last_fetch_label.text = "Last fetched: " + time_str func _create_room_list_container(): """Create the container for displaying available rooms""" if room_list_container: return # Already exists if not room_fetch_status_container: return # Create a ScrollContainer for the room list var scroll_container = ScrollContainer.new() scroll_container.name = "RoomListScrollContainer" scroll_container.custom_minimum_size = Vector2(0, 150) # Set a reasonable height scroll_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL # Create VBoxContainer inside scroll container room_list_container = VBoxContainer.new() room_list_container.name = "RoomListContainer" room_list_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL scroll_container.add_child(room_list_container) # Add scroll container to room_fetch_status_container (after LastFetchLabel) room_fetch_status_container.add_child(scroll_container) func _clear_room_list(): """Clear all room items from the list""" if room_list_container: for child in room_list_container.get_children(): child.queue_free() func _add_room_list_header(text: String): """Add a header label to the room list""" if not room_list_container: _create_room_list_container() if not room_list_container: return var label = Label.new() label.text = text label.add_theme_font_size_override("font_size", 14) room_list_container.add_child(label) func _add_room_item(room_code: String, players: int, level: int): """Add a room item with join button to the list""" if not room_list_container: _create_room_list_container() if not room_list_container: return # Create HBoxContainer for this room var room_row = HBoxContainer.new() room_row.name = "RoomRow_" + room_code # Room code label var code_label = Label.new() code_label.text = room_code code_label.custom_minimum_size = Vector2(100, 0) code_label.add_theme_font_size_override("font_size", 12) room_row.add_child(code_label) # Info label (players, level) var info_label = Label.new() info_label.text = "Players: %d | Level: %d" % [players, level] info_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL info_label.add_theme_font_size_override("font_size", 12) room_row.add_child(info_label) # Join button var room_join_button = Button.new() room_join_button.text = "Join" room_join_button.custom_minimum_size = Vector2(80, 0) # Connect button to join function room_join_button.pressed.connect(func(): _join_room(room_code)) room_row.add_child(room_join_button) room_list_container.add_child(room_row) func _join_room(room_code: String): """Join a room by setting the address and clicking join""" if room_code.is_empty(): return # Set the address input if address_input: address_input.text = room_code # Get local player count var local_count = int(local_players_spinbox.value) network_manager.set_local_player_count(local_count) # Join the game if network_manager.join_game(room_code): LogManager.log("Joining room: " + room_code, LogManager.CATEGORY_UI) func _on_network_mode_changed(index: int): # On web builds, index 0 = WebRTC, index 1 = WebSocket # On native builds, index 0 = ENet, index 1 = WebRTC, index 2 = WebSocket var actual_mode: int if OS.get_name() == "Web": # Web builds: 0 = WebRTC, 1 = WebSocket actual_mode = index + 1 # Map 0->1 (WebRTC), 1->2 (WebSocket) else: # Native builds: 0 = ENet, 1 = WebRTC, 2 = WebSocket actual_mode = index network_manager.set_network_mode(actual_mode) # Update address input placeholder based on mode if address_input: match actual_mode: 0: # ENet address_input.placeholder_text = "Server IP or domain" 1: # WebRTC address_input.placeholder_text = "Enter Room Code (e.g., ABC123)" 2: # WebSocket address_input.placeholder_text = "Enter Room Code (e.g., ABC123)" var mode_names = ["ENet", "WebRTC", "WebSocket"] LogManager.log("GameUI: Network mode changed to: " + mode_names[actual_mode], LogManager.CATEGORY_UI) # If WebRTC is selected, fetch available rooms (unless we're auto-joining or hosting) if actual_mode == 1 and not is_auto_joining and not is_hosting: # WebRTC mode _start_room_fetch() elif actual_mode != 1: # Not WebRTC mode # Hide room fetch status if switching away from WebRTC _hide_room_fetch_status() func _on_host_pressed(): is_hosting = true # Set flag so we don't fetch rooms var local_count = int(local_players_spinbox.value) network_manager.set_local_player_count(local_count) if network_manager.host_game(): var mode = network_manager.network_mode if mode == 1 or mode == 2: # WebRTC or WebSocket var room_id = network_manager.get_room_id() var mode_name = "WebRTC" if mode == 1 else "WebSocket" print("Hosting ", mode_name, " game - Room Code: ", room_id) print("Share this code with players!") else: print("Hosting ENet game with ", local_count, " local players") _start_game() func _on_join_pressed(): # Reset error state when attempting new connection connection_error_shown = false _hide_connection_error() is_joining_attempt = true var address = address_input.text if address.is_empty(): var mode = network_manager.network_mode if mode == 1 or mode == 2: # WebRTC or WebSocket LogManager.log("Error: Please enter a room code", LogManager.CATEGORY_UI) return else: # ENet mode without address - use default address = "127.0.0.1" var local_count = int(local_players_spinbox.value) network_manager.set_local_player_count(local_count) if network_manager.join_game(address): last_join_address = address var mode = network_manager.network_mode if mode == 1: # WebRTC LogManager.log("Joining WebRTC game with room code: " + address, LogManager.CATEGORY_UI) elif mode == 2: # WebSocket LogManager.log("Joining WebSocket game with room code: " + address, LogManager.CATEGORY_UI) else: # ENet LogManager.log("Joining ENet game at " + address + " with " + str(local_count) + " local players", LogManager.CATEGORY_UI) func _on_connection_succeeded(): LogManager.log("GameUI: Connection succeeded signal received, starting game", LogManager.CATEGORY_UI) is_joining_attempt = false # Check if node is still valid before starting game if not is_inside_tree(): LogManager.log_error("GameUI: Cannot start game - node not in tree", LogManager.CATEGORY_UI) return # Use call_deferred to ensure we're in a safe state to change scenes call_deferred("_start_game") func _on_connection_failed(): LogManager.log("Connection failed", LogManager.CATEGORY_UI) if connection_error_shown: # Already shown, don't spam return connection_error_shown = true var mode = network_manager.network_mode var mode_name = "ENet" if mode == 1: mode_name = "WebRTC" elif mode == 2: mode_name = "WebSocket" if is_joining_attempt: var code_hint = (" (" + last_join_address + ")") if not last_join_address.is_empty() else "" _show_connection_error("Failed to join room" + code_hint + ". Did you enter the correct code?") # For WebRTC, refresh signaling connection after a failed join if mode == 1 and network_manager and network_manager.has_method("refresh_webrtc_servers"): network_manager.refresh_webrtc_servers(last_join_address) # Show room list UI and refresh rooms after a failed WebRTC join _show_room_fetch_status() _show_loading_indicator() _start_room_fetch() else: _show_connection_error("Connection failed (" + mode_name + "). Please try again.") func _show_connection_error(message: String): """Show a temporary error message in the UI""" if not main_menu: return # Remove existing error label if any _hide_connection_error() # Create error label connection_error_label = Label.new() connection_error_label.name = "ConnectionErrorLabel" connection_error_label.text = message connection_error_label.modulate = Color.RED connection_error_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER connection_error_label.add_theme_font_size_override("font_size", 14) # Add to main menu VBoxContainer (after the title) var vbox = main_menu.get_node_or_null("VBoxContainer") if vbox: # Insert after title (index 0) or at the beginning vbox.add_child(connection_error_label) vbox.move_child(connection_error_label, 1) # Move to position 1 (after title) # Auto-hide after 5 seconds await get_tree().create_timer(5.0).timeout _hide_connection_error() func _hide_connection_error(): """Hide and remove the connection error label""" if connection_error_label: connection_error_label.queue_free() connection_error_label = null func _start_game(): # Check if node is still in the tree before trying to access get_tree() if not is_inside_tree(): LogManager.log_error("GameUI: Cannot change scene - node is not in tree", LogManager.CATEGORY_UI) return # Hide menu if main_menu: main_menu.visible = false # Load the game scene var tree = get_tree() if tree: tree.change_scene_to_file("res://scenes/game_world.tscn") else: LogManager.log_error("GameUI: Cannot change scene - get_tree() is null", LogManager.CATEGORY_UI)