Files
DungeonsOfKharadum/src/scripts/game_ui.gd

658 lines
24 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 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)