From 49c8d794c21f7e09c2eee7019d3386c217c35ab0 Mon Sep 17 00:00:00 2001 From: Yogi Wiguna Date: Tue, 17 Mar 2026 10:32:14 +0800 Subject: [PATCH] feat: Introduce a comprehensive game lobby system including UI, room management, and player interactions. --- .../enhanced_gridmap/meshlibrary/default.tres | 2 - scenes/lobby.gd | 153 +++++++----------- scenes/lobby.tscn | 76 ++------- scenes/wall_3d.tscn | 3 - scripts/managers/lobby_manager.gd | 85 +++++++++- 5 files changed, 156 insertions(+), 163 deletions(-) diff --git a/addons/enhanced_gridmap/meshlibrary/default.tres b/addons/enhanced_gridmap/meshlibrary/default.tres index f86b4d4..0110c8f 100644 --- a/addons/enhanced_gridmap/meshlibrary/default.tres +++ b/addons/enhanced_gridmap/meshlibrary/default.tres @@ -9,7 +9,6 @@ [ext_resource type="ArrayMesh" uid="uid://cv4bedhida00g" path="res://assets/models/tiles/tile_star.tres" id="7_p5epg"] [ext_resource type="ArrayMesh" uid="uid://gpnl4cjrivor" path="res://assets/models/tiles/tile_speed.tres" id="7_sx8rm"] [ext_resource type="ArrayMesh" uid="uid://bfv8cw1vho5p5" path="res://assets/models/meshes/ancient_lightning_stones.res" id="8_cg50n"] -[ext_resource type="BoxMesh" uid="uid://fy4bhoeii40c" path="res://addons/enhanced_gridmap/meshlibrary/tile_safe_zone.tres" id="8_uwjsj"] [ext_resource type="BoxMesh" uid="uid://dy5p77cjb3geo" path="res://addons/enhanced_gridmap/meshlibrary/tile_start.tres" id="9_pgnbl"] [ext_resource type="ArrayMesh" uid="uid://dtr46jmckif0p" path="res://assets/models/meshes/block.res" id="9_uwjsj"] [ext_resource type="Texture2D" uid="uid://dpkx1a780pvwv" path="res://assets/textures/tile_diamond.png" id="10_sx8rm"] @@ -81,7 +80,6 @@ item/1/shapes = [] item/1/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) item/1/navigation_layers = 1 item/2/name = "safe_zone" -item/2/mesh = ExtResource("8_uwjsj") item/2/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) item/2/mesh_cast_shadow = 1 item/2/shapes = [] diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 1179560..aed0209 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -2,7 +2,6 @@ extends Control # UI References - Main Menu @onready var main_menu_panel = $MainMenuPanel -@onready var player_name_input = $MainMenuPanel/VBoxContainer/InputSection/PlayerNameInput @onready var create_room_btn = $MainMenuPanel/VBoxContainer/ButtonSection/CreateRoomBtn @onready var browse_rooms_btn = $MainMenuPanel/VBoxContainer/ButtonSection/BrowseRoomsBtn @onready var main_menu_profile_btn = $MainMenuPanel/VBoxContainer/ButtonSection/ProfileBtn @@ -12,10 +11,6 @@ extends Control # UI References - Server Selection @onready var server_option = $MainMenuPanel/VBoxContainer/ServerSelectionSection/ServerOption @onready var server_ip_input = $MainMenuPanel/VBoxContainer/ServerSelectionSection/ServerIPInput -@onready var lan_section = $MainMenuPanel/VBoxContainer/ServerSelectionSection/LANSection -@onready var lan_ip_input = $MainMenuPanel/VBoxContainer/ServerSelectionSection/LANSection/LANIPInput -@onready var lan_host_btn = $MainMenuPanel/VBoxContainer/ServerSelectionSection/LANSection/LANHostBtn -@onready var lan_join_btn = $MainMenuPanel/VBoxContainer/ServerSelectionSection/LANSection/LANJoinBtn # Leaderboard Reference @onready var leaderboard_btn = $MainMenuPanel/VBoxContainer/ButtonSection/LeaderboardBtn @@ -119,24 +114,11 @@ func _ready(): # Setup Game Mode specific UI dynamically _create_custom_settings_ui() - # Set player name from profile and configure input visibility - if player_name_input: - # Get the parent container for the input to hide/show properly - var input_section = player_name_input.get_parent() - - if AuthManager.is_guest: - # Guest user - show name input and let them enter a name - if input_section: - input_section.visible = true - player_name_input.text = "Guest" - player_name_input.editable = true - else: - # Logged-in user - hide name input and use profile name automatically - if input_section: - input_section.visible = false - player_name_input.text = UserProfileManager.get_display_name() - # Also set the LobbyManager name immediately - LobbyManager.local_player_name = UserProfileManager.get_display_name() + # Set player name from profile + if AuthManager.is_guest: + LobbyManager.local_player_name = "Guest" + else: + LobbyManager.local_player_name = UserProfileManager.get_display_name() # Connect button signals - Main Menu create_room_btn.pressed.connect(_on_create_room_pressed) @@ -158,10 +140,6 @@ func _ready(): if server_ip_input: server_ip_input.text_submitted.connect(_on_server_ip_submitted) server_ip_input.focus_exited.connect(func(): _on_server_ip_submitted(server_ip_input.text)) - if lan_host_btn: - lan_host_btn.pressed.connect(_on_lan_host_pressed) - if lan_join_btn: - lan_join_btn.pressed.connect(func(): _on_lan_join_pressed(lan_ip_input.text if lan_ip_input else "127.0.0.1")) # Connect button signals - Room List refresh_btn.pressed.connect(_on_refresh_pressed) @@ -258,56 +236,22 @@ func _on_server_option_selected(index: int) -> void: if index == 0: # Nakama Localhost if server_ip_input: server_ip_input.visible = false - if lan_section: lan_section.visible = false NakamaManager.set_server("localhost") + LobbyManager.is_lan_mode = false elif index == 1: # Nakama Remote if server_ip_input: server_ip_input.visible = true - if lan_section: lan_section.visible = false if server_ip_input: NakamaManager.set_server(server_ip_input.text) + LobbyManager.is_lan_mode = false else: # LAN Direct if server_ip_input: server_ip_input.visible = false - if lan_section: lan_section.visible = true + LobbyManager.is_lan_mode = true func _on_server_ip_submitted(new_text: String) -> void: if server_option and server_option.selected == 1: NakamaManager.set_server(new_text.strip_edges()) -func _on_lan_host_pressed() -> void: - """Host a LAN game without Nakama.""" - var player_name = player_name_input.text.strip_edges() if player_name_input else "" - if player_name.is_empty(): - player_name = "Host" - LobbyManager.local_player_name = player_name - - if connection_status: - connection_status.text = "Starting LAN server..." - var ok = await LobbyManager.create_room_lan() - if not ok: - if connection_status: - connection_status.text = "Failed to start LAN server. Check port 7777." - -func _on_lan_join_pressed(host_ip: String) -> void: - """Join a LAN game by entering the host's IP.""" - var ip = host_ip.strip_edges() - if ip.is_empty(): - if connection_status: - connection_status.text = "Enter the host's IP address." - return - - var player_name = player_name_input.text.strip_edges() if player_name_input else "" - if player_name.is_empty(): - player_name = "Player" - LobbyManager.local_player_name = player_name - - if connection_status: - connection_status.text = "Connecting to %s..." % ip - var ok = LobbyManager.join_room_lan(ip) - if not ok: - if connection_status: - connection_status.text = "Failed to connect to %s. Is host running?" % ip - func _setup_game_modes() -> void: if not game_mode_option: return game_mode_option.clear() @@ -362,21 +306,35 @@ func _show_panel(panel_name: String) -> void: # ============================================================================= func _on_create_room_pressed() -> void: - # Use profile name for logged-in users, or input name for guests + # Use profile name for logged-in users, or guest for others if AuthManager.is_guest: - LobbyManager.local_player_name = player_name_input.text.strip_edges() - if LobbyManager.local_player_name.is_empty(): + if LobbyManager.local_player_name.is_empty() or LobbyManager.local_player_name == "Player": LobbyManager.local_player_name = "Guest" else: LobbyManager.local_player_name = UserProfileManager.get_display_name() - connection_status.text = "Creating room..." - LobbyManager.create_room("Room %d" % randi_range(1000, 9999)) + if LobbyManager.is_lan_mode: + connection_status.text = "Starting LAN room..." + var ok = await LobbyManager.create_room_lan("LAN Room " + str(randi_range(100, 999))) + if not ok: + connection_status.text = "Failed to start LAN room. Check port 7777." + else: + connection_status.text = "Creating Nakama room..." + LobbyManager.create_room("Room %d" % randi_range(1000, 9999)) func _on_browse_rooms_pressed() -> void: _show_panel("room_list") - connection_status.text = "Loading rooms..." - LobbyManager.refresh_room_list() + + if LobbyManager.is_lan_mode: + connection_status.text = "LAN Mode - Enter Host IP to join" + match_id_input.placeholder_text = "Enter Host IP (e.g. 192.168.1.10)..." + $RoomListPanel/VBoxContainer/MatchIdLabel.text = "DIRECT CONNECT (HOST IP)" + _on_refresh_pressed() # Try to discover rooms if implemented + else: + connection_status.text = "Loading Nakama rooms..." + match_id_input.placeholder_text = "Paste match ID here..." + $RoomListPanel/VBoxContainer/MatchIdLabel.text = "DIRECT CONNECT (MATCH ID)" + LobbyManager.refresh_room_list() # ============================================================================= # Room List Button Handlers @@ -390,7 +348,7 @@ func _on_refresh_pressed() -> void: func _on_join_pressed() -> void: var match_id = match_id_input.text.strip_edges() - if match_id.is_empty(): + if match_id.is_empty() and not LobbyManager.is_lan_mode: var selected_items = room_list.get_selected_items() if selected_items.size() == 0: connection_status.text = "Please select a room or enter Match ID" @@ -400,20 +358,36 @@ func _on_join_pressed() -> void: if selected_idx < LobbyManager.available_rooms.size(): match_id = LobbyManager.available_rooms[selected_idx].get("match_id", "") - if match_id.is_empty(): - connection_status.text = "No room selected" - return - - # Use profile name for logged-in users, or input name for guests + # Determine player name if AuthManager.is_guest: - LobbyManager.local_player_name = player_name_input.text.strip_edges() if LobbyManager.local_player_name.is_empty(): LobbyManager.local_player_name = "Guest" else: LobbyManager.local_player_name = UserProfileManager.get_display_name() - connection_status.text = "Joining room..." - LobbyManager.join_room(match_id) + if LobbyManager.is_lan_mode: + if match_id.is_empty(): + # If nothing entered but something selected in list (discovered), use it + var selected_items = room_list.get_selected_items() + if selected_items.size() > 0: + var idx = selected_items[0] + if idx < LobbyManager.available_rooms.size(): + match_id = LobbyManager.available_rooms[idx].get("ip", "") + + if match_id.is_empty(): + connection_status.text = "Enter Host IP to join" + return + + connection_status.text = "Connecting to %s..." % match_id + var ok = LobbyManager.join_room_lan(match_id) + if not ok: + connection_status.text = "Failed to connect to %s" % match_id + else: + if match_id.is_empty(): + connection_status.text = "No room selected" + return + connection_status.text = "Joining Nakama room..." + LobbyManager.join_room(match_id) func _on_back_pressed() -> void: _show_panel("main_menu") @@ -728,13 +702,6 @@ func _on_room_joined(room_data: Dictionary) -> void: _update_player_slots() connection_status.text = "Connected to room" - - # LAN solo mode: host is auto-ready, enable Start Game immediately - if LobbyManager.is_lan_mode and is_host: - ready_btn.button_pressed = true - ready_btn.text = "READY ✓" - LobbyManager.force_solo_ready() - status_label.text = "LAN Solo — press Start Game when ready!" func _on_room_left() -> void: _show_panel("main_menu") @@ -758,8 +725,12 @@ func _on_ready_state_changed(_player_id: int, _is_ready: bool) -> void: func _on_all_players_ready() -> void: if LobbyManager.is_host: - start_game_btn.disabled = false - status_label.text = "All ready! Start the match!" + if LobbyManager.is_lan_mode and LobbyManager.players_in_room.size() == 1: + # Auto-start for solo LAN testing + LobbyManager.start_game() + else: + start_game_btn.disabled = false + status_label.text = "All ready! Start the match!" else: status_label.text = "All ready! Waiting for host..." @@ -801,10 +772,6 @@ func _on_profile_updated() -> void: """Handle profile updates (name/avatar change).""" var new_name = UserProfileManager.get_display_name() - # Update input if visible - if player_name_input: - player_name_input.text = new_name - # Sync to LobbyManager if we are in a room or just locally LobbyManager.set_player_name(new_name) diff --git a/scenes/lobby.tscn b/scenes/lobby.tscn index 1c09d54..a1badd8 100644 --- a/scenes/lobby.tscn +++ b/scenes/lobby.tscn @@ -43,10 +43,10 @@ anchor_left = 0.5 anchor_top = 0.5 anchor_right = 0.5 anchor_bottom = 0.5 -offset_left = -220.0 -offset_top = -240.0 -offset_right = 220.0 -offset_bottom = 240.0 +offset_left = -221.0 +offset_top = -320.0 +offset_right = 219.0 +offset_bottom = 286.0 grow_horizontal = 2 grow_vertical = 2 @@ -76,22 +76,6 @@ horizontal_alignment = 1 [node name="Separator" type="HSeparator" parent="MainMenuPanel/VBoxContainer" unique_id=126990892] layout_mode = 2 -[node name="InputSection" type="VBoxContainer" parent="MainMenuPanel/VBoxContainer" unique_id=1865748579] -layout_mode = 2 -theme_override_constants/separation = 10 - -[node name="PlayerNameLabel" type="Label" parent="MainMenuPanel/VBoxContainer/InputSection" unique_id=1017736748] -layout_mode = 2 -theme_override_colors/font_color = Color(0.69, 0.529, 0.357, 1) -theme_override_font_sizes/font_size = 13 -text = "YOUR NAME" - -[node name="PlayerNameInput" type="LineEdit" parent="MainMenuPanel/VBoxContainer/InputSection" unique_id=1668571796] -custom_minimum_size = Vector2(0, 44) -layout_mode = 2 -text = "Player" -placeholder_text = "Enter your name..." - [node name="ServerSelectionSection" type="VBoxContainer" parent="MainMenuPanel/VBoxContainer" unique_id=748392101] layout_mode = 2 theme_override_constants/separation = 10 @@ -121,43 +105,6 @@ layout_mode = 2 text = "127.0.0.1" placeholder_text = "Enter Nakama Server IP..." -[node name="LANSection" type="VBoxContainer" parent="MainMenuPanel/VBoxContainer/ServerSelectionSection" unique_id=748392110] -visible = false -layout_mode = 2 -theme_override_constants/separation = 8 - -[node name="LANInfo" type="Label" parent="MainMenuPanel/VBoxContainer/ServerSelectionSection/LANSection" unique_id=748392111] -layout_mode = 2 -theme_override_colors/font_color = Color(0.7, 0.7, 0.7, 1) -theme_override_font_sizes/font_size = 12 -text = "Play over LAN without any server.\nFirewall may need to allow port 7777." -autowrap_mode = 3 - -[node name="LANHostBtn" type="Button" parent="MainMenuPanel/VBoxContainer/ServerSelectionSection/LANSection" unique_id=748392112] -custom_minimum_size = Vector2(0, 44) -layout_mode = 2 -theme_override_font_sizes/font_size = 14 -text = "HOST LAN GAME" - -[node name="LANOrLabel" type="Label" parent="MainMenuPanel/VBoxContainer/ServerSelectionSection/LANSection" unique_id=748392113] -layout_mode = 2 -theme_override_colors/font_color = Color(0.5, 0.5, 0.5, 1) -theme_override_font_sizes/font_size = 11 -text = "── or join a friend ──" -horizontal_alignment = 1 - -[node name="LANIPInput" type="LineEdit" parent="MainMenuPanel/VBoxContainer/ServerSelectionSection/LANSection" unique_id=748392114] -custom_minimum_size = Vector2(0, 44) -layout_mode = 2 -text = "127.0.0.1" -placeholder_text = "Host IP (e.g. 192.168.1.10)" - -[node name="LANJoinBtn" type="Button" parent="MainMenuPanel/VBoxContainer/ServerSelectionSection/LANSection" unique_id=748392115] -custom_minimum_size = Vector2(0, 44) -layout_mode = 2 -theme_override_font_sizes/font_size = 14 -text = "JOIN LAN GAME" - [node name="ServerSeparator" type="HSeparator" parent="MainMenuPanel/VBoxContainer" unique_id=748392105] layout_mode = 2 @@ -177,7 +124,7 @@ layout_mode = 2 theme_override_font_sizes/font_size = 16 text = "BROWSE ROOMS" -[node name="LeaderboardBtn" type="Button" parent="MainMenuPanel/VBoxContainer/ButtonSection"] +[node name="LeaderboardBtn" type="Button" parent="MainMenuPanel/VBoxContainer/ButtonSection" unique_id=216339260] custom_minimum_size = Vector2(0, 48) layout_mode = 2 theme_override_font_sizes/font_size = 16 @@ -190,14 +137,14 @@ theme_override_font_sizes/font_size = 16 text = "SETTINGS" [node name="ProfileBtn" type="Button" parent="MainMenuPanel/VBoxContainer/ButtonSection" unique_id=1640960506] -layout_mode = 2 custom_minimum_size = Vector2(0, 36) +layout_mode = 2 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) +layout_mode = 2 theme_override_font_sizes/font_size = 14 text = "QUIT GAME" @@ -278,6 +225,7 @@ layout_mode = 2 text = "PROFILE" [node name="LobbyPanel" type="Control" parent="." unique_id=1745714811] +visible = false layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -1188,10 +1136,10 @@ anchors_preset = 12 anchor_top = 1.0 anchor_right = 1.0 anchor_bottom = 1.0 -offset_left = 464.0 -offset_top = -93.0 -offset_right = -461.0 -offset_bottom = -44.0 +offset_left = 466.0 +offset_top = -65.0 +offset_right = -459.0 +offset_bottom = -16.0 grow_horizontal = 2 grow_vertical = 0 diff --git a/scenes/wall_3d.tscn b/scenes/wall_3d.tscn index e6e3f71..4a275ae 100644 --- a/scenes/wall_3d.tscn +++ b/scenes/wall_3d.tscn @@ -1,7 +1,5 @@ [gd_scene format=3 uid="uid://cggmcgvdj6wxt"] -[ext_resource type="ArrayMesh" uid="uid://dtr46jmckif0p" path="res://assets/models/meshes/block.res" id="1_block"] - [sub_resource type="BoxShape3D" id="BoxShape3D_wall"] size = Vector3(1.68, 1.5, 0.05) @@ -10,7 +8,6 @@ collision_mask = 0 [node name="MeshInstance3D" type="MeshInstance3D" parent="." unique_id=1405008923] transform = Transform3D(1.68, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) -mesh = ExtResource("1_block") [node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1446599023] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.35764623, 0) diff --git a/scripts/managers/lobby_manager.gd b/scripts/managers/lobby_manager.gd index c4cb3b6..459f45c 100644 --- a/scripts/managers/lobby_manager.gd +++ b/scripts/managers/lobby_manager.gd @@ -38,8 +38,12 @@ var available_rooms: Array = [] var is_host: bool = false var is_lan_mode: bool = false # True when using direct ENet (no Nakama) const LAN_PORT: int = 7777 # Port for LAN direct connections +const LAN_DISCOVERY_PORT: int = 7778 # Port for LAN discovery var local_player_name: String = "Player" +var _udp_peer: PacketPeerUDP +var _broadcast_timer: Timer + # Match duration in seconds (configurable in lobby by host) var match_duration: int = 180 # Default 3 minutes @@ -93,12 +97,35 @@ var _all_ready: bool = false func _ready(): _update_available_areas(game_mode) + # Setup UDP for LAN discovery + _udp_peer = PacketPeerUDP.new() + # Connect to Nakama signals NakamaManager.match_joined.connect(_on_match_joined) multiplayer.peer_connected.connect(_on_peer_connected) multiplayer.peer_disconnected.connect(_on_peer_disconnected) multiplayer.server_disconnected.connect(_on_server_disconnected) +func _process(_delta): + _listen_for_lan_discovery() + +func _listen_for_lan_discovery(): + if not _udp_peer or not is_lan_mode or is_host: return + if not _udp_peer.is_bound(): return + + while _udp_peer.get_available_packet_count() > 0: + var packet = _udp_peer.get_packet() + var ip = _udp_peer.get_packet_ip() + var data_str = packet.get_string_from_utf8() + + if not data_str.begins_with("TEKTON_HOST:"): continue + + var room_info_json = data_str.trim_prefix("TEKTON_HOST:") + var room_info = JSON.parse_string(room_info_json) + if room_info is Dictionary: + room_info["ip"] = ip + _update_lan_room_list(room_info) + func _update_available_areas(mode: String) -> void: match mode: "Freemode": @@ -179,9 +206,55 @@ func create_room_lan(room_name: String = "LAN Game") -> bool: }) print("[LAN] Server created on port %d. Waiting for players..." % LAN_PORT) + _start_lan_broadcast(room_name) emit_signal("room_joined", current_room) return true +func _start_lan_broadcast(room_name: String): + _stop_lan_broadcast() + if _udp_peer.bind(0) != OK: + push_error("[LAN] Failed to bind UDP for broadcasting") + return + _udp_peer.set_broadcast_enabled(true) + + _broadcast_timer = Timer.new() + _broadcast_timer.name = "LANBroadcastTimer" + _broadcast_timer.wait_time = 2.0 + _broadcast_timer.autostart = true + add_child(_broadcast_timer) + _broadcast_timer.timeout.connect(_broadcast_lan_room.bind(room_name)) + +func _broadcast_lan_room(room_name: String): + var room_data = { + "room_name": room_name, + "host_name": local_player_name, + "player_count": players_in_room.size(), + "max_players": GameStateManager.max_players, + "match_id": "LAN" + } + var msg = "TEKTON_HOST:" + JSON.stringify(room_data) + _udp_peer.set_dest_address("255.255.255.255", LAN_DISCOVERY_PORT) + _udp_peer.put_packet(msg.to_utf8_buffer()) + +func _stop_lan_broadcast(): + if _broadcast_timer: + _broadcast_timer.stop() + _broadcast_timer.queue_free() + _broadcast_timer = null + if _udp_peer and _udp_peer.is_bound(): + _udp_peer.close() + +func _update_lan_room_list(room_info: Dictionary): + var found = false + for i in range(available_rooms.size()): + if available_rooms[i].get("ip") == room_info["ip"]: + available_rooms[i] = room_info + found = true + break + if not found: + available_rooms.append(room_info) + emit_signal("room_list_updated", available_rooms) + func join_room_lan(host_ip: String) -> bool: """Client joins a LAN room by the host's IP address. No Nakama/Docker required.""" is_host = false @@ -215,6 +288,7 @@ func leave_room() -> void: # Important: Reset all lobby settings and player lists first reset() + _stop_lan_broadcast() if is_lan_mode: # LAN mode: just close the ENet peer directly @@ -231,7 +305,16 @@ func leave_room() -> void: emit_signal("room_left") func refresh_room_list() -> void: - """Request updated room list from Nakama.""" + """Request updated room list from Nakama or scan for LAN rooms.""" + if is_lan_mode: + available_rooms.clear() + if _udp_peer.is_bound(): _udp_peer.close() + var err = _udp_peer.bind(LAN_DISCOVERY_PORT) + if err != OK: + push_error("[LAN] Failed to bind to discovery port %d" % LAN_DISCOVERY_PORT) + emit_signal("room_list_updated", available_rooms) + return + if not NakamaManager.is_connected_to_nakama(): var success = await NakamaManager.connect_to_nakama_async() if not success: