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 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) 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() 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()) 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 # Sync full state first rpc_id(new_peer_id, "sync_game_state", players, bots, game_started, turn_based_mode) # Then add players in correct order for peer_id in connected_peer_ids: rpc_id(new_peer_id, "add_player_character", peer_id) # Finally add the new player add_player_character(new_peer_id) rpc("add_newly_connected_player_character", new_peer_id) replace_bot_with_player(new_peer_id) # Sync all players' goals to the new peer for player in get_tree().get_nodes_in_group("Players"): var player_id = String(player.name).to_int() # Convert StringName to String first, then to int rpc_id(new_peer_id, "sync_player_goals", player_id, player.goals) 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): connected_peer_ids.append(peer_id) var player_character = player_scene.instantiate() player_character.set_multiplayer_authority(peer_id) 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 add_child(player_character) player_character.add_to_group("Players", true) # 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(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 # Find host player (ID 1) for player in all_players: if int(String(player.name)) == 1: host_player = player break # Update host board 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-4 var other_players = all_players.filter(func(p): return int(String(p.name)) != 1) other_players.sort_custom(func(a, b): return int(String(a.name)) < int(String(b.name))) # Update boards 2-4 with remaining players for i in range(min(other_players.size(), 3)): var board_num = i + 2 # boards 2,3,4 var player = other_players[i] var board = all_player_boards.get_node(str(board_num)) if board and board.has_node("PlayerboardUI"): board.visible = true var board_ui = board.get_node("PlayerboardUI") for slot_idx in range(25): update_board_slot(board_ui, slot_idx, player.playerboard[slot_idx]) # 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(): if not game_started and not multiplayer.is_server(): return var all_players = get_tree().get_nodes_in_group("Players") # Fix: Convert name to string and handle potential scene-unique names properly all_players.sort_custom(func(a, b): var a_str = String(a.name).get_slice("@", 0) # Explicitly convert StringName to String var b_str = String(b.name).get_slice("@", 0) return int(a_str) < int(b_str) ) # Force clear all goals first for i in range(4): # 4 goal boards var goals_grid = $AllPlayerGoals.get_child(i) for slot_idx in range(9): # 9 slots per goal board var slot = goals_grid.get_child(slot_idx) for tile in ["TileHeart", "TileDiamond", "TileStar", "TileCoin"]: slot.get_node(tile).hide() # Then update with current player goals 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) @rpc("any_peer", "call_local") func sync_player_goals(player_id: int, goals: Array): # Update the player's goals first var player = get_node_or_null(str(player_id)) if player: player.goals = goals.duplicate() # Get player index in current players list var player_idx = -1 var sorted_players = get_tree().get_nodes_in_group("Players") sorted_players.sort_custom(func(a, b): return int(String(a.name).get_slice("@", 0)) < int(String(b.name).get_slice("@", 0)) ) for idx in range(sorted_players.size()): if sorted_players[idx] == player: player_idx = idx break if player_idx >= 0 and player_idx < 4: _update_player_goals_ui(player_idx, goals) # Update full UI if we're the server or if this is our local player if multiplayer.is_server() or player_id == multiplayer.get_unique_id(): 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