Attempt to using Nakama as replacement of Low-Level ENet
This commit is contained in:
+358
-1155
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user