Files
DungeonsOfKharadum/src/scripts/game_ui.gd

917 lines
37 KiB
GDScript

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)