# ------------------------------------------------------------------------------------- # Tekton Dash - Multiplayer Board Game - 2024 # ------------------------------------------------------------------------------------- extends Node3D # Manager references var ui_manager var obstacle_manager var goals_cycle_manager # 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() # 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) # 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) # Message Bar Configuration const MAX_MESSAGES := 5 const MESSAGE_DURATION := 3.0 @onready var message_bar: PanelContainer = $MessageBar @onready var message_container: VBoxContainer = $MessageBar/MarginContainer/MessageContainer func add_message_to_bar(player_name: String, message: String): if not message_container: return # Create message label var label = Label.new() label.text = "[%s] %s" % [player_name, message] label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.add_theme_color_override("font_color", Color.WHITE) label.add_theme_font_size_override("font_size", 14) # Add to container message_container.add_child(label) # Show the message bar message_bar.visible = true # 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 await get_tree().create_timer(MESSAGE_DURATION).timeout if is_instance_valid(label): label.queue_free() # Hide bar when empty await get_tree().process_frame if message_container.get_child_count() == 0: 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 _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): $NetworkPanel/NetworkInfo/UniquePeerID.text = str(multiplayer.get_unique_id()) if multiplayer.is_server(): $NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Server (Match: %s)" % match_id _setup_host_game() else: $NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Client" _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) # Wait for player to be fully ready (player.gd has 0.1s await in _ready before managers init) await get_tree().create_timer(0.2).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() # Spawn client players that joined via lobby var lobby_players = LobbyManager.get_players() for lobby_player in lobby_players: var peer_id = lobby_player.get("id", 0) if peer_id != 1 and peer_id != 0: # Skip host (1) and invalid (0) print("Spawning lobby player: ", peer_id) await get_tree().create_timer(0.3).timeout _spawn_lobby_client(peer_id) # Add bots (only if no lobby players connected) if GameStateManager.enable_bots and lobby_players.size() <= 1: for i in range(2, GameStateManager.max_players + 1): _add_bot(i) _start_game() func _spawn_lobby_client(peer_id: int): """Spawn a client player that was in the lobby.""" if has_node(str(peer_id)): return var player_character = PlayerManager.add_player_character(peer_id) add_child(player_character) player_character.add_to_group("Players", true) GameStateManager.add_player(peer_id) # Tell all clients to create this player rpc("add_newly_connected_player_character", peer_id) # Wait for player to be ready then assign goals await get_tree().create_timer(0.3).timeout var player_index = GameStateManager.players.find(peer_id) if player_index >= 0 and player_index < GoalManager.preset_goals.size(): var player_goals = GoalManager.preset_goals[player_index].duplicate() player_character.goals = player_goals call_deferred("_deferred_set_player_goals", peer_id, player_goals) func _setup_client_game(): """Setup client when transitioning from lobby.""" var my_id = multiplayer.get_unique_id() print("Client setup - my peer ID: ", my_id) # Create local player immediately if not has_node(str(my_id)): var player_character = PlayerManager.add_player_character(my_id) add_child(player_character) player_character.add_to_group("Players", true) GameStateManager.add_player(my_id) GameStateManager.local_player_character = player_character ui_manager.set_local_player(player_character) ui_manager.update_button_states() print("Created local player for client: ", my_id) # Wait for host to be ready, then request full sync await get_tree().create_timer(2.0).timeout rpc_id(1, "request_full_player_sync", my_id) func _auto_start_from_lobby(): """Called when main.tscn is loaded from lobby - game is already connected.""" $NetworkPanel/NetworkInfo/UniquePeerID.text = str(multiplayer.get_unique_id()) # Get match ID from LobbyManager var match_id = LobbyManager.current_room.get("match_id", "") var short_id = match_id.substr(0, 8) if match_id.length() > 8 else match_id if multiplayer.is_server(): print("Auto-starting as HOST - Match: ", short_id) $NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Host (Match: %s)" % short_id _setup_host_game() else: print("Auto-starting as CLIENT - Match: ", short_id) $NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Client (Match: %s)" % short_id _setup_client_game() func _start_game(): 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 goals cycle timer if goals_cycle_manager: goals_cycle_manager.start_cycle() # ============================================================================= # 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) 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() # ============================================================================= # 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): 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 timer display on all player goal panels var time_text = "%02d:%02d" % [int(time_remaining) / 60, int(time_remaining) % 60] for i in range($AllPlayerGoals.get_child_count()): var panel = $AllPlayerGoals.get_child(i) if panel.visible: var timer_label = panel.get_node_or_null("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 _update_leaderboard_display() func _update_leaderboard_display(): var leaderboard_panel = get_node_or_null("LeaderboardPanel") if not leaderboard_panel: return var sorted_scores = goals_cycle_manager.get_leaderboard() if goals_cycle_manager else [] # Get player names and update entries for i in range(4): # Max 4 entries var entry = leaderboard_panel.get_node_or_null("Entry" + str(i + 1)) if not entry: continue if i < sorted_scores.size(): var score_data = sorted_scores[i] var player = get_node_or_null(str(score_data.peer_id)) var player_name = player.name if player else str(score_data.peer_id) 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_name if score_label: score_label.text = str(score_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"