# ------------------------------------------------------------------------------------- # 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 (only if no lobby players connected) if GameStateManager.enable_bots and lobby_players.size() <= 1: for i in range(2, 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: if peer_id != 1: 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({ "peer_id": p.get_multiplayer_authority(), "name": p.name, "score": goals_cycle_manager.get_player_score(p.get_multiplayer_authority()) 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.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 %s" % 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({ "peer_id": p.get_multiplayer_authority(), "name": p.name, "score": goals_cycle_manager.get_player_score(p.get_multiplayer_authority()) 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 = "Player " + 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.get_multiplayer_authority() 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.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()