feat: Implement comprehensive lobby system with main menu, room management, and loading screen.

This commit is contained in:
Yogi Wiguna
2026-03-17 12:02:20 +08:00
parent b877f94e34
commit 6eb6dfa20d
5 changed files with 71 additions and 37 deletions
+2 -7
View File
@@ -31,13 +31,8 @@ border_width_right = 5
border_width_bottom = 5
border_color = Color(0, 0, 0, 1)
[node name="loading_screen" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="loading_screen" type="CanvasLayer"]
layer = 128
script = ExtResource("1_u2jrd")
[node name="Bg" type="TextureRect" parent="."]
+2 -1
View File
@@ -708,8 +708,9 @@ func _on_room_left() -> void:
connection_status.text = "Left room"
func _on_host_disconnected() -> void:
# Keep the connection status updated in the UI
connection_status.text = "Host disconnected. Returning to menu..."
_show_panel("main_menu")
connection_status.text = "Host disconnected. Match terminated."
func _on_player_joined(player_data: Dictionary) -> void:
_update_player_slots()
+49 -21
View File
@@ -460,6 +460,8 @@ func _setup_global_match_timer_ui():
add_child(panel)
func _process(delta):
if not is_inside_tree(): return
if not check_multiplayer(): return
if multiplayer.is_server() and GameStateManager.is_game_started():
if TurnManager.turn_based_mode:
rpc("sync_turn_index", TurnManager.current_turn_index)
@@ -528,7 +530,9 @@ func _setup_host_game():
# Moved _assign_random_spawn_positions() to after bot loop
# Wait for players to be fully ready (player.gd has 0.1s await in _ready before managers init)
await get_tree().create_timer(0.3).timeout
# Faster for LAN mode
var setup_delay = 0.1 if LobbyManager.is_lan_mode else 0.3
await get_tree().create_timer(setup_delay).timeout
# Set host goals - get goals directly from GoalManager
var host_goals = GoalManager.get_goals_for_player(0)
@@ -651,18 +655,20 @@ func _setup_client_game():
powerup_ui.setup(player_character)
print("Client: PowerUpInventoryUI setup forced for ", my_id)
# Wait shorter time for host to be ready, then request full sync to correct positions/state
await get_tree().create_timer(1.0).timeout
# Wait for host to be ready, then request full sync
# Snappier for LAN mode as peer is already established
var client_setup_delay = 0.2 if LobbyManager.is_lan_mode else 1.0
await get_tree().create_timer(client_setup_delay).timeout
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
if check_multiplayer():
# Ensure we see the server (Peer 1)
if 1 in multiplayer.get_peers():
rpc_id(1, "request_full_player_sync", my_id)
rpc_id(1, "request_full_grid_sync")
else:
print("Client: Connected but Peer 1 not found yet. Retrying in 1s...")
await get_tree().create_timer(1.0).timeout
if 1 in multiplayer.get_peers():
await get_tree().create_timer(0.5).timeout
if check_multiplayer() and 1 in multiplayer.get_peers():
rpc_id(1, "request_full_player_sync", my_id)
rpc_id(1, "request_full_grid_sync")
@@ -692,17 +698,19 @@ func _auto_start_from_lobby():
func _start_game():
if multiplayer.is_server():
# Wait for Nakama websocket to actually be open, up to 5 seconds
var nakama = get_node_or_null("/root/NakamaManager")
if nakama and nakama.has_method("is_connected_to_nakama"):
var wait_time = 0.0
while not nakama.is_connected_to_nakama() and wait_time < 5.0:
await get_tree().create_timer(0.2).timeout
wait_time += 0.2
# SKIP THIS FOR LAN MODE
if not LobbyManager.is_lan_mode:
var nakama = get_node_or_null("/root/NakamaManager")
if nakama and nakama.has_method("is_connected_to_nakama"):
var wait_time = 0.0
while not nakama.is_connected_to_nakama() and wait_time < 5.0:
await get_tree().create_timer(0.2).timeout
wait_time += 0.2
# Allow socket/peer to stabilize before blasting RPCs
# Snappier delay since we already waited for scene load
var delay = 0.2 if LobbyManager.is_lan_mode else 0.5
await get_tree().create_timer(delay).timeout
# Stabilization delay to allow clients to finish loading and spawning
# We wait 1.5s to ensure the 1.2s loading screen buffer has finished
# before the countdown starts.
await get_tree().create_timer(1.5).timeout
# NOW assign spawn positions for EVERYONE (Host, Client, Bots)
# This safely sends RPCs over the completed socket connection
@@ -1230,6 +1238,7 @@ func add_player_character(peer_id: int, is_bot: bool = false):
ui_manager.update_playerboard_ui()
func _on_peer_connected(new_peer_id: int):
if not is_inside_tree(): return
if multiplayer.is_server():
await get_tree().create_timer(0.1).timeout
add_player_character(new_peer_id)
@@ -1252,6 +1261,7 @@ func add_newly_connected_player_character(new_peer_id: int):
add_player_character(new_peer_id)
func _on_peer_disconnected(peer_id: int):
if not is_inside_tree(): return
if multiplayer.is_server():
print("[Main] Peer %d disconnected. Checking for bot replacement..." % peer_id)
@@ -1305,14 +1315,24 @@ func create_bot_with_state(bot_id: int, pos: Vector2i, p_score: int, p_goals: Ar
bot_character.update_player_position(pos)
func _on_host_disconnected():
"""Called when the host leaves. Returns clients to the main menu."""
if not is_inside_tree(): return
"""Called when the host leaves. Returns clients to the lobby."""
print("[Main] Host disconnected. Match terminated. Cleaning up and returning to lobby...")
LobbyManager.leave_room()
get_tree().change_scene_to_file("res://scenes/lobby.tscn")
# Use loading screen to return to lobby
var loading_screen_scene = load("res://scenes/loading_screen/loading_screen.tscn")
if loading_screen_scene:
var loading_screen = loading_screen_scene.instantiate()
get_tree().root.add_child(loading_screen)
loading_screen.load_level("res://scenes/lobby.tscn")
else:
get_tree().change_scene_to_file("res://scenes/lobby.tscn")
func _on_rematch_starting():
if not is_inside_tree(): return
"""Called when a rematch is triggered. Reloads the game scene."""
print("[Main] Rematch starting! Resetting state and reloading scene...")
print("[Main] Rematch starting. Resetting local state...")
# Reset singletons/managers that persist across scene reloads
GameStateManager.reset()
@@ -2439,8 +2459,7 @@ func _on_joystick_toggled(enabled: bool):
touch_controls._save_settings()
func can_rpc() -> bool:
if not multiplayer.has_multiplayer_peer(): return false
if multiplayer.multiplayer_peer.get_connection_status() != MultiplayerPeer.CONNECTION_CONNECTED: return false
if not check_multiplayer(): return false
if LobbyManager.is_lan_mode:
return true
@@ -2451,6 +2470,15 @@ func can_rpc() -> bool:
return true
func check_multiplayer() -> bool:
"""Safety check for multiplayer peer access."""
if not is_inside_tree(): return false
# Accessing multiplayer here is safe because we checked is_inside_tree
var peer = multiplayer.multiplayer_peer
if not peer: return false
if peer.get_connection_status() == MultiplayerPeer.CONNECTION_DISCONNECTED: return false
return true
@rpc("authority", "call_local", "reliable")
func display_message(message: String, type: int = 0):
"""Broadcasts a message to the local player's UI. This is called via main.rpc from various managers."""
+8 -6
View File
@@ -290,19 +290,21 @@ func leave_room() -> void:
reset()
_stop_lan_broadcast()
# Emit before nulling peer so UI can still access peer info if needed
emit_signal("room_left")
if is_lan_mode:
# LAN mode: just close the ENet peer directly
if multiplayer.has_multiplayer_peer():
multiplayer.set_multiplayer_peer(null)
is_lan_mode = false
# LAN mode: Host should keep peer alive long enough to reach lobby
if not is_host or get_tree().current_scene.name == "Lobby":
if multiplayer.has_multiplayer_peer():
multiplayer.set_multiplayer_peer(null)
is_lan_mode = false
else:
# Nakama mode: full Nakama cleanup
NakamaManager.cleanup()
# Important: Clean up game state as well to prevent ghost players
GameStateManager.reset()
emit_signal("room_left")
func refresh_room_list() -> void:
"""Request updated room list from Nakama or scan for LAN rooms."""
+10 -2
View File
@@ -1,4 +1,4 @@
extends Control
extends CanvasLayer
@export var tips: Array[String] = [
"Use your cards wisely!",
@@ -79,6 +79,14 @@ func change_scene(resource: PackedScene):
get_tree().change_scene_to_packed(resource)
# Update label to show we are initializing the game world
if scene_name_label:
scene_name_label.text = "Preparing Game..."
# Wait for assets (Tektons, Spawn Tiles) to initialize in the background
# This keeps the loading screen visible while main.gd runs its _ready() setup
await get_tree().create_timer(1.2).timeout
# Clean up self (Loading Screen)
queue_free()
@@ -86,7 +94,7 @@ func change_scene(resource: PackedScene):
func load_level(_path: String):
print("Starting load for: ", _path)
path = _path
show()
visible = true
content_control.show()
if scene_name_label: