1220 lines
40 KiB
GDScript
1220 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 the current obstacle direction
|
|
var current_obstacle_direction = EnhancedGridMap.Direction.NORTH # Default to NORTH
|
|
var current_obstacle_item = 12 # Starting with first obstacle item (12)
|
|
|
|
enum ObstacleDirection {
|
|
VERTICAL,
|
|
HORIZONTAL
|
|
}
|
|
|
|
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)
|
|
)
|
|
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()
|
|
ActionState.PLACING_OBSTACLE:
|
|
local_player_character.highlight_valid_obstacle_cells()
|
|
|
|
# Update the place_obstacle function for floor 3
|
|
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 direction = EnhancedGridMap.Direction.BLOCKED_NORTH
|
|
|
|
match current_obstacle_direction:
|
|
ObstacleDirection.VERTICAL:
|
|
direction = EnhancedGridMap.Direction.BLOCKED_NORTH # Block movement along east-west axis
|
|
ObstacleDirection.HORIZONTAL:
|
|
direction = EnhancedGridMap.Direction.BLOCKED_EAST # Block movement along north-south axis
|
|
|
|
var success = $EnhancedGridMap.place_obstacle(
|
|
Vector3i(grid_position.x, floor_index, grid_position.y),
|
|
current_obstacle_item,
|
|
direction
|
|
)
|
|
|
|
if success:
|
|
local_player_character.action_points -= 1
|
|
local_player_character.clear_highlights()
|
|
|
|
# Don't exit the obstacle placement mode to allow multiple placements
|
|
local_player_character.highlight_valid_obstacle_cells()
|
|
|
|
# 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, direction)
|
|
|
|
return true
|
|
return false
|
|
|
|
# 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, direction: int):
|
|
$EnhancedGridMap.place_obstacle(Vector3i(x, floor_index, y), item_index, direction)
|
|
|
|
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_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():
|
|
# 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])
|
|
|
|
# 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 direction cycle button
|
|
var direction_button = Button.new()
|
|
direction_button.text = "Direction: North"
|
|
direction_button.pressed.connect(func():
|
|
direction_button.text = cycle_obstacle_direction()
|
|
)
|
|
$ActionMenu/ActionButtonContainer.add_child(direction_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)
|
|
|
|
# Add a 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)
|
|
|
|
func cycle_obstacle_direction():
|
|
var directions = [EnhancedGridMap.Direction.NORTH, EnhancedGridMap.Direction.EAST, EnhancedGridMap.Direction.SOUTH, EnhancedGridMap.Direction.WEST]
|
|
var current_index = directions.find(current_obstacle_direction)
|
|
current_index = (current_index + 1) % directions.size()
|
|
current_obstacle_direction = directions[current_index]
|
|
|
|
var direction_names = ["North", "East", "South", "West"]
|
|
return "Direction: " + direction_names[current_index]
|
|
|
|
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)
|