Files
tekton/scenes/main.gd
T

2183 lines
77 KiB
GDScript

# -------------------------------------------------------------------------------------
# Tekton Dash - Multiplayer Board Game - 2024
# -------------------------------------------------------------------------------------
extends Node3D
# Manager references
var ui_manager
var goals_cycle_manager
var screen_shake_manager
var touch_controls
var camera_context_manager
var stop_n_go_manager
var stop_n_go_winner_id: int = -1 # Track who finished first in Stop n Go mode
var obstacle_manager
var portal_mode_manager
# Minimal local state
var _connection_check_timer: float = 0.0
var reserved_static_positions: Array[Vector2i] = []
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_playerboard_label(self) # NEW
ui_manager.setup_leaderboard_ui(self)
ui_manager.setup_powerup_bar_ui(self)
# 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()
# Hide MessageBar (User Request)
if message_bar:
message_bar.visible = false
# Ensure grid is randomized with Scarcity if server
if multiplayer.is_server():
randomize_game_grid()
# Force gridmap cell size to match player logic (1, 0.05, 1) - >0.001 to avoid errors
var em = $EnhancedGridMap
if em:
em.cell_size = Vector3(1, 0.05, 1)
# Setup MultiplayerSpawner for Static Tekton Stands
# Create a container node for strict pathing
var stands_container = Node3D.new()
stands_container.name = "Stands"
add_child(stands_container)
var stand_spawner = MultiplayerSpawner.new()
stand_spawner.name = "StandSpawner"
stand_spawner.spawn_path = NodePath("../Stands") # Relative to Spawner, finding sibling
stand_spawner.add_spawnable_scene("res://scenes/static_tekton_stand.tscn")
stand_spawner.add_spawnable_scene("res://scenes/portal_door.tscn")
add_child(stand_spawner)
func _on_goal_count_updated(peer_id: int, count: int):
# Only update for local player
if peer_id == multiplayer.get_unique_id():
ui_manager.update_goal_count_label(count)
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)
# 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)
# Stop n Go manager for phase-based gameplay
if LobbyManager.game_mode == "Stop n Go":
stop_n_go_manager = load("res://scripts/managers/stop_n_go_manager.gd").new()
stop_n_go_manager.name = "StopNGoManager"
add_child(stop_n_go_manager)
# No direct initialize() yet, but we'll call start_game_mode later
# Portal manager for Tekton Doors mode
if LobbyManager.game_mode == "Tekton Doors":
portal_mode_manager = load("res://scripts/managers/portal_mode_manager.gd").new()
portal_mode_manager.name = "PortalModeManager"
add_child(portal_mode_manager)
portal_mode_manager.initialize(self, $EnhancedGridMap)
# 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($Camera3D200)
# Touch controls for mobile
touch_controls = get_node_or_null("TouchControls")
if not touch_controls:
print("TouchControls node not found in scene, creating instance...")
touch_controls = load("res://scripts/managers/touch_controls.gd").new()
touch_controls.name = "TouchControls"
add_child(touch_controls)
touch_controls.initialize(self)
# NEW: Camera Context Manager for dynamic camera position
camera_context_manager = load("res://scripts/managers/camera_context_manager.gd").new()
camera_context_manager.name = "CameraContextManager"
add_child(camera_context_manager)
camera_context_manager.initialize($Camera3D200, screen_shake_manager)
# Obstacle manager for dynamic walls
obstacle_manager = load("res://scripts/managers/obstacle_manager.gd").new()
obstacle_manager.name = "ObstacleManager"
add_child(obstacle_manager)
obstacle_manager.initialize(self, $EnhancedGridMap)
# 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.goal_count_updated.connect(_on_goal_count_updated) # NEW
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
var last_messages = {} # {player_name: {text: String, time: int}}
# 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):
# Deduplication check
var current_time = Time.get_ticks_msec()
if player_name in last_messages:
var last = last_messages[player_name]
# Ignore if same message within 2 seconds
if last.text == message and current_time - last.time < 2000:
return
last_messages[player_name] = {"text": message, "time": current_time}
if not message_container:
return
# Create message label with rich styling
var label = Label.new()
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
#label.custom_minimum_size = Vector2(400, 0) # Width constraint for wrapping
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
if is_instance_valid(label):
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)
message_container.remove_child(oldest)
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 _start_pre_game_countdown():
"""Show a 3-second countdown on all clients before starting."""
for i in range(3, 0, -1):
if can_rpc():
rpc("sync_countdown", str(i))
await get_tree().create_timer(1.0).timeout
if can_rpc():
rpc("sync_countdown", "GO!")
await get_tree().create_timer(0.5).timeout
if can_rpc():
rpc("sync_countdown", "")
@rpc("call_local", "reliable")
func sync_countdown(text: String):
var label = get_node_or_null("CountdownLabel")
if not label and text != "":
label = Label.new()
label.name = "CountdownLabel"
add_child(label)
# Center and Style
label.anchors_preset = Control.PRESET_CENTER
label.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
label.grow_horizontal = Control.GROW_DIRECTION_BOTH
label.grow_vertical = Control.GROW_DIRECTION_BOTH
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.add_theme_font_size_override("font_size", 120)
label.add_theme_color_override("font_outline_color", Color.BLACK)
label.add_theme_constant_override("outline_size", 12)
label.add_theme_color_override("font_color", Color.YELLOW)
if label:
label.text = text
if text == "":
label.queue_free()
elif text == "GO!":
label.add_theme_color_override("font_color", Color.GREEN)
func _setup_global_match_timer_ui():
"""Create the global match timer display at the top of the screen."""
# Check if timer check is enabled in lobby settings OR if in Stop n Go mode
if not LobbyManager.enable_cycle_timer and LobbyManager.game_mode != "Stop n Go":
var existing = get_node_or_null("GlobalMatchTimer")
if existing:
existing.visible = false
return
var existing = get_node_or_null("GlobalMatchTimer")
if existing:
existing.visible = true
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)
if camera_context_manager:
camera_context_manager.set_player(player_character)
# Set host name
player_character.display_name = LobbyManager.local_player_name
# 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)
# 1. PVT: Pre-calculate Static Tekton positions so we know where NOT to spawn players
_precalculate_static_positions()
# Delay spawn assignment until ALL players (including bots) are spawned
# Moved _assign_random_spawn_positions() to after bot loop
# 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)
# Ensure Bots are in the tree before assigning positions
await get_tree().process_frame
# INITIALIZE ARENA SIZE for Stop n Go BEFORE spawning players, to prevent out-of-bounds
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager:
stop_n_go_manager._setup_arena()
# Arena is set up, wait for __start_game to assign positions where Socket is open
_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)
# Set name from LobbyManager data if available
var lobby_players = LobbyManager.get_players()
for p_data in lobby_players:
if p_data.get("id") == peer_id:
player_character.display_name = p_data.get("name", "Player")
break
# 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)
# INITIALIZE ARENA SIZE for Stop n Go locally to prevent out-of-bounds before sync arrives
if LobbyManager.game_mode == "Stop n Go":
if not stop_n_go_manager:
stop_n_go_manager = load("res://scripts/managers/stop_n_go_manager.gd").new()
stop_n_go_manager.name = "StopNGoManager"
add_child(stop_n_go_manager)
stop_n_go_manager._apply_arena_setup()
# Pre-spawn ALL players known from LobbyManager (including Host ID 1)
# This ensures nodes exist to receive RPCs (like 'set_spawn_position') that might arrive before full sync
var lobby_players = LobbyManager.get_players()
for player_data in lobby_players:
var p_id = player_data.get("id", 0)
if p_id != 0:
add_player_character(p_id)
print("Client: Pre-spawned player ", p_id)
# Pre-spawn potential bots (IDs from count+1 to MaxPlayers) to prevent RPC "Node not found" errors
# Bots use small integer IDs (e.g. 2, 3, 4...) while clients use large unique IDs
if GameStateManager.enable_bots:
# Server spawns bots starting after the last human player index
# So if we have 2 humans, bots start at ID 3.
var start_bot_id = lobby_players.size() + 1
for i in range(start_bot_id, GameStateManager.max_players + 1):
# Only spawn if not already existing (e.g. if a human somehow got this ID, though unlikely)
if not has_node(str(i)):
# Spawning as BOT
add_player_character(i, true)
print("Client: Pre-spawned potential bot ", i)
# Ensure local player setup (UI, controls) is verified
var player_character = get_node_or_null(str(my_id))
if player_character:
# If we just spawned it above, we need to set these locally too
if GameStateManager.local_player_character != player_character:
GameStateManager.local_player_character = player_character
ui_manager.set_local_player(player_character)
if touch_controls:
touch_controls.set_player(player_character)
if camera_context_manager:
camera_context_manager.set_player(player_character)
ui_manager.update_button_states()
print("Client: Configured local player ", my_id)
# ALWAYS setup PowerUpUI when we have the local player, just in case
var powerup_ui = get_node_or_null("PowerUpInventoryUI")
if powerup_ui:
powerup_ui.setup(player_character)
print("Client: PowerUpInventoryUI setup forced for ", my_id)
# Wait shorter time for host to be ready, then request full sync to correct positions/state
await get_tree().create_timer(1.0).timeout
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
# Ensure we see the server (Peer 1)
if 1 in multiplayer.get_peers():
rpc_id(1, "request_full_player_sync", my_id)
rpc_id(1, "request_full_grid_sync")
else:
print("Client: Connected but Peer 1 not found yet. Retrying in 1s...")
await get_tree().create_timer(1.0).timeout
if 1 in multiplayer.get_peers():
rpc_id(1, "request_full_player_sync", my_id)
rpc_id(1, "request_full_grid_sync")
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():
# Wait for Nakama websocket to actually be open, up to 5 seconds
var nakama = get_node_or_null("/root/NakamaManager")
if nakama and nakama.has_method("is_connected_to_nakama"):
var wait_time = 0.0
while not nakama.is_connected_to_nakama() and wait_time < 5.0:
await get_tree().create_timer(0.2).timeout
wait_time += 0.2
# Allow socket/peer to stabilize before blasting RPCs
await get_tree().create_timer(2.0).timeout
# NOW assign random spawn positions for EVERYONE (Host, Client, Bots)
# This safely sends RPCs over the completed socket connection
_assign_random_spawn_positions()
# PRE-GAME COUNTDOWN (3s)
# Spawn static obstacles before countdown starts (Stop n Go only)
if obstacle_manager and LobbyManager.game_mode == "Stop n Go":
obstacle_manager.spawn_random_obstacles(15)
# Spawn mission tiles BEFORE countdown but AFTER walls (Stop n Go only)
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager:
stop_n_go_manager.setup_mission_tiles()
# Spawn Static Tektons BEFORE countdown (Free Mode Only)
# Exclude for Stop n Go and Tekton Doors
if LobbyManager.game_mode != "Stop n Go" and LobbyManager.game_mode != "Tekton Doors":
spawn_static_tektons()
await _start_pre_game_countdown()
GameStateManager.start_game()
if can_rpc():
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)
if can_rpc():
rpc("set_current_turn", next_player)
# Start the global match timer (this also starts the first cycle)
if LobbyManager.game_mode == "Stop n Go":
# Only Server starts the mode logic (arena setup, missions, etc)
if stop_n_go_manager:
stop_n_go_manager.start_game_mode()
# Also start global match timer for Stop n Go
if goals_cycle_manager:
var match_duration = LobbyManager.get_match_duration()
goals_cycle_manager.start_match(float(match_duration), false) # No cycles for Stop n Go
elif LobbyManager.game_mode == "Tekton Doors":
if portal_mode_manager:
portal_mode_manager.start_game_mode()
if goals_cycle_manager:
var match_duration = LobbyManager.get_match_duration()
goals_cycle_manager.start_match(float(match_duration))
elif 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)
# Spawn Tekton NPC
spawn_tekton_npc()
func _assign_random_spawn_positions():
"""Assign spawn positions distributed to 4 corners (2 per corner for 8 players)."""
var enhanced_gridmap = $EnhancedGridMap
if not enhanced_gridmap:
return
# Lists for each quadrant
var spawns_TL = [] # Top-Left
var spawns_TR = [] # Top-Right
var spawns_BL = [] # Bottom-Left
var spawns_BR = [] # Bottom-Right
var all_spawns = [] # Fallback
# Stop n Go Custom Spawn Logic
if LobbyManager.game_mode == "Stop n Go":
var all_players = get_tree().get_nodes_in_group("Players")
_assign_stop_n_go_spawn_positions(all_players)
return
# Tekton Doors Custom Spawn Logic
if LobbyManager.game_mode == "Tekton Doors":
var all_players = get_tree().get_nodes_in_group("Players")
_assign_portal_mode_spawn_positions(all_players)
return
var mid_x = enhanced_gridmap.columns / 2
var mid_z = enhanced_gridmap.rows / 2
for x in range(enhanced_gridmap.columns):
for z in range(enhanced_gridmap.rows):
var ground = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z))
if ground == 0: # Walkable
var pos = Vector2i(x, z)
# SAFETY CHECK: Is this reserved for a Static Tekton Stand?
# Stand covers [center-1, center+1]
var is_safe = true
for reserved in reserved_static_positions:
if abs(x - reserved.x) <= 1 and abs(z - reserved.y) <= 1:
is_safe = false
break
if not is_safe:
continue
all_spawns.append(pos)
if x < mid_x and z < mid_z:
spawns_TL.append(pos)
elif x >= mid_x and z < mid_z:
spawns_TR.append(pos)
elif x < mid_x and z >= mid_z:
spawns_BL.append(pos)
else:
spawns_BR.append(pos)
# Sort lists by distance to corners (closest to corner should be last, to be popped first)
# TL: Close to (0,0) -> Sort descending distance (so closest is at end)
spawns_TL.sort_custom(func(a, b): return a.length_squared() > b.length_squared())
# TR: Close to (13, 0)
var tr_corner = Vector2i(enhanced_gridmap.columns - 1, 0)
spawns_TR.sort_custom(func(a, b): return a.distance_squared_to(tr_corner) > b.distance_squared_to(tr_corner))
# BL: Close to (0, 13)
var bl_corner = Vector2i(0, enhanced_gridmap.rows - 1)
spawns_BL.sort_custom(func(a, b): return a.distance_squared_to(bl_corner) > b.distance_squared_to(bl_corner))
# BR: Close to (13, 13)
var br_corner = Vector2i(enhanced_gridmap.columns - 1, enhanced_gridmap.rows - 1)
spawns_BR.sort_custom(func(a, b): return a.distance_squared_to(br_corner) > b.distance_squared_to(br_corner))
# Fallback shuffle
all_spawns.shuffle()
# Get all players and sort them for deterministic assignment
var all_players = get_tree().get_nodes_in_group("Players")
all_players.sort_custom(func(a, b): return a.name.to_int() < b.name.to_int())
var spawn_index = 0
# Round-robin assignment to corners: TL, TR, BR, BL, TL, TR, BR, BL...
# Order: TL -> TR -> BR -> BL (Clockwise-ish)
var quadrants = [spawns_TL, spawns_TR, spawns_BR, spawns_BL]
for player in all_players:
var assigned_pos = Vector2i(-1, -1)
# Try to get from the current quadrant
var quadrant_idx = spawn_index % 4
var quadrant = quadrants[quadrant_idx]
if quadrant.size() > 0:
assigned_pos = quadrant.pop_back()
else:
# Fallback: Try other quadrants if preferred one is empty
for q in quadrants:
if q.size() > 0:
assigned_pos = q.pop_back()
break
# Ultimate fallback: Random from anywhere
if assigned_pos == Vector2i(-1, -1) and all_spawns.size() > 0:
assigned_pos = all_spawns.pop_back()
if assigned_pos != Vector2i(-1, -1):
# Set position and sync to all clients
player.current_position = assigned_pos
player.position = player.grid_to_world(assigned_pos)
player.is_player_moving = false
player.spawn_point_selected = true
if can_rpc():
player.rpc("set_spawn_position", assigned_pos)
else:
print("Critical: No spawn point found for player ", player.name)
spawn_index += 1
print("Assigned spawn %s to player %s" % [assigned_pos, player.name])
func _assign_stop_n_go_spawn_positions(all_players: Array):
"""Assigns spawns to the far left columns (Start Line) for Stop N Go mode."""
# Sort players for deterministic assignment based on ID
all_players.sort_custom(func(a, b): return a.name.to_int() < b.name.to_int())
var spawn_index = 0
for player in all_players:
# Use deterministic assignment from (0, 1) to (0, 8) to keep players separate
# Start Line is Column 0. We use rows 1 to 8.
var assigned_pos = Vector2i(0, spawn_index + 1)
# Ensure immediate sync
player.position = player.grid_to_world(assigned_pos)
player.current_position = assigned_pos
player.is_player_moving = false
player.spawn_point_selected = true
if can_rpc():
player.rpc("set_spawn_position", assigned_pos)
spawn_index += 1
print("[StopNGo] Assigned fixed starting block %s to player %s" % [assigned_pos, player.name])
func _assign_portal_mode_spawn_positions(all_players: Array):
"""Assigns spawns to different quadrants for Tekton Doors mode."""
if not portal_mode_manager:
_assign_random_spawn_positions() # Fallback
return
# Sort players for deterministic assignment
all_players.sort_custom(func(a, b): return a.name.to_int() < b.name.to_int())
var spawn_points = portal_mode_manager.get_spawn_points()
var spawn_index = 0
for player in all_players:
var assigned_pos = spawn_points[spawn_index % spawn_points.size()]
# Sync
player.position = player.grid_to_world(assigned_pos)
player.current_position = assigned_pos
player.is_player_moving = false
player.spawn_point_selected = true
if can_rpc():
player.rpc("set_spawn_position", assigned_pos)
spawn_index += 1
print("[PortalMode] Assigned Room Quadrant %s to player %s" % [assigned_pos, player.name])
# =============================================================================
# Tekton NPC Management
# =============================================================================
const StaticTektonManager = preload("res://scripts/managers/static_tekton_manager.gd")
var static_tekton_manager
func spawn_tekton_npc():
"""Spawn a Tekton NPC at a random location."""
if not multiplayer.is_server(): return
# Find random valid position
var enhanced_gridmap = $EnhancedGridMap
if not enhanced_gridmap: return
# Spawn 3 Roaming Tektons
var spawned_count = 0
var attempts = 0
while spawned_count < 3 and attempts < 50:
attempts += 1
# Find random valid position
var valid_pos = Vector2i(-1, -1)
var x = randi() % enhanced_gridmap.columns
var y = randi() % enhanced_gridmap.rows
var cell = Vector3i(x, 0, y)
# Check if walkable and no existing Tekton nearby?
if enhanced_gridmap.get_cell_item(cell) == 0: # Walkable floor
# Ensure not occupied by static tekton stand (Item 4)
var item_id = enhanced_gridmap.get_cell_item(Vector3i(x, 1, y))
if item_id == 4: continue # Wall/Stand
# Also check RESERVED positions (if they haven't spawned yet or for safety)
var is_safe = true
for reserved in reserved_static_positions:
if abs(x - reserved.x) <= 1 and abs(y - reserved.y) <= 1:
is_safe = false
break
if not is_safe: continue
valid_pos = Vector2i(x, y)
# Generate a consistent ID/Name for sync (add index to ensure uniqueness)
var tekton_id = Time.get_ticks_msec() + spawned_count
_create_tekton(valid_pos, tekton_id)
if can_rpc():
rpc("sync_spawn_tekton", valid_pos, tekton_id)
spawned_count += 1
print("[Main] Spawned Tekton %d at %s" % [spawned_count, valid_pos])
@rpc("call_remote", "reliable")
func sync_spawn_tekton(pos: Vector2i, tekton_id: int):
_create_tekton(pos, tekton_id)
func _create_tekton(pos: Vector2i, tekton_id: int, is_static: bool = false):
var node_name = "Tekton_%d" % tekton_id
if has_node(node_name): return
var tekton_scene = load("res://scenes/tekton.tscn")
var tekton = tekton_scene.instantiate()
tekton.name = node_name
add_child(tekton)
# Initialize
if is_static:
tekton.is_static_turret = true
if tekton.has_method("initialize"):
if has_node("EnhancedGridMap"):
tekton.initialize(pos, $EnhancedGridMap)
# If Static, swap controller
if is_static:
# tekton.is_static_turret = true # Already set above
var old_controller = tekton.get_node_or_null("TektonController")
if old_controller:
old_controller.queue_free()
var static_controller = load("res://scripts/static_tekton_controller.gd").new()
static_controller.name = "StaticTektonController"
tekton.add_child(static_controller)
print("[Main] Spawned STATIC Tekton at %s (ID: %d)" % [pos, tekton_id])
else:
print("[Main] Spawned Tekton at %s (ID: %d)" % [pos, tekton_id])
func _precalculate_static_positions():
"""Calculate and reserve Static Tekton positions early."""
if not multiplayer.is_server(): return
var enhanced_gridmap = $EnhancedGridMap
if not enhanced_gridmap: return
if not static_tekton_manager:
static_tekton_manager = StaticTektonManager.new()
# Calculate 3 spots and STORE them
var points: Array[Vector2i] = []
points = static_tekton_manager.calculate_spawn_points(3, enhanced_gridmap)
reserved_static_positions = points
print("[Main] Pre-calculated Static Tekton Positions: %s" % str(reserved_static_positions))
func spawn_static_tektons():
"""Spawn fixed static tektons using StaticTektonManager."""
if not multiplayer.is_server(): return
# Disable for Stop n Go mode
if LobbyManager.game_mode == "Stop n Go":
return
var enhanced_gridmap = $EnhancedGridMap
if not enhanced_gridmap: return
print("[Main] Initializing StaticTektonManager...")
if not static_tekton_manager:
static_tekton_manager = StaticTektonManager.new()
# Use pre-calculated points if available, otherwise calculate new ones
var spawn_points = []
if not reserved_static_positions.is_empty():
spawn_points = reserved_static_positions
else:
spawn_points = static_tekton_manager.calculate_spawn_points(3, enhanced_gridmap)
print("[Main] Spawning Static Tektons at: %s" % str(spawn_points))
for i in range(spawn_points.size()):
var pos = spawn_points[i]
# ID: 99000 + i (Consistent IDs for Static Tektons)
var id = 99000 + i
# Pick Shape on Server (0:Cyl, 1:Box, 2:Prism, 3:Sphere)
var shape_idx = randi() % 4
# Spawn on Server AND Sync to Clients (call_local handles both)
rpc("sync_spawn_static_setup", pos, id, shape_idx)
@rpc("call_local", "reliable")
func sync_spawn_static_setup(pos: Vector2i, tekton_id: int, shape_idx: int):
# Call local creation logic on all peers.
# Server: Spawns Stand + Void + Tekton
# Client: Avoids Stand (Spawner) + Void + Tekton
_create_static_setup(pos, tekton_id, shape_idx)
func _create_static_setup(pos: Vector2i, tekton_id: int, shape_idx: int):
"""Creates both the Stand and the Static Tekton at the position."""
var enhanced_gridmap = $EnhancedGridMap
# 1. Create Stand (Server Only - Synced via Spawner)
# IMPORTANT: Clients receive the Stand via MultiplayerSpawner.
# They MUST NOT spawn it manually here or we get duplicates.
if multiplayer.is_server():
var stands_container = get_node_or_null("Stands")
if stands_container:
var stand_name = "StaticStand_%d" % tekton_id
if not stands_container.has_node(stand_name):
var stand_scene = load("res://scenes/static_tekton_stand.tscn")
if stand_scene:
var stand = stand_scene.instantiate()
stand.name = stand_name
# Set Shape Index BEFORE adding to tree (so _ready picks it up/syncs)
if "shape_index" in stand:
stand.shape_index = shape_idx
stands_container.add_child(stand)
# Position Stand
if enhanced_gridmap:
# Convert grid to world
var world_pos = Vector3(pos.x + 0.5, 0, pos.y + 0.5)
if "cell_size" in enhanced_gridmap:
world_pos = Vector3(
pos.x * enhanced_gridmap.cell_size.x + enhanced_gridmap.cell_size.x / 2,
0,
pos.y * enhanced_gridmap.cell_size.z + enhanced_gridmap.cell_size.z / 2
)
stand.global_position = world_pos
# 2. Modify Base (Void) - Runs on ALL peers to update local GridMap visual/collision
if enhanced_gridmap:
var floor_count = 3
if "floors" in enhanced_gridmap:
floor_count = enhanced_gridmap.floors
for dx in range(-1, 2):
for dy in range(-1, 2):
var tile_pos_x = pos.x + dx
var tile_pos_z = pos.y + dy
# Clear ALL vertical layers (Ground, Items, etc.)
for f in range(floor_count):
var tile_pos = Vector3i(tile_pos_x, f, tile_pos_z)
enhanced_gridmap.set_cell_item(tile_pos, -1) # -1 = Empty/Void
# CRITICAL: Force AStar update so Bots and Pathfinding know about the new walls
if enhanced_gridmap.has_method("update_astar_costs"):
enhanced_gridmap.update_astar_costs()
# 3. Create Tekton Visual - Runs on ALL peers
# NOTE: Tekton NPC is currently not managed by a specialized Spawner for static setup?
# Or it is? If _create_tekton adds it to a path watched by a spawner, we should duplicate check.
# _create_tekton instantiates 'tekton.tscn' and adds to 'Main'.
# Main usually has a MultiplayerSpawner for 'Players' etc., but let's check.
# The original logic spawned it everywhere, so we keep that behavior to be safe.
# But we add a check to avoid duplicates if it already came in via sync.
if not has_node("Tekton_%d" % tekton_id):
_create_tekton(pos, tekton_id, true)
# =============================================================================
# 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 (race_manager is created at T=0.5)
await get_tree().create_timer(0.75).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, is_bot: bool = false):
print("[Main] add_player_character called for %d (is_bot: %s)" % [peer_id, is_bot])
if has_node(str(peer_id)):
print("[Main] Player %d already exists! Skipping spawn." % peer_id)
return
var player_character
if is_bot:
player_character = PlayerManager.create_bot(peer_id)
player_character.add_to_group("Bots", true)
else:
player_character = PlayerManager.add_player_character(peer_id)
# Set properties BEFORE adding to tree (ensure _ready sees correct state)
# create_bot already sets is_bot=true, but we ensure consistency
player_character.is_bot = is_bot
add_child(player_character)
player_character.add_to_group("Players", true)
GameStateManager.add_player(peer_id)
# Try to set name from LobbyManager data if available
var lobby_players = LobbyManager.get_players()
for p_data in lobby_players:
if p_data.get("id") == peer_id:
var p_name = p_data.get("name", "Player")
player_character.display_name = p_name
print("[Main] Set player %d name to %s from Lobby data" % [peer_id, p_name])
break
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)
if camera_context_manager:
camera_context_manager.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(0.1).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()
if LobbyManager.game_mode == "Stop n Go":
if not stop_n_go_manager:
stop_n_go_manager = load("res://scripts/managers/stop_n_go_manager.gd").new()
stop_n_go_manager.name = "StopNGoManager"
add_child(stop_n_go_manager)
stop_n_go_manager.activate_client_side()
# 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()
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)
# =============================================================================
# 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,
"name": player.display_name,
"goals": player.goals,
"playerboard": player.playerboard,
"is_bot": player.is_bot || player.is_in_group("Bots"),
"spawn_point_selected": player.spawn_point_selected
}
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)
# Set display name
if data.has("name"):
player_character.display_name = data["name"]
if data["is_bot"]:
player_character.add_to_group("Bots", true)
player_character.is_bot = true
# Set spawn flag and visibility for BOTH new and existing players
if data.has("spawn_point_selected") and data["spawn_point_selected"]:
player_character.spawn_point_selected = true
player_character.visible = true
# Ensure visual position matches logical
var new_pos = player_character.grid_to_world(data["position"])
player_character.global_position = new_pos
player_character.target_visual_position = new_pos
# Force collision update
if player_character.has_node("CollisionShape3D"):
player_character.get_node("CollisionShape3D").disabled = false
# 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"]
# Use the player's own grid_to_world to respect cell_size (1,1,1)
var new_world_pos = player_character.grid_to_world(data["position"])
player_character.global_position = new_world_pos
player_character.target_visual_position = new_world_pos
# 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 exists, replace it (scarcity aware)
# If current item exists OR we are forcing a spawn on valid ground
var floor_0_item = enhanced_gridmap.get_cell_item(Vector3i(grid_position.x, 0, grid_position.y))
var is_ground = (floor_0_item != -1) # Simple check, or check specific ground items
# Prevent stacking on players
if is_ground:
for player in get_tree().get_nodes_in_group("Players"):
if Vector2i(player.current_position.x, player.current_position.y) == grid_position:
is_ground = false
break
if is_ground:
var new_item = 7
# Use ScarcityController
new_item = ScarcityController.get_random_tile_id()
# If we are replacing an existing item, try to ensure it changes
if current_item != -1:
var max_retries = 3
while new_item == current_item and max_retries > 0:
new_item = ScarcityController.get_random_tile_id()
max_retries -= 1
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)
# Sync grid update (no need to sync whole grid if we do it at start, but if we do it late we might need to sync)
# For simplicity, we trust the grid syncs via normal mechanisms or initial state.
func randomize_game_grid():
if LobbyManager.game_mode == "Stop n Go":
return # Stop n Go manages its own arena setup
var enhanced_gridmap = $EnhancedGridMap
if enhanced_gridmap:
# Randomize Floor 1 using ScarcityController
enhanced_gridmap.randomize_floor(1, ScarcityController.get_random_tile_id)
# Sync to clients if needed (usually handled by initial state sync or explicit item syncs)
# Since Main.gd doesn't have a "Sync Floor" RPC, we rely on clients running the same seed or syncing individual cells.
# For now, let's assume server authority + sync on connect handles it, or add sync loop if critical.
pass
@rpc("authority", "call_local", "reliable")
func sync_full_grid_data_stop_n_go(floor0: PackedInt32Array, floor1: PackedInt32Array, cols: int, rows: int):
# Deprecated: Kept for signature compatibility but disabled to prevent MTU payload overflow.
pass
@rpc("any_peer")
func request_full_grid_sync():
if multiplayer.is_server():
var sender_id = multiplayer.get_remote_sender_id()
print("[Main] Grid sync requested by %d" % sender_id)
var enhanced_gridmap = $EnhancedGridMap
if enhanced_gridmap:
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager:
# Resync Phase and Missions since they might have missed the initial broadcast
var phase_name = "GO" if stop_n_go_manager.current_phase == stop_n_go_manager.Phase.GO else "STOP"
stop_n_go_manager.rpc_id(sender_id, "sync_phase", phase_name, stop_n_go_manager.phase_timer)
if stop_n_go_manager.player_missions.has(sender_id):
var mission_dict = {sender_id: stop_n_go_manager.player_missions[sender_id]}
stop_n_go_manager.rpc_id(sender_id, "sync_missions", mission_dict)
# For all modes, only sync Floor 1 (Items) to prevent MTU packet overflow.
# Floor 0 logic is deterministic and generated locally on level load.
var grid_data = enhanced_gridmap.get_floor_data(1)
print("[Main] Server: Prepared grid data. Size: %d. Sending to %d..." % [grid_data.size(), sender_id])
# Delay slightly to ensure socket stability after player syncs
await get_tree().create_timer(0.2).timeout
if sender_id in multiplayer.get_peers():
rpc_id(sender_id, "sync_full_grid_data", grid_data)
print("[Main] Server: Sent grid sync rpc_id to %d" % sender_id)
@rpc("authority", "call_local", "reliable")
func sync_full_grid_data(data: PackedInt32Array):
print("[Main] sync_full_grid_data received. Items: %d" % (data.size() / 3))
var enhanced_gridmap = $EnhancedGridMap
if not enhanced_gridmap:
print("[Main] Error: EnhancedGridMap not found!")
return
# Reapply deterministic Floor 0 before syncing Floor 1 items
if LobbyManager.game_mode == "Stop n Go":
if not stop_n_go_manager:
stop_n_go_manager = load("res://scripts/managers/stop_n_go_manager.gd").new()
stop_n_go_manager.name = "StopNGoManager"
add_child(stop_n_go_manager)
stop_n_go_manager._apply_arena_setup()
# Apply the synced data to Floor 1
enhanced_gridmap.set_floor_data(1, data)
enhanced_gridmap.update_grid_data()
enhanced_gridmap.initialize_astar()
print("[Main] Grid sync complete.")
# =============================================================================
# Goals Cycle & Leaderboard UI
# =============================================================================
func _on_timer_updated(time_remaining: float):
# Update standalone timer display
# DISABLED: Now used for PowerUp Cooldown (handled in UIManager)
# 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
pass
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)
# Update player rank visuals for everyone (Client + Server)
var all_players = get_tree().get_nodes_in_group("Players")
var sorted_players = []
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
sorted_players.append({"node": p, "score": score})
# Sort by score descending (with Stop n Go winner priority)
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_winner_id != -1:
sorted_players.sort_custom(func(a, b):
var a_id = a.node.name.to_int()
var b_id = b.node.name.to_int()
if a_id == stop_n_go_winner_id: return true
if b_id == stop_n_go_winner_id: return false
return a.score > b.score
)
else:
sorted_players.sort_custom(func(a, b): return a.score > b.score)
# Assign rank
for i in range(sorted_players.size()):
var p_node = sorted_players[i].node
var rank = i + 1
if p_node.has_method("update_rank_visuals"):
p_node.update_rank_visuals(rank)
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]
@rpc("any_peer", "call_local", "reliable")
func sync_game_end_stop_n_go(winner_id: int):
print("[STOP n GO] Game ended! Winner: ", winner_id)
stop_n_go_winner_id = winner_id
if goals_cycle_manager:
goals_cycle_manager.stop_n_go_winner_id = winner_id
var winner_name = "Player " + str(winner_id)
var player_node = get_node_or_null(str(winner_id))
if player_node:
winner_name = player_node.display_name
# Broadcast win (Validation already done in check_win_condition)
add_message_to_bar("MATCH COMPLETE", winner_name + " Wins with 3 Missions!", MessageType.GOAL)
# Stop logic
if stop_n_go_manager:
stop_n_go_manager.is_active = false
# Trigger match end
_on_match_ended()
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)
var player_scores = []
for p in get_tree().get_nodes_in_group("Players"):
player_scores.append({
"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
})
# Custom Sort for Stop n Go: Winner always first
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_winner_id != -1:
player_scores.sort_custom(func(a, b):
if a.peer_id == stop_n_go_winner_id: return true
if b.peer_id == stop_n_go_winner_id: return false
return a.score > b.score
)
else:
player_scores.sort_custom(func(a, b): return a.score > b.score)
# Display each player
for i in range(min(player_scores.size(), 8)):
var entry = HBoxContainer.new()
entry.add_theme_constant_override("separation", 20)
var rank_colors = [
Color(1.0, 0.84, 0.0), # Gold
Color(0.75, 0.75, 0.75), # Silver
Color(0.8, 0.5, 0.2), # Bronze
Color(0.5, 0.5, 0.5), # 4th
Color(0.5, 0.5, 0.5), # 5th
Color(0.5, 0.5, 0.5), # 6th
Color(0.5, 0.5, 0.5), # 7th
Color(0.5, 0.5, 0.5) # 8th
]
var rank_emojis = ["🥇", "🥈", "🥉", "4th", "5th", "6th", "7th", "8th"]
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():
if 1 in multiplayer.get_peers():
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 (with Stop n Go winner priority)
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_winner_id != -1:
player_data.sort_custom(func(a, b):
if a.peer_id == stop_n_go_winner_id: return true
if b.peer_id == stop_n_go_winner_id: return false
return a.score > b.score
)
else:
player_data.sort_custom(func(a, b): return a.score > b.score)
# Update entries
_render_leaderboard_entries(player_data)
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 (with Stop n Go winner priority)
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_winner_id != -1:
player_data.sort_custom(func(a, b):
if a.peer_id == stop_n_go_winner_id: return true
if b.peer_id == stop_n_go_winner_id: return false
return a.score > b.score
)
else:
player_data.sort_custom(func(a, b): return a.score > b.score)
_render_leaderboard_entries(player_data)
func _render_leaderboard_entries(sorted_player_data: Array):
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: vbox = leaderboard_panel.get_node_or_null("VBox")
if not vbox: return
var my_id = multiplayer.get_unique_id()
var my_index = -1
for i in range(sorted_player_data.size()):
if sorted_player_data[i].peer_id == my_id:
my_index = i
break
# Determine items to display (Max 4 slots for HUD)
var items_to_display = []
# add top 3
for i in range(min(3, sorted_player_data.size())):
items_to_display.append({"data": sorted_player_data[i], "rank": i + 1})
# add 4th slot (Smart Slot)
if sorted_player_data.size() >= 4:
# If local player is outside top 3 (index > 2), show them in 4th slot
# But if they are exactly 4th (index 3), it's the same as showing 4th place.
# If they are 5th (index 4) or worse, we replace 4th place with them.
if my_index > 3:
# Show local player
items_to_display.append({"data": sorted_player_data[my_index], "rank": my_index + 1})
else:
# Show standard 4th place
items_to_display.append({"data": sorted_player_data[3], "rank": 4})
# Render
# We assume the HUD has 4 slots max, but code iterates up to 8 just in case UI has more and we want to hide them.
for i in range(8):
var entry = vbox.get_node_or_null("Entry" + str(i + 1))
if not entry: continue
# Only show up to 4 entries in this new "Smart" mode
if i < items_to_display.size() and i < 4:
var item = items_to_display[i]
var data = item.data
var rank = item.rank
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(rank)
if name_label: name_label.text = str(data.name)
if score_label: score_label.text = str(data.score)
entry.visible = true
if data.peer_id == my_id:
entry.modulate = Color(1.0, 1.0, 0.0) # Yellow
else:
entry.modulate = Color.WHITE
else:
entry.visible = false
func _get_ordinal(n: int) -> String:
match n:
1: return "1st"
2: return "2nd"
3: return "3rd"
_: return str(n) + "th"
# =============================================================================
# Pause Menu & Settings
# =============================================================================
func _input(event):
if event.is_action_pressed("ui_cancel"):
_toggle_pause_menu()
# DEBUG: check all floors
if event is InputEventKey and event.pressed and event.keycode == KEY_F9:
check_all_floors()
func check_all_floors():
print("--- CHECKING ALL FLOORS (Debug F9) ---")
var enhanced_gridmap = get_node_or_null("EnhancedGridMap")
if not enhanced_gridmap:
print("Error: EnhancedGridMap not found.")
return
var missing_count = 0
var total_checked = 0
# Assuming standard 14x14 board (0-13)
for x in range(14):
for y in range(14):
total_checked += 1
var cell_3d = Vector3i(x, 0, y)
var item = enhanced_gridmap.get_cell_item(cell_3d)
if item == -1:
print("MISSING FLOOR at [%d, %d]! (Item: -1)" % [x, y])
missing_count += 1
# Optional: Auto-fix?
# enhanced_gridmap.set_cell_item(cell_3d, 1)
elif item == 6:
print("ICE/CRACK FLOOR at [%d, %d] (Item: 6)" % [x, y])
print("--- CHECK COMPLETE: Found %d missing floors out of %d checked. ---" % [missing_count, total_checked])
var msg_type = NotificationManager.MessageType.WARNING if missing_count > 0 else NotificationManager.MessageType.NORMAL
NotificationManager.send_message(GameStateManager.local_player_character, "Checked Floors: %d Missing" % missing_count, msg_type)
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()
func can_rpc() -> bool:
if not multiplayer.has_multiplayer_peer() or multiplayer.multiplayer_peer.get_connection_status() != MultiplayerPeer.CONNECTION_CONNECTED:
return false
var nakama = get_node_or_null("/root/NakamaManager")
if nakama and nakama.has_method("is_connected_to_nakama") and not nakama.is_connected_to_nakama():
return false
return true
@rpc("authority", "call_local", "reliable")
func display_message(message: String, type: int = 0):
"""Broadcasts a message to the local player's UI. This is called via main.rpc from various managers."""
# Find local player
var all_players = get_tree().get_nodes_in_group("Players")
for player in all_players:
# Check if this player is controlled by THIS client
if player.is_multiplayer_authority():
if player.has_method("display_message"):
player.display_message(message, type)
break