feat: Add initial lobby and main scenes with Nakama and lobby management scripts.

This commit is contained in:
2025-12-06 02:27:08 +08:00
parent 5000f3e269
commit 438c0c0d6e
9 changed files with 1061 additions and 39 deletions
+2 -1
View File
@@ -11,7 +11,7 @@ config_version=5
[application] [application]
config/name="tekton-local" config/name="tekton-local"
run/main_scene="uid://dxn87yj8qnfpp" run/main_scene="res://scenes/lobby.tscn"
config/features=PackedStringArray("4.4", "Forward Plus") config/features=PackedStringArray("4.4", "Forward Plus")
config/icon="res://icon.svg" config/icon="res://icon.svg"
@@ -26,6 +26,7 @@ PlayerManager="*res://scripts/managers/player_manager.gd"
TurnManager="*res://scripts/managers/turn_manager.gd" TurnManager="*res://scripts/managers/turn_manager.gd"
GoalManager="*res://scripts/managers/goal_manager.gd" GoalManager="*res://scripts/managers/goal_manager.gd"
GameStateManager="*res://scripts/managers/game_state_manager.gd" GameStateManager="*res://scripts/managers/game_state_manager.gd"
LobbyManager="*res://scripts/managers/lobby_manager.gd"
[display] [display]
+246
View File
@@ -0,0 +1,246 @@
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
# UI References - Room List
@onready var room_list_panel = $RoomListPanel
@onready var room_list = $RoomListPanel/VBoxContainer/RoomList
@onready var match_id_input = $RoomListPanel/VBoxContainer/MatchIdInput
@onready var refresh_btn = $RoomListPanel/VBoxContainer/ButtonContainer/RefreshBtn
@onready var join_btn = $RoomListPanel/VBoxContainer/ButtonContainer/JoinBtn
@onready var back_btn = $RoomListPanel/VBoxContainer/ButtonContainer/BackBtn
# UI References - Lobby
@onready var lobby_panel = $LobbyPanel
@onready var room_name_header = $LobbyPanel/VBoxContainer/RoomNameHeader
@onready var match_id_display = $LobbyPanel/VBoxContainer/MatchIdContainer/MatchIdDisplay
@onready var copy_id_btn = $LobbyPanel/VBoxContainer/MatchIdContainer/CopyIdBtn
@onready var player_list = $LobbyPanel/VBoxContainer/PlayerList
@onready var status_label = $LobbyPanel/VBoxContainer/StatusLabel
@onready var ready_btn = $LobbyPanel/VBoxContainer/ButtonContainer/ReadyBtn
@onready var start_game_btn = $LobbyPanel/VBoxContainer/ButtonContainer/StartGameBtn
@onready var leave_btn = $LobbyPanel/VBoxContainer/ButtonContainer/LeaveBtn
# UI References - Status
@onready var connection_status = $StatusBar/ConnectionStatus
# Store current match ID for copy function
var current_match_id: String = ""
func _ready():
# Connect button signals
create_room_btn.pressed.connect(_on_create_room_pressed)
browse_rooms_btn.pressed.connect(_on_browse_rooms_pressed)
refresh_btn.pressed.connect(_on_refresh_pressed)
join_btn.pressed.connect(_on_join_pressed)
back_btn.pressed.connect(_on_back_pressed)
ready_btn.toggled.connect(_on_ready_toggled)
start_game_btn.pressed.connect(_on_start_game_pressed)
leave_btn.pressed.connect(_on_leave_pressed)
copy_id_btn.pressed.connect(_on_copy_id_pressed)
# Connect LobbyManager signals
LobbyManager.room_list_updated.connect(_on_room_list_updated)
LobbyManager.room_joined.connect(_on_room_joined)
LobbyManager.room_left.connect(_on_room_left)
LobbyManager.player_joined.connect(_on_player_joined)
LobbyManager.player_left.connect(_on_player_left)
LobbyManager.ready_state_changed.connect(_on_ready_state_changed)
LobbyManager.all_players_ready.connect(_on_all_players_ready)
LobbyManager.game_starting.connect(_on_game_starting)
# Connect NakamaManager signals
NakamaManager.connected_to_nakama.connect(_on_connected_to_nakama)
NakamaManager.connection_failed.connect(_on_connection_failed)
# Show main menu initially
_show_panel("main_menu")
# =============================================================================
# Panel Management
# =============================================================================
func _show_panel(panel_name: String) -> void:
main_menu_panel.visible = panel_name == "main_menu"
room_list_panel.visible = panel_name == "room_list"
lobby_panel.visible = panel_name == "lobby"
# =============================================================================
# Main Menu Button Handlers
# =============================================================================
func _on_create_room_pressed() -> void:
# Set player name
LobbyManager.local_player_name = player_name_input.text.strip_edges()
if LobbyManager.local_player_name.is_empty():
LobbyManager.local_player_name = "Host"
connection_status.text = "Creating room..."
# Room name auto-generated since Nakama doesn't support custom names
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()
# =============================================================================
# Room List Button Handlers
# =============================================================================
func _on_refresh_pressed() -> void:
connection_status.text = "Refreshing..."
room_list.clear()
LobbyManager.refresh_room_list()
func _on_join_pressed() -> void:
# First check if there's a match ID entered
var match_id = match_id_input.text.strip_edges()
if match_id.is_empty():
# Try to use selected room from list
var selected_items = room_list.get_selected_items()
if selected_items.size() == 0:
connection_status.text = "Please select a room or enter Match ID"
return
var selected_idx = selected_items[0]
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
# Set player name
LobbyManager.local_player_name = player_name_input.text.strip_edges()
if LobbyManager.local_player_name.is_empty():
LobbyManager.local_player_name = "Player"
connection_status.text = "Joining room..."
LobbyManager.join_room(match_id)
func _on_back_pressed() -> void:
_show_panel("main_menu")
connection_status.text = ""
# =============================================================================
# Lobby Button Handlers
# =============================================================================
func _on_ready_toggled(is_ready: bool) -> void:
LobbyManager.set_ready(is_ready)
ready_btn.text = "READY ✓" if is_ready else "READY"
func _on_start_game_pressed() -> void:
LobbyManager.start_game()
func _on_leave_pressed() -> void:
LobbyManager.leave_room()
_show_panel("main_menu")
ready_btn.button_pressed = false
ready_btn.text = "READY"
func _on_copy_id_pressed() -> void:
DisplayServer.clipboard_set(current_match_id)
connection_status.text = "Match ID copied to clipboard!"
# =============================================================================
# LobbyManager Signal Handlers
# =============================================================================
func _on_room_list_updated(rooms: Array) -> void:
room_list.clear()
for room in rooms:
var room_name = room.get("room_name", "Unknown")
var host_name = room.get("host_name", "Unknown")
var player_count = room.get("player_count", 1)
var max_players = room.get("max_players", 4)
room_list.add_item("%s - %s (%d/%d)" % [room_name, host_name, player_count, max_players])
if rooms.size() == 0:
connection_status.text = "No rooms available"
else:
connection_status.text = "Found %d room(s)" % rooms.size()
func _on_room_joined(room_data: Dictionary) -> void:
_show_panel("lobby")
current_match_id = room_data.get("match_id", "")
room_name_header.text = "ROOM: %s" % room_data.get("room_name", "Unknown")
match_id_display.text = "Match ID: %s " % current_match_id
# Update start button visibility (host only)
start_game_btn.visible = LobbyManager.is_host
_update_player_list()
connection_status.text = "Connected to room"
func _on_room_left() -> void:
_show_panel("main_menu")
connection_status.text = "Left room"
func _on_player_joined(player_data: Dictionary) -> void:
_update_player_list()
status_label.text = "%s joined!" % player_data.get("name", "Player")
func _on_player_left(_player_id: int) -> void:
_update_player_list()
status_label.text = "A player left"
func _on_ready_state_changed(_player_id: int, _is_ready: bool) -> void:
_update_player_list()
_update_status()
func _on_all_players_ready() -> void:
if LobbyManager.is_host:
start_game_btn.disabled = false
status_label.text = "All players ready! Host can start."
else:
status_label.text = "All players ready! Waiting for host..."
func _on_game_starting() -> void:
connection_status.text = "Starting game..."
# Small delay for visual feedback
await get_tree().create_timer(0.5).timeout
get_tree().change_scene_to_file("res://scenes/main.tscn")
# =============================================================================
# NakamaManager Signal Handlers
# =============================================================================
func _on_connected_to_nakama() -> void:
connection_status.text = "Connected to server"
func _on_connection_failed(error_message: String) -> void:
connection_status.text = "Connection failed: %s" % error_message
_show_panel("main_menu")
# =============================================================================
# Helper Functions
# =============================================================================
func _update_player_list() -> void:
player_list.clear()
var players = LobbyManager.get_players()
for player in players:
var player_name = player.get("name", "Unknown")
var is_ready = player.get("is_ready", false)
var ready_icon = "" if is_ready else ""
var host_tag = " (Host)" if player.get("id") == 1 else ""
player_list.add_item("%s%s%s" % [player_name, host_tag, ready_icon])
func _update_status() -> void:
var players = LobbyManager.get_players()
var ready_count = 0
for player in players:
if player.get("is_ready", false):
ready_count += 1
status_label.text = "Ready: %d/%d" % [ready_count, players.size()]
if LobbyManager.is_host:
start_game_btn.disabled = not LobbyManager.is_all_ready()
+1
View File
@@ -0,0 +1 @@
uid://b5q6yekyk0tld
+448
View File
@@ -0,0 +1,448 @@
[gd_scene load_steps=7 format=3 uid="uid://b7nxt2hc4kqp8"]
[ext_resource type="Script" uid="uid://b5q6yekyk0tld" path="res://scenes/lobby.gd" id="1_lobby"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_panel"]
content_margin_left = 24.0
content_margin_top = 20.0
content_margin_right = 24.0
content_margin_bottom = 20.0
bg_color = Color(0.12, 0.12, 0.14, 0.95)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.9, 0.45, 0.1, 0.8)
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_input"]
content_margin_left = 12.0
content_margin_top = 8.0
content_margin_right = 12.0
content_margin_bottom = 8.0
bg_color = Color(0.08, 0.08, 0.1, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.3, 0.3, 0.35, 1)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button_hover"]
content_margin_left = 16.0
content_margin_top = 8.0
content_margin_right = 16.0
content_margin_bottom = 8.0
bg_color = Color(0.9, 0.45, 0.1, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(1, 0.6, 0.2, 1)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button_pressed"]
content_margin_left = 16.0
content_margin_top = 8.0
content_margin_right = 16.0
content_margin_bottom = 8.0
bg_color = Color(0.7, 0.35, 0.05, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.9, 0.5, 0.15, 1)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button_normal"]
content_margin_left = 16.0
content_margin_top = 8.0
content_margin_right = 16.0
content_margin_bottom = 8.0
bg_color = Color(0.18, 0.18, 0.2, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.4, 0.4, 0.45, 1)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
[node name="Lobby" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_lobby")
[node name="Background" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.06, 0.06, 0.08, 1)
[node name="BackgroundPattern" type="ColorRect" parent="."]
modulate = Color(1, 1, 1, 0.03)
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.9, 0.45, 0.1, 1)
[node name="MainMenuPanel" type="PanelContainer" parent="."]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -220.0
offset_top = -220.0
offset_right = 220.0
offset_bottom = 220.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_panel")
[node name="VBoxContainer" type="VBoxContainer" parent="MainMenuPanel"]
layout_mode = 2
theme_override_constants/separation = 16
[node name="TitleContainer" type="VBoxContainer" parent="MainMenuPanel/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 4
[node name="Title" type="Label" parent="MainMenuPanel/VBoxContainer/TitleContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(0.95, 0.5, 0.15, 1)
theme_override_font_sizes/font_size = 42
text = "TEKTON DASH"
horizontal_alignment = 1
vertical_alignment = 1
[node name="Subtitle" type="Label" parent="MainMenuPanel/VBoxContainer/TitleContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(0.5, 0.5, 0.55, 1)
theme_override_font_sizes/font_size = 12
text = "ARMAGEDDON VERSION"
horizontal_alignment = 1
[node name="Separator" type="HSeparator" parent="MainMenuPanel/VBoxContainer"]
modulate = Color(0.9, 0.45, 0.1, 0.5)
layout_mode = 2
[node name="InputSection" type="VBoxContainer" parent="MainMenuPanel/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 8
[node name="PlayerNameLabel" type="Label" parent="MainMenuPanel/VBoxContainer/InputSection"]
layout_mode = 2
theme_override_colors/font_color = Color(0.7, 0.7, 0.75, 1)
theme_override_font_sizes/font_size = 13
text = "YOUR NAME"
[node name="PlayerNameInput" type="LineEdit" parent="MainMenuPanel/VBoxContainer/InputSection"]
custom_minimum_size = Vector2(0, 36)
layout_mode = 2
theme_override_colors/font_placeholder_color = Color(0.4, 0.4, 0.45, 1)
theme_override_colors/font_color = Color(0.9, 0.9, 0.95, 1)
theme_override_font_sizes/font_size = 14
theme_override_styles/normal = SubResource("StyleBoxFlat_input")
text = "Player"
placeholder_text = "Enter your name..."
[node name="ButtonSection" type="VBoxContainer" parent="MainMenuPanel/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="CreateRoomBtn" type="Button" parent="MainMenuPanel/VBoxContainer/ButtonSection"]
custom_minimum_size = Vector2(0, 44)
layout_mode = 2
theme_override_colors/font_hover_color = Color(0.1, 0.1, 0.12, 1)
theme_override_colors/font_color = Color(0.95, 0.95, 1, 1)
theme_override_colors/font_pressed_color = Color(1, 1, 1, 1)
theme_override_font_sizes/font_size = 15
theme_override_styles/hover = SubResource("StyleBoxFlat_button_hover")
theme_override_styles/pressed = SubResource("StyleBoxFlat_button_pressed")
theme_override_styles/normal = SubResource("StyleBoxFlat_button_normal")
text = "CREATE ROOM"
[node name="BrowseRoomsBtn" type="Button" parent="MainMenuPanel/VBoxContainer/ButtonSection"]
custom_minimum_size = Vector2(0, 44)
layout_mode = 2
theme_override_colors/font_hover_color = Color(0.1, 0.1, 0.12, 1)
theme_override_colors/font_color = Color(0.95, 0.95, 1, 1)
theme_override_colors/font_pressed_color = Color(1, 1, 1, 1)
theme_override_font_sizes/font_size = 15
theme_override_styles/hover = SubResource("StyleBoxFlat_button_hover")
theme_override_styles/pressed = SubResource("StyleBoxFlat_button_pressed")
theme_override_styles/normal = SubResource("StyleBoxFlat_button_normal")
text = "BROWSE ROOMS"
[node name="RoomListPanel" type="PanelContainer" parent="."]
visible = false
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -280.0
offset_top = -240.0
offset_right = 280.0
offset_bottom = 240.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_panel")
[node name="VBoxContainer" type="VBoxContainer" parent="RoomListPanel"]
layout_mode = 2
theme_override_constants/separation = 12
[node name="Header" type="Label" parent="RoomListPanel/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(0.95, 0.5, 0.15, 1)
theme_override_font_sizes/font_size = 28
text = "SERVER BROWSER"
horizontal_alignment = 1
[node name="HSeparator" type="HSeparator" parent="RoomListPanel/VBoxContainer"]
modulate = Color(0.9, 0.45, 0.1, 0.5)
layout_mode = 2
[node name="MatchIdLabel" type="Label" parent="RoomListPanel/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(0.6, 0.6, 0.65, 1)
theme_override_font_sizes/font_size = 12
text = "DIRECT CONNECT (MATCH ID)"
[node name="MatchIdInput" type="LineEdit" parent="RoomListPanel/VBoxContainer"]
custom_minimum_size = Vector2(0, 36)
layout_mode = 2
theme_override_colors/font_placeholder_color = Color(0.4, 0.4, 0.45, 1)
theme_override_colors/font_color = Color(0.9, 0.9, 0.95, 1)
theme_override_font_sizes/font_size = 14
theme_override_styles/normal = SubResource("StyleBoxFlat_input")
placeholder_text = "Paste match ID here..."
[node name="RoomListLabel" type="Label" parent="RoomListPanel/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(0.6, 0.6, 0.65, 1)
theme_override_font_sizes/font_size = 12
text = "AVAILABLE SERVERS"
[node name="RoomList" type="ItemList" parent="RoomListPanel/VBoxContainer"]
custom_minimum_size = Vector2(0, 180)
layout_mode = 2
theme_override_colors/font_selected_color = Color(0.1, 0.1, 0.12, 1)
theme_override_colors/font_color = Color(0.85, 0.85, 0.9, 1)
theme_override_font_sizes/font_size = 14
allow_reselect = true
[node name="ButtonContainer" type="HBoxContainer" parent="RoomListPanel/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 12
alignment = 1
[node name="RefreshBtn" type="Button" parent="RoomListPanel/VBoxContainer/ButtonContainer"]
custom_minimum_size = Vector2(100, 40)
layout_mode = 2
theme_override_colors/font_hover_color = Color(0.1, 0.1, 0.12, 1)
theme_override_colors/font_color = Color(0.95, 0.95, 1, 1)
theme_override_font_sizes/font_size = 13
theme_override_styles/hover = SubResource("StyleBoxFlat_button_hover")
theme_override_styles/pressed = SubResource("StyleBoxFlat_button_pressed")
theme_override_styles/normal = SubResource("StyleBoxFlat_button_normal")
text = "REFRESH"
[node name="JoinBtn" type="Button" parent="RoomListPanel/VBoxContainer/ButtonContainer"]
custom_minimum_size = Vector2(120, 40)
layout_mode = 2
theme_override_colors/font_hover_color = Color(0.1, 0.1, 0.12, 1)
theme_override_colors/font_color = Color(0.95, 0.95, 1, 1)
theme_override_font_sizes/font_size = 13
theme_override_styles/hover = SubResource("StyleBoxFlat_button_hover")
theme_override_styles/pressed = SubResource("StyleBoxFlat_button_pressed")
theme_override_styles/normal = SubResource("StyleBoxFlat_button_normal")
text = "JOIN SERVER"
[node name="BackBtn" type="Button" parent="RoomListPanel/VBoxContainer/ButtonContainer"]
custom_minimum_size = Vector2(100, 40)
layout_mode = 2
theme_override_colors/font_hover_color = Color(0.1, 0.1, 0.12, 1)
theme_override_colors/font_color = Color(0.95, 0.95, 1, 1)
theme_override_font_sizes/font_size = 13
theme_override_styles/hover = SubResource("StyleBoxFlat_button_hover")
theme_override_styles/pressed = SubResource("StyleBoxFlat_button_pressed")
theme_override_styles/normal = SubResource("StyleBoxFlat_button_normal")
text = "BACK"
[node name="LobbyPanel" type="PanelContainer" parent="."]
visible = false
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -280.0
offset_top = -240.0
offset_right = 280.0
offset_bottom = 240.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_panel")
[node name="VBoxContainer" type="VBoxContainer" parent="LobbyPanel"]
layout_mode = 2
theme_override_constants/separation = 12
[node name="RoomNameHeader" type="Label" parent="LobbyPanel/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(0.95, 0.5, 0.15, 1)
theme_override_font_sizes/font_size = 26
text = "ROOM: "
horizontal_alignment = 1
[node name="MatchIdContainer" type="HBoxContainer" parent="LobbyPanel/VBoxContainer"]
layout_mode = 2
alignment = 1
[node name="MatchIdDisplay" type="Label" parent="LobbyPanel/VBoxContainer/MatchIdContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(0.45, 0.45, 0.5, 1)
theme_override_font_sizes/font_size = 10
text = "Match ID: "
[node name="CopyIdBtn" type="Button" parent="LobbyPanel/VBoxContainer/MatchIdContainer"]
custom_minimum_size = Vector2(70, 24)
layout_mode = 2
theme_override_colors/font_hover_color = Color(0.1, 0.1, 0.12, 1)
theme_override_colors/font_color = Color(0.7, 0.7, 0.75, 1)
theme_override_font_sizes/font_size = 10
theme_override_styles/hover = SubResource("StyleBoxFlat_button_hover")
theme_override_styles/pressed = SubResource("StyleBoxFlat_button_pressed")
theme_override_styles/normal = SubResource("StyleBoxFlat_button_normal")
text = "COPY"
[node name="HSeparator" type="HSeparator" parent="LobbyPanel/VBoxContainer"]
modulate = Color(0.9, 0.45, 0.1, 0.5)
layout_mode = 2
[node name="PlayersLabel" type="Label" parent="LobbyPanel/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(0.6, 0.6, 0.65, 1)
theme_override_font_sizes/font_size = 13
text = "PLAYERS"
[node name="PlayerList" type="ItemList" parent="LobbyPanel/VBoxContainer"]
custom_minimum_size = Vector2(0, 140)
layout_mode = 2
theme_override_colors/font_selected_color = Color(0.1, 0.1, 0.12, 1)
theme_override_colors/font_color = Color(0.85, 0.85, 0.9, 1)
theme_override_font_sizes/font_size = 14
allow_reselect = true
[node name="StatusLabel" type="Label" parent="LobbyPanel/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(0.95, 0.6, 0.2, 1)
theme_override_font_sizes/font_size = 14
text = "Waiting for players..."
horizontal_alignment = 1
[node name="ButtonContainer" type="HBoxContainer" parent="LobbyPanel/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 12
alignment = 1
[node name="ReadyBtn" type="Button" parent="LobbyPanel/VBoxContainer/ButtonContainer"]
custom_minimum_size = Vector2(100, 44)
layout_mode = 2
theme_override_colors/font_hover_color = Color(0.1, 0.1, 0.12, 1)
theme_override_colors/font_color = Color(0.95, 0.95, 1, 1)
theme_override_font_sizes/font_size = 14
theme_override_styles/hover = SubResource("StyleBoxFlat_button_hover")
theme_override_styles/pressed = SubResource("StyleBoxFlat_button_pressed")
theme_override_styles/normal = SubResource("StyleBoxFlat_button_normal")
toggle_mode = true
text = "READY"
[node name="StartGameBtn" type="Button" parent="LobbyPanel/VBoxContainer/ButtonContainer"]
custom_minimum_size = Vector2(130, 44)
layout_mode = 2
theme_override_colors/font_hover_color = Color(0.1, 0.1, 0.12, 1)
theme_override_colors/font_color = Color(0.95, 0.95, 1, 1)
theme_override_font_sizes/font_size = 14
theme_override_styles/hover = SubResource("StyleBoxFlat_button_hover")
theme_override_styles/pressed = SubResource("StyleBoxFlat_button_pressed")
theme_override_styles/normal = SubResource("StyleBoxFlat_button_normal")
disabled = true
text = "START GAME"
[node name="LeaveBtn" type="Button" parent="LobbyPanel/VBoxContainer/ButtonContainer"]
custom_minimum_size = Vector2(100, 44)
layout_mode = 2
theme_override_colors/font_hover_color = Color(0.1, 0.1, 0.12, 1)
theme_override_colors/font_color = Color(0.95, 0.95, 1, 1)
theme_override_font_sizes/font_size = 14
theme_override_styles/hover = SubResource("StyleBoxFlat_button_hover")
theme_override_styles/pressed = SubResource("StyleBoxFlat_button_pressed")
theme_override_styles/normal = SubResource("StyleBoxFlat_button_normal")
text = "LEAVE"
[node name="StatusBar" type="PanelContainer" parent="."]
layout_mode = 1
anchors_preset = 12
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 39.0
offset_top = -83.0
offset_right = -39.0
offset_bottom = -26.0
grow_horizontal = 2
grow_vertical = 0
theme_override_styles/panel = SubResource("StyleBoxFlat_panel")
[node name="ConnectionStatus" type="Label" parent="StatusBar"]
layout_mode = 2
theme_override_colors/font_color = Color(0.5, 0.5, 0.55, 1)
theme_override_font_sizes/font_size = 12
text = "NOT CONNECTED"
horizontal_alignment = 1
[node name="VersionLabel" type="Label" parent="."]
layout_mode = 1
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -126.0
offset_top = 19.0
offset_right = -18.0
offset_bottom = 37.0
grow_horizontal = 0
theme_override_colors/font_color = Color(0.35, 0.35, 0.4, 1)
theme_override_font_sizes/font_size = 11
text = "v0.1.0 ALPHA"
horizontal_alignment = 2
+73 -38
View File
@@ -9,7 +9,6 @@ var ui_manager
var obstacle_manager var obstacle_manager
# Minimal local state # Minimal local state
var match_id_input: LineEdit
var _connection_check_timer: float = 0.0 var _connection_check_timer: float = 0.0
func _ready(): func _ready():
@@ -27,7 +26,12 @@ func _ready():
ui_manager.setup_action_buttons(_set_action_state_callback) ui_manager.setup_action_buttons(_set_action_state_callback)
ui_manager.setup_playerboard_ui() ui_manager.setup_playerboard_ui()
_setup_obstacle_ui() _setup_obstacle_ui()
_setup_match_input()
# Auto-start game if coming from lobby (already connected to match)
if NakamaManager.is_connected_to_nakama() and multiplayer.get_unique_id() != 0:
print("Coming from lobby - auto-starting game...")
await get_tree().process_frame
_auto_start_from_lobby()
func _init_managers(): func _init_managers():
# Create and attach scene managers # Create and attach scene managers
@@ -41,13 +45,6 @@ func _init_managers():
add_child(obstacle_manager) add_child(obstacle_manager)
obstacle_manager.initialize($EnhancedGridMap) obstacle_manager.initialize($EnhancedGridMap)
func _setup_match_input():
match_id_input = LineEdit.new()
match_id_input.placeholder_text = "Enter Match ID to Join"
match_id_input.custom_minimum_size = Vector2(200, 30)
match_id_input.position = Vector2(10, 50)
$Menu.add_child(match_id_input)
func _setup_obstacle_ui(): func _setup_obstacle_ui():
var obstacle_button = Button.new() var obstacle_button = Button.new()
obstacle_button.text = "Place Obstacle" obstacle_button.text = "Place Obstacle"
@@ -80,34 +77,9 @@ func _process(delta):
verify_all_connections() verify_all_connections()
# ============================================================================= # =============================================================================
# Network Button Handlers # Network Callbacks
# ============================================================================= # =============================================================================
func _on_host_pressed():
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Server (Creating Match...)"
$Menu.visible = false
var success = await NakamaManager.connect_to_nakama_async()
if not success:
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Connection Failed"
$Menu.visible = true
return
NakamaManager.host_game()
func _on_join_pressed():
var match_id = match_id_input.text.strip_edges()
if match_id.is_empty():
print("Please enter a Match ID")
return
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Client (Joining...)"
$Menu.visible = false
var success = await NakamaManager.connect_to_nakama_async()
if not success:
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Connection Failed"
$Menu.visible = true
return
NakamaManager.join_game(match_id)
func _on_match_joined(match_id: String): func _on_match_joined(match_id: String):
$NetworkPanel/NetworkInfo/UniquePeerID.text = str(multiplayer.get_unique_id()) $NetworkPanel/NetworkInfo/UniquePeerID.text = str(multiplayer.get_unique_id())
@@ -151,16 +123,79 @@ func _setup_host_game():
_update_player_goals_ui(0, host_goals) _update_player_goals_ui(0, host_goals)
ui_manager.update_playerboard_ui() ui_manager.update_playerboard_ui()
# Add bots # Spawn client players that joined via lobby
if GameStateManager.enable_bots: var lobby_players = LobbyManager.get_players()
for lobby_player in lobby_players:
var peer_id = lobby_player.get("id", 0)
if peer_id != 1 and peer_id != 0: # Skip host (1) and invalid (0)
print("Spawning lobby player: ", peer_id)
await get_tree().create_timer(0.3).timeout
_spawn_lobby_client(peer_id)
# Add bots (only if no lobby players connected)
if GameStateManager.enable_bots and lobby_players.size() <= 1:
for i in range(2, GameStateManager.max_players + 1): for i in range(2, GameStateManager.max_players + 1):
_add_bot(i) _add_bot(i)
_start_game() _start_game()
func _spawn_lobby_client(peer_id: int):
"""Spawn a client player that was in the lobby."""
if has_node(str(peer_id)):
return
var player_character = PlayerManager.add_player_character(peer_id)
add_child(player_character)
player_character.add_to_group("Players", true)
GameStateManager.add_player(peer_id)
# Tell all clients to create this player
rpc("add_newly_connected_player_character", peer_id)
# Wait for player to be ready then assign goals
await get_tree().create_timer(0.3).timeout
var player_index = GameStateManager.players.find(peer_id)
if player_index >= 0 and player_index < GoalManager.preset_goals.size():
var player_goals = GoalManager.preset_goals[player_index].duplicate()
player_character.goals = player_goals
call_deferred("_deferred_set_player_goals", peer_id, player_goals)
func _setup_client_game(): func _setup_client_game():
"""Setup client when transitioning from lobby."""
var my_id = multiplayer.get_unique_id()
print("Client setup - my peer ID: ", my_id)
# Create local player immediately
if not has_node(str(my_id)):
var player_character = PlayerManager.add_player_character(my_id)
add_child(player_character)
player_character.add_to_group("Players", true)
GameStateManager.add_player(my_id)
GameStateManager.local_player_character = player_character
ui_manager.set_local_player(player_character)
ui_manager.update_button_states()
print("Created local player for client: ", my_id)
# Wait for host to be ready, then request full sync
await get_tree().create_timer(2.0).timeout await get_tree().create_timer(2.0).timeout
rpc_id(1, "request_full_player_sync", multiplayer.get_unique_id()) rpc_id(1, "request_full_player_sync", my_id)
func _auto_start_from_lobby():
"""Called when main.tscn is loaded from lobby - game is already connected."""
$NetworkPanel/NetworkInfo/UniquePeerID.text = str(multiplayer.get_unique_id())
# Get match ID from LobbyManager
var match_id = LobbyManager.current_room.get("match_id", "")
var short_id = match_id.substr(0, 8) if match_id.length() > 8 else match_id
if multiplayer.is_server():
print("Auto-starting as HOST - Match: ", short_id)
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Host (Match: %s)" % short_id
_setup_host_game()
else:
print("Auto-starting as CLIENT - Match: ", short_id)
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Client (Match: %s)" % short_id
_setup_client_game()
func _start_game(): func _start_game():
if multiplayer.is_server(): if multiplayer.is_server():
+1
View File
@@ -994,6 +994,7 @@ layout_mode = 2
text = "Arrange" text = "Arrange"
[node name="Menu" type="VBoxContainer" parent="."] [node name="Menu" type="VBoxContainer" parent="."]
visible = false
anchors_preset = 8 anchors_preset = 8
anchor_left = 0.5 anchor_left = 0.5
anchor_top = 0.5 anchor_top = 0.5
+249
View File
@@ -0,0 +1,249 @@
extends Node
# LobbyManager - Manages room/lobby state across scenes
# Signals
signal room_list_updated(rooms: Array)
signal room_joined(room_data: Dictionary)
signal room_left()
signal player_joined(player_data: Dictionary)
signal player_left(player_id: int)
signal ready_state_changed(player_id: int, is_ready: bool)
signal all_players_ready()
signal game_starting()
# Room data structure
var current_room: Dictionary = {}
var players_in_room: Array = [] # [{id, name, is_ready}]
var available_rooms: Array = []
var is_host: bool = false
var local_player_name: String = "Player"
# Ready to start game check
var _all_ready: bool = false
func _ready():
# 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)
# =============================================================================
# Room Creation / Joining
# =============================================================================
func create_room(room_name: String) -> void:
"""Host creates a new room with the given name."""
is_host = true
current_room = {
"room_name": room_name,
"host_name": local_player_name,
"max_players": GameStateManager.max_players
}
# Connect to Nakama and create match
var success = await NakamaManager.connect_to_nakama_async()
if not success:
push_error("Failed to connect to Nakama")
return
NakamaManager.host_game()
func join_room(match_id: String) -> void:
"""Client joins an existing room by match ID."""
is_host = false
var success = await NakamaManager.connect_to_nakama_async()
if not success:
push_error("Failed to connect to Nakama")
return
NakamaManager.join_game(match_id)
func leave_room() -> void:
"""Leave the current room."""
current_room = {}
players_in_room.clear()
is_host = false
_all_ready = false
# Disconnect from Nakama match
if NakamaManager.socket:
NakamaManager.socket.close()
emit_signal("room_left")
func refresh_room_list() -> void:
"""Request updated room list from Nakama."""
if not NakamaManager.is_connected_to_nakama():
var success = await NakamaManager.connect_to_nakama_async()
if not success:
return
var rooms = await NakamaManager.list_matches_async()
available_rooms = rooms
emit_signal("room_list_updated", rooms)
# =============================================================================
# Ready State Management
# =============================================================================
func set_ready(is_ready: bool) -> void:
"""Set local player's ready state."""
var my_id = multiplayer.get_unique_id()
# Update local state
for player in players_in_room:
if player["id"] == my_id:
player["is_ready"] = is_ready
break
# Sync to all peers
rpc("sync_ready_state", my_id, is_ready)
@rpc("any_peer", "call_local", "reliable")
func sync_ready_state(player_id: int, is_ready: bool) -> void:
"""Sync ready state across all clients."""
for player in players_in_room:
if player["id"] == player_id:
player["is_ready"] = is_ready
break
emit_signal("ready_state_changed", player_id, is_ready)
_check_all_ready()
func _check_all_ready() -> void:
"""Check if all players are ready."""
if players_in_room.size() < 2:
_all_ready = false
return
for player in players_in_room:
if not player["is_ready"]:
_all_ready = false
return
_all_ready = true
emit_signal("all_players_ready")
func is_all_ready() -> bool:
return _all_ready
# =============================================================================
# Game Start
# =============================================================================
func start_game() -> void:
"""Host triggers game start (transitions all players to main.tscn)."""
if not is_host:
push_error("Only host can start the game")
return
if not _all_ready:
push_error("Not all players are ready")
return
# Notify all clients to start
rpc("_on_game_starting")
@rpc("call_local", "reliable")
func _on_game_starting() -> void:
"""Called on all clients when game is starting."""
emit_signal("game_starting")
# Scene change will be handled by lobby.gd after receiving this signal
# =============================================================================
# Player Management
# =============================================================================
func _on_match_joined(match_id: String) -> void:
"""Called when successfully joined a Nakama match."""
current_room["match_id"] = match_id
# 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
current_room["room_name"] = short_id
# Add self to player list
var my_id = multiplayer.get_unique_id()
var my_data = {
"id": my_id,
"name": local_player_name,
"is_ready": false
}
players_in_room.append(my_data)
if is_host:
# Host is automatically in the room
emit_signal("room_joined", current_room)
# Client will request room info when peer connection is established
@rpc("any_peer", "reliable")
func request_room_info(requester_id: int) -> void:
"""Client requests room info from host."""
if not multiplayer.is_server():
return
# Send room data to requester
rpc_id(requester_id, "receive_room_info", current_room, players_in_room)
@rpc("reliable")
func receive_room_info(room_data: Dictionary, player_list: Array) -> void:
"""Client receives room info from host."""
current_room = room_data
players_in_room = player_list
emit_signal("room_joined", current_room)
func _on_peer_connected(peer_id: int) -> void:
"""Called when new peer connects."""
print("Peer connected: ", peer_id)
if multiplayer.is_server():
# Host: add new player and sync list
var new_player = {
"id": peer_id,
"name": "Player %d" % peer_id,
"is_ready": false
}
players_in_room.append(new_player)
# Sync player list to all clients
rpc("sync_player_list", players_in_room)
emit_signal("player_joined", new_player)
else:
# Client: if we connected to the host (peer_id 1), request room info
if peer_id == 1 and not is_host:
# Wait a frame to ensure connection is stable
await get_tree().process_frame
rpc_id(1, "request_room_info", multiplayer.get_unique_id())
func _on_peer_disconnected(peer_id: int) -> void:
"""Called when peer disconnects."""
for i in range(players_in_room.size()):
if players_in_room[i]["id"] == peer_id:
players_in_room.remove_at(i)
break
if multiplayer.is_server():
rpc("sync_player_list", players_in_room)
emit_signal("player_left", peer_id)
_check_all_ready()
@rpc("reliable")
func sync_player_list(player_list: Array) -> void:
"""Sync player list from host to all clients."""
players_in_room = player_list
func get_players() -> Array:
return players_in_room
func get_room_name() -> String:
return current_room.get("room_name", "Unknown Room")
func reset() -> void:
"""Reset lobby state."""
current_room = {}
players_in_room.clear()
available_rooms.clear()
is_host = false
_all_ready = false
+1
View File
@@ -0,0 +1 @@
uid://d23uvudhylph
+40
View File
@@ -112,6 +112,46 @@ func _on_bridge_match_join_error(error) -> void:
func is_connected_to_nakama() -> bool: func is_connected_to_nakama() -> bool:
return socket != null and socket.is_connected_to_host() return socket != null and socket.is_connected_to_host()
# --- Match Listing ---
func list_matches_async() -> Array:
"""Query available matches from Nakama server."""
if not client:
push_error("Cannot list matches: Client not initialized")
return []
if not session or session.is_expired():
push_error("Cannot list matches: No valid session")
return []
print("Querying matches from Nakama server...")
# Query matches - min 0, max 8 players, limit 20, authoritative=false for relayed matches
var result = await client.list_matches_async(session, 0, 8, 20, false, "", "")
if result.is_exception():
printerr("Failed to list matches: ", result.get_exception().message)
return []
var rooms: Array = []
if result.matches:
print("Found %d matches" % result.matches.size())
for match_data in result.matches:
print(" Match: ", match_data.match_id, " - Size: ", match_data.size)
# Use first 8 chars of match ID as room identifier since Nakama doesn't store custom names
var short_id = match_data.match_id.substr(0, 8) if match_data.match_id.length() > 8 else match_data.match_id
rooms.append({
"match_id": match_data.match_id,
"room_name": short_id,
"host_name": "Host",
"player_count": match_data.size if match_data.size else 1,
"max_players": 4
})
else:
print("No matches found")
return rooms
func _exit_tree(): func _exit_tree():
if socket: if socket:
socket.close() socket.close()