From 71d688ed63db2026ac9e39d04725951434c3d25e Mon Sep 17 00:00:00 2001 From: adtpdn Date: Mon, 16 Mar 2026 05:21:49 +0800 Subject: [PATCH] feat: Implement core lobby system including UI, player management, and game mode settings. --- scenes/lobby.gd | 7 +++++ scenes/lobby.tscn | 8 +++++- scenes/main.gd | 34 ++++++++++++++++++------- scenes/portal_door.tscn | 5 ++++ scripts/managers/lobby_manager.gd | 28 +++++++++++++++++++- scripts/managers/playerboard_manager.gd | 13 +++++++++- scripts/managers/portal_mode_manager.gd | 2 +- 7 files changed, 84 insertions(+), 13 deletions(-) diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 73e9f62..517edca 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -7,6 +7,7 @@ extends Control @onready var browse_rooms_btn = $MainMenuPanel/VBoxContainer/ButtonSection/BrowseRoomsBtn @onready var main_menu_profile_btn = $MainMenuPanel/VBoxContainer/ButtonSection/ProfileBtn @onready var lobby_settings_btn = $MainMenuPanel/VBoxContainer/ButtonSection/SettingsBtn +@onready var quit_btn = $MainMenuPanel/VBoxContainer/ButtonSection/QuitBtn # UI References - Server Selection @onready var server_option = $MainMenuPanel/VBoxContainer/ServerSelectionSection/ServerOption @@ -142,6 +143,8 @@ func _ready(): lobby_settings_btn.pressed.connect(_on_settings_pressed) if leaderboard_btn: leaderboard_btn.pressed.connect(_on_leaderboard_pressed) + if quit_btn: + quit_btn.pressed.connect(_on_quit_pressed) # Connect Server Selection signals if server_option: @@ -585,6 +588,10 @@ func _on_logout_pressed() -> void: AuthManager.logout() _go_to_login() +func _on_quit_pressed() -> void: + print("[Lobby] Quitting game...") + get_tree().quit() + func _on_settings_pressed(): var settings_menu = get_node_or_null("SettingsMenu") if not settings_menu: diff --git a/scenes/lobby.tscn b/scenes/lobby.tscn index 3801b3d..2296eb8 100644 --- a/scenes/lobby.tscn +++ b/scenes/lobby.tscn @@ -151,11 +151,17 @@ theme_override_font_sizes/font_size = 16 text = "SETTINGS" [node name="ProfileBtn" type="Button" parent="MainMenuPanel/VBoxContainer/ButtonSection" unique_id=1640960506] -custom_minimum_size = Vector2(0, 36) layout_mode = 2 +custom_minimum_size = Vector2(0, 36) theme_override_font_sizes/font_size = 14 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] visible = false layout_mode = 1 diff --git a/scenes/main.gd b/scenes/main.gd index ada918e..2bebdd5 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -1286,7 +1286,8 @@ func create_bot_with_state(bot_id: int, pos: Vector2i, p_score: int, p_goals: Ar func _on_host_disconnected(): """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") func _on_rematch_starting(): @@ -1575,6 +1576,14 @@ func sync_grid_item(x: int, y: int, z: int, item: int): # OR Layer 1 is already a wall (4) if f0 in [4, -1] or f1 == 4: 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) # Force visual update @@ -1601,6 +1610,17 @@ func sync_grid_items_batch(data: Array): if f0 in [4, -1] or f1 == 4: 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) # 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.""" print("[Main] Returning to lobby...") - # Proper ordered cleanup to avoid ghost players - GameStateManager.end_game() - LobbyManager.reset() - - # Properly disconnect from Nakama match - _cleanup_multiplayer() + # Proper ordered cleanup to avoid ghost players and desync + LobbyManager.leave_room() # Small delay to let cleanup settle await get_tree().create_timer(0.2).timeout @@ -2357,8 +2373,8 @@ func _on_settings_pressed(): func _on_quit_match_pressed(): get_tree().paused = false # Ensure unpaused when returning to menu - # Properly disconnect from Nakama match - _cleanup_multiplayer() + # Properly disconnect from Nakama and clear lobby state to prevent desync + LobbyManager.leave_room() # Return to lobby or main menu get_tree().change_scene_to_file("res://scenes/lobby.tscn") diff --git a/scenes/portal_door.tscn b/scenes/portal_door.tscn index df4882b..064c101 100644 --- a/scenes/portal_door.tscn +++ b/scenes/portal_door.tscn @@ -50,6 +50,11 @@ properties/3/replication_mode = 2 [node name="PortalDoor" type="StaticBody3D"] 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="."] replication_config = SubResource("SceneReplicationConfig_portal") diff --git a/scripts/managers/lobby_manager.gd b/scripts/managers/lobby_manager.gd index c8d2b58..d424fb7 100644 --- a/scripts/managers/lobby_manager.gd +++ b/scripts/managers/lobby_manager.gd @@ -144,6 +144,13 @@ func leave_room() -> void: """Leave the current room.""" 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 reset() @@ -214,6 +221,16 @@ func _check_all_ready() -> void: func is_all_ready() -> bool: 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 # ============================================================================= @@ -636,6 +653,14 @@ func _on_peer_connected(peer_id: int) -> void: func _on_peer_disconnected(peer_id: int) -> void: """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()): if players_in_room[i]["id"] == peer_id: players_in_room.remove_at(i) @@ -652,8 +677,9 @@ func _on_server_disconnected() -> void: print("[LobbyManager] Server (Host) disconnected. Terminating room...") disconnect_reason = "Host disconnected. Match terminated." rematch_votes.clear() - emit_signal("host_disconnected") + # Ensure full cleanup and state reset leave_room() + emit_signal("host_disconnected") # ============================================================================= # Rematch Logic diff --git a/scripts/managers/playerboard_manager.gd b/scripts/managers/playerboard_manager.gd index bb0bb59..4859193 100644 --- a/scripts/managers/playerboard_manager.gd +++ b/scripts/managers/playerboard_manager.gd @@ -370,7 +370,18 @@ func auto_put_item() -> bool: var pos = neighbor.position 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): - 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(): return false diff --git a/scripts/managers/portal_mode_manager.gd b/scripts/managers/portal_mode_manager.gd index f070aa7..3dfe68b 100644 --- a/scripts/managers/portal_mode_manager.gd +++ b/scripts/managers/portal_mode_manager.gd @@ -479,7 +479,7 @@ func _refresh_tiles(): var door_positions = [] for door in doors: 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)) for x in range(GRID_SIZE):