Attempt to using Nakama as replacement of Low-Level ENet

This commit is contained in:
2025-12-02 00:58:44 +08:00
parent b27b612989
commit ead155afed
74 changed files with 14205 additions and 23315 deletions
+358 -1155
View File
@@ -1,1103 +1,362 @@
# -------------------------------------------------------------------------------------
# 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"
# Manager references
var ui_manager
var obstacle_manager
@export var enable_bots: bool = true # Add this line
var connected_peer_ids = []
# Minimal local state
var match_id_input: LineEdit
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()
# Initialize scene managers
_init_managers()
# Connect to multiplayer signals
multiplayer.peer_connected.connect(_on_peer_connected)
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
# Connect to Nakama signals
NakamaManager.match_joined.connect(_on_match_joined)
# Setup UI
ui_manager.setup_action_buttons(_set_action_state_callback)
ui_manager.setup_playerboard_ui()
_setup_obstacle_ui()
_setup_match_input()
func _init_managers():
# Create and attach scene managers
ui_manager = load("res://scripts/managers/ui_manager.gd").new()
ui_manager.name = "UIManager"
add_child(ui_manager)
ui_manager.initialize(self)
obstacle_manager = load("res://scripts/managers/obstacle_manager.gd").new()
obstacle_manager.name = "ObstacleManager"
add_child(obstacle_manager)
obstacle_manager.initialize($EnhancedGridMap)
func _setup_match_input():
match_id_input = LineEdit.new()
match_id_input.placeholder_text = "Enter Match ID to Join"
match_id_input.custom_minimum_size = Vector2(200, 30)
match_id_input.position = Vector2(10, 50)
$Menu.add_child(match_id_input)
func _setup_obstacle_ui():
var obstacle_button = Button.new()
obstacle_button.text = "Place Obstacle"
obstacle_button.pressed.connect(func(): _set_action_state(ui_manager.ActionState.PLACING_OBSTACLE))
$ActionMenu/ActionButtonContainer.add_child(obstacle_button)
var orientation_button = Button.new()
orientation_button.text = "Direction: North"
orientation_button.pressed.connect(func():
orientation_button.text = obstacle_manager.cycle_obstacle_orientation()
)
$ActionMenu/ActionButtonContainer.add_child(orientation_button)
var type_button = Button.new()
type_button.text = "Type: 1"
type_button.pressed.connect(func():
type_button.text = obstacle_manager.cycle_obstacle_type()
)
$ActionMenu/ActionButtonContainer.add_child(type_button)
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
if multiplayer.is_server() and GameStateManager.is_game_started():
if TurnManager.turn_based_mode:
rpc("sync_turn_index", TurnManager.current_turn_index)
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()
# =============================================================================
# Network Button Handlers
# =============================================================================
func _on_host_pressed():
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Server"
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Server (Creating Match...)"
$Menu.visible = false
multiplayer_peer.create_server(PORT)
multiplayer.multiplayer_peer = multiplayer_peer
$NetworkPanel/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
NakamaManager.connect_to_nakama_async()
await NakamaManager.connected_to_nakama
NakamaManager.host_game()
func _on_join_pressed():
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Client"
var match_id = match_id_input.text.strip_edges()
if match_id.is_empty():
print("Please enter a Match ID")
return
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Client (Joining...)"
$Menu.visible = false
multiplayer_peer.create_client(ADDRESS, PORT)
multiplayer.multiplayer_peer = multiplayer_peer
NakamaManager.connect_to_nakama_async()
await NakamaManager.connected_to_nakama
NakamaManager.join_game(match_id)
func _on_match_joined(match_id: String):
$NetworkPanel/NetworkInfo/UniquePeerID.text = str(multiplayer.get_unique_id())
# After connection is established
if multiplayer.is_server():
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Server (Match: %s)" % match_id
_setup_host_game()
else:
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Client"
_setup_client_game()
# =============================================================================
# Game Setup
# =============================================================================
func _setup_host_game():
# Generate goals
GoalManager.generate_preset_goals(GameStateManager.max_players)
# Add host player
var player_id = 1
var player_character = PlayerManager.add_player_character(player_id)
add_child(player_character)
player_character.add_to_group("Players", true)
GameStateManager.add_player(player_id)
GameStateManager.local_player_character = player_character
ui_manager.set_local_player(player_character)
# Set host goals
player_character.goals = GoalManager.get_goals_for_player(0)
rpc("sync_player_goals", player_id, player_character.goals)
rpc("sync_preset_goals", GoalManager.preset_goals)
# Add bots
if GameStateManager.enable_bots:
for i in range(2, GameStateManager.max_players + 1):
_add_bot(i)
_start_game()
func _setup_client_game():
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):
func _start_game():
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")
GameStateManager.start_game()
rpc("sync_game_start", GameStateManager.players, TurnManager.turn_based_mode)
if TurnManager.turn_based_mode:
TurnManager.reset_turn()
var next_player = TurnManager.next_turn(GameStateManager.players)
rpc("set_current_turn", next_player)
@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)
# =============================================================================
# Player Management
# =============================================================================
@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):
func _add_bot(bot_id: int):
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")
func create_bot(bot_id: int):
if not GameStateManager.enable_bots:
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
var bot_character = PlayerManager.create_bot(bot_id)
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)
GameStateManager.add_bot(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()
if goal_index < GoalManager.preset_goals.size():
bot_character.goals = GoalManager.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:
func add_player_character(peer_id: int):
if has_node(str(peer_id)):
return
var player_character = PlayerManager.add_player_character(peer_id)
add_child(player_character)
player_character.add_to_group("Players", true)
GameStateManager.add_player(peer_id)
if peer_id == multiplayer.get_unique_id():
GameStateManager.local_player_character = player_character
ui_manager.set_local_player(player_character)
ui_manager.update_button_states()
ui_manager.update_playerboard_ui()
func _on_peer_connected(new_peer_id: int):
if multiplayer.is_server():
await get_tree().create_timer(1.5).timeout
add_player_character(new_peer_id)
rpc("add_newly_connected_player_character", new_peer_id)
@rpc
func add_newly_connected_player_character(new_peer_id: int):
add_player_character(new_peer_id)
func _on_peer_disconnected(peer_id: int):
if multiplayer.is_server():
GameStateManager.remove_player(peer_id)
if GameStateManager.enable_bots:
var next_id = PlayerManager.get_next_available_bot_id(GameStateManager.max_players, GameStateManager.players)
if next_id != -1:
_add_bot(next_id)
# =============================================================================
# Turn Management (RPC Handlers)
# =============================================================================
@rpc("reliable")
func sync_turn_index(index: int):
TurnManager.current_turn_index = index
@rpc("any_peer", "call_local")
func set_current_turn(player_id: int):
if not TurnManager.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)
@rpc("call_local")
func sync_game_start(player_list: Array, is_turn_based: bool):
GameStateManager.players = player_list
TurnManager.turn_based_mode = is_turn_based
GameStateManager.start_game()
func update_all_players_boards():
if not game_started:
# =============================================================================
# UI / Action State Management
# =============================================================================
func _set_action_state_callback(new_state):
_set_action_state(new_state)
func _set_action_state(new_state):
var local_player = GameStateManager.local_player_character
if not local_player or not local_player.is_multiplayer_authority():
return
var local_id = multiplayer.get_unique_id()
var all_players = get_tree().get_nodes_in_group("Players")
var all_player_boards = $AllPlayerBoards
if local_player.is_bot or local_player.is_in_group("Bots"):
ui_manager.current_action_state = new_state
return
# Store current active tab
var current_tab = all_player_boards.current_tab
if ui_manager.current_action_state == new_state or local_player.action_points <= 0:
return
# 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
ui_manager.current_action_state = new_state
local_player.clear_highlights()
local_player.clear_playerboard_highlights()
# 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))
match new_state:
ui_manager.ActionState.MOVING:
local_player.highlight_movement_range()
ui_manager.ActionState.GRABBING:
local_player.highlight_adjacent_cells()
ui_manager.ActionState.PUTTING:
local_player.highlight_occupied_playerboard_slots()
ui_manager.ActionState.RANDOMIZING:
local_player.highlight_random_valid_cells()
ui_manager.ActionState.ARRANGING:
_show_arrangement_ui()
local_player.highlight_occupied_playerboard_slots()
ui_manager.ActionState.PLACING_OBSTACLE:
local_player.highlight_valid_obstacle_cells()
func _show_arrangement_ui():
if ui_manager.playerboard_ui:
ui_manager.playerboard_ui.visible = true
ui_manager.update_playerboard_ui()
func _on_playerboard_slot_clicked(event, slot_index):
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
var local_player = GameStateManager.local_player_character
if not local_player:
return
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])
match ui_manager.current_action_state:
ui_manager.ActionState.ARRANGING:
local_player.arrange_playerboard_item(slot_index)
# =============================================================================
# Obstacle Management
# =============================================================================
func place_obstacle(grid_position: Vector2i) -> bool:
var local_player = GameStateManager.local_player_character
var success = obstacle_manager.place_obstacle(grid_position, local_player)
# 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
if success:
local_player.clear_highlights()
_set_action_state(ui_manager.ActionState.NONE)
if is_multiplayer_authority():
rpc("sync_place_obstacle", grid_position.x, grid_position.y, 3,
obstacle_manager.current_obstacle_item, obstacle_manager.current_obstacle_orientation)
# Restore previous active tab
all_player_boards.current_tab = current_tab
return success
@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)
# =============================================================================
# Goal & Playerboard Sync
# =============================================================================
@rpc("reliable")
func sync_preset_goals(goals_list: Array):
GoalManager.preset_goals = goals_list
@rpc("any_peer", "call_local")
func sync_player_goals(player_id: int, goals: Array):
var player = get_node_or_null(str(player_id))
if player:
player.goals = goals.duplicate()
if multiplayer.is_server():
update_all_players_goals()
@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
if player_id == multiplayer.get_unique_id() and GameStateManager.local_player_character:
ui_manager.update_playerboard_ui()
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)
# =============================================================================
# UI Update Functions
# =============================================================================
func update_all_players_goals():
if not game_started:
if not GameStateManager.is_game_started():
return
var all_players = get_tree().get_nodes_in_group("Players")
@@ -1107,11 +366,9 @@ func update_all_players_goals():
return a_id < b_id
)
# Hide all panels first
for i in range($AllPlayerGoals.get_child_count()):
$AllPlayerGoals.get_child(i).visible = false
# Update only for connected players (up to number of panels available)
var max_panels = $AllPlayerGoals.get_child_count()
for i in range(min(all_players.size(), max_panels)):
var player = all_players[i]
@@ -1119,83 +376,7 @@ func update_all_players_goals():
$AllPlayerGoals.get_child(i).visible = true
_update_player_goals_ui(i, player.goals)
# Server also syncs to clients
if multiplayer.is_server():
var all_goals_data = []
for i in range(min(all_players.size(), max_panels)):
var player = all_players[i]
all_goals_data.append({
"player_idx": i,
"goals": player.goals.duplicate() if player else []
})
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("reliable")
func sync_all_goals_to_clients(all_goals_data: Array):
if multiplayer.is_server():
return # Only clients should process this
# Hide all panels first
for i in range($AllPlayerGoals.get_child_count()):
$AllPlayerGoals.get_child(i).visible = false
# Apply received goals
for goal_data in all_goals_data:
var player_idx = goal_data.get("player_idx", -1)
var goals = goal_data.get("goals", [])
if player_idx >= 0 and player_idx < $AllPlayerGoals.get_child_count() and goals.size() > 0:
$AllPlayerGoals.get_child(player_idx).visible = true
_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()
# Helper function to update specific player's goals UI
func _update_player_goals_ui(player_idx: int, goals: Array):
# Safety: ensure player_idx is within bounds of AllPlayerGoals children
if player_idx < 0 or player_idx >= $AllPlayerGoals.get_child_count():
return
@@ -1204,7 +385,6 @@ func _update_player_goals_ui(player_idx: int, goals: Array):
return
var goals_grid = panel.get_node("MarginContainer/Playergoals")
for slot_idx in range(9):
if slot_idx >= goals_grid.get_child_count():
break
@@ -1212,12 +392,10 @@ func _update_player_goals_ui(player_idx: int, goals: Array):
var slot = goals_grid.get_child(slot_idx)
var goal_value = goals[slot_idx] if slot_idx < goals.size() else -1
# Hide all goal tiles first
for tile_name in ["TileHeart", "TileDiamond", "TileStar", "TileCoin"]:
if slot.has_node(tile_name):
slot.get_node(tile_name).hide()
# Show appropriate tile based on goal value
match goal_value:
7:
if slot.has_node("TileHeart"):
@@ -1232,94 +410,119 @@ func _update_player_goals_ui(player_idx: int, goals: Array):
if slot.has_node("TileCoin"):
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])
func update_all_players_boards():
if not GameStateManager.is_game_started():
return
var all_players = get_tree().get_nodes_in_group("Players")
var all_player_boards = $AllPlayerBoards
# Update boards (simplified version - full implementation would mirror original)
pass
# 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()
# =============================================================================
# Connection Verification
# =============================================================================
func verify_all_connections():
if multiplayer.is_server():
for peer_id in GameStateManager.players:
if peer_id != 1:
rpc_id(peer_id, "connection_verify", GameStateManager.players)
@rpc
func connection_verify(expected_players: Array):
for peer_id in expected_players:
if peer_id != multiplayer.get_unique_id() and not has_node(str(peer_id)):
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")
}
rpc_id(multiplayer.get_remote_sender_id(), "create_specific_player", player_data)
@rpc("any_peer")
func request_full_player_sync(requesting_peer_id: int):
if multiplayer.is_server():
for peer_id in GameStateManager.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)
await get_tree().create_timer(0.1).timeout
@rpc("reliable")
func create_specific_player(data: Dictionary):
var peer_id = data["peer_id"]
if has_node(str(peer_id)):
return
var null_val = 0
var max_nulls = 3
var player_character = PlayerManager.add_player_character(peer_id)
player_character.current_position = data["position"]
add_child(player_character)
player_character.add_to_group("Players", true)
const SPECIAL_VALUES = {1: 7, 2: 8, 3: 9, 4: 10}
if data["is_bot"]:
player_character.add_to_group("Bots", true)
player_character.is_bot = true
player_character.rpc("sync_bot_status", true)
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
player_character.goals = data["goals"].duplicate()
player_character.playerboard = data["playerboard"].duplicate()
player_character.global_position = Vector3(
data["position"].x * 2 + 1,
1.0,
data["position"].y * 2 + 1
)
player_character.rpc("sync_position", data["position"])
# =============================================================================
# Grid Item Randomization
# =============================================================================
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
if not multiplayer.is_server():
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)
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)
enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item)