Files
tekton/scenes/main.gd
T
adtpdn f9f7d34f80 Add example scene and update gridmap assets
Added a new example scene and player script for EnhancedGridMap. Updated mesh library, materials, and tile assets for improved visuals and structure. Modified main scene and logic to support new gridmap configuration and auto item handling. Adjusted project settings for resolution and main scene path.
2025-10-27 12:35:06 +08:00

1237 lines
40 KiB
GDScript

# -------------------------------------------------------------------------------------
# Tekton Dash - Multiplayer Board Game - 2024
# -------------------------------------------------------------------------------------
# [x] Move is working, you can move the character to another tile on gridmap
# [x] Make the Randomize tile, currently there's no logic to handle it
# it's randomly highlighted random tile on gridmap
# [ ] 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
# [x] 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
# [x] 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,
PLACING_OBSTACLE
}
# Obstacle
# Add these properties to track current obstacle settings
var current_obstacle_orientation = ObstacleOrientation.NORTH
var current_obstacle_item = 12 # Starting with first obstacle item (12)
enum ObstacleDirection {
VERTICAL,
HORIZONTAL
}
enum ObstacleOrientation {
NORTH = 0, # Blocks movement to the north (top)
EAST = 1, # Blocks movement to the east (right)
SOUTH = 2, # Blocks movement to the south (bottom)
WEST = 3 # Blocks movement to the west (left)
}
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()
# Obstacles
setup_obstacle_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)
#)
put_button.pressed.connect(func():
if local_player_character:
local_player_character.auto_put_item()
)
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()
# Deactivated since, using Auto Grabber
#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()
ActionState.PLACING_OBSTACLE:
local_player_character.highlight_valid_obstacle_cells()
func place_obstacle(grid_position: Vector2i):
if not local_player_character or local_player_character.action_points < 1:
return false
var floor_index = 3 # Always place on floor 3
var success = $EnhancedGridMap.place_obstacle(
Vector3i(grid_position.x, floor_index, grid_position.y),
current_obstacle_item,
current_obstacle_orientation
)
if success:
local_player_character.action_points -= 1
# Clear all highlights after placing obstacle
local_player_character.clear_highlights()
# Exit obstacle placement mode and return to default state
set_action_state(ActionState.NONE)
# Sync the obstacle with other clients
if is_multiplayer_authority():
rpc("sync_place_obstacle", grid_position.x, grid_position.y, floor_index, current_obstacle_item, current_obstacle_orientation)
return true
return false
# Updated function to cycle through the four orientations
# Updated function to cycle through the four orientations
func cycle_obstacle_orientation():
var orientations = [
ObstacleOrientation.NORTH,
ObstacleOrientation.EAST,
ObstacleOrientation.SOUTH,
ObstacleOrientation.WEST
]
var current_index = orientations.find(current_obstacle_orientation)
current_index = (current_index + 1) % orientations.size()
current_obstacle_orientation = orientations[current_index]
var direction_names = ["North", "East", "South", "West"]
return "Direction: " + direction_names[current_index]
# Function to cycle through obstacle types
func cycle_obstacle_type():
var obstacle_types = [12, 13, 14, 15]
var current_index = obstacle_types.find(current_obstacle_item)
current_index = (current_index + 1) % obstacle_types.size()
current_obstacle_item = obstacle_types[current_index]
return "Type: " + str(current_index + 1)
# Update the RPC for obstacle synchronization
@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)
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)
# Deactivated since we're using auto grab
#ActionState.GRABBING:
#local_player_character.handle_playerboard_slot_selected(slot_index)
# Deactivated since we're using auto put
#ActionState.PUTTING:
#local_player_character.handle_put_slot_selected(slot_index)
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 and we need them
if enable_bots:
var needed_bots = max_players - 1 # -1 for the host
for i in range(2, needed_bots + 2): # +2 because we start from ID 2
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():
# 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)
# Only add replacement bot if bots are enabled
if enable_bots:
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):
# First check if bots are enabled
if not enable_bots:
print("Attempted to create bot while bots are disabled")
return
# 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 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])
# Update the obstacle UI setup function
func setup_obstacle_ui():
# Create the obstacle button
var obstacle_button = Button.new()
obstacle_button.text = "Place Obstacle"
obstacle_button.pressed.connect(func(): set_action_state(ActionState.PLACING_OBSTACLE))
$ActionMenu/ActionButtonContainer.add_child(obstacle_button)
# Create the rotation/orientation cycle button
var orientation_button = Button.new()
orientation_button.text = "Direction: North"
orientation_button.pressed.connect(func():
orientation_button.text = cycle_obstacle_orientation()
)
$ActionMenu/ActionButtonContainer.add_child(orientation_button)
# Create the type cycle button
var type_button = Button.new()
type_button.text = "Type: 1"
type_button.pressed.connect(func():
type_button.text = cycle_obstacle_type()
)
$ActionMenu/ActionButtonContainer.add_child(type_button)
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
func randomize_item_at_position(grid_position: Vector2i):
if not multiplayer.is_server() and multiplayer.get_unique_id() == multiplayer.get_remote_sender_id():
# Request server to randomize item
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)
# Only randomize if there's already an item
if current_item != -1:
# Generate a random item value (7-10 based on the game's item system)
var rng = RandomNumberGenerator.new()
rng.randomize()
var new_item = rng.randi_range(7, 10)
# Make sure it's different from the current item
while new_item == current_item:
new_item = rng.randi_range(7, 10)
# Update the item and sync to all clients
sync_grid_item(cell.x, cell.y, cell.z, new_item)
rpc("sync_grid_item", cell.x, cell.y, cell.z, new_item)
# Consume action points for the player who performed the action
var player
if multiplayer.get_remote_sender_id() != 0:
player = get_node_or_null(str(multiplayer.get_remote_sender_id()))
else:
player = get_node_or_null(str(multiplayer.get_unique_id()))
if player and player.has_method("consume_action_points"):
player.has_performed_action = true
player.consume_action_points(1)
player.clear_highlights()
set_action_state(ActionState.NONE)
return true
return false
@rpc("any_peer")
func request_randomize_item(grid_position: Vector2i):
if multiplayer.is_server():
# Verify request came from a valid authority
var sender_id = multiplayer.get_remote_sender_id()
var player = get_node_or_null(str(sender_id))
if player and player.is_multiplayer_authority() and player.action_points > 0:
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:
var cell = Vector3i(x, y, z)
# Log the change for debugging
print("Main: Setting grid item at ", cell, " to ", item, " (called by ", multiplayer.get_remote_sender_id(), ")")
# Make sure we set the cell reliably
enhanced_gridmap.set_cell_item(cell, item)