feat: implement initial lobby scene with main menu, server browser, and networking options.
This commit is contained in:
+58
-3
@@ -12,6 +12,10 @@ extends Control
|
|||||||
# UI References - Server Selection
|
# UI References - Server Selection
|
||||||
@onready var server_option = $MainMenuPanel/VBoxContainer/ServerSelectionSection/ServerOption
|
@onready var server_option = $MainMenuPanel/VBoxContainer/ServerSelectionSection/ServerOption
|
||||||
@onready var server_ip_input = $MainMenuPanel/VBoxContainer/ServerSelectionSection/ServerIPInput
|
@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
|
# Leaderboard Reference
|
||||||
@onready var leaderboard_btn = $MainMenuPanel/VBoxContainer/ButtonSection/LeaderboardBtn
|
@onready var leaderboard_btn = $MainMenuPanel/VBoxContainer/ButtonSection/LeaderboardBtn
|
||||||
@@ -154,6 +158,10 @@ func _ready():
|
|||||||
if server_ip_input:
|
if server_ip_input:
|
||||||
server_ip_input.text_submitted.connect(_on_server_ip_submitted)
|
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))
|
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
|
# Connect button signals - Room List
|
||||||
refresh_btn.pressed.connect(_on_refresh_pressed)
|
refresh_btn.pressed.connect(_on_refresh_pressed)
|
||||||
@@ -248,18 +256,58 @@ func _load_character_textures() -> void:
|
|||||||
|
|
||||||
func _on_server_option_selected(index: int) -> void:
|
func _on_server_option_selected(index: int) -> void:
|
||||||
if index == 0:
|
if index == 0:
|
||||||
# Localhost
|
# Nakama Localhost
|
||||||
if server_ip_input: server_ip_input.visible = false
|
if server_ip_input: server_ip_input.visible = false
|
||||||
|
if lan_section: lan_section.visible = false
|
||||||
NakamaManager.set_server("localhost")
|
NakamaManager.set_server("localhost")
|
||||||
else:
|
elif index == 1:
|
||||||
# Remote
|
# Nakama Remote
|
||||||
if server_ip_input: server_ip_input.visible = true
|
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)
|
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:
|
func _on_server_ip_submitted(new_text: String) -> void:
|
||||||
if server_option and server_option.selected == 1:
|
if server_option and server_option.selected == 1:
|
||||||
NakamaManager.set_server(new_text.strip_edges())
|
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:
|
func _setup_game_modes() -> void:
|
||||||
if not game_mode_option: return
|
if not game_mode_option: return
|
||||||
game_mode_option.clear()
|
game_mode_option.clear()
|
||||||
@@ -681,6 +729,13 @@ func _on_room_joined(room_data: Dictionary) -> void:
|
|||||||
_update_player_slots()
|
_update_player_slots()
|
||||||
connection_status.text = "Connected to room"
|
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:
|
func _on_room_left() -> void:
|
||||||
_show_panel("main_menu")
|
_show_panel("main_menu")
|
||||||
connection_status.text = "Left room"
|
connection_status.text = "Left room"
|
||||||
|
|||||||
+44
-5
@@ -100,24 +100,63 @@ theme_override_constants/separation = 10
|
|||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_colors/font_color = Color(0.69, 0.529, 0.357, 1)
|
theme_override_colors/font_color = Color(0.69, 0.529, 0.357, 1)
|
||||||
theme_override_font_sizes/font_size = 13
|
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]
|
[node name="ServerOption" type="OptionButton" parent="MainMenuPanel/VBoxContainer/ServerSelectionSection" unique_id=748392103]
|
||||||
custom_minimum_size = Vector2(0, 44)
|
custom_minimum_size = Vector2(0, 44)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
selected = 0
|
selected = 0
|
||||||
item_count = 2
|
item_count = 3
|
||||||
popup/item_0/text = "Localhost (Testing)"
|
popup/item_0/text = "Nakama - Localhost (Testing)"
|
||||||
popup/item_0/id = 0
|
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_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]
|
[node name="ServerIPInput" type="LineEdit" parent="MainMenuPanel/VBoxContainer/ServerSelectionSection" unique_id=748392104]
|
||||||
visible = false
|
visible = false
|
||||||
custom_minimum_size = Vector2(0, 44)
|
custom_minimum_size = Vector2(0, 44)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "127.0.0.1"
|
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]
|
[node name="ServerSeparator" type="HSeparator" parent="MainMenuPanel/VBoxContainer" unique_id=748392105]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|||||||
+20
-4
@@ -51,7 +51,9 @@ func _ready():
|
|||||||
# NetworkPanel is visible during gameplay
|
# NetworkPanel is visible during gameplay
|
||||||
|
|
||||||
# Auto-start game if coming from lobby (already connected to match)
|
# 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...")
|
print("Coming from lobby - auto-starting game...")
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
_auto_start_from_lobby()
|
_auto_start_from_lobby()
|
||||||
@@ -685,9 +687,11 @@ func _start_game():
|
|||||||
wait_time += 0.2
|
wait_time += 0.2
|
||||||
|
|
||||||
# Allow socket/peer to stabilize before blasting RPCs
|
# 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
|
# This safely sends RPCs over the completed socket connection
|
||||||
_assign_random_spawn_positions()
|
_assign_random_spawn_positions()
|
||||||
|
|
||||||
@@ -972,7 +976,8 @@ func spawn_tekton_npc():
|
|||||||
# Generate a consistent ID/Name for sync (add index to ensure uniqueness)
|
# Generate a consistent ID/Name for sync (add index to ensure uniqueness)
|
||||||
var tekton_id = Time.get_ticks_msec() + spawned_count
|
var tekton_id = Time.get_ticks_msec() + spawned_count
|
||||||
_create_tekton(valid_pos, tekton_id)
|
_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)
|
rpc("sync_spawn_tekton", valid_pos, tekton_id)
|
||||||
|
|
||||||
spawned_count += 1
|
spawned_count += 1
|
||||||
@@ -980,6 +985,8 @@ func spawn_tekton_npc():
|
|||||||
|
|
||||||
@rpc("call_remote", "reliable")
|
@rpc("call_remote", "reliable")
|
||||||
func sync_spawn_tekton(pos: Vector2i, tekton_id: int):
|
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)
|
_create_tekton(pos, tekton_id)
|
||||||
|
|
||||||
func _create_tekton(pos: Vector2i, tekton_id: int, is_static: bool = false):
|
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):
|
func sync_grid_item(x: int, y: int, z: int, item: int):
|
||||||
var enhanced_gridmap = $EnhancedGridMap
|
var enhanced_gridmap = $EnhancedGridMap
|
||||||
if enhanced_gridmap:
|
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)
|
# 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.
|
# 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:
|
if y == 1 and item >= 7 and item <= 20:
|
||||||
@@ -1602,6 +1615,9 @@ func sync_grid_items_batch(data: Array):
|
|||||||
var y = entry.get("y", 0)
|
var y = entry.get("y", 0)
|
||||||
var z = entry.get("z", 0)
|
var z = entry.get("z", 0)
|
||||||
var item = entry.get("item", -1)
|
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
|
# PROTECTED FLOOR CHECK
|
||||||
if y == 1 and item >= 7 and item <= 20:
|
if y == 1 and item >= 7 and item <= 20:
|
||||||
|
|||||||
+8
-6
@@ -1186,13 +1186,15 @@ func find_valid_starting_position() -> Vector2i:
|
|||||||
if is_bot:
|
if is_bot:
|
||||||
return _find_random_spawn_position()
|
return _find_random_spawn_position()
|
||||||
else:
|
else:
|
||||||
# Auto-assign the first available spawn point for fixed spawning
|
# For Stop n Go, use Column 0
|
||||||
for spawn_pos in spawn_locations:
|
if LobbyManager.game_mode == "Stop n Go":
|
||||||
if not is_position_occupied(spawn_pos):
|
for spawn_pos in spawn_locations:
|
||||||
return spawn_pos
|
if not is_position_occupied(spawn_pos):
|
||||||
|
return spawn_pos
|
||||||
|
|
||||||
# Fallback (should typically not be reached if spawn_locations > max_players)
|
# For Freemode, try corners if random spawn is OFF, or just find any walkable
|
||||||
return Vector2i(0, 0)
|
# 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
|
# highlight_available_spawn_points is no longer needed for manual selection in this mode
|
||||||
func highlight_available_spawn_points():
|
func highlight_available_spawn_points():
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ var current_room: Dictionary = {}
|
|||||||
var players_in_room: Array = [] # [{id, name, is_ready}]
|
var players_in_room: Array = [] # [{id, name, is_ready}]
|
||||||
var available_rooms: Array = []
|
var available_rooms: Array = []
|
||||||
var is_host: bool = false
|
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
|
||||||
var local_player_name: String = "Player"
|
var local_player_name: String = "Player"
|
||||||
|
|
||||||
# Match duration in seconds (configurable in lobby by host)
|
# Match duration in seconds (configurable in lobby by host)
|
||||||
@@ -113,8 +115,9 @@ func _update_available_areas(mode: String) -> void:
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
func create_room(room_name: String) -> void:
|
func create_room(room_name: String) -> void:
|
||||||
"""Host creates a new room with the given name."""
|
"""Host creates a new room with the given name (Nakama)."""
|
||||||
is_host = true
|
is_host = true
|
||||||
|
is_lan_mode = false
|
||||||
current_room = {
|
current_room = {
|
||||||
"room_name": room_name,
|
"room_name": room_name,
|
||||||
"host_name": local_player_name,
|
"host_name": local_player_name,
|
||||||
@@ -130,8 +133,9 @@ func create_room(room_name: String) -> void:
|
|||||||
NakamaManager.host_game()
|
NakamaManager.host_game()
|
||||||
|
|
||||||
func join_room(match_id: String) -> void:
|
func join_room(match_id: String) -> void:
|
||||||
"""Client joins an existing room by match ID."""
|
"""Client joins an existing room by match ID (Nakama)."""
|
||||||
is_host = false
|
is_host = false
|
||||||
|
is_lan_mode = false
|
||||||
|
|
||||||
var success = await NakamaManager.connect_to_nakama_async()
|
var success = await NakamaManager.connect_to_nakama_async()
|
||||||
if not success:
|
if not success:
|
||||||
@@ -140,6 +144,66 @@ func join_room(match_id: String) -> void:
|
|||||||
|
|
||||||
NakamaManager.join_game(match_id)
|
NakamaManager.join_game(match_id)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# LAN Mode (Direct ENet, no Nakama/Docker needed)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
func create_room_lan(room_name: String = "LAN Game") -> bool:
|
||||||
|
"""Host creates a LAN room via direct ENet. No Nakama/Docker required."""
|
||||||
|
is_host = true
|
||||||
|
is_lan_mode = true
|
||||||
|
|
||||||
|
var peer = ENetMultiplayerPeer.new()
|
||||||
|
var err = peer.create_server(LAN_PORT, GameStateManager.max_players)
|
||||||
|
if err != OK:
|
||||||
|
push_error("[LAN] Failed to create ENet server on port %d: %s" % [LAN_PORT, err])
|
||||||
|
return false
|
||||||
|
|
||||||
|
multiplayer.set_multiplayer_peer(peer)
|
||||||
|
|
||||||
|
current_room = {
|
||||||
|
"room_name": room_name,
|
||||||
|
"host_name": local_player_name,
|
||||||
|
"max_players": GameStateManager.max_players,
|
||||||
|
"match_id": "LAN"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add host to player list
|
||||||
|
var my_id = multiplayer.get_unique_id() # Will be 1
|
||||||
|
players_in_room.clear()
|
||||||
|
players_in_room.append({
|
||||||
|
"id": my_id,
|
||||||
|
"name": local_player_name,
|
||||||
|
"is_ready": false,
|
||||||
|
"character": available_characters[local_character_index]
|
||||||
|
})
|
||||||
|
|
||||||
|
print("[LAN] Server created on port %d. Waiting for players..." % LAN_PORT)
|
||||||
|
emit_signal("room_joined", current_room)
|
||||||
|
return true
|
||||||
|
|
||||||
|
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
|
||||||
|
is_lan_mode = true
|
||||||
|
|
||||||
|
var peer = ENetMultiplayerPeer.new()
|
||||||
|
var err = peer.create_client(host_ip, LAN_PORT)
|
||||||
|
if err != OK:
|
||||||
|
push_error("[LAN] Failed to connect to %s:%d: %s" % [host_ip, LAN_PORT, err])
|
||||||
|
return false
|
||||||
|
|
||||||
|
multiplayer.set_multiplayer_peer(peer)
|
||||||
|
|
||||||
|
current_room = {
|
||||||
|
"room_name": "LAN Game",
|
||||||
|
"match_id": "LAN"
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[LAN] Connecting to %s:%d..." % [host_ip, LAN_PORT])
|
||||||
|
# _on_peer_connected will fire once connected and trigger request_room_info.
|
||||||
|
return true
|
||||||
|
|
||||||
func leave_room() -> void:
|
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.")
|
||||||
@@ -147,15 +211,19 @@ func leave_room() -> void:
|
|||||||
# If we are the host, notify all clients to kick them back to menu/lobby
|
# 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():
|
if is_host and multiplayer.has_multiplayer_peer() and multiplayer.is_server():
|
||||||
print("[LobbyManager] Host is leaving. Kicking all clients...")
|
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()
|
kick_all_clients.rpc()
|
||||||
|
|
||||||
# Important: Reset all lobby settings and player lists first
|
# Important: Reset all lobby settings and player lists first
|
||||||
reset()
|
reset()
|
||||||
|
|
||||||
# Disconnect from Nakama and reset multiplayer peer
|
if is_lan_mode:
|
||||||
NakamaManager.cleanup()
|
# LAN mode: just close the ENet peer directly
|
||||||
|
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
|
# Important: Clean up game state as well to prevent ghost players
|
||||||
GameStateManager.reset()
|
GameStateManager.reset()
|
||||||
@@ -206,7 +274,9 @@ func sync_ready_state(player_id: int, is_ready: bool) -> void:
|
|||||||
|
|
||||||
func _check_all_ready() -> void:
|
func _check_all_ready() -> void:
|
||||||
"""Check if all players are ready."""
|
"""Check if all players are ready."""
|
||||||
if players_in_room.size() < 2:
|
# In LAN mode allow solo play - only 1 player needed
|
||||||
|
var min_players = 1 if is_lan_mode else 2
|
||||||
|
if players_in_room.size() < min_players:
|
||||||
_all_ready = false
|
_all_ready = false
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -218,6 +288,17 @@ func _check_all_ready() -> void:
|
|||||||
_all_ready = true
|
_all_ready = true
|
||||||
emit_signal("all_players_ready")
|
emit_signal("all_players_ready")
|
||||||
|
|
||||||
|
func force_solo_ready() -> void:
|
||||||
|
"""Mark the local player as ready immediately (for solo LAN play)."""
|
||||||
|
if not multiplayer.has_multiplayer_peer():
|
||||||
|
return
|
||||||
|
var my_id = multiplayer.get_unique_id()
|
||||||
|
for player in players_in_room:
|
||||||
|
if player["id"] == my_id:
|
||||||
|
player["is_ready"] = true
|
||||||
|
break
|
||||||
|
_check_all_ready()
|
||||||
|
|
||||||
func is_all_ready() -> bool:
|
func is_all_ready() -> bool:
|
||||||
return _all_ready
|
return _all_ready
|
||||||
|
|
||||||
@@ -562,6 +643,11 @@ func _on_game_starting() -> void:
|
|||||||
|
|
||||||
func _on_match_joined(match_id: String) -> void:
|
func _on_match_joined(match_id: String) -> void:
|
||||||
"""Called when successfully joined a Nakama match."""
|
"""Called when successfully joined a Nakama match."""
|
||||||
|
# LAN mode handles room setup entirely in create_room_lan() / join_room_lan().
|
||||||
|
# Skip this Nakama-specific handler to avoid double-adding the player.
|
||||||
|
if is_lan_mode:
|
||||||
|
return
|
||||||
|
|
||||||
current_room["match_id"] = match_id
|
current_room["match_id"] = match_id
|
||||||
# Use first 8 chars of match ID as room name (matches server browser)
|
# Use first 8 chars of match ID as room name (matches server browser)
|
||||||
var short_id = match_id.substr(0, 8) if match_id.length() > 8 else match_id
|
var short_id = match_id.substr(0, 8) if match_id.length() > 8 else match_id
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ var is_loading: bool = false
|
|||||||
# Server Selection Controls
|
# Server Selection Controls
|
||||||
var server_option: OptionButton
|
var server_option: OptionButton
|
||||||
var server_ip_input: LineEdit
|
var server_ip_input: LineEdit
|
||||||
|
var lan_section: VBoxContainer # LAN-specific controls
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_connect_signals()
|
_connect_signals()
|
||||||
@@ -203,7 +204,7 @@ func _setup_server_config_ui() -> void:
|
|||||||
|
|
||||||
# Server Label
|
# Server Label
|
||||||
var label = Label.new()
|
var label = Label.new()
|
||||||
label.text = "NAKAMA SERVER"
|
label.text = "CONNECTION MODE"
|
||||||
label.add_theme_color_override("font_color", Color(0.69, 0.529, 0.357, 1))
|
label.add_theme_color_override("font_color", Color(0.69, 0.529, 0.357, 1))
|
||||||
label.add_theme_font_size_override("font_size", 13)
|
label.add_theme_font_size_override("font_size", 13)
|
||||||
server_section.add_child(label)
|
server_section.add_child(label)
|
||||||
@@ -212,8 +213,9 @@ func _setup_server_config_ui() -> void:
|
|||||||
server_option = OptionButton.new()
|
server_option = OptionButton.new()
|
||||||
server_option.name = "ServerOption"
|
server_option.name = "ServerOption"
|
||||||
server_option.custom_minimum_size = Vector2(0, 44)
|
server_option.custom_minimum_size = Vector2(0, 44)
|
||||||
server_option.add_item("Localhost (Testing)")
|
server_option.add_item("Nakama - Localhost (Testing)")
|
||||||
server_option.add_item("Remote Server (Host IP)")
|
server_option.add_item("Nakama - Remote Server (Host IP)")
|
||||||
|
server_option.add_item("LAN Direct (No Server)")
|
||||||
|
|
||||||
# Set initial state based on NakamaManager
|
# Set initial state based on NakamaManager
|
||||||
if NakamaManager.nakama_host == "localhost":
|
if NakamaManager.nakama_host == "localhost":
|
||||||
@@ -224,17 +226,62 @@ func _setup_server_config_ui() -> void:
|
|||||||
server_option.item_selected.connect(_on_server_option_selected)
|
server_option.item_selected.connect(_on_server_option_selected)
|
||||||
server_section.add_child(server_option)
|
server_section.add_child(server_option)
|
||||||
|
|
||||||
# Server IP Input
|
# Nakama Server IP Input
|
||||||
server_ip_input = LineEdit.new()
|
server_ip_input = LineEdit.new()
|
||||||
server_ip_input.name = "ServerIPInput"
|
server_ip_input.name = "ServerIPInput"
|
||||||
server_ip_input.custom_minimum_size = Vector2(0, 44)
|
server_ip_input.custom_minimum_size = Vector2(0, 44)
|
||||||
server_ip_input.placeholder_text = "Enter Server IP Address..."
|
server_ip_input.placeholder_text = "Enter Nakama Server IP..."
|
||||||
server_ip_input.text = NakamaManager.nakama_host if NakamaManager.nakama_host != "localhost" else "127.0.0.1"
|
server_ip_input.text = NakamaManager.nakama_host if NakamaManager.nakama_host != "localhost" else "127.0.0.1"
|
||||||
server_ip_input.visible = server_option.selected == 1
|
server_ip_input.visible = server_option.selected == 1
|
||||||
server_ip_input.text_submitted.connect(_on_server_ip_submitted)
|
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))
|
server_ip_input.focus_exited.connect(func(): _on_server_ip_submitted(server_ip_input.text))
|
||||||
server_section.add_child(server_ip_input)
|
server_section.add_child(server_ip_input)
|
||||||
|
|
||||||
|
# --- LAN Section ---
|
||||||
|
lan_section = VBoxContainer.new()
|
||||||
|
lan_section.name = "LANSection"
|
||||||
|
lan_section.add_theme_constant_override("separation", 8)
|
||||||
|
lan_section.visible = false
|
||||||
|
server_section.add_child(lan_section)
|
||||||
|
|
||||||
|
var lan_info = Label.new()
|
||||||
|
lan_info.text = "Play over LAN without any server.\nFirewall may need to allow port 7777."
|
||||||
|
lan_info.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7, 1))
|
||||||
|
lan_info.add_theme_font_size_override("font_size", 12)
|
||||||
|
lan_info.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||||
|
lan_section.add_child(lan_info)
|
||||||
|
|
||||||
|
# Host LAN button
|
||||||
|
var lan_host_btn = Button.new()
|
||||||
|
lan_host_btn.name = "LANHostBtn"
|
||||||
|
lan_host_btn.text = "HOST LAN GAME"
|
||||||
|
lan_host_btn.custom_minimum_size = Vector2(0, 44)
|
||||||
|
lan_host_btn.pressed.connect(_on_lan_host_pressed)
|
||||||
|
lan_section.add_child(lan_host_btn)
|
||||||
|
|
||||||
|
var lan_sep = Label.new()
|
||||||
|
lan_sep.text = "── or join a friend ──"
|
||||||
|
lan_sep.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
lan_sep.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5, 1))
|
||||||
|
lan_sep.add_theme_font_size_override("font_size", 11)
|
||||||
|
lan_section.add_child(lan_sep)
|
||||||
|
|
||||||
|
# LAN Host IP input
|
||||||
|
var lan_ip = LineEdit.new()
|
||||||
|
lan_ip.name = "LANIPInput"
|
||||||
|
lan_ip.custom_minimum_size = Vector2(0, 44)
|
||||||
|
lan_ip.placeholder_text = "Host IP (e.g. 192.168.1.10)"
|
||||||
|
lan_ip.text = "127.0.0.1"
|
||||||
|
lan_section.add_child(lan_ip)
|
||||||
|
|
||||||
|
# Join LAN button
|
||||||
|
var lan_join_btn = Button.new()
|
||||||
|
lan_join_btn.name = "LANJoinBtn"
|
||||||
|
lan_join_btn.text = "JOIN LAN GAME"
|
||||||
|
lan_join_btn.custom_minimum_size = Vector2(0, 44)
|
||||||
|
lan_join_btn.pressed.connect(func(): _on_lan_join_pressed(lan_ip.text))
|
||||||
|
lan_section.add_child(lan_join_btn)
|
||||||
|
|
||||||
# Add a separator after the section
|
# Add a separator after the section
|
||||||
var separator = HSeparator.new()
|
var separator = HSeparator.new()
|
||||||
vbox.add_child(separator)
|
vbox.add_child(separator)
|
||||||
@@ -242,18 +289,55 @@ func _setup_server_config_ui() -> void:
|
|||||||
|
|
||||||
func _on_server_option_selected(index: int) -> void:
|
func _on_server_option_selected(index: int) -> void:
|
||||||
if index == 0:
|
if index == 0:
|
||||||
# Localhost
|
# Nakama Localhost
|
||||||
server_ip_input.visible = false
|
server_ip_input.visible = false
|
||||||
|
if lan_section: lan_section.visible = false
|
||||||
NakamaManager.set_server("localhost")
|
NakamaManager.set_server("localhost")
|
||||||
else:
|
elif index == 1:
|
||||||
# Remote
|
# Nakama Remote
|
||||||
server_ip_input.visible = true
|
server_ip_input.visible = true
|
||||||
|
if lan_section: lan_section.visible = false
|
||||||
NakamaManager.set_server(server_ip_input.text)
|
NakamaManager.set_server(server_ip_input.text)
|
||||||
|
else:
|
||||||
|
# LAN Direct
|
||||||
|
server_ip_input.visible = false
|
||||||
|
if lan_section: lan_section.visible = true
|
||||||
|
|
||||||
func _on_server_ip_submitted(new_text: String) -> void:
|
func _on_server_ip_submitted(new_text: String) -> void:
|
||||||
if server_option.selected == 1:
|
if server_option and server_option.selected == 1:
|
||||||
NakamaManager.set_server(new_text.strip_edges())
|
NakamaManager.set_server(new_text.strip_edges())
|
||||||
|
|
||||||
|
func _on_lan_host_pressed() -> void:
|
||||||
|
"""Host a LAN game without logging in to Nakama."""
|
||||||
|
var player_name = email_input.text.strip_edges()
|
||||||
|
if player_name.is_empty():
|
||||||
|
player_name = "Host"
|
||||||
|
LobbyManager.local_player_name = player_name
|
||||||
|
|
||||||
|
var ok = await LobbyManager.create_room_lan()
|
||||||
|
if ok:
|
||||||
|
_go_to_lobby()
|
||||||
|
else:
|
||||||
|
_show_error("Failed to create LAN server. Check firewall for port 7777.")
|
||||||
|
|
||||||
|
func _on_lan_join_pressed(host_ip: String) -> void:
|
||||||
|
"""Join a LAN game without logging in to Nakama."""
|
||||||
|
var ip = host_ip.strip_edges()
|
||||||
|
if ip.is_empty():
|
||||||
|
_show_error("Please enter the host's IP address.")
|
||||||
|
return
|
||||||
|
|
||||||
|
var player_name = email_input.text.strip_edges()
|
||||||
|
if player_name.is_empty():
|
||||||
|
player_name = "Player"
|
||||||
|
LobbyManager.local_player_name = player_name
|
||||||
|
|
||||||
|
var ok = LobbyManager.join_room_lan(ip)
|
||||||
|
if ok:
|
||||||
|
_go_to_lobby()
|
||||||
|
else:
|
||||||
|
_show_error("Failed to connect to %s. Is the host running?" % ip)
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Registration Handlers
|
# Registration Handlers
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user