1325 lines
45 KiB
GDScript
1325 lines
45 KiB
GDScript
# -------------------------------------------------------------------------------------
|
|
# Tekton Dash - Multiplayer Board Game - 2024
|
|
# -------------------------------------------------------------------------------------
|
|
|
|
extends Node3D
|
|
|
|
# Manager references
|
|
var ui_manager
|
|
var obstacle_manager
|
|
var goals_cycle_manager
|
|
var screen_shake_manager
|
|
var touch_controls
|
|
|
|
# Minimal local state
|
|
var _connection_check_timer: float = 0.0
|
|
|
|
func _ready():
|
|
# Initialize scene managers
|
|
_init_managers()
|
|
|
|
# Connect to multiplayer signals
|
|
multiplayer.peer_connected.connect(_on_peer_connected)
|
|
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
|
|
|
# Connect to Nakama signals
|
|
NakamaManager.match_joined.connect(_on_match_joined)
|
|
|
|
# Setup UI
|
|
ui_manager.setup_action_buttons(_set_action_state_callback)
|
|
ui_manager.setup_playerboard_ui()
|
|
ui_manager.setup_timer_labels(self)
|
|
ui_manager.setup_leaderboard_ui(self)
|
|
ui_manager.setup_powerup_bar_ui(self)
|
|
_setup_obstacle_ui()
|
|
# GlobalMatchTimer is now static in main.tscn - no setup needed
|
|
# 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:
|
|
print("Coming from lobby - auto-starting game...")
|
|
await get_tree().process_frame
|
|
_auto_start_from_lobby()
|
|
|
|
func _init_managers():
|
|
# Create and attach scene managers
|
|
ui_manager = load("res://scripts/managers/ui_manager.gd").new()
|
|
ui_manager.name = "UIManager"
|
|
add_child(ui_manager)
|
|
ui_manager.initialize(self)
|
|
|
|
obstacle_manager = load("res://scripts/managers/obstacle_manager.gd").new()
|
|
obstacle_manager.name = "ObstacleManager"
|
|
add_child(obstacle_manager)
|
|
obstacle_manager.initialize($EnhancedGridMap)
|
|
|
|
# Goals cycle manager for 60-second timer and scoring
|
|
goals_cycle_manager = load("res://scripts/managers/goals_cycle_manager.gd").new()
|
|
goals_cycle_manager.name = "GoalsCycleManager"
|
|
add_child(goals_cycle_manager)
|
|
goals_cycle_manager.initialize(self)
|
|
|
|
# Screen shake manager for impact feedback
|
|
screen_shake_manager = load("res://scripts/managers/screen_shake.gd").new()
|
|
screen_shake_manager.name = "ScreenShakeManager"
|
|
add_child(screen_shake_manager)
|
|
screen_shake_manager.initialize($Camera3D)
|
|
|
|
# Touch controls for mobile
|
|
touch_controls = load("res://scripts/managers/touch_controls.gd").new()
|
|
touch_controls.name = "TouchControls"
|
|
add_child(touch_controls)
|
|
touch_controls.initialize(self)
|
|
|
|
# Connect signals for UI updates
|
|
goals_cycle_manager.timer_updated.connect(_on_timer_updated)
|
|
goals_cycle_manager.score_updated.connect(_on_score_updated)
|
|
goals_cycle_manager.leaderboard_updated.connect(_on_leaderboard_updated)
|
|
goals_cycle_manager.global_timer_updated.connect(_on_global_timer_updated)
|
|
goals_cycle_manager.match_ended.connect(_on_match_ended)
|
|
|
|
# Message Bar Configuration
|
|
const MAX_MESSAGES := 5
|
|
const MESSAGE_DURATION := 4.0
|
|
|
|
@onready var message_bar: PanelContainer = $MessageBar
|
|
@onready var message_container: VBoxContainer = $MessageBar/MarginContainer/MessageContainer
|
|
|
|
# Message types for different styling
|
|
enum MessageType {NORMAL, POWERUP, GOAL, CYCLE, WARNING}
|
|
|
|
func add_message_to_bar(player_name: String, message: String, type: int = MessageType.NORMAL):
|
|
if not message_container:
|
|
return
|
|
|
|
# Create message label with rich styling
|
|
var label = Label.new()
|
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
label.add_theme_font_size_override("font_size", 16)
|
|
label.modulate.a = 0.0 # Start invisible for fade-in
|
|
|
|
# Style based on message type
|
|
var icon = ""
|
|
var color = Color.WHITE
|
|
match type:
|
|
MessageType.POWERUP:
|
|
icon = "⚡ "
|
|
color = Color(0.4, 1.0, 0.4) # Bright green
|
|
MessageType.GOAL:
|
|
icon = "🎯 "
|
|
color = Color(1.0, 0.85, 0.2) # Gold
|
|
MessageType.CYCLE:
|
|
icon = "⏱️ "
|
|
color = Color(0.4, 0.8, 1.0) # Light blue
|
|
MessageType.WARNING:
|
|
icon = "⚠️ "
|
|
color = Color(1.0, 0.5, 0.3) # Orange
|
|
_:
|
|
icon = "💬 "
|
|
color = Color(0.9, 0.9, 0.9) # Light gray
|
|
|
|
# Include player name in message if provided
|
|
if player_name and player_name != "":
|
|
label.text = "%s[%s] %s" % [icon, player_name, message]
|
|
else:
|
|
label.text = "%s%s" % [icon, message]
|
|
label.add_theme_color_override("font_color", color)
|
|
|
|
# Add shadow for better visibility
|
|
label.add_theme_constant_override("shadow_offset_x", 2)
|
|
label.add_theme_constant_override("shadow_offset_y", 2)
|
|
label.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.7))
|
|
|
|
# Add to container
|
|
message_container.add_child(label)
|
|
|
|
# Show the message bar with fade
|
|
if not message_bar.visible:
|
|
message_bar.visible = true
|
|
message_bar.modulate.a = 0.0
|
|
var bar_tween = create_tween()
|
|
bar_tween.tween_property(message_bar, "modulate:a", 1.0, 0.2)
|
|
|
|
# Animate label entrance (slide in + fade)
|
|
label.position.x = -50
|
|
var entrance_tween = create_tween()
|
|
entrance_tween.set_parallel(true)
|
|
entrance_tween.tween_property(label, "modulate:a", 1.0, 0.3)
|
|
entrance_tween.tween_property(label, "position:x", 0.0, 0.3).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
|
|
|
# Powerup gets extra pulse effect
|
|
if type == MessageType.POWERUP:
|
|
await entrance_tween.finished
|
|
var pulse_tween = create_tween()
|
|
pulse_tween.set_loops(2)
|
|
pulse_tween.tween_property(label, "scale", Vector2(1.1, 1.1), 0.15).set_trans(Tween.TRANS_SINE)
|
|
pulse_tween.tween_property(label, "scale", Vector2(1.0, 1.0), 0.15).set_trans(Tween.TRANS_SINE)
|
|
|
|
# Remove oldest messages if over limit
|
|
while message_container.get_child_count() > MAX_MESSAGES:
|
|
var oldest = message_container.get_child(0)
|
|
oldest.queue_free()
|
|
|
|
# Auto-remove after duration with fade-out
|
|
await get_tree().create_timer(MESSAGE_DURATION).timeout
|
|
if is_instance_valid(label):
|
|
var exit_tween = create_tween()
|
|
exit_tween.set_parallel(true)
|
|
exit_tween.tween_property(label, "modulate:a", 0.0, 0.3)
|
|
exit_tween.tween_property(label, "position:x", 50.0, 0.3)
|
|
await exit_tween.finished
|
|
if is_instance_valid(label):
|
|
label.queue_free()
|
|
|
|
# Hide bar when empty with fade
|
|
await get_tree().process_frame
|
|
if message_container.get_child_count() == 0:
|
|
var hide_tween = create_tween()
|
|
hide_tween.tween_property(message_bar, "modulate:a", 0.0, 0.3)
|
|
await hide_tween.finished
|
|
message_bar.visible = false
|
|
|
|
@rpc("any_peer", "call_local")
|
|
func broadcast_message(player_name: String, message: String):
|
|
add_message_to_bar(player_name, message)
|
|
|
|
func _setup_obstacle_ui():
|
|
var obstacle_button = Button.new()
|
|
obstacle_button.text = "Place Obstacle"
|
|
obstacle_button.pressed.connect(func(): _set_action_state(ui_manager.ActionState.PLACING_OBSTACLE))
|
|
$ActionMenu/ActionButtonContainer.add_child(obstacle_button)
|
|
|
|
var orientation_button = Button.new()
|
|
orientation_button.text = "Direction: North"
|
|
orientation_button.pressed.connect(func():
|
|
orientation_button.text = obstacle_manager.cycle_obstacle_orientation()
|
|
)
|
|
$ActionMenu/ActionButtonContainer.add_child(orientation_button)
|
|
|
|
var type_button = Button.new()
|
|
type_button.text = "Type: 1"
|
|
type_button.pressed.connect(func():
|
|
type_button.text = obstacle_manager.cycle_obstacle_type()
|
|
)
|
|
$ActionMenu/ActionButtonContainer.add_child(type_button)
|
|
|
|
func _setup_global_match_timer_ui():
|
|
"""Create the global match timer display at the top of the screen."""
|
|
var existing = get_node_or_null("GlobalMatchTimer")
|
|
if existing:
|
|
return
|
|
|
|
# Create timer panel
|
|
var panel = PanelContainer.new()
|
|
panel.name = "GlobalMatchTimer"
|
|
|
|
# Position at top center
|
|
panel.set_anchors_preset(Control.PRESET_CENTER_TOP)
|
|
panel.offset_left = -80
|
|
panel.offset_right = 80
|
|
panel.offset_top = 10
|
|
panel.offset_bottom = 60
|
|
|
|
# Style
|
|
var style = StyleBoxFlat.new()
|
|
style.bg_color = Color(0.1, 0.1, 0.15, 0.9)
|
|
style.border_width_left = 2
|
|
style.border_width_top = 2
|
|
style.border_width_right = 2
|
|
style.border_width_bottom = 2
|
|
style.border_color = Color(0.647, 0.996, 0.224, 0.8)
|
|
style.corner_radius_top_left = 8
|
|
style.corner_radius_top_right = 8
|
|
style.corner_radius_bottom_right = 8
|
|
style.corner_radius_bottom_left = 8
|
|
panel.add_theme_stylebox_override("panel", style)
|
|
|
|
# VBox for content
|
|
var vbox = VBoxContainer.new()
|
|
vbox.name = "VBox"
|
|
vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
panel.add_child(vbox)
|
|
|
|
# Label
|
|
var label = Label.new()
|
|
label.name = "TimerLabel"
|
|
label.text = "3:00"
|
|
label.add_theme_font_size_override("font_size", 28)
|
|
label.add_theme_color_override("font_color", Color(0.647, 0.996, 0.224))
|
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
vbox.add_child(label)
|
|
|
|
add_child(panel)
|
|
|
|
func _process(delta):
|
|
if multiplayer.is_server() and GameStateManager.is_game_started():
|
|
if TurnManager.turn_based_mode:
|
|
rpc("sync_turn_index", TurnManager.current_turn_index)
|
|
update_all_players_goals()
|
|
|
|
_connection_check_timer += delta
|
|
if _connection_check_timer >= 5.0:
|
|
_connection_check_timer = 0.0
|
|
verify_all_connections()
|
|
|
|
# =============================================================================
|
|
# Network Callbacks
|
|
# =============================================================================
|
|
|
|
func _on_match_joined(match_id: String):
|
|
var network_panel = get_node_or_null("PauseMenu/Panel/NetworkPanel")
|
|
if network_panel:
|
|
network_panel.get_node("NetworkInfo/UniquePeerID").text = str(multiplayer.get_unique_id())
|
|
if multiplayer.is_server():
|
|
network_panel.get_node("NetworkInfo/NetworkSideDisplay").text = "Server (Match: %s)" % match_id
|
|
_setup_host_game()
|
|
else:
|
|
network_panel.get_node("NetworkInfo/NetworkSideDisplay").text = "Client"
|
|
_setup_client_game()
|
|
else:
|
|
if multiplayer.is_server():
|
|
_setup_host_game()
|
|
else:
|
|
_setup_client_game()
|
|
|
|
# =============================================================================
|
|
# Game Setup
|
|
# =============================================================================
|
|
|
|
func _setup_host_game():
|
|
# Generate goals
|
|
GoalManager.generate_preset_goals(GameStateManager.max_players)
|
|
|
|
# Add host player
|
|
var player_id = 1
|
|
var player_character = PlayerManager.add_player_character(player_id)
|
|
add_child(player_character)
|
|
player_character.add_to_group("Players", true)
|
|
GameStateManager.add_player(player_id)
|
|
GameStateManager.local_player_character = player_character
|
|
ui_manager.set_local_player(player_character)
|
|
if touch_controls:
|
|
touch_controls.set_player(player_character)
|
|
|
|
# Spawn client players that joined via lobby (need to add them first)
|
|
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)
|
|
_spawn_lobby_client_sync(peer_id)
|
|
|
|
# IMMEDIATELY assign random spawn positions before any player _ready() completes
|
|
# Player _ready() has 0.1s await, so we assign before that completes
|
|
if LobbyManager.get_randomize_spawn():
|
|
_assign_random_spawn_positions()
|
|
|
|
# Wait for players to be fully ready (player.gd has 0.1s await in _ready before managers init)
|
|
await get_tree().create_timer(0.3).timeout
|
|
|
|
# Set host goals - get goals directly from GoalManager
|
|
var host_goals = GoalManager.get_goals_for_player(0)
|
|
player_character.goals = host_goals
|
|
rpc("sync_player_goals", player_id, host_goals)
|
|
|
|
rpc("sync_preset_goals", GoalManager.preset_goals)
|
|
|
|
# Update the goals UI immediately for the host
|
|
var panel = $AllPlayerGoals.get_child(0)
|
|
panel.visible = true
|
|
_update_player_goals_ui(0, host_goals)
|
|
ui_manager.update_playerboard_ui()
|
|
|
|
# Set goals for lobby client players
|
|
var player_index = 1
|
|
for lobby_player in lobby_players:
|
|
var peer_id = lobby_player.get("id", 0)
|
|
if peer_id != 1 and peer_id != 0:
|
|
var client_player = get_node_or_null(str(peer_id))
|
|
if client_player and player_index < GoalManager.preset_goals.size():
|
|
var client_goals = GoalManager.preset_goals[player_index].duplicate()
|
|
client_player.goals = client_goals
|
|
call_deferred("_deferred_set_player_goals", peer_id, client_goals)
|
|
player_index += 1
|
|
|
|
# Add bots to fill remaining slots (regardless of player count)
|
|
if GameStateManager.enable_bots:
|
|
var current_players = lobby_players.size()
|
|
for i in range(current_players + 1, GameStateManager.max_players + 1):
|
|
_add_bot(i)
|
|
|
|
_start_game()
|
|
|
|
func _spawn_lobby_client_sync(peer_id: int):
|
|
"""Spawn a client player synchronously (no await)."""
|
|
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)
|
|
|
|
# Goals will be assigned after players are ready in _setup_host_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)
|
|
if touch_controls:
|
|
touch_controls.set_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
|
|
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."""
|
|
# 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
|
|
|
|
# Update NetworkPanel in PauseMenu (if exists)
|
|
var network_panel = get_node_or_null("PauseMenu/Panel/NetworkPanel")
|
|
if network_panel:
|
|
network_panel.get_node("NetworkInfo/UniquePeerID").text = str(multiplayer.get_unique_id())
|
|
if multiplayer.is_server():
|
|
network_panel.get_node("NetworkInfo/NetworkSideDisplay").text = "Host (Match: %s)" % short_id
|
|
else:
|
|
network_panel.get_node("NetworkInfo/NetworkSideDisplay").text = "Client (Match: %s)" % short_id
|
|
|
|
if multiplayer.is_server():
|
|
print("Auto-starting as HOST - Match: ", short_id)
|
|
_setup_host_game()
|
|
else:
|
|
print("Auto-starting as CLIENT - Match: ", short_id)
|
|
_setup_client_game()
|
|
|
|
func _start_game():
|
|
if multiplayer.is_server():
|
|
GameStateManager.start_game()
|
|
rpc("sync_game_start", GameStateManager.players, TurnManager.turn_based_mode)
|
|
if TurnManager.turn_based_mode:
|
|
TurnManager.reset_turn()
|
|
var next_player = TurnManager.next_turn(GameStateManager.players)
|
|
rpc("set_current_turn", next_player)
|
|
|
|
# Start the global match timer (this also starts the first cycle)
|
|
if goals_cycle_manager:
|
|
var match_duration = LobbyManager.get_match_duration()
|
|
goals_cycle_manager.start_match(float(match_duration))
|
|
|
|
# Initialize leaderboard with all players
|
|
if ui_manager:
|
|
var all_players = get_tree().get_nodes_in_group("Players")
|
|
ui_manager.initialize_leaderboard_with_players(all_players)
|
|
|
|
func _assign_random_spawn_positions():
|
|
"""Assign random unique spawn positions to all players."""
|
|
var spawn_locations = [
|
|
Vector2i(0, 0), Vector2i(0, 1), Vector2i(0, 2), Vector2i(0, 3),
|
|
Vector2i(0, 4), Vector2i(0, 5), Vector2i(0, 6), Vector2i(0, 7),
|
|
Vector2i(0, 8), Vector2i(0, 9), Vector2i(0, 10), Vector2i(0, 11)
|
|
]
|
|
|
|
# Shuffle spawn locations
|
|
var shuffled_spawns = spawn_locations.duplicate()
|
|
shuffled_spawns.shuffle()
|
|
|
|
# Get all players
|
|
var all_players = get_tree().get_nodes_in_group("Players")
|
|
|
|
# Assign positions
|
|
var spawn_index = 0
|
|
for player in all_players:
|
|
if spawn_index >= shuffled_spawns.size():
|
|
break
|
|
var spawn_pos = shuffled_spawns[spawn_index]
|
|
# Set position and sync to all clients
|
|
player.current_position = spawn_pos
|
|
player.position = player.grid_to_world(spawn_pos)
|
|
player.spawn_point_selected = true
|
|
player.rpc("set_spawn_position", spawn_pos)
|
|
spawn_index += 1
|
|
|
|
# =============================================================================
|
|
# Player Management
|
|
# =============================================================================
|
|
|
|
func _add_bot(bot_id: int):
|
|
rpc("create_bot", bot_id)
|
|
|
|
@rpc("call_local")
|
|
func create_bot(bot_id: int):
|
|
if not GameStateManager.enable_bots:
|
|
return
|
|
|
|
if has_node(str(bot_id)):
|
|
return
|
|
|
|
var bot_character = PlayerManager.create_bot(bot_id)
|
|
call_deferred("add_child", bot_character)
|
|
bot_character.add_to_group("Players", true)
|
|
bot_character.add_to_group("Bots", true)
|
|
|
|
if multiplayer.is_server():
|
|
GameStateManager.add_bot(bot_id)
|
|
|
|
var goal_index = bot_id - 1
|
|
if goal_index < GoalManager.preset_goals.size():
|
|
# Wait for bot managers to be ready
|
|
await get_tree().create_timer(0.2).timeout
|
|
bot_character.goals = GoalManager.preset_goals[goal_index].duplicate()
|
|
# Use deferred goals sync to avoid timing issues
|
|
call_deferred("_deferred_set_player_goals", bot_id, bot_character.goals)
|
|
|
|
@rpc("any_peer", "call_local")
|
|
func add_player_character(peer_id: int):
|
|
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)
|
|
|
|
if peer_id == multiplayer.get_unique_id():
|
|
GameStateManager.local_player_character = player_character
|
|
ui_manager.set_local_player(player_character)
|
|
if touch_controls:
|
|
touch_controls.set_player(player_character)
|
|
ui_manager.update_button_states()
|
|
ui_manager.update_playerboard_ui()
|
|
|
|
func _on_peer_connected(new_peer_id: int):
|
|
if multiplayer.is_server():
|
|
await get_tree().create_timer(1.5).timeout
|
|
add_player_character(new_peer_id)
|
|
rpc("add_newly_connected_player_character", new_peer_id)
|
|
|
|
# Wait for player to be ready then assign goals
|
|
await get_tree().create_timer(0.3).timeout
|
|
var player = get_node_or_null(str(new_peer_id))
|
|
if player:
|
|
# Get the next available goal set for this player
|
|
var player_index = GameStateManager.players.find(new_peer_id)
|
|
if player_index >= 0 and player_index < GoalManager.preset_goals.size():
|
|
var player_goals = GoalManager.preset_goals[player_index].duplicate()
|
|
player.goals = player_goals
|
|
# Update goals UI for all clients
|
|
call_deferred("_deferred_set_player_goals", new_peer_id, player_goals)
|
|
|
|
@rpc
|
|
func add_newly_connected_player_character(new_peer_id: int):
|
|
add_player_character(new_peer_id)
|
|
|
|
func _on_peer_disconnected(peer_id: int):
|
|
if multiplayer.is_server():
|
|
GameStateManager.remove_player(peer_id)
|
|
if GameStateManager.enable_bots:
|
|
var next_id = PlayerManager.get_next_available_bot_id(GameStateManager.max_players, GameStateManager.players)
|
|
if next_id != -1:
|
|
_add_bot(next_id)
|
|
|
|
# =============================================================================
|
|
# Turn Management (RPC Handlers)
|
|
# =============================================================================
|
|
|
|
@rpc("reliable")
|
|
func sync_turn_index(index: int):
|
|
TurnManager.current_turn_index = index
|
|
|
|
@rpc("any_peer", "call_local")
|
|
func set_current_turn(player_id: int):
|
|
if not TurnManager.turn_based_mode:
|
|
return
|
|
|
|
for player in get_tree().get_nodes_in_group("Players"):
|
|
var is_current_turn = player.name == str(player_id)
|
|
player.is_my_turn = is_current_turn
|
|
|
|
if is_current_turn and not (player.is_bot or player.is_in_group("Bots")):
|
|
player.action_points = 2
|
|
player.has_moved_this_turn = false
|
|
player.has_performed_action = false
|
|
player.start_turn()
|
|
player.clear_highlights()
|
|
player.clear_playerboard_highlights()
|
|
else:
|
|
player.is_my_turn = false
|
|
|
|
@rpc("call_local")
|
|
func sync_game_start(player_list: Array, is_turn_based: bool):
|
|
GameStateManager.players = player_list
|
|
TurnManager.turn_based_mode = is_turn_based
|
|
GameStateManager.start_game()
|
|
|
|
# Initialize leaderboard for all peers (after a delay to ensure players loaded)
|
|
call_deferred("_deferred_init_leaderboard")
|
|
|
|
# =============================================================================
|
|
# UI / Action State Management
|
|
# =============================================================================
|
|
|
|
func _set_action_state_callback(new_state):
|
|
_set_action_state(new_state)
|
|
|
|
func _set_action_state(new_state):
|
|
var local_player = GameStateManager.local_player_character
|
|
if not local_player or not local_player.is_multiplayer_authority():
|
|
return
|
|
|
|
if local_player.is_bot or local_player.is_in_group("Bots"):
|
|
ui_manager.current_action_state = new_state
|
|
return
|
|
|
|
if ui_manager.current_action_state == new_state or local_player.action_points <= 0:
|
|
return
|
|
|
|
ui_manager.current_action_state = new_state
|
|
local_player.clear_highlights()
|
|
local_player.clear_playerboard_highlights()
|
|
|
|
match new_state:
|
|
ui_manager.ActionState.MOVING:
|
|
local_player.highlight_movement_range()
|
|
ui_manager.ActionState.GRABBING:
|
|
local_player.highlight_adjacent_cells()
|
|
ui_manager.ActionState.PUTTING:
|
|
local_player.highlight_occupied_playerboard_slots()
|
|
ui_manager.ActionState.RANDOMIZING:
|
|
local_player.highlight_random_valid_cells()
|
|
ui_manager.ActionState.ARRANGING:
|
|
_show_arrangement_ui()
|
|
local_player.highlight_occupied_playerboard_slots()
|
|
ui_manager.ActionState.PLACING_OBSTACLE:
|
|
local_player.highlight_valid_obstacle_cells()
|
|
|
|
func _show_arrangement_ui():
|
|
if ui_manager.playerboard_ui:
|
|
ui_manager.playerboard_ui.visible = true
|
|
ui_manager.update_playerboard_ui()
|
|
|
|
func _on_playerboard_slot_clicked(event, slot_index):
|
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
|
var local_player = GameStateManager.local_player_character
|
|
if not local_player:
|
|
return
|
|
|
|
match ui_manager.current_action_state:
|
|
ui_manager.ActionState.ARRANGING:
|
|
local_player.arrange_playerboard_item(slot_index)
|
|
|
|
# =============================================================================
|
|
# Obstacle Management
|
|
# =============================================================================
|
|
|
|
func place_obstacle(grid_position: Vector2i) -> bool:
|
|
var local_player = GameStateManager.local_player_character
|
|
var success = obstacle_manager.place_obstacle(grid_position, local_player)
|
|
|
|
if success:
|
|
local_player.clear_highlights()
|
|
_set_action_state(ui_manager.ActionState.NONE)
|
|
|
|
if is_multiplayer_authority():
|
|
rpc("sync_place_obstacle", grid_position.x, grid_position.y, 3,
|
|
obstacle_manager.current_obstacle_item, obstacle_manager.current_obstacle_orientation)
|
|
|
|
return success
|
|
|
|
@rpc("any_peer", "call_local")
|
|
func sync_place_obstacle(x: int, y: int, floor_index: int, item_index: int, orientation: int):
|
|
$EnhancedGridMap.place_obstacle(Vector3i(x, floor_index, y), item_index, orientation)
|
|
|
|
# =============================================================================
|
|
# Goal & Playerboard Sync
|
|
# =============================================================================
|
|
|
|
@rpc("reliable")
|
|
func sync_preset_goals(goals_list: Array):
|
|
GoalManager.preset_goals = goals_list
|
|
|
|
@rpc("any_peer", "call_local")
|
|
func sync_player_goals(player_id: int, goals: Array):
|
|
var player = get_node_or_null(str(player_id))
|
|
if player:
|
|
# Defer the goal setting to ensure managers are ready
|
|
call_deferred("_deferred_set_player_goals", player_id, goals)
|
|
|
|
func _deferred_set_player_goals(player_id: int, goals: Array):
|
|
await get_tree().create_timer(0.25).timeout
|
|
var player = get_node_or_null(str(player_id))
|
|
if player and player.race_manager:
|
|
player.goals = goals.duplicate()
|
|
# Update the goals UI for all clients
|
|
_update_goals_ui_for_player(player_id, goals)
|
|
|
|
func _update_goals_ui_for_player(player_id: int, goals: Array):
|
|
# Find the player index among all players
|
|
var all_players = get_tree().get_nodes_in_group("Players")
|
|
all_players.sort_custom(func(a, b):
|
|
var a_id = int(String(a.name).get_slice("@", 0))
|
|
var b_id = int(String(b.name).get_slice("@", 0))
|
|
return a_id < b_id
|
|
)
|
|
|
|
var player_idx = -1
|
|
for i in range(all_players.size()):
|
|
if all_players[i].name == str(player_id):
|
|
player_idx = i
|
|
break
|
|
|
|
# Changed >= 0 to include index 0 (host player)
|
|
if player_idx != -1 and player_idx < $AllPlayerGoals.get_child_count():
|
|
$AllPlayerGoals.get_child(player_idx).visible = true
|
|
_update_player_goals_ui(player_idx, goals)
|
|
|
|
@rpc("any_peer", "call_local")
|
|
func sync_playerboard(player_id: int, new_playerboard: Array):
|
|
# Find the player and update their playerboard
|
|
var player = get_node_or_null(str(player_id))
|
|
if player:
|
|
player.playerboard = new_playerboard.duplicate()
|
|
|
|
# Update UI for local player
|
|
if player_id == multiplayer.get_unique_id() and GameStateManager.local_player_character:
|
|
ui_manager.update_playerboard_ui()
|
|
update_all_players_boards()
|
|
|
|
# =============================================================================
|
|
# UI Update Functions
|
|
# =============================================================================
|
|
|
|
func update_all_players_goals():
|
|
if not GameStateManager.is_game_started():
|
|
return
|
|
|
|
var all_players = get_tree().get_nodes_in_group("Players")
|
|
all_players.sort_custom(func(a, b):
|
|
var a_id = int(String(a.name).get_slice("@", 0))
|
|
var b_id = int(String(b.name).get_slice("@", 0))
|
|
return a_id < b_id
|
|
)
|
|
|
|
for i in range($AllPlayerGoals.get_child_count()):
|
|
$AllPlayerGoals.get_child(i).visible = false
|
|
|
|
var max_panels = $AllPlayerGoals.get_child_count()
|
|
for i in range(min(all_players.size(), max_panels)):
|
|
var player = all_players[i]
|
|
if player and player.goals.size() > 0:
|
|
$AllPlayerGoals.get_child(i).visible = true
|
|
_update_player_goals_ui(i, player.goals)
|
|
|
|
func _update_player_goals_ui(player_idx: int, goals: Array):
|
|
if player_idx < 0 or player_idx >= $AllPlayerGoals.get_child_count():
|
|
return
|
|
|
|
var panel = $AllPlayerGoals.get_child(player_idx)
|
|
if not panel.has_node("MarginContainer/Playergoals"):
|
|
return
|
|
|
|
var goals_grid = panel.get_node("MarginContainer/Playergoals")
|
|
for slot_idx in range(9):
|
|
if slot_idx >= goals_grid.get_child_count():
|
|
break
|
|
|
|
var slot = goals_grid.get_child(slot_idx)
|
|
var goal_value = goals[slot_idx] if slot_idx < goals.size() else -1
|
|
|
|
for tile_name in ["TileHeart", "TileDiamond", "TileStar", "TileCoin"]:
|
|
if slot.has_node(tile_name):
|
|
slot.get_node(tile_name).hide()
|
|
|
|
match goal_value:
|
|
7:
|
|
if slot.has_node("TileHeart"):
|
|
slot.get_node("TileHeart").show()
|
|
8:
|
|
if slot.has_node("TileDiamond"):
|
|
slot.get_node("TileDiamond").show()
|
|
9:
|
|
if slot.has_node("TileStar"):
|
|
slot.get_node("TileStar").show()
|
|
10:
|
|
if slot.has_node("TileCoin"):
|
|
slot.get_node("TileCoin").show()
|
|
|
|
func update_all_players_boards():
|
|
if not GameStateManager.is_game_started():
|
|
return
|
|
|
|
var all_players = get_tree().get_nodes_in_group("Players")
|
|
var all_player_boards = $AllPlayerBoards
|
|
|
|
# Update boards (simplified version - full implementation would mirror original)
|
|
pass
|
|
|
|
# =============================================================================
|
|
# Connection Verification
|
|
# =============================================================================
|
|
|
|
func verify_all_connections():
|
|
if multiplayer.is_server():
|
|
for peer_id in GameStateManager.players:
|
|
# Skip host (1) and bots (bots don't have real network connections)
|
|
if peer_id != 1 and not peer_id in GameStateManager.bots:
|
|
rpc_id(peer_id, "connection_verify", GameStateManager.players)
|
|
|
|
@rpc
|
|
func connection_verify(expected_players: Array):
|
|
for peer_id in expected_players:
|
|
if peer_id != multiplayer.get_unique_id() and not has_node(str(peer_id)):
|
|
rpc_id(1, "request_specific_player_data", peer_id)
|
|
|
|
@rpc("any_peer")
|
|
func request_specific_player_data(requested_peer_id: int):
|
|
if multiplayer.is_server():
|
|
var player = get_node_or_null(str(requested_peer_id))
|
|
if player:
|
|
var player_data = {
|
|
"peer_id": requested_peer_id,
|
|
"position": player.current_position,
|
|
"goals": player.goals,
|
|
"playerboard": player.playerboard,
|
|
"is_bot": player.is_bot || player.is_in_group("Bots")
|
|
}
|
|
rpc_id(multiplayer.get_remote_sender_id(), "create_specific_player", player_data)
|
|
|
|
@rpc("any_peer")
|
|
func request_full_player_sync(requesting_peer_id: int):
|
|
if multiplayer.is_server():
|
|
for peer_id in GameStateManager.players:
|
|
var player = get_node_or_null(str(peer_id))
|
|
if player:
|
|
var player_data = {
|
|
"peer_id": peer_id,
|
|
"position": player.current_position,
|
|
"goals": player.goals,
|
|
"playerboard": player.playerboard,
|
|
"is_bot": player.is_bot || player.is_in_group("Bots")
|
|
}
|
|
rpc_id(requesting_peer_id, "create_specific_player", player_data)
|
|
await get_tree().create_timer(0.1).timeout
|
|
|
|
@rpc("reliable")
|
|
func create_specific_player(data: Dictionary):
|
|
var peer_id = data["peer_id"]
|
|
var player_character = null
|
|
var node_already_exists = has_node(str(peer_id))
|
|
|
|
if node_already_exists:
|
|
# Player already exists, just get the reference
|
|
player_character = get_node(str(peer_id))
|
|
else:
|
|
# Create new player
|
|
player_character = PlayerManager.add_player_character(peer_id)
|
|
player_character.current_position = data["position"]
|
|
add_child(player_character)
|
|
player_character.add_to_group("Players", true)
|
|
|
|
if data["is_bot"]:
|
|
player_character.add_to_group("Bots", true)
|
|
player_character.is_bot = true
|
|
|
|
# Check if this is the local player (client's own player)
|
|
var is_local_player = (peer_id == multiplayer.get_unique_id())
|
|
if is_local_player and GameStateManager.local_player_character == null:
|
|
GameStateManager.local_player_character = player_character
|
|
ui_manager.set_local_player(player_character)
|
|
ui_manager.update_button_states()
|
|
|
|
# Wait for player managers to initialize (player.gd has 0.1s await in _ready)
|
|
await get_tree().create_timer(0.2).timeout
|
|
|
|
# Now set goals and playerboard after managers are ready
|
|
var goals_to_set = data["goals"].duplicate() if data.has("goals") else []
|
|
if goals_to_set.size() > 0 and player_character.race_manager:
|
|
player_character.goals = goals_to_set
|
|
|
|
var playerboard_to_set = data["playerboard"].duplicate() if data.has("playerboard") else []
|
|
if playerboard_to_set.size() > 0 and player_character.race_manager:
|
|
player_character.playerboard = playerboard_to_set
|
|
|
|
# Always update position (including for existing nodes, so client sees host correctly)
|
|
player_character.current_position = data["position"]
|
|
player_character.global_position = Vector3(
|
|
data["position"].x * 2 + 1,
|
|
1.0,
|
|
data["position"].y * 2 + 1
|
|
)
|
|
|
|
# Update playerboard UI for local player
|
|
if is_local_player:
|
|
ui_manager.update_playerboard_ui()
|
|
|
|
# Update goals UI for this player - use direct panel update
|
|
if goals_to_set.size() > 0:
|
|
# Find the correct panel index for this player
|
|
var all_players = get_tree().get_nodes_in_group("Players")
|
|
all_players.sort_custom(func(a, b):
|
|
var a_id = int(String(a.name).get_slice("@", 0))
|
|
var b_id = int(String(b.name).get_slice("@", 0))
|
|
return a_id < b_id
|
|
)
|
|
|
|
var player_idx = -1
|
|
for i in range(all_players.size()):
|
|
var player_name_id = int(String(all_players[i].name).get_slice("@", 0))
|
|
if player_name_id == peer_id:
|
|
player_idx = i
|
|
break
|
|
|
|
if player_idx != -1 and player_idx < $AllPlayerGoals.get_child_count():
|
|
$AllPlayerGoals.get_child(player_idx).visible = true
|
|
_update_player_goals_ui(player_idx, goals_to_set)
|
|
|
|
# =============================================================================
|
|
# Grid Item Randomization
|
|
# =============================================================================
|
|
|
|
func randomize_item_at_position(grid_position: Vector2i):
|
|
if not multiplayer.is_server():
|
|
rpc_id(1, "request_randomize_item", grid_position)
|
|
return
|
|
|
|
var enhanced_gridmap = $EnhancedGridMap
|
|
if enhanced_gridmap:
|
|
var cell = Vector3i(grid_position.x, 1, grid_position.y)
|
|
var current_item = enhanced_gridmap.get_cell_item(cell)
|
|
|
|
if current_item != -1:
|
|
var rng = RandomNumberGenerator.new()
|
|
rng.randomize()
|
|
var new_item = rng.randi_range(7, 10)
|
|
|
|
while new_item == current_item:
|
|
new_item = rng.randi_range(7, 10)
|
|
|
|
sync_grid_item(cell.x, cell.y, cell.z, new_item)
|
|
rpc("sync_grid_item", cell.x, cell.y, cell.z, new_item)
|
|
|
|
@rpc("any_peer")
|
|
func request_randomize_item(grid_position: Vector2i):
|
|
if multiplayer.is_server():
|
|
randomize_item_at_position(grid_position)
|
|
|
|
@rpc("any_peer", "call_local", "reliable")
|
|
func sync_grid_item(x: int, y: int, z: int, item: int):
|
|
var enhanced_gridmap = $EnhancedGridMap
|
|
if enhanced_gridmap:
|
|
enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item)
|
|
|
|
# =============================================================================
|
|
# Goals Cycle & Leaderboard UI
|
|
# =============================================================================
|
|
|
|
func _on_timer_updated(time_remaining: float):
|
|
# Update standalone timer display
|
|
var time_text = str(int(time_remaining))
|
|
|
|
var timer_panel = get_node_or_null("GoalsTimer")
|
|
if timer_panel:
|
|
var timer_label = timer_panel.get_node_or_null("VBox/TimerLabel")
|
|
if timer_label:
|
|
timer_label.text = time_text
|
|
|
|
func _on_score_updated(peer_id: int, new_score: int):
|
|
# Update player's score display
|
|
var player = get_node_or_null(str(peer_id))
|
|
if player:
|
|
player.score = new_score
|
|
|
|
# Update leaderboard UI
|
|
_update_leaderboard_display()
|
|
|
|
func _on_leaderboard_updated(sorted_scores: Array):
|
|
# Update the leaderboard panel locally
|
|
_update_leaderboard_display()
|
|
|
|
# Server broadcasts updated leaderboard to all clients
|
|
if multiplayer.is_server():
|
|
var player_data = []
|
|
for p in get_tree().get_nodes_in_group("Players"):
|
|
player_data.append({
|
|
# Use name.to_int() to correctly identify bots (Authority 1) vs Players
|
|
"peer_id": p.name.to_int(),
|
|
"name": p.display_name if not p.display_name.is_empty() else str(p.name),
|
|
"score": goals_cycle_manager.get_player_score(p.name.to_int()) if goals_cycle_manager else 0
|
|
})
|
|
rpc("sync_leaderboard_data", player_data)
|
|
|
|
func _on_global_timer_updated(time_remaining: float):
|
|
"""Update the global match timer display."""
|
|
var global_timer_panel = get_node_or_null("GlobalMatchTimer")
|
|
if global_timer_panel:
|
|
var timer_label = global_timer_panel.get_node_or_null("VBox/TimerLabel")
|
|
if timer_label:
|
|
var minutes = int(time_remaining) / 60
|
|
var seconds = int(time_remaining) % 60
|
|
timer_label.text = "%d:%02d" % [minutes, seconds]
|
|
|
|
func _on_match_ended():
|
|
"""Called when the global match timer ends - show game over screen."""
|
|
print("[Main] Match ended! Showing game over screen...")
|
|
|
|
# Disable player controls
|
|
var local_player = GameStateManager.local_player_character
|
|
if local_player:
|
|
local_player.action_points = 0
|
|
|
|
# Show game over overlay
|
|
_show_game_over_panel()
|
|
|
|
func _show_game_over_panel():
|
|
"""Create and display the game over panel with final leaderboard."""
|
|
# Check if panel already exists
|
|
var existing_panel = get_node_or_null("GameOverPanel")
|
|
if existing_panel:
|
|
existing_panel.show()
|
|
return
|
|
|
|
# Create game over panel
|
|
var panel = PanelContainer.new()
|
|
panel.name = "GameOverPanel"
|
|
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
|
|
# Semi-transparent dark background
|
|
var style = StyleBoxFlat.new()
|
|
style.bg_color = Color(0.0, 0.0, 0.0, 0.85)
|
|
panel.add_theme_stylebox_override("panel", style)
|
|
|
|
# Content container
|
|
var vbox = VBoxContainer.new()
|
|
vbox.name = "VBox"
|
|
vbox.set_anchors_preset(Control.PRESET_CENTER)
|
|
vbox.add_theme_constant_override("separation", 20)
|
|
vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
panel.add_child(vbox)
|
|
|
|
# Center the vbox
|
|
var margin = MarginContainer.new()
|
|
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
margin.add_theme_constant_override("margin_left", 200)
|
|
margin.add_theme_constant_override("margin_right", 200)
|
|
margin.add_theme_constant_override("margin_top", 100)
|
|
margin.add_theme_constant_override("margin_bottom", 100)
|
|
panel.add_child(margin)
|
|
|
|
var inner_vbox = VBoxContainer.new()
|
|
inner_vbox.add_theme_constant_override("separation", 30)
|
|
inner_vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
margin.add_child(inner_vbox)
|
|
|
|
# Title
|
|
var title = Label.new()
|
|
title.text = "⏱️ TIME'S UP!"
|
|
title.add_theme_font_size_override("font_size", 64)
|
|
title.add_theme_color_override("font_color", Color(0.992, 0.796, 0.047))
|
|
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
inner_vbox.add_child(title)
|
|
|
|
# Subtitle
|
|
var subtitle = Label.new()
|
|
subtitle.text = "FINAL STANDINGS"
|
|
subtitle.add_theme_font_size_override("font_size", 24)
|
|
subtitle.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
|
|
subtitle.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
inner_vbox.add_child(subtitle)
|
|
|
|
# Leaderboard container
|
|
var leaderboard_container = VBoxContainer.new()
|
|
leaderboard_container.add_theme_constant_override("separation", 15)
|
|
inner_vbox.add_child(leaderboard_container)
|
|
|
|
# Get final scores
|
|
var player_scores = []
|
|
for p in get_tree().get_nodes_in_group("Players"):
|
|
player_scores.append({
|
|
"name": p.display_name if not p.display_name.is_empty() else str(p.name),
|
|
"score": goals_cycle_manager.get_player_score(p.get_multiplayer_authority()) if goals_cycle_manager else 0
|
|
})
|
|
player_scores.sort_custom(func(a, b): return a.score > b.score)
|
|
|
|
# Display each player
|
|
for i in range(min(player_scores.size(), 4)):
|
|
var entry = HBoxContainer.new()
|
|
entry.add_theme_constant_override("separation", 20)
|
|
|
|
var rank_colors = [Color(1.0, 0.84, 0.0), Color(0.75, 0.75, 0.75), Color(0.8, 0.5, 0.2), Color(0.5, 0.5, 0.5)]
|
|
var rank_emojis = ["🥇", "🥈", "🥉", "4th"]
|
|
|
|
var rank_label = Label.new()
|
|
rank_label.text = rank_emojis[i]
|
|
rank_label.add_theme_font_size_override("font_size", 32)
|
|
entry.add_child(rank_label)
|
|
|
|
var name_label = Label.new()
|
|
name_label.text = player_scores[i].name
|
|
name_label.add_theme_font_size_override("font_size", 28)
|
|
name_label.add_theme_color_override("font_color", rank_colors[i])
|
|
name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
entry.add_child(name_label)
|
|
|
|
var score_label = Label.new()
|
|
score_label.text = str(player_scores[i].score)
|
|
score_label.add_theme_font_size_override("font_size", 28)
|
|
score_label.add_theme_color_override("font_color", Color(0.4, 1.0, 0.4))
|
|
entry.add_child(score_label)
|
|
|
|
leaderboard_container.add_child(entry)
|
|
|
|
# Back to Menu button
|
|
var back_btn = Button.new()
|
|
back_btn.name = "BackToMenuBtn"
|
|
back_btn.text = "BACK TO MAIN MENU"
|
|
back_btn.custom_minimum_size = Vector2(300, 60)
|
|
back_btn.add_theme_font_size_override("font_size", 20)
|
|
back_btn.pressed.connect(_on_back_to_menu_pressed)
|
|
|
|
# Center the button in a container
|
|
var btn_container = HBoxContainer.new()
|
|
btn_container.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
btn_container.add_child(back_btn)
|
|
inner_vbox.add_child(btn_container)
|
|
|
|
add_child(panel)
|
|
|
|
func _on_back_to_menu_pressed():
|
|
"""Return to lobby/main menu and clean up game state."""
|
|
print("[Main] Returning to lobby...")
|
|
|
|
# Clean up game state
|
|
GameStateManager.end_game()
|
|
LobbyManager.reset()
|
|
# Properly disconnect from Nakama match
|
|
_cleanup_multiplayer()
|
|
|
|
# Go back to lobby
|
|
if get_tree():
|
|
get_tree().change_scene_to_file("res://scenes/lobby.tscn")
|
|
|
|
func _cleanup_multiplayer():
|
|
"""Properly leave Nakama match and cleanup multiplayer state."""
|
|
print("[Main] Cleaning up multiplayer connection...")
|
|
|
|
# Leave the Nakama match through the bridge
|
|
if NakamaManager.bridge:
|
|
NakamaManager.bridge.leave()
|
|
|
|
# Clear the current match ID
|
|
NakamaManager.current_match_id = ""
|
|
|
|
# Reset multiplayer peer to disconnect cleanly
|
|
if multiplayer.get_multiplayer_peer():
|
|
multiplayer.set_multiplayer_peer(null)
|
|
|
|
func _deferred_init_leaderboard():
|
|
"""Initialize leaderboard after a delay to ensure all players are loaded."""
|
|
# Longer delay ensures players are synced
|
|
await get_tree().create_timer(1.5).timeout
|
|
|
|
# Request leaderboard sync from server for accurate data
|
|
if not multiplayer.is_server():
|
|
rpc_id(1, "request_leaderboard_sync")
|
|
else:
|
|
# Server can update directly
|
|
_update_leaderboard_display()
|
|
|
|
@rpc("any_peer")
|
|
func request_leaderboard_sync():
|
|
"""Client requests leaderboard data from server."""
|
|
if multiplayer.is_server():
|
|
var sender_id = multiplayer.get_remote_sender_id()
|
|
# Build player list with peer_ids and names
|
|
var player_data = []
|
|
for p in get_tree().get_nodes_in_group("Players"):
|
|
player_data.append({
|
|
# Use name.to_int() for consistent ID
|
|
"peer_id": p.name.to_int(),
|
|
"name": p.display_name if not p.display_name.is_empty() else str(p.name),
|
|
"score": goals_cycle_manager.get_player_score(p.name.to_int()) if goals_cycle_manager else 0
|
|
})
|
|
rpc_id(sender_id, "sync_leaderboard_data", player_data)
|
|
|
|
@rpc("authority", "call_local", "reliable")
|
|
func sync_leaderboard_data(player_data: Array):
|
|
"""Receive leaderboard data from server and update UI."""
|
|
var leaderboard_panel = get_node_or_null("LeaderboardPanel")
|
|
if not leaderboard_panel:
|
|
return
|
|
|
|
var vbox = leaderboard_panel.get_node_or_null("MarginContainer/VBox")
|
|
if not vbox:
|
|
return
|
|
|
|
# Sort by score descending
|
|
player_data.sort_custom(func(a, b): return a.score > b.score)
|
|
|
|
# Update entries
|
|
for i in range(4):
|
|
var entry = vbox.get_node_or_null("Entry" + str(i + 1))
|
|
if not entry:
|
|
continue
|
|
|
|
if i < player_data.size():
|
|
var data = player_data[i]
|
|
var rank_label = entry.get_node_or_null("RankLabel")
|
|
var name_label = entry.get_node_or_null("NameLabel")
|
|
var score_label = entry.get_node_or_null("ScoreLabel")
|
|
|
|
if rank_label:
|
|
rank_label.text = _get_ordinal(i + 1)
|
|
if name_label:
|
|
name_label.text = str(data.name)
|
|
if score_label:
|
|
score_label.text = str(data.score)
|
|
|
|
entry.visible = true
|
|
else:
|
|
entry.visible = false
|
|
|
|
func _update_leaderboard_display():
|
|
var leaderboard_panel = get_node_or_null("LeaderboardPanel")
|
|
if not leaderboard_panel:
|
|
return
|
|
|
|
# Try both possible paths for vbox
|
|
var vbox = leaderboard_panel.get_node_or_null("MarginContainer/VBox")
|
|
if not vbox:
|
|
vbox = leaderboard_panel.get_node_or_null("VBox")
|
|
if not vbox:
|
|
return
|
|
|
|
# Get all players in game
|
|
var all_players = get_tree().get_nodes_in_group("Players")
|
|
|
|
# Build scores array with all players
|
|
var player_data = []
|
|
for p in all_players:
|
|
var peer_id = p.name.to_int()
|
|
var score = goals_cycle_manager.get_player_score(peer_id) if goals_cycle_manager else 0
|
|
player_data.append({"peer_id": peer_id, "name": p.display_name if not p.display_name.is_empty() else str(p.name), "score": score})
|
|
|
|
# Sort by score descending
|
|
player_data.sort_custom(func(a, b): return a.score > b.score)
|
|
|
|
# Update entries
|
|
for i in range(4): # Max 4 entries
|
|
var entry = vbox.get_node_or_null("Entry" + str(i + 1))
|
|
if not entry:
|
|
continue
|
|
|
|
if i < player_data.size():
|
|
var data = player_data[i]
|
|
|
|
var rank_label = entry.get_node_or_null("RankLabel")
|
|
var name_label = entry.get_node_or_null("NameLabel")
|
|
var score_label = entry.get_node_or_null("ScoreLabel")
|
|
|
|
if rank_label:
|
|
rank_label.text = _get_ordinal(i + 1)
|
|
if name_label:
|
|
name_label.text = str(data.name)
|
|
if score_label:
|
|
score_label.text = str(data.score)
|
|
|
|
entry.visible = true
|
|
else:
|
|
entry.visible = false
|
|
|
|
func _get_ordinal(n: int) -> String:
|
|
match n:
|
|
1: return "1st"
|
|
2: return "2nd"
|
|
3: return "3rd"
|
|
4: return "4th"
|
|
_: return str(n) + "th"
|
|
|
|
# =============================================================================
|
|
# Pause Menu & Settings
|
|
# =============================================================================
|
|
|
|
func _input(event):
|
|
if event.is_action_pressed("ui_cancel"):
|
|
_toggle_pause_menu()
|
|
|
|
func _toggle_pause_menu():
|
|
var pause_menu = get_node_or_null("PauseMenu")
|
|
if pause_menu:
|
|
pause_menu.visible = not pause_menu.visible
|
|
get_tree().paused = pause_menu.visible
|
|
|
|
func _on_resume_pressed():
|
|
var pause_menu = get_node_or_null("PauseMenu")
|
|
if pause_menu:
|
|
pause_menu.visible = false
|
|
get_tree().paused = false
|
|
|
|
func _on_settings_pressed():
|
|
var pause_menu = get_node_or_null("PauseMenu")
|
|
var settings_panel = get_node_or_null("SettingsPanel")
|
|
if pause_menu:
|
|
pause_menu.visible = false
|
|
if settings_panel:
|
|
settings_panel.visible = true
|
|
|
|
# Sync settings UI with current state
|
|
var joystick_toggle = settings_panel.get_node_or_null("Panel/VBox/JoystickToggle")
|
|
if joystick_toggle and touch_controls:
|
|
joystick_toggle.set_pressed_no_signal(touch_controls.joystick_enabled)
|
|
|
|
var size_slider = settings_panel.get_node_or_null("Panel/VBox/ButtonSizeRow/ButtonSizeSlider")
|
|
if size_slider and touch_controls:
|
|
size_slider.set_value_no_signal(touch_controls.button_size)
|
|
|
|
var opacity_slider = settings_panel.get_node_or_null("Panel/VBox/OpacityRow/OpacitySlider")
|
|
if opacity_slider and touch_controls:
|
|
opacity_slider.set_value_no_signal(touch_controls.button_opacity)
|
|
|
|
func _on_quit_match_pressed():
|
|
get_tree().paused = false
|
|
# Properly disconnect from Nakama match
|
|
_cleanup_multiplayer()
|
|
# Return to lobby or main menu
|
|
get_tree().change_scene_to_file("res://scenes/lobby.tscn")
|
|
|
|
func _on_settings_back_pressed():
|
|
var pause_menu = get_node_or_null("PauseMenu")
|
|
var settings_panel = get_node_or_null("SettingsPanel")
|
|
if settings_panel:
|
|
settings_panel.visible = false
|
|
if pause_menu:
|
|
pause_menu.visible = true
|
|
|
|
func _on_button_size_changed(value: float):
|
|
if touch_controls:
|
|
touch_controls.button_size = value
|
|
touch_controls._save_settings()
|
|
|
|
func _on_opacity_changed(value: float):
|
|
if touch_controls:
|
|
touch_controls.button_opacity = value
|
|
touch_controls._save_settings()
|
|
|
|
func _on_joystick_toggled(enabled: bool):
|
|
if touch_controls:
|
|
touch_controls.set_joystick_enabled(enabled)
|
|
touch_controls._save_settings()
|