Files
tekton/scenes/main.gd
T

1121 lines
36 KiB
GDScript

# -------------------------------------------------------------------------------------
# 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