extends Node3D var multiplayer_peer = ENetMultiplayerPeer.new() const PORT = 9999 const ADDRESS = "127.0.0.1" 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_message_input_char = 51 var max_players = 4 var bots = [] @export var turn_based_mode: bool = true # Toggle between turn-based and realtime modes var bot_move_timer: float = 0.0 # Timer for bot movement in realtime mode const BOT_MOVE_INTERVAL: float = 2.0 # Time between bot movements in realtime mode # Bot movement state tracking var moving_bots = {} # Dictionary to track which bots are currently moving # Action State Management enum ActionState { NONE, MOVING, GRABBING, PUTTING, RANDOMIZING, ARRANGING } var current_action_state = ActionState.NONE # UI References @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 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(): if game_started: if turn_based_mode: rpc("sync_turn_index", current_turn_index) else: # Handle realtime bot movement bot_move_timer += delta if bot_move_timer >= BOT_MOVE_INTERVAL: bot_move_timer = 0.0 for bot_id in bots: if not moving_bots.get(bot_id, false): # Only move if bot isn't already moving move_bot(bot_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(): set_action_state(ActionState.PUTTING)) randomize_button.pressed.connect(func(): set_action_state(ActionState.RANDOMIZING)) arrange_button.pressed.connect(func(): set_action_state(ActionState.ARRANGING)) func setup_playerboard_ui(): # Clear existing children for child in playerboard_ui.get_children(): child.queue_free() # Setup grid playerboard_ui.columns = 5 # Create slots for i in range(25): var slot = TextureRect.new() slot.custom_minimum_size = Vector2(40, 40) slot.gui_input.connect(func(event): _on_playerboard_slot_clicked(event, i)) # Add a colored rectangle as background var style = StyleBoxFlat.new() style.bg_color = Color(0.2, 0.2, 0.2, 1.0) style.border_color = Color(0.4, 0.4, 0.4, 1.0) slot.add_theme_stylebox_override("panel", style) playerboard_ui.add_child(slot) func set_action_state(new_state): if not local_player_character: return current_action_state = new_state # Clear previous highlights local_player_character.clear_highlights() match new_state: ActionState.MOVING: local_player_character.highlight_movement_range() ActionState.GRABBING: local_player_character.highlight_adjacent_cells() ActionState.PUTTING: local_player_character.highlight_empty_adjacent_cells() ActionState.RANDOMIZING: local_player_character.highlight_random_valid_cells() ActionState.ARRANGING: show_arrangement_ui() func update_button_states(): if not local_player_character: return var can_move = not local_player_character.is_player_moving and not local_player_character.has_performed_action var can_grab = local_player_character.has_item_at_current_position() var can_put = local_player_character.has_items_in_playerboard() and not local_player_character.has_item_at_current_position() var can_randomize = not local_player_character.has_performed_action var can_arrange = local_player_character.has_items_in_playerboard() move_button.disabled = not can_move grab_button.disabled = not can_grab put_button.disabled = not can_put randomize_button.disabled = not can_randomize arrange_button.disabled = not can_arrange func _on_playerboard_slot_clicked(event, slot_index): if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: if current_action_state == ActionState.ARRANGING and local_player_character: local_player_character.arrange_playerboard_item(slot_index) func update_playerboard_ui(): if not local_player_character: return for i in range(25): var slot = playerboard_ui.get_child(i) var item = local_player_character.playerboard[i] if item != -1: # Update slot appearance based on item slot.modulate = Color(1, 1, 1, 1) # You can set different colors or textures based on item type slot.self_modulate = Color(0.5 + (item * 0.1), 0.7, 0.3, 1.0) else: slot.modulate = Color(0.5, 0.5, 0.5, 0.5) slot.self_modulate = Color(1, 1, 1, 1) 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()) add_player_character(1) players.append(1) # Add bots to fill remaining slots for i in range(2, max_players + 1): add_bot(i) start_game() 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(): await get_tree().create_timer(1).timeout rpc("add_newly_connected_player_character", new_peer_id) rpc_id(new_peer_id, "add_previously_connected_player_characters", connected_peer_ids) rpc_id(new_peer_id, "sync_game_state", players, bots, game_started, turn_based_mode) add_player_character(new_peer_id) replace_bot_with_player(new_peer_id) 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()) 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) add_child(player_character) player_character.add_to_group("Players", true) if peer_id == multiplayer.get_unique_id(): local_player_character = player_character update_button_states() update_playerboard_ui() func add_bot(bot_id): if multiplayer.is_server(): rpc("create_bot", bot_id) moving_bots[bot_id] = false @rpc("call_local") func create_bot(bot_id): var bot_character = player_scene.instantiate() bot_character.set_multiplayer_authority(1) # Set server as authority for bots bot_character.name = str(bot_id) add_child(bot_character) bot_character.add_to_group("Players", true) bot_character.add_to_group("Bots", true) if multiplayer.is_server(): bots.append(bot_id) players.append(bot_id) moving_bots[bot_id] = false func replace_bot_with_player(player_id): if multiplayer.is_server() and bots.size() > 0: var bot_id = bots.pop_front() players.erase(bot_id) players.append(player_id) moving_bots.erase(bot_id) # Remove bot from moving tracking rpc("remove_bot", bot_id) rpc("sync_players", players) @rpc("call_local") func remove_bot(bot_id): var bot_node = get_node(str(bot_id)) if bot_node: bot_node.queue_free() func get_next_available_bot_id(): 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 # Create bot characters for existing bots 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 += 1 if current_turn_index >= players.size(): current_turn_index = 0 rpc("set_current_turn", players[current_turn_index]) if players[current_turn_index] in bots: move_bot(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 var players = get_tree().get_nodes_in_group("Players") for player in players: player.is_my_turn = (player.name == str(player_id)) if player.is_my_turn: player.start_turn() func end_current_turn(): if multiplayer.is_server(): next_turn() rpc("sync_turn_index", current_turn_index) func _on_message_input_text_submitted(new_text): if new_text.length() > max_message_input_char: new_text = new_text.substr(0, max_message_input_char) + " ... " local_player_character.rpc("display_message", new_text) $MessageInput.text = "" $MessageInput.release_focus() func move_bot(bot_id): if multiplayer.is_server(): if moving_bots.get(bot_id, false): return var bot = get_node(str(bot_id)) if bot and (turn_based_mode or not bot.is_player_moving): moving_bots[bot_id] = true var best_move = evaluate_bot_move(bot) match best_move.action: "grab": if bot.grab_item(): bot.has_performed_action = true moving_bots[bot_id] = false # Don't end turn, allow movement after grab move_bot(bot_id) # Try to move after grabbing "put": if bot.put_item(): bot.has_performed_action = true moving_bots[bot_id] = false # Don't end turn, allow movement after put move_bot(bot_id) # Try to move after putting "move": if bot.has_performed_action: var path = bot.enhanced_gridmap.find_path(Vector2(bot.current_position), Vector2(best_move.position)) if path.size() > 1: path.pop_front() var trimmed_path = path.slice(0, bot.movement_range) bot.rotate_towards_target(best_move.position) bot.move_bot_along_path(trimmed_path, bot_id) else: moving_bots[bot_id] = false if turn_based_mode: end_current_turn() else: moving_bots[bot_id] = false if turn_based_mode: end_current_turn() _: moving_bots[bot_id] = false if turn_based_mode: end_current_turn() func bot_movement_completed(bot_id): if multiplayer.is_server(): moving_bots[bot_id] = false func evaluate_bot_move(bot: Node) -> Dictionary: var best_move = { "action": "none", "position": bot.current_position, "value": -1 } # If no action performed yet, prioritize grab/put if not bot.has_performed_action: # Check current position for grabbing var current_cell = Vector3i(bot.current_position.x, 1, bot.current_position.y) var current_item = bot.enhanced_gridmap.get_cell_item(current_cell) # Evaluate grabbing current item if it matches goals if current_item != -1 and current_item in bot.goals and bot.playerboard.find(-1) != -1: return { "action": "grab", "position": bot.current_position, "value": 10 } # Evaluate putting item from playerboard for goal in bot.goals: var item_index = bot.playerboard.find(goal) if item_index != -1 and current_item == -1: return { "action": "put", "position": bot.current_position, "value": 15 } else: # Bot has performed action, look for movement var neighbors = bot.enhanced_gridmap.get_neighbors(bot.current_position, 1) for neighbor in neighbors: if not neighbor.is_walkable: continue if not bot.is_within_movement_range(neighbor.position): continue var cell = Vector3i(neighbor.position.x, 1, neighbor.position.y) var item = bot.enhanced_gridmap.get_cell_item(cell) # Evaluate position for next turn if item in bot.goals or item == -1: var value = 8 if value > best_move.value: best_move = { "action": "move", "position": neighbor.position, "value": value } return best_move