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 select_race_button = $Control/MainMenu/VBoxContainer/SelectRaceButton @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 select_class_scene: PackedScene = preload("res://scenes/select_class.tscn") var select_class_instance: Node = null # Race select overlay (before dungeon) var _race_select_standalone: bool = false # True when joiner picks race before Join (don't start game on confirm) var pending_auto_join_after_race_select: bool = false # --join --webrtc with no --race=: show race select first, then auto-join 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 var active_room_join_button: Button = null # Join button we're currently using (reset on fail) 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) if select_race_button: select_race_button.pressed.connect(_on_select_race_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]) elif arg.begins_with("--race="): var race_arg = arg.split("=")[1].strip_edges().to_lower() var gs = get_node_or_null("/root/GameState") if gs: if race_arg == "dwarf": gs.selected_race = "Dwarf" gs.skip_race_select = true LogManager.log("GameUI: Race set from argument: Dwarf (skip race select)", LogManager.CATEGORY_UI) elif race_arg == "elf": gs.selected_race = "Elf" gs.skip_race_select = true LogManager.log("GameUI: Race set from argument: Elf (skip race select)", LogManager.CATEGORY_UI) elif race_arg == "human": gs.selected_race = "Human" gs.skip_race_select = true LogManager.log("GameUI: Race set from argument: Human (skip race select)", LogManager.CATEGORY_UI) else: LogManager.log("GameUI: Ignoring invalid --race=" + race_arg + " (use dwarf, elf, or human)", LogManager.CATEGORY_UI) 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(): call_deferred("_show_race_select") 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): # WebRTC/WebSocket with no address: auto-join first room. If no --race=, show race select first. var gs = get_node_or_null("/root/GameState") if gs and gs.skip_race_select: # --race= was passed: skip race select and auto-join immediately LogManager.log("Auto-joining: No address provided, fetching available rooms (mode: " + str(current_mode) + ", race: " + gs.selected_race + ")...", LogManager.CATEGORY_UI) network_manager.set_local_player_count(local_count) is_auto_joining = true is_joining_attempt = true room_fetch_timer = Timer.new() room_fetch_timer.name = "RoomFetchTimer" room_fetch_timer.wait_time = 2.0 room_fetch_timer.timeout.connect(_retry_room_fetch) room_fetch_timer.autostart = false add_child(room_fetch_timer) 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() _start_room_fetch() else: # No --race=: show race select first; after they confirm we start auto-join LogManager.log("Auto-join with WebRTC/WebSocket: choose race first (no --race= passed)", LogManager.CATEGORY_UI) pending_auto_join_after_race_select = true _race_select_standalone = true call_deferred("_show_race_select_ui") 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: LogManager.log("GameUI: Ignoring rooms_fetched_display - still in auto-join mode", LogManager.CATEGORY_UI) return # Let auto-join handler take care of it LogManager.log("GameUI: Received rooms for display: " + str(rooms.size()) + " rooms", LogManager.CATEGORY_UI) # 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() # DON'T set is_auto_joining = false yet - wait until connection succeeds or fails # DON'T hide room fetch status UI yet - keep it visible in case join fails # Disconnect from auto-join handler (we'll connect to display handler if join fails) 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 # Reconnect to auto-join handler if not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_auto_join): network_manager.rooms_fetched.connect(_on_rooms_fetched_auto_join) # 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 # Note: We'll hide the UI and set is_auto_joining = false in _on_connection_succeeded pass else: # Join failed immediately - switch to display mode LogManager.log("Auto-join failed immediately, switching to room browser mode", LogManager.CATEGORY_UI) is_auto_joining = false # Connect to display handler if not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_display): network_manager.rooms_fetched.connect(_on_rooms_fetched_display) # Keep room fetch status UI visible (with refresh button) _show_room_fetch_status() # Fetch and display available rooms _show_loading_indicator() _start_room_fetch() func _start_pending_auto_join(): """Start auto-join flow after user chose race (--join --webrtc with no --race=).""" if main_menu: main_menu.visible = true # Room fetch status lives inside MainMenu var local_count = int(local_players_spinbox.value) if local_players_spinbox else 1 network_manager.set_local_player_count(local_count) is_auto_joining = true is_joining_attempt = true if not room_fetch_timer: room_fetch_timer = Timer.new() room_fetch_timer.name = "RoomFetchTimer" room_fetch_timer.wait_time = 2.0 room_fetch_timer.timeout.connect(_retry_room_fetch) room_fetch_timer.autostart = false add_child(room_fetch_timer) 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() _start_room_fetch() 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""" LogManager.log("GameUI: Refresh button pressed", LogManager.CATEGORY_UI) # CRITICAL: Ensure we're not in auto-join mode when refreshing manually # This prevents _on_rooms_fetched_display from ignoring the signal if is_auto_joining: LogManager.log("GameUI: Switching from auto-join to display mode for refresh", LogManager.CATEGORY_UI) is_auto_joining = false # Stop auto-join timer if it's running if room_fetch_timer: room_fetch_timer.stop() # Disconnect auto-join handler if network_manager and network_manager.rooms_fetched.is_connected(_on_rooms_fetched_auto_join): network_manager.rooms_fetched.disconnect(_on_rooms_fetched_auto_join) # Ensure display handler is connected (in case it was disconnected) if network_manager and not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_display): LogManager.log("GameUI: Reconnecting rooms_fetched signal to display handler", LogManager.CATEGORY_UI) network_manager.rooms_fetched.connect(_on_rooms_fetched_display) # 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""" # Try to find the label if it's null (might not be ready yet) if not last_fetch_label: last_fetch_label = get_node_or_null("Control/MainMenu/VBoxContainer/RoomFetchStatusContainer/LastFetchLabel") 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 LogManager.log("GameUI: Updated last fetch time to: " + time_str, LogManager.CATEGORY_UI) else: LogManager.log_error("GameUI: Cannot update last fetch time - last_fetch_label is null!") 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.name = "JoinButton" room_join_button.text = "Join" room_join_button.custom_minimum_size = Vector2(80, 0) room_join_button.pressed.connect(func(): _join_room(room_code, room_join_button)) room_row.add_child(room_join_button) room_list_container.add_child(room_row) func _disable_all_room_join_buttons(): """Disable all room Join buttons to prevent multiple clicks""" if not room_list_container: return for row in room_list_container.get_children(): if row.name.begins_with("RoomRow_"): var btn = row.get_node_or_null("JoinButton") if btn and is_instance_valid(btn): btn.disabled = true func _reset_room_join_buttons(): """Re-enable all room Join buttons and restore 'Join' text""" if not room_list_container: return for row in room_list_container.get_children(): if row.name.begins_with("RoomRow_"): var btn = row.get_node_or_null("JoinButton") if btn and is_instance_valid(btn): btn.disabled = false btn.text = "Join" active_room_join_button = null func _join_room(room_code: String, room_join_button: Button = null): """Join a room by setting the address and clicking join""" if room_code.is_empty(): return # Prevent multiple clicks: disable all join buttons and show loader state _disable_all_room_join_buttons() if room_join_button and is_instance_valid(room_join_button): room_join_button.text = "Joining..." active_room_join_button = room_join_button # 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) is_joining_attempt = true last_join_address = room_code # Join the game if not network_manager.join_game(room_code): _reset_room_join_buttons() is_joining_attempt = false return 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) # Handle room fetching based on mode if actual_mode == 1: # WebRTC mode # Only fetch if not auto-joining and not hosting if not is_auto_joining and not is_hosting and not network_manager.is_hosting: LogManager.log("GameUI: Switched to WebRTC mode, fetching rooms", LogManager.CATEGORY_UI) # Ensure display handler is connected if network_manager and not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_display): network_manager.rooms_fetched.connect(_on_rooms_fetched_display) # Show room fetch status UI (with refresh button) _show_room_fetch_status() # Fetch rooms _start_room_fetch() else: LogManager.log("GameUI: Switched to WebRTC mode but skipping room fetch (auto_joining: " + str(is_auto_joining) + ", hosting: " + str(is_hosting) + ")", LogManager.CATEGORY_UI) else: # Not WebRTC mode (ENet or WebSocket) # Hide room fetch status if switching away from WebRTC LogManager.log("GameUI: Switched away from WebRTC mode, hiding room fetch UI", LogManager.CATEGORY_UI) _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") _show_race_select() 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 # If we were in auto-join mode, now we can safely exit it if is_auto_joining: is_auto_joining = false # Hide room fetch status UI since we're connecting _hide_room_fetch_status() # Disconnect from auto-join handler if still connected if network_manager.rooms_fetched.is_connected(_on_rooms_fetched_auto_join): network_manager.rooms_fetched.disconnect(_on_rooms_fetched_auto_join) # 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 # Joiner must load game_world immediately so host's spawn RPCs can be processed. Use race from # GameState (set in _on_race_selected when they picked; do not overwrite). No race select after connect. var gs = get_node_or_null("/root/GameState") if gs: gs.race_chosen_before_connect = false # Only default to Dwarf if nothing was ever set (e.g. --join --webrtc --race=elf skips UI) if gs.selected_race.is_empty(): gs.selected_race = "Dwarf" print("GameUI: Connection succeeded, starting game - GameState.selected_race = '", gs.selected_race if gs else "Dwarf", "' (joiner will use this for their player)") LogManager.log("GameUI: Connection succeeded, starting game (race: " + (gs.selected_race if gs else "Dwarf") + ")", LogManager.CATEGORY_UI) call_deferred("_start_game") func _on_connection_failed(): LogManager.log("Connection failed", LogManager.CATEGORY_UI) # Always reset room Join buttons on failure (they may be stuck on "Joining...") if is_joining_attempt: _reset_room_join_buttons() 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 we were in auto-join mode, switch to display mode and show rooms if is_auto_joining: LogManager.log("Connection failed during auto-join, switching to room browser mode", LogManager.CATEGORY_UI) is_auto_joining = false # Stop auto-join timer if room_fetch_timer: room_fetch_timer.stop() # Disconnect from auto-join handler if network_manager.rooms_fetched.is_connected(_on_rooms_fetched_auto_join): network_manager.rooms_fetched.disconnect(_on_rooms_fetched_auto_join) # Connect to display handler instead if not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_display): network_manager.rooms_fetched.connect(_on_rooms_fetched_display) # Show room fetch status UI (refresh button and room list) _show_room_fetch_status() # Fetch and display available rooms _show_loading_indicator() _start_room_fetch() # Show error message _show_connection_error("Failed to auto-join room. Showing available rooms below.") return 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() # Also connect to display handler if not already connected if not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_display): network_manager.rooms_fetched.connect(_on_rooms_fetched_display) 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 _on_select_race_pressed(): """Joiner picks race before connecting. Show race select; on confirm we just store choice and return to menu.""" _race_select_standalone = true _show_race_select_ui() func _show_race_select(): """Show race select before dungeon (after Host or before Join). User picks race; on confirm we start game.""" if not is_inside_tree(): return var gs = get_node_or_null("/root/GameState") if gs and gs.skip_race_select: LogManager.log("GameUI: Skipping race select (race from args: " + gs.selected_race + ")", LogManager.CATEGORY_UI) _start_game() return _race_select_standalone = false _show_race_select_ui() func _show_race_select_ui(): """Show race select overlay. _race_select_standalone: true = joiner picking before Join (don't start game).""" if not is_inside_tree(): return if select_class_instance and is_instance_valid(select_class_instance): return if main_menu: main_menu.visible = false var sel = select_class_scene.instantiate() add_child(sel) select_class_instance = sel if sel.has_signal("race_selected"): sel.race_selected.connect(_on_race_selected) LogManager.log("GameUI: Race select shown" + (" (choose before Join)" if _race_select_standalone else ""), LogManager.CATEGORY_UI) func _on_race_selected(race_name: String): """User confirmed race. Standalone = joiner chose before Join: store and return to menu. Else start game.""" if select_class_instance and is_instance_valid(select_class_instance): select_class_instance.queue_free() select_class_instance = null # Always persist chosen race to GameState (select_class sets it too; this guarantees it from signal) var gs = get_node_or_null("/root/GameState") if gs and not race_name.is_empty(): gs.selected_race = race_name LogManager.log("GameUI: JOINER PICKED race='" + race_name + "', GameState.selected_race set to '" + gs.selected_race + "'", LogManager.CATEGORY_UI) if _race_select_standalone: _race_select_standalone = false if gs: gs.race_chosen_before_connect = true if pending_auto_join_after_race_select: # --join --webrtc with no --race=: they just chose race, now start auto-join pending_auto_join_after_race_select = false _start_pending_auto_join() LogManager.log("GameUI: Race chosen (" + race_name + "), fetching rooms to auto-join...", LogManager.CATEGORY_UI) else: if main_menu: main_menu.visible = true LogManager.log("GameUI: Race chosen for joining (" + race_name + "). Press Join when ready.", LogManager.CATEGORY_UI) return _start_game() 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 # Disconnect network callbacks so we don't get signals on freed game_ui after scene change if network_manager: if network_manager.connection_succeeded.is_connected(_on_connection_succeeded): network_manager.connection_succeeded.disconnect(_on_connection_succeeded) if network_manager.connection_failed.is_connected(_on_connection_failed): network_manager.connection_failed.disconnect(_on_connection_failed) # Hide menu (and race select if still present) if main_menu: main_menu.visible = false if select_class_instance and is_instance_valid(select_class_instance): select_class_instance.queue_free() select_class_instance = null # 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)