feat: Implement core lobby system including UI, player management, and game mode settings.
This commit is contained in:
@@ -7,6 +7,7 @@ extends Control
|
|||||||
@onready var browse_rooms_btn = $MainMenuPanel/VBoxContainer/ButtonSection/BrowseRoomsBtn
|
@onready var browse_rooms_btn = $MainMenuPanel/VBoxContainer/ButtonSection/BrowseRoomsBtn
|
||||||
@onready var main_menu_profile_btn = $MainMenuPanel/VBoxContainer/ButtonSection/ProfileBtn
|
@onready var main_menu_profile_btn = $MainMenuPanel/VBoxContainer/ButtonSection/ProfileBtn
|
||||||
@onready var lobby_settings_btn = $MainMenuPanel/VBoxContainer/ButtonSection/SettingsBtn
|
@onready var lobby_settings_btn = $MainMenuPanel/VBoxContainer/ButtonSection/SettingsBtn
|
||||||
|
@onready var quit_btn = $MainMenuPanel/VBoxContainer/ButtonSection/QuitBtn
|
||||||
|
|
||||||
# UI References - Server Selection
|
# UI References - Server Selection
|
||||||
@onready var server_option = $MainMenuPanel/VBoxContainer/ServerSelectionSection/ServerOption
|
@onready var server_option = $MainMenuPanel/VBoxContainer/ServerSelectionSection/ServerOption
|
||||||
@@ -142,6 +143,8 @@ func _ready():
|
|||||||
lobby_settings_btn.pressed.connect(_on_settings_pressed)
|
lobby_settings_btn.pressed.connect(_on_settings_pressed)
|
||||||
if leaderboard_btn:
|
if leaderboard_btn:
|
||||||
leaderboard_btn.pressed.connect(_on_leaderboard_pressed)
|
leaderboard_btn.pressed.connect(_on_leaderboard_pressed)
|
||||||
|
if quit_btn:
|
||||||
|
quit_btn.pressed.connect(_on_quit_pressed)
|
||||||
|
|
||||||
# Connect Server Selection signals
|
# Connect Server Selection signals
|
||||||
if server_option:
|
if server_option:
|
||||||
@@ -585,6 +588,10 @@ func _on_logout_pressed() -> void:
|
|||||||
AuthManager.logout()
|
AuthManager.logout()
|
||||||
_go_to_login()
|
_go_to_login()
|
||||||
|
|
||||||
|
func _on_quit_pressed() -> void:
|
||||||
|
print("[Lobby] Quitting game...")
|
||||||
|
get_tree().quit()
|
||||||
|
|
||||||
func _on_settings_pressed():
|
func _on_settings_pressed():
|
||||||
var settings_menu = get_node_or_null("SettingsMenu")
|
var settings_menu = get_node_or_null("SettingsMenu")
|
||||||
if not settings_menu:
|
if not settings_menu:
|
||||||
|
|||||||
+7
-1
@@ -151,11 +151,17 @@ theme_override_font_sizes/font_size = 16
|
|||||||
text = "SETTINGS"
|
text = "SETTINGS"
|
||||||
|
|
||||||
[node name="ProfileBtn" type="Button" parent="MainMenuPanel/VBoxContainer/ButtonSection" unique_id=1640960506]
|
[node name="ProfileBtn" type="Button" parent="MainMenuPanel/VBoxContainer/ButtonSection" unique_id=1640960506]
|
||||||
custom_minimum_size = Vector2(0, 36)
|
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(0, 36)
|
||||||
theme_override_font_sizes/font_size = 14
|
theme_override_font_sizes/font_size = 14
|
||||||
text = "PROFILE"
|
text = "PROFILE"
|
||||||
|
|
||||||
|
[node name="QuitBtn" type="Button" parent="MainMenuPanel/VBoxContainer/ButtonSection" unique_id=123456780]
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(0, 36)
|
||||||
|
theme_override_font_sizes/font_size = 14
|
||||||
|
text = "QUIT GAME"
|
||||||
|
|
||||||
[node name="RoomListPanel" type="PanelContainer" parent="." unique_id=1782359692]
|
[node name="RoomListPanel" type="PanelContainer" parent="." unique_id=1782359692]
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
|
|||||||
+25
-9
@@ -1286,7 +1286,8 @@ func create_bot_with_state(bot_id: int, pos: Vector2i, p_score: int, p_goals: Ar
|
|||||||
|
|
||||||
func _on_host_disconnected():
|
func _on_host_disconnected():
|
||||||
"""Called when the host leaves. Returns clients to the main menu."""
|
"""Called when the host leaves. Returns clients to the main menu."""
|
||||||
print("[Main] Host disconnected. Match terminated. Returning to 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")
|
get_tree().change_scene_to_file("res://scenes/lobby.tscn")
|
||||||
|
|
||||||
func _on_rematch_starting():
|
func _on_rematch_starting():
|
||||||
@@ -1576,6 +1577,14 @@ func sync_grid_item(x: int, y: int, z: int, item: int):
|
|||||||
if f0 in [4, -1] or f1 == 4:
|
if f0 in [4, -1] or f1 == 4:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# TEKTON DOORS: Prevent placing items on portal doors
|
||||||
|
if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS):
|
||||||
|
var doors = get_tree().get_nodes_in_group("PortalDoors")
|
||||||
|
for door in doors:
|
||||||
|
var door_grid = enhanced_gridmap.local_to_map(enhanced_gridmap.to_local(door.global_position))
|
||||||
|
if door_grid.x == x and door_grid.z == z:
|
||||||
|
return
|
||||||
|
|
||||||
enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item)
|
enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item)
|
||||||
# Force visual update
|
# Force visual update
|
||||||
if enhanced_gridmap.has_method("update_grid_data"):
|
if enhanced_gridmap.has_method("update_grid_data"):
|
||||||
@@ -1601,6 +1610,17 @@ func sync_grid_items_batch(data: Array):
|
|||||||
if f0 in [4, -1] or f1 == 4:
|
if f0 in [4, -1] or f1 == 4:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# TEKTON DOORS: Prevent placing items on portal doors
|
||||||
|
if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS) and y == 1:
|
||||||
|
var doors = get_tree().get_nodes_in_group("PortalDoors")
|
||||||
|
var on_door = false
|
||||||
|
for door in doors:
|
||||||
|
var door_grid = enhanced_gridmap.local_to_map(enhanced_gridmap.to_local(door.global_position))
|
||||||
|
if door_grid.x == x and door_grid.z == z:
|
||||||
|
on_door = true
|
||||||
|
break
|
||||||
|
if on_door: continue
|
||||||
|
|
||||||
enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item)
|
enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item)
|
||||||
|
|
||||||
# Force visual update ONCE after batch
|
# Force visual update ONCE after batch
|
||||||
@@ -2084,12 +2104,8 @@ func _on_back_to_menu_pressed():
|
|||||||
"""Return to lobby/main menu and clean up game state."""
|
"""Return to lobby/main menu and clean up game state."""
|
||||||
print("[Main] Returning to lobby...")
|
print("[Main] Returning to lobby...")
|
||||||
|
|
||||||
# Proper ordered cleanup to avoid ghost players
|
# Proper ordered cleanup to avoid ghost players and desync
|
||||||
GameStateManager.end_game()
|
LobbyManager.leave_room()
|
||||||
LobbyManager.reset()
|
|
||||||
|
|
||||||
# Properly disconnect from Nakama match
|
|
||||||
_cleanup_multiplayer()
|
|
||||||
|
|
||||||
# Small delay to let cleanup settle
|
# Small delay to let cleanup settle
|
||||||
await get_tree().create_timer(0.2).timeout
|
await get_tree().create_timer(0.2).timeout
|
||||||
@@ -2357,8 +2373,8 @@ func _on_settings_pressed():
|
|||||||
|
|
||||||
func _on_quit_match_pressed():
|
func _on_quit_match_pressed():
|
||||||
get_tree().paused = false # Ensure unpaused when returning to menu
|
get_tree().paused = false # Ensure unpaused when returning to menu
|
||||||
# Properly disconnect from Nakama match
|
# Properly disconnect from Nakama and clear lobby state to prevent desync
|
||||||
_cleanup_multiplayer()
|
LobbyManager.leave_room()
|
||||||
# Return to lobby or main menu
|
# Return to lobby or main menu
|
||||||
get_tree().change_scene_to_file("res://scenes/lobby.tscn")
|
get_tree().change_scene_to_file("res://scenes/lobby.tscn")
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ properties/3/replication_mode = 2
|
|||||||
[node name="PortalDoor" type="StaticBody3D"]
|
[node name="PortalDoor" type="StaticBody3D"]
|
||||||
script = ExtResource("1_script")
|
script = ExtResource("1_script")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.1, 0)
|
||||||
|
shape = SubResource("BoxShape3D_trigger")
|
||||||
|
|
||||||
|
|
||||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
|
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
|
||||||
replication_config = SubResource("SceneReplicationConfig_portal")
|
replication_config = SubResource("SceneReplicationConfig_portal")
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,13 @@ func leave_room() -> void:
|
|||||||
"""Leave the current room."""
|
"""Leave the current room."""
|
||||||
print("[LobbyManager] Leaving room. Clearing all local state.")
|
print("[LobbyManager] Leaving room. Clearing all local state.")
|
||||||
|
|
||||||
|
# If we are the host, notify all clients to kick them back to menu/lobby
|
||||||
|
if is_host and multiplayer.has_multiplayer_peer() and multiplayer.is_server():
|
||||||
|
print("[LobbyManager] Host is leaving. Kicking all clients...")
|
||||||
|
# We use rpc() instead of .rpc() for compatibility with older Godot 4 versions if applicable,
|
||||||
|
# but .rpc() is standard in 4.x. Let's stick to standard.
|
||||||
|
kick_all_clients.rpc()
|
||||||
|
|
||||||
# Important: Reset all lobby settings and player lists first
|
# Important: Reset all lobby settings and player lists first
|
||||||
reset()
|
reset()
|
||||||
|
|
||||||
@@ -214,6 +221,16 @@ func _check_all_ready() -> void:
|
|||||||
func is_all_ready() -> bool:
|
func is_all_ready() -> bool:
|
||||||
return _all_ready
|
return _all_ready
|
||||||
|
|
||||||
|
@rpc("authority", "call_local", "reliable")
|
||||||
|
func kick_all_clients() -> void:
|
||||||
|
"""Called on all clients when the host leaves to ensure they are returned to the menu."""
|
||||||
|
if not is_host:
|
||||||
|
print("[LobbyManager] Received kick from host. Returning to menu...")
|
||||||
|
disconnect_reason = "Host left the lobby. Room closed."
|
||||||
|
emit_signal("host_disconnected")
|
||||||
|
# We use call_deferred to avoid potential issues during RPC stack processing
|
||||||
|
call_deferred("leave_room")
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Game Start
|
# Game Start
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -636,6 +653,14 @@ func _on_peer_connected(peer_id: int) -> void:
|
|||||||
|
|
||||||
func _on_peer_disconnected(peer_id: int) -> void:
|
func _on_peer_disconnected(peer_id: int) -> void:
|
||||||
"""Called when peer disconnects."""
|
"""Called when peer disconnects."""
|
||||||
|
print("Peer disconnected: ", peer_id)
|
||||||
|
|
||||||
|
# If the host (peer 1) disconnected and we are not host, we should be kicked
|
||||||
|
if peer_id == 1 and not is_host:
|
||||||
|
print("[LobbyManager] Host peer disconnected. Kicking self...")
|
||||||
|
_on_server_disconnected()
|
||||||
|
return
|
||||||
|
|
||||||
for i in range(players_in_room.size()):
|
for i in range(players_in_room.size()):
|
||||||
if players_in_room[i]["id"] == peer_id:
|
if players_in_room[i]["id"] == peer_id:
|
||||||
players_in_room.remove_at(i)
|
players_in_room.remove_at(i)
|
||||||
@@ -652,8 +677,9 @@ func _on_server_disconnected() -> void:
|
|||||||
print("[LobbyManager] Server (Host) disconnected. Terminating room...")
|
print("[LobbyManager] Server (Host) disconnected. Terminating room...")
|
||||||
disconnect_reason = "Host disconnected. Match terminated."
|
disconnect_reason = "Host disconnected. Match terminated."
|
||||||
rematch_votes.clear()
|
rematch_votes.clear()
|
||||||
emit_signal("host_disconnected")
|
# Ensure full cleanup and state reset
|
||||||
leave_room()
|
leave_room()
|
||||||
|
emit_signal("host_disconnected")
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Rematch Logic
|
# Rematch Logic
|
||||||
|
|||||||
@@ -370,7 +370,18 @@ func auto_put_item() -> bool:
|
|||||||
var pos = neighbor.position
|
var pos = neighbor.position
|
||||||
var cell_3d = Vector3i(pos.x, 1, pos.y)
|
var cell_3d = Vector3i(pos.x, 1, pos.y)
|
||||||
if enhanced_gridmap.get_cell_item(cell_3d) == -1 and not player.is_position_occupied(pos):
|
if enhanced_gridmap.get_cell_item(cell_3d) == -1 and not player.is_position_occupied(pos):
|
||||||
valid_put_positions.append(pos)
|
# TEKTON DOORS: Avoid portal doors
|
||||||
|
var is_on_portal = false
|
||||||
|
if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS):
|
||||||
|
var doors = get_tree().get_nodes_in_group("PortalDoors")
|
||||||
|
for door in doors:
|
||||||
|
var door_grid = enhanced_gridmap.local_to_map(enhanced_gridmap.to_local(door.global_position))
|
||||||
|
if Vector2i(door_grid.x, door_grid.z) == pos:
|
||||||
|
is_on_portal = true
|
||||||
|
break
|
||||||
|
|
||||||
|
if not is_on_portal:
|
||||||
|
valid_put_positions.append(pos)
|
||||||
|
|
||||||
if valid_put_positions.is_empty():
|
if valid_put_positions.is_empty():
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -479,7 +479,7 @@ func _refresh_tiles():
|
|||||||
var door_positions = []
|
var door_positions = []
|
||||||
for door in doors:
|
for door in doors:
|
||||||
if is_instance_valid(door):
|
if is_instance_valid(door):
|
||||||
var local_pos = gridmap.local_to_map(door.global_position)
|
var local_pos = gridmap.local_to_map(gridmap.to_local(door.global_position))
|
||||||
door_positions.append(Vector2i(local_pos.x, local_pos.z))
|
door_positions.append(Vector2i(local_pos.x, local_pos.z))
|
||||||
|
|
||||||
for x in range(GRID_SIZE):
|
for x in range(GRID_SIZE):
|
||||||
|
|||||||
Reference in New Issue
Block a user