added so you can choose race when starting the game.

This commit is contained in:
2026-01-30 08:31:33 +01:00
parent dabec8a119
commit 3b2af36231
73 changed files with 4241 additions and 1107 deletions

View File

@@ -5,6 +5,7 @@ extends CanvasLayer
@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
@@ -16,17 +17,22 @@ extends CanvasLayer
@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 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)
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
@@ -49,6 +55,8 @@ func _ready():
# 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:
@@ -87,7 +95,7 @@ func _ready():
# 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
if current_mode == 1: # WebRTC
_start_room_fetch()
func _check_command_line_args():
@@ -99,7 +107,7 @@ func _check_command_line_args():
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 join_address = "" # Empty by default - will fetch rooms if WebRTC/WebSocket
var local_count = 1
for arg in args:
@@ -119,17 +127,35 @@ func _check_command_line_args():
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
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
network_mode_option.selected = 0 # WebRTC is first option on web
else:
network_mode_option.selected = 1 # WebRTC is second option on native
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)
@@ -142,33 +168,39 @@ func _check_command_line_args():
# Auto-start based on arguments
if should_host:
is_hosting = true # Set flag so we don't fetch rooms
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()
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):
# 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
is_joining_attempt = true # Mark as joining attempt so connection failure handler works
# 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()
# 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
@@ -191,7 +223,7 @@ func _on_rooms_fetched_display(rooms: Array):
# 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
return # Let auto-join handler take care of it
LogManager.log("GameUI: Received rooms for display: " + str(rooms.size()) + " rooms", LogManager.CATEGORY_UI)
@@ -224,7 +256,7 @@ func _on_rooms_fetched_display(rooms: Array):
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
return # Not in auto-join mode, ignore
# Hide loading indicator - request completed
_hide_loading_indicator()
@@ -291,6 +323,26 @@ func _on_rooms_fetched_auto_join(rooms: Array):
_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:
@@ -304,7 +356,7 @@ func _retry_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
if network_manager.network_mode != 1: # Not WebRTC
return
if is_hosting or network_manager.is_hosting:
@@ -357,7 +409,7 @@ func _hide_room_fetch_status():
func _create_refresh_button():
"""Create a refresh button for manually reloading the room list"""
if refresh_button:
return # Already exists
return # Already exists
if not room_fetch_status_container:
return
@@ -448,7 +500,7 @@ func _update_last_fetch_time():
func _create_room_list_container():
"""Create the container for displaying available rooms"""
if room_list_container:
return # Already exists
return # Already exists
if not room_fetch_status_container:
return
@@ -456,7 +508,7 @@ func _create_room_list_container():
# 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.custom_minimum_size = Vector2(0, 150) # Set a reasonable height
scroll_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
# Create VBoxContainer inside scroll container
@@ -579,7 +631,7 @@ func _on_network_mode_changed(index: int):
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)
actual_mode = index + 1 # Map 0->1 (WebRTC), 1->2 (WebSocket)
else:
# Native builds: 0 = ENet, 1 = WebRTC, 2 = WebSocket
actual_mode = index
@@ -589,18 +641,18 @@ func _on_network_mode_changed(index: int):
# Update address input placeholder based on mode
if address_input:
match actual_mode:
0: # ENet
0: # ENet
address_input.placeholder_text = "Server IP or domain"
1: # WebRTC
1: # WebRTC
address_input.placeholder_text = "Enter Room Code (e.g., ABC123)"
2: # WebSocket
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
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)
@@ -613,26 +665,26 @@ func _on_network_mode_changed(index: int):
_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)
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
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
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()
_show_race_select()
func _on_join_pressed():
# Reset error state when attempting new connection
@@ -642,10 +694,10 @@ func _on_join_pressed():
var address = address_input.text
if address.is_empty():
var mode = network_manager.network_mode
if mode == 1 or mode == 2: # WebRTC or WebSocket
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
else: # ENet mode without address - use default
address = "127.0.0.1"
var local_count = int(local_players_spinbox.value)
@@ -654,11 +706,11 @@ func _on_join_pressed():
if network_manager.join_game(address):
last_join_address = address
var mode = network_manager.network_mode
if mode == 1: # WebRTC
if mode == 1: # WebRTC
LogManager.log("Joining WebRTC game with room code: " + address, LogManager.CATEGORY_UI)
elif mode == 2: # WebSocket
elif mode == 2: # WebSocket
LogManager.log("Joining WebSocket game with room code: " + address, LogManager.CATEGORY_UI)
else: # ENet
else: # ENet
LogManager.log("Joining ENet game at " + address + " with " + str(local_count) + " local players", LogManager.CATEGORY_UI)
func _on_connection_succeeded():
@@ -678,7 +730,16 @@ func _on_connection_succeeded():
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
# 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():
@@ -757,7 +818,7 @@ func _show_connection_error(message: String):
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)
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
@@ -769,15 +830,83 @@ func _hide_connection_error():
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
# Hide menu
# 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()