# ------------------------------------------------------------------------------------- # Tekton Dash - Multiplayer Board Game - 2024 # ------------------------------------------------------------------------------------- # [x] Move is working, you can move the character to another tile on gridmap # [ ] Make the Randomize tile, currently there's no logic to handle it # it's randomly highlighted random tile on gridmap # [x] Arrange tile is working, you can move the tile to another slot on the playerboard # [x] Put tile is working, you can put the tile from playerboard to the gridmap # [x] Grab tile is working, you can grab the tile from gridmap to playerboard # ------------------------------------------------------------------------------------- # [ ] Implement the Boosts tile, that can be used to boost player movement to next tile # [ ] Implement the Obstacle tile, that can be used to block player movement to next tile # ------------------------------------------------------------------------------------- # [x] Added multiplayer support - with act as server and client # [x] Added UPnP support for automatic port forwarding, for android and desktop # [x] Added Randomized Goals for each player # [x] Added bot support ( currently broken ) # [x] Added turn-based mode # [x] Added Realtime mode # [ ] Implement ActionState costs # [ ] Implement ActionState disable condition # [ ] Implement the special item that can trigger card draw, with realtime effect # ------------------------------------------------------------------------------------- # [ ] Reskin the Game UI with offline prototype assets # [ ] Grab the GUI for Main Menu, character selector, from Promotional Video # [ ] Reskin the 3D models with offline prototype assets # [ ] Implement the statemachine animation for the character # ------------------------------------------------------------------------------------- # [ ] Implement specialty character with unique ability # [ ] Implement the sabotage meter # ------------------------------------------------------------------------------------- extends Node3D var multiplayer_peer = ENetMultiplayerPeer.new() const PORT = 9999 const ADDRESS = "127.0.0.1" @export var enable_bots: bool = true # Add this line var connected_peer_ids = [] var _connection_check_timer: float = 0.0 var local_player_character : CharacterBody3D var player_scene = preload("res://scenes/player.tscn") var current_turn_index = 0 @export var players = [] var game_started = false var max_players = 4 var bots = [] var preset_goals = [] @export var turn_based_mode: bool = true #var bot_move_timer: float = 0.0 #const BOT_MOVE_INTERVAL: float = 2.0 #var moving_bots = {} enum ActionState { NONE, MOVING, GRABBING, PUTTING, RANDOMIZING, ARRANGING } var current_action_state = ActionState.NONE @onready var action_menu = $ActionMenu @onready var move_button = $ActionMenu/ActionButtonContainer/MoveButton @onready var grab_button = $ActionMenu/ActionButtonContainer/GrabButton @onready var put_button = $ActionMenu/ActionButtonContainer/PutButton @onready var randomize_button = $ActionMenu/ActionButtonContainer/RandomizeButton @onready var arrange_button = $ActionMenu/ActionButtonContainer/ArrangeButton @onready var playerboard_ui = $PlayerboardUI const item_tex = [ preload("res://assets/textures/player_board_and_blue_print/tile_null.tres"), preload("res://assets/textures/player_board_and_blue_print/tile_heart.tres"), preload("res://assets/textures/player_board_and_blue_print/tile_diamond.tres"), preload("res://assets/textures/player_board_and_blue_print/tile_star.tres"), preload("res://assets/textures/player_board_and_blue_print/tile_coin.tres") ] func _ready(): multiplayer_peer.peer_connected.connect(_on_peer_connected) multiplayer_peer.peer_disconnected.connect(_on_peer_disconnected) setup_action_buttons() setup_playerboard_ui() func _process(delta): if multiplayer.is_server() and game_started: if turn_based_mode: rpc("sync_turn_index", current_turn_index) # Sync all players' goals to the new peer update_all_players_goals() # Also periodically verify client connections _connection_check_timer += delta if _connection_check_timer >= 5.0: _connection_check_timer = 0.0 verify_all_connections() func verify_all_connections(): if multiplayer.is_server(): for peer_id in players: if peer_id != 1: # Skip server # Ping each client rpc_id(peer_id, "connection_verify", players) @rpc func connection_verify(expected_players: Array): # Client checks if it has all expected players for peer_id in expected_players: if peer_id != multiplayer.get_unique_id() and not has_node(str(peer_id)): # Missing a player - request it rpc_id(1, "request_specific_player_data", peer_id) print("Requesting missing player: ", peer_id) func setup_action_buttons(): move_button.pressed.connect(func(): set_action_state(ActionState.MOVING)) grab_button.pressed.connect(func(): set_action_state(ActionState.GRABBING)) put_button.pressed.connect(func(): if local_player_character: local_player_character.handle_put_action() set_action_state(ActionState.PUTTING) ) randomize_button.pressed.connect(func(): set_action_state(ActionState.RANDOMIZING)) arrange_button.pressed.connect(func(): if local_player_character and local_player_character.action_points >= 2: set_action_state(ActionState.ARRANGING) ) func setup_playerboard_ui(): for child in playerboard_ui.get_children(): child.queue_free() playerboard_ui.columns = 5 for i in range(25): var slot = TextureRect.new() var highlight_rect = TextureRect.new() var hr_tex = load("res://assets/models/pboard/HighlightRect.tres") var select_rect = TextureRect.new() var sr_tex = load("res://assets/models/pboard/SelectRect.tres") var adjacent_rect = TextureRect.new() var ar_tex = load("res://assets/models/pboard/AdjacentRect.tres") slot.custom_minimum_size = Vector2(36, 36) slot.gui_input.connect(func(event): _on_playerboard_slot_clicked(event, i)) slot.texture = item_tex[0] playerboard_ui.add_child(slot, true) highlight_rect.texture = hr_tex highlight_rect.size = Vector2(36, 36) select_rect.texture = sr_tex select_rect.size = Vector2(36, 36) adjacent_rect.texture = ar_tex adjacent_rect.size = Vector2(36, 36) slot.add_child(highlight_rect) slot.add_child(select_rect) slot.add_child(adjacent_rect) slot.get_child(0).hide() slot.get_child(1).hide() slot.get_child(2).hide() func set_action_state(new_state): if not local_player_character or not local_player_character.is_multiplayer_authority(): return if local_player_character.is_bot or local_player_character.is_in_group("Bots"): current_action_state = new_state return if current_action_state == new_state or local_player_character.action_points <= 0: return current_action_state = new_state local_player_character.clear_highlights() local_player_character.clear_playerboard_highlights() match new_state: ActionState.MOVING: local_player_character.highlight_movement_range() ActionState.GRABBING: local_player_character.highlight_adjacent_cells() if local_player_character.has_item_at_current_position(): local_player_character.highlighted_cells.append(local_player_character.current_position) local_player_character.enhanced_gridmap.set_cell_item( Vector3i(local_player_character.current_position.x, 0, local_player_character.current_position.y), local_player_character.enhanced_gridmap.hover_item ) ActionState.PUTTING: local_player_character.highlight_occupied_playerboard_slots() # Make sure this is client-friendly if not multiplayer.is_server(): rpc_id(1, "notify_server_of_action_state", new_state) ActionState.RANDOMIZING: local_player_character.highlight_random_valid_cells() ActionState.ARRANGING: show_arrangement_ui() local_player_character.highlight_occupied_playerboard_slots() func update_button_states(): if not local_player_character or local_player_character.is_in_group("Bots"): move_button.visible = false grab_button.visible = false put_button.visible = false randomize_button.visible = false arrange_button.visible = false return move_button.visible = true grab_button.visible = true put_button.visible = true randomize_button.visible = true arrange_button.visible = true # Only keep randomize button's disable condition randomize_button.disabled = local_player_character.has_performed_action # Remove disabled conditions for other buttons: move_button.disabled = false grab_button.disabled = false put_button.disabled = false arrange_button.disabled = false func _on_playerboard_slot_clicked(event, slot_index): if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: if not local_player_character: return match current_action_state: ActionState.ARRANGING: local_player_character.arrange_playerboard_item(slot_index) ActionState.GRABBING: local_player_character.handle_playerboard_slot_selected(slot_index) ActionState.PUTTING: local_player_character.handle_put_slot_selected(slot_index) #func update_goals_ui(): #if not local_player_character: #return # #for i in range(9): # 9 slots in the goals UI #var slot = $PlayergoalsUI.get_child(i) #var goal_value = local_player_character.goals[i] # ## Hide all tile textures first #slot.get_node("TileHeart").hide() #slot.get_node("TileDiamond").hide() #slot.get_node("TileStar").hide() #slot.get_node("TileCoin").hide() # ## Show the appropriate texture based on goal value #match goal_value: #7: slot.get_node("TileHeart").show() #8: slot.get_node("TileDiamond").show() #9: slot.get_node("TileStar").show() #10: slot.get_node("TileCoin").show() func update_playerboard_ui(): if not local_player_character: return #update_goals_ui() # Update goals UI whenever playerboard updates update_all_players_goals() # Update all players' goals UI for i in range(25): var slot = playerboard_ui.get_child(i) var item = local_player_character.playerboard[i] slot.texture = item_tex[0] match item: 7: slot.texture = item_tex[1] 8: slot.texture = item_tex[2] 9: slot.texture = item_tex[3] 10: slot.texture = item_tex[4] func update_playerboard_highlights(highlighted_slots: Array): for i in range(playerboard_ui.get_child_count()): var slot = playerboard_ui.get_child(i) if slot.get_child_count() > 1: slot.get_child(1).visible = highlighted_slots.has(i) func show_arrangement_ui(): if playerboard_ui: playerboard_ui.visible = true update_playerboard_ui() func _on_host_pressed(): $NetworkInfo/NetworkSideDisplay.text = "Server" $Menu.visible = false multiplayer_peer.create_server(PORT) multiplayer.multiplayer_peer = multiplayer_peer $NetworkInfo/UniquePeerID.text = str(multiplayer.get_unique_id()) # Generate all goals first preset_goals.clear() for i in range(max_players): var goals = initialize_random_goals(9, 7, 10, 1.0) # Convert to int array explicitly var int_goals: Array[int] = [] for g in goals: int_goals.append(g) preset_goals.append(int_goals) # Now add host with first set of goals add_player_character(1) # Explicitly assign host's goals and force UI update var host_player = get_node_or_null("1") if host_player: host_player.goals = preset_goals[0].duplicate() rpc("sync_player_goals", 1, preset_goals[0]) update_all_players_goals() players.append(1) # Sync goals to all clients after host is set up rpc("sync_preset_goals", preset_goals) # Only add bots if enable_bots is true if enable_bots: # Add bots with their own goals for i in range(2, max_players + 1): add_bot(i) start_game() @rpc("reliable") func sync_preset_goals(goals_list: Array): preset_goals = goals_list func _on_join_pressed(): $NetworkInfo/NetworkSideDisplay.text = "Client" $Menu.visible = false multiplayer_peer.create_client(ADDRESS, PORT) multiplayer.multiplayer_peer = multiplayer_peer $NetworkInfo/UniquePeerID.text = str(multiplayer.get_unique_id()) # After connection is established await get_tree().create_timer(2.0).timeout rpc_id(1, "request_full_player_sync", multiplayer.get_unique_id()) #func _on_peer_connected(new_peer_id): #if multiplayer.is_server(): ## Increase delay to ensure scene is ready #await get_tree().create_timer(1.5).timeout # ## First sync game state #rpc_id(new_peer_id, "sync_game_state", players, bots, game_started, turn_based_mode) #rpc_id(new_peer_id, "sync_preset_goals", preset_goals) # ## Wait a bit for the client to process state #await get_tree().create_timer(0.5).timeout # ## Then sync all existing players in order #var sorted_players = players.duplicate() #sorted_players.sort() #for peer_id in sorted_players: #if peer_id != new_peer_id: ## First ensure player exists #var player = get_node_or_null(str(peer_id)) #if player: ## Sync player's full state #var player_data = { #"position": player.current_position, #"goals": player.goals, #"playerboard": player.playerboard #} #rpc_id(new_peer_id, "sync_existing_player", peer_id, player_data) #await get_tree().create_timer(0.1).timeout # Small delay between players # ## Finally add the new player #await get_tree().create_timer(0.5).timeout #add_player_character(new_peer_id) #rpc("add_newly_connected_player_character", new_peer_id) # ## Replace bot if needed #if bots.size() > 0: #replace_bot_with_player(new_peer_id) # ## Final sync of all goals #await get_tree().create_timer(0.5).timeout #rpc("force_update_all_goals") func _on_peer_connected(new_peer_id): if multiplayer.is_server(): # Create a more robust state sync process await get_tree().create_timer(1.5).timeout # First sync complete game state var complete_state = { "players": players, "bots": bots, "game_started": game_started, "turn_based": turn_based_mode, "preset_goals": preset_goals, "player_states": {} } # Gather all existing player states for peer_id in players: var player = get_node_or_null(str(peer_id)) if player: complete_state["player_states"][peer_id] = { "position": player.current_position, "goals": player.goals, "playerboard": player.playerboard, "is_bot": player.is_bot || player.is_in_group("Bots") } # Send complete state in one RPC rpc_id(new_peer_id, "receive_complete_game_state", complete_state) # Finally add the new player await get_tree().create_timer(0.5).timeout add_player_character(new_peer_id) rpc("add_newly_connected_player_character", new_peer_id) # Make sure all clients know about all players rpc("sync_complete_player_list", players) ## Replace bot if needed #if bots.size() > 0: #replace_bot_with_player(new_peer_id) # # Final sync of all goals await get_tree().create_timer(0.5).timeout rpc("force_update_all_goals") @rpc("reliable") func sync_complete_player_list(player_list: Array): # Ensure we have all players in our list players = player_list.duplicate() # Check which players we don't have nodes for for peer_id in players: if not has_node(str(peer_id)) and peer_id != multiplayer.get_unique_id(): # Request this specific player's data from server 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") } # Send to the requesting client only rpc_id(multiplayer.get_remote_sender_id(), "create_specific_player", player_data) @rpc("any_peer") func request_full_player_sync(requesting_peer_id): if multiplayer.is_server(): print("Full sync requested by: ", requesting_peer_id) # Send the complete list of players rpc_id(requesting_peer_id, "sync_complete_player_list", players) # Send each player's data for peer_id in 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) # Allow a short delay between player creations await get_tree().create_timer(0.1).timeout @rpc("reliable") func create_specific_player(data: Dictionary): var peer_id = data["peer_id"] # Don't create if already exists if has_node(str(peer_id)): return # Create the player var player_character = player_scene.instantiate() player_character.set_multiplayer_authority(peer_id) player_character.name = str(peer_id) # Set properties before adding to tree player_character.current_position = data["position"] # Add to scene add_child(player_character) # Apply properties after adding player_character.add_to_group("Players", true) if data["is_bot"]: player_character.add_to_group("Bots", true) player_character.is_bot = true player_character.rpc("sync_bot_status", true) # Apply data player_character.goals = data["goals"].duplicate() player_character.playerboard = data["playerboard"].duplicate() # Force position sync player_character.global_position = Vector3( data["position"].x * 2 + 1, 1.0, data["position"].y * 2 + 1 ) player_character.rpc("sync_position", data["position"]) # Update UI update_all_players_goals() update_all_players_boards() @rpc("reliable") func force_update_all_goals(): # This is called but might be getting lost in the sequence # Make sure it's called after all players are created await get_tree().create_timer(0.2).timeout update_all_players_goals() update_all_players_boards() # Set @rpc("reliable") func receive_complete_game_state(state): # Apply complete game state players = state["players"] bots = state["bots"] game_started = state["game_started"] turn_based_mode = state["turn_based"] preset_goals = state["preset_goals"] # Process each player state in a consistent order var sorted_peers = state["player_states"].keys() sorted_peers.sort() for peer_id in sorted_peers: var player_data = state["player_states"][peer_id] # Create player if doesn't exist if not has_node(str(peer_id)): var player_character = player_scene.instantiate() player_character.set_multiplayer_authority(peer_id) player_character.name = str(peer_id) # Set basic properties before adding to scene tree player_character.current_position = player_data["position"] # Add to scene add_child(player_character) # Apply state after adding to tree player_character.add_to_group("Players", true) if player_data["is_bot"]: player_character.add_to_group("Bots", true) player_character.is_bot = true player_character.rpc("sync_bot_status", true) player_character.goals = player_data["goals"].duplicate() player_character.playerboard = player_data["playerboard"].duplicate() # Ensure proper grid-aligned positioning player_character.global_position = Vector3( player_data["position"].x * 2 + 1, 1.0, player_data["position"].y * 2 + 1 ) # Force position sync player_character.rpc("sync_position", player_data["position"]) # Force UI updates update_all_players_goals() update_all_players_boards() @rpc("reliable") func sync_existing_player(peer_id: int, player_data: Dictionary): # Create player if doesn't exist if not has_node(str(peer_id)): var player_character = player_scene.instantiate() player_character.set_multiplayer_authority(peer_id) player_character.name = str(peer_id) player_character.current_position = player_data["position"] add_child(player_character) player_character.add_to_group("Players", true) # Get player node and wait a frame to ensure it's ready await get_tree().create_timer(0.1).timeout var player = get_node_or_null(str(peer_id)) if player: # Apply synced state player.current_position = player_data["position"] player.goals = player_data["goals"].duplicate() player.playerboard = player_data["playerboard"].duplicate() # Ensure proper grid-aligned positioning player.global_position = Vector3( player_data["position"].x * 2 + 1, # cell_size.x = 2 1.0, player_data["position"].y * 2 + 1 # cell_size.z = 2 ) # Force position sync player.rpc("sync_position", player_data["position"]) # Update UI update_all_players_goals() update_all_players_boards() func _on_peer_disconnected(peer_id): if multiplayer.is_server(): connected_peer_ids.erase(peer_id) players.erase(peer_id) add_bot(get_next_available_bot_id()) @rpc("any_peer", "call_local") func add_player_character(peer_id): # First check if this player already exists if has_node(str(peer_id)): print("Player already exists: ", peer_id) return print("Adding player: ", peer_id) connected_peer_ids.append(peer_id) var player_character = player_scene.instantiate() player_character.set_multiplayer_authority(peer_id) player_character.name = str(peer_id) # Handle bot replacement position if multiplayer.is_server() and bots.size() > 0: var bot_to_replace = get_node_or_null(str(bots[0])) if bot_to_replace: player_character.current_position = bot_to_replace.current_position # Set initial grid-aligned position player_character.global_position = Vector3( bot_to_replace.current_position.x * 2 + 1, 1.0, bot_to_replace.current_position.y * 2 + 1 ) add_child(player_character) player_character.add_to_group("Players", true) # Wait for the node to be properly added to the scene await get_tree().process_frame # Ensure the player list is updated if not peer_id in players: players.append(peer_id) # Finish setup and sync position if multiplayer.is_server(): await get_tree().create_timer(0.1).timeout player_character.rpc("sync_position", player_character.current_position) # Set goals based on player ID if server if multiplayer.is_server(): var goal_index = peer_id - 1 if goal_index >= 0 and goal_index < preset_goals.size(): # Convert to int array before assigning var goals: Array[int] = [] for g in preset_goals[goal_index]: goals.append(g) player_character.goals = goals # Force sync goals to everyone, including host rpc("sync_player_goals", peer_id, goals) # Update UI immediately for server update_all_players_goals() # Local player setup if peer_id == multiplayer.get_unique_id(): local_player_character = player_character update_button_states() update_playerboard_ui() update_all_players_goals() # Force UI update for local player # Request goals from server if we're a client if not multiplayer.is_server(): rpc_id(1, "request_goals_from_server", peer_id) if multiplayer.is_server(): if peer_id > 1: # Not the host # Assign preset goals var goal_index = peer_id - 2 if goal_index < preset_goals.size(): player_character.goals = preset_goals[goal_index] rpc("sync_player_goals", peer_id, player_character.goals) if multiplayer.is_server(): # If replacing a bot, inherit its goals var bot_to_replace = get_node_or_null(str(bots[0])) if bots.size() > 0 else null if bot_to_replace: player_character.goals = bot_to_replace.goals.duplicate() else: # Only generate new goals if not inheriting from a bot player_character.append_random_goals() func add_bot(bot_id): rpc("create_bot", bot_id) @rpc("call_local") func create_bot(bot_id): # Ensure we're not duplicating bots if has_node(str(bot_id)): push_error("Bot already exists: " + str(bot_id)) return var bot_character = player_scene.instantiate() if not bot_character: push_error("Failed to instantiate bot scene") return bot_character.set_multiplayer_authority(1) # Server controls bots bot_character.name = str(bot_id) # Add to scene tree call_deferred("add_child", bot_character) # Add to groups after adding to scene tree bot_character.add_to_group("Players", true) bot_character.add_to_group("Bots", true) if not enable_bots: bot_character.set_process(false) bot_character.set_physics_process(false) # Disable Beehave tree if it exists var behavior_tree = bot_character.get_node_or_null("BehaviorTree") if behavior_tree: behavior_tree.enabled = false if multiplayer.is_server(): bots.append(bot_id) players.append(bot_id) # Assign goals from preset array var goal_index = bot_id - 1 if goal_index < preset_goals.size(): bot_character.goals = preset_goals[goal_index].duplicate() rpc("sync_player_goals", bot_id, bot_character.goals) # Sync bot status after a short delay to ensure node is ready await get_tree().create_timer(0.1).timeout bot_character.rpc("sync_bot_status", true) # Sync bot's goals rpc("sync_player_goals", bot_id, bot_character.goals) # Only generate goals for new bots, not replacement bots #if not (players.size() > max_players): #bot_character.append_random_goals() # Always sync the bot's goals rpc("sync_player_goals", bot_id, bot_character.goals) func replace_bot_with_player(player_id): if multiplayer.is_server() and bots.size() > 0: var bot_id = bots[0] var bot_node = get_node_or_null(str(bot_id)) if bot_node: # Get bot's state var goals = bot_node.goals var playerboard = bot_node.playerboard.duplicate() var current_pos = bot_node.current_position # Transfer state to new player var player_node = get_node_or_null(str(player_id)) if player_node: player_node.goals = goals player_node.playerboard = playerboard.duplicate() # Make sure to duplicate player_node.current_position = current_pos # Sync state rpc("sync_player_goals", player_id, goals) rpc("sync_playerboard", player_id, playerboard) # Remove bot but keep board structure intact bots.pop_front() players.erase(bot_id) players.append(player_id) rpc("remove_bot_keep_board", bot_id) rpc("sync_players", players) @rpc("call_local") func remove_bot_keep_board(bot_id): # This RPC is called but not implemented in your code var bot_node = get_node_or_null(str(bot_id)) if bot_node: # Don't immediately queue_free - this can cause timing issues # Instead, mark for removal and remove after a short delay bot_node.visible = false # Hide immediately bot_node.set_process(false) bot_node.set_physics_process(false) # Disable all input and behavior var behavior_tree = bot_node.get_node_or_null("BehaviorTree") if behavior_tree: behavior_tree.enabled = false # Remove after a short delay await get_tree().create_timer(0.5).timeout if is_instance_valid(bot_node) and bot_node.get_parent() == self: bot_node.queue_free() @rpc("call_local") func remove_bot(bot_id): var bot_node = get_node_or_null(str(bot_id)) if bot_node: bot_node.queue_free() func get_next_available_bot_id() -> int: for i in range(2, max_players + 1): if not i in players: return i return -1 @rpc func add_newly_connected_player_character(new_peer_id): add_player_character(new_peer_id) @rpc func add_previously_connected_player_characters(peer_ids): for peer_id in peer_ids: add_player_character(peer_id) @rpc("call_local") func sync_game_state(current_players, current_bots, is_game_started, is_turn_based): players = current_players bots = current_bots game_started = is_game_started turn_based_mode = is_turn_based for bot_id in bots: if not has_node(str(bot_id)): create_bot(bot_id) func start_game(): if multiplayer.is_server(): game_started = true connected_peer_ids.sort() rpc("sync_game_start", connected_peer_ids, players, bots, turn_based_mode) if turn_based_mode: current_turn_index = -1 next_turn() @rpc("call_local") func sync_game_start(peer_ids, current_players, current_bots, is_turn_based): connected_peer_ids = peer_ids players = current_players bots = current_bots turn_based_mode = is_turn_based game_started = true @rpc("reliable") func sync_turn_index(index): current_turn_index = index @rpc("reliable") func sync_players(new_players): players = new_players func next_turn(): if multiplayer.is_server() and turn_based_mode: current_turn_index = (current_turn_index + 1) % players.size() rpc("set_current_turn", players[current_turn_index]) func request_next_turn(): if multiplayer.is_server(): end_current_turn() else: rpc_id(1, "server_end_current_turn") @rpc("any_peer") func server_end_current_turn(): if multiplayer.is_server(): end_current_turn() @rpc("any_peer", "call_local") func set_current_turn(player_id): if not 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 # Only reset state for human players 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() # Clear any existing highlights from other players player.clear_highlights() player.clear_playerboard_highlights() else: player.is_my_turn = false func end_current_turn(): if multiplayer.is_server(): next_turn() rpc("sync_turn_index", current_turn_index) func update_all_players_boards(): if not game_started: return var local_id = multiplayer.get_unique_id() var all_players = get_tree().get_nodes_in_group("Players") var all_player_boards = $AllPlayerBoards # Store current active tab var current_tab = all_player_boards.current_tab # Board 1 should show host (server) var host_player = null for player in all_players: if int(String(player.name)) == 1: host_player = player break # Update host board (board 1) var host_board = all_player_boards.get_node("1") if host_player and host_board and host_board.has_node("PlayerboardUI"): host_board.visible = true var board_ui = host_board.get_node("PlayerboardUI") for slot_idx in range(25): update_board_slot(board_ui, slot_idx, host_player.playerboard[slot_idx]) # Sort remaining players by ID for boards 2,3,4 var other_players = all_players.filter(func(p): var id = int(String(p.name)) return id != 1 and not p.is_in_group("Bots") # Exclude host and bots ) other_players.sort_custom(func(a, b): return int(String(a.name)) < int(String(b.name)) ) # Update client boards - board 2 for first client, board 3 for second client, etc. for i in range(min(other_players.size(), 3)): var board_idx = i + 2 # Start from board 2 var player = other_players[i] var board = all_player_boards.get_node(str(board_idx)) if board and board.has_node("PlayerboardUI"): board.visible = true board.name = str(board_idx) # Ensure board name matches index var board_ui = board.get_node("PlayerboardUI") for slot_idx in range(25): update_board_slot(board_ui, slot_idx, player.playerboard[slot_idx]) # Hide unused boards for i in range(other_players.size() + 2, 5): var unused_board = all_player_boards.get_node_or_null(str(i)) if unused_board: unused_board.visible = false # Restore previous active tab all_player_boards.current_tab = current_tab @rpc("any_peer", "call_local") func sync_playerboard(player_id: int, new_playerboard: Array): # Update local player's board if it's their board if player_id == multiplayer.get_unique_id() and local_player_character: update_playerboard_ui() # Important: Always update all boards when any board changes update_all_players_boards() # Update specific board in AllPlayerBoards UI var board_index = players.find(player_id) if board_index >= 0 and board_index < max_players: var target_board_index = board_index + 1 if target_board_index != 1: # Skip local player's board var container = $AllPlayerBoards.get_node_or_null(str(target_board_index)) if container and container.has_node("PlayerboardUI"): var board_ui = container.get_node("PlayerboardUI") for slot_idx in range(25): update_board_slot(board_ui, slot_idx, new_playerboard[slot_idx]) func update_board_slot(board_ui: Node, slot_idx: int, value: int): var slot_node = board_ui.get_node_or_null("Slot%d" % (slot_idx + 1)) if slot_node: # Hide all tiles first for tile in ["TileHeart", "TileDiamond", "TileStar", "TileCoin"]: slot_node.get_node(tile).hide() # Show appropriate tile match value: 7: slot_node.get_node("TileHeart").show() 8: slot_node.get_node("TileDiamond").show() 9: slot_node.get_node("TileStar").show() 10: slot_node.get_node("TileCoin").show() func update_all_players_goals(): # Only server/host should manage goals display if not game_started: return var all_players = get_tree().get_nodes_in_group("Players") all_players.sort_custom(func(a, b): var a_str = String(a.name).get_slice("@", 0) var b_str = String(b.name).get_slice("@", 0) return int(a_str) < int(b_str) ) # If we're the host, update all goals and sync to clients if multiplayer.is_server(): # Clear all goals first for player_idx in range(4): var goals_grid = $AllPlayerGoals.get_child(player_idx) for slot_idx in range(9): var slot = goals_grid.get_child(slot_idx) for tile in ["TileHeart", "TileDiamond", "TileStar", "TileCoin"]: slot.get_node(tile).hide() # Update with current goals and gather goals data var all_goals_data = [] for i in range(min(all_players.size(), 4)): var player = all_players[i] if player and player.goals.size() > 0: _update_player_goals_ui(i, player.goals) all_goals_data.append({"player_idx": i, "goals": player.goals.duplicate()}) else: all_goals_data.append({"player_idx": i, "goals": []}) # Sync to clients rpc("sync_all_goals_to_clients", all_goals_data) @rpc("reliable") func sync_all_goals_to_clients(all_goals_data: Array): if not multiplayer.is_server(): # Only clients should process this # Clear all goals first for player_idx in range(4): var goals_grid = $AllPlayerGoals.get_child(player_idx) for slot_idx in range(9): var slot = goals_grid.get_child(slot_idx) for tile in ["TileHeart", "TileDiamond", "TileStar", "TileCoin"]: slot.get_node(tile).hide() # Apply received goals for goal_data in all_goals_data: var player_idx = goal_data["player_idx"] var goals = goal_data["goals"] if player_idx >= 0 and player_idx < 4 and goals.size() > 0: _update_player_goals_ui(player_idx, goals) @rpc("any_peer", "call_local") func sync_player_goals(player_id: int, goals: Array): # Update the player's goals in their node var player = get_node_or_null(str(player_id)) if player: player.goals = goals.duplicate() # Only server should update the UI directly if multiplayer.is_server(): update_all_players_goals() # Helper function to update specific player's goals UI func _update_player_goals_ui(player_idx: int, goals: Array): var goals_grid = $AllPlayerGoals.get_child(player_idx) for slot_idx in range(9): var slot = goals_grid.get_child(slot_idx) var goal_value = goals[slot_idx] if slot_idx < goals.size() else -1 # Hide all tiles first for tile in ["TileHeart", "TileDiamond", "TileStar", "TileCoin"]: slot.get_node(tile).hide() # Show appropriate tile match goal_value: 7: slot.get_node("TileHeart").show() 8: slot.get_node("TileDiamond").show() 9: slot.get_node("TileStar").show() 10: slot.get_node("TileCoin").show() @rpc("any_peer") func request_goals_from_server(requesting_peer_id: int): if multiplayer.is_server(): var goal_index = requesting_peer_id - 1 if goal_index >= 0 and goal_index < preset_goals.size(): rpc("sync_player_goals", requesting_peer_id, preset_goals[goal_index]) # Add this function near the top with other helper functions func initialize_random_goals(_size:int, min_value:int, max_value:int, null_count:float) -> Array: var goals = [] var rng = RandomNumberGenerator.new() rng.randomize() var null_val = 0 var max_nulls = 3 const SPECIAL_VALUES = {1: 7, 2: 8, 3: 9, 4: 10} for i in range(_size): if null_val < max_nulls and rng.randf() < null_count: goals.append(-1) null_val += 1 else: var val = rng.randi_range(min_value, max_value) goals.append(val if not val in SPECIAL_VALUES else SPECIAL_VALUES[val]) return goals