feat: implement initial lobby scene with main menu, server browser, and networking options.

This commit is contained in:
Yogi Wiguna
2026-03-16 16:19:30 +08:00
parent 64dc1de15a
commit eb018903aa
6 changed files with 317 additions and 35 deletions
+58 -3
View File
@@ -12,6 +12,10 @@ 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
@@ -154,6 +158,10 @@ 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)
@@ -248,18 +256,58 @@ func _load_character_textures() -> void:
func _on_server_option_selected(index: int) -> void:
if index == 0:
# Localhost
# Nakama Localhost
if server_ip_input: server_ip_input.visible = false
if lan_section: lan_section.visible = false
NakamaManager.set_server("localhost")
else:
# Remote
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)
else:
# LAN Direct
if server_ip_input: server_ip_input.visible = false
if lan_section: lan_section.visible = 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()
@@ -680,6 +728,13 @@ 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")
+44 -5
View File
@@ -100,24 +100,63 @@ theme_override_constants/separation = 10
layout_mode = 2
theme_override_colors/font_color = Color(0.69, 0.529, 0.357, 1)
theme_override_font_sizes/font_size = 13
text = "NAKAMA SERVER"
text = "CONNECTION MODE"
[node name="ServerOption" type="OptionButton" parent="MainMenuPanel/VBoxContainer/ServerSelectionSection" unique_id=748392103]
custom_minimum_size = Vector2(0, 44)
layout_mode = 2
selected = 0
item_count = 2
popup/item_0/text = "Localhost (Testing)"
item_count = 3
popup/item_0/text = "Nakama - Localhost (Testing)"
popup/item_0/id = 0
popup/item_1/text = "Remote Server (Host IP)"
popup/item_1/text = "Nakama - Remote Server (Host IP)"
popup/item_1/id = 1
popup/item_2/text = "LAN Direct (No Server)"
popup/item_2/id = 2
[node name="ServerIPInput" type="LineEdit" parent="MainMenuPanel/VBoxContainer/ServerSelectionSection" unique_id=748392104]
visible = false
custom_minimum_size = Vector2(0, 44)
layout_mode = 2
text = "127.0.0.1"
placeholder_text = "Enter Server IP Address..."
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
+21 -5
View File
@@ -51,7 +51,9 @@ func _ready():
# NetworkPanel is visible during gameplay
# Auto-start game if coming from lobby (already connected to match)
if NakamaManager.is_connected_to_nakama() and multiplayer.get_unique_id() != 0:
# Works for both Nakama mode and LAN direct mode (ENet).
var is_lan_connected = LobbyManager.is_lan_mode and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED
if (NakamaManager.is_connected_to_nakama() or is_lan_connected) and multiplayer.get_unique_id() != 0:
print("Coming from lobby - auto-starting game...")
await get_tree().process_frame
_auto_start_from_lobby()
@@ -685,9 +687,11 @@ func _start_game():
wait_time += 0.2
# Allow socket/peer to stabilize before blasting RPCs
await get_tree().create_timer(2.0).timeout
# Snappier delay for LAN mode
var delay = 0.5 if LobbyManager.is_lan_mode else 2.0
await get_tree().create_timer(delay).timeout
# NOW assign random spawn positions for EVERYONE (Host, Client, Bots)
# NOW assign spawn positions for EVERYONE (Host, Client, Bots)
# This safely sends RPCs over the completed socket connection
_assign_random_spawn_positions()
@@ -972,7 +976,8 @@ func spawn_tekton_npc():
# Generate a consistent ID/Name for sync (add index to ensure uniqueness)
var tekton_id = Time.get_ticks_msec() + spawned_count
_create_tekton(valid_pos, tekton_id)
if can_rpc():
# Only broadcast to clients if there are remote peers connected
if can_rpc() and multiplayer.get_peers().size() > 0:
rpc("sync_spawn_tekton", valid_pos, tekton_id)
spawned_count += 1
@@ -980,6 +985,8 @@ func spawn_tekton_npc():
@rpc("call_remote", "reliable")
func sync_spawn_tekton(pos: Vector2i, tekton_id: int):
# Safety: only create if scene is fully ready
if not is_inside_tree(): return
_create_tekton(pos, tekton_id)
func _create_tekton(pos: Vector2i, tekton_id: int, is_static: bool = false):
@@ -1566,6 +1573,12 @@ func request_randomize_item(grid_position: Vector2i):
func sync_grid_item(x: int, y: int, z: int, item: int):
var enhanced_gridmap = $EnhancedGridMap
if enhanced_gridmap:
# FLOOR ENFORCEMENT: Visual tiles (IDs 7-20) must ALWAYS be on layer Y=1.
# If somehow sent to Y=0, redirect them to Y=1.
if item >= 7 and item <= 20 and y == 0:
push_warning("[Main] Tile %d was sent to floor Y=0 at (%d,0,%d). Redirecting to Y=1." % [item, x, z])
y = 1
# PROTECTED FLOOR CHECK: Block tiles (7-20) from being placed on walls (4) or void (-1)
# Note: We allow spawning on Safe Zones, Start, and Finish as it's on Layer 1.
if y == 1 and item >= 7 and item <= 20:
@@ -1602,7 +1615,10 @@ func sync_grid_items_batch(data: Array):
var y = entry.get("y", 0)
var z = entry.get("z", 0)
var item = entry.get("item", -1)
# FLOOR ENFORCEMENT: Visual tiles (IDs 7-20) must ALWAYS be on layer Y=1.
if item >= 7 and item <= 20 and y == 0:
y = 1
# PROTECTED FLOOR CHECK
if y == 1 and item >= 7 and item <= 20:
var f0 = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z))
+8 -6
View File
@@ -1186,13 +1186,15 @@ func find_valid_starting_position() -> Vector2i:
if is_bot:
return _find_random_spawn_position()
else:
# Auto-assign the first available spawn point for fixed spawning
for spawn_pos in spawn_locations:
if not is_position_occupied(spawn_pos):
return spawn_pos
# For Stop n Go, use Column 0
if LobbyManager.game_mode == "Stop n Go":
for spawn_pos in spawn_locations:
if not is_position_occupied(spawn_pos):
return spawn_pos
# Fallback (should typically not be reached if spawn_locations > max_players)
return Vector2i(0, 0)
# For Freemode, try corners if random spawn is OFF, or just find any walkable
# But wait, host will assign most of the time. This is just for initial _ready.
return Vector2i(1, 1) # Default away from the very edge if possible
# highlight_available_spawn_points is no longer needed for manual selection in this mode
func highlight_available_spawn_points():