799 lines
28 KiB
GDScript
799 lines
28 KiB
GDScript
extends Node
|
|
|
|
|
|
|
|
# PlayerboardManager - Handles all playerboard operations including grab, put, arrange
|
|
|
|
var player: Node3D
|
|
var enhanced_gridmap: Node
|
|
|
|
# Playerboard state
|
|
var selected_gridmap_position = Vector2i(-1, -1)
|
|
var selected_playerboard_slot = -1
|
|
var targeted_playerboard_slot = -1
|
|
|
|
# 0-based indices corresponding to User's 1-based request: 1,5,6,10,11,15,16,20,21,22,23,24,25
|
|
const HIDDEN_SLOTS = [0, 4, 5, 9, 10, 14, 15, 19, 20, 21, 22, 23, 24]
|
|
|
|
func is_valid_playerboard_slot(slot_index: int) -> bool:
|
|
return not (slot_index in HIDDEN_SLOTS)
|
|
|
|
func initialize(p_player: Node3D, p_gridmap: Node):
|
|
player = p_player
|
|
enhanced_gridmap = p_gridmap
|
|
|
|
func _normalize_tile(tile: int) -> int:
|
|
"""Normal tiles 7-10 are goals. 11-14 are powerups and not goals."""
|
|
return tile
|
|
|
|
# =============================================================================
|
|
# GRAB Operations
|
|
# =============================================================================
|
|
|
|
func grab_item(grid_position: Vector2i) -> bool:
|
|
var has_ap = player.action_points > 0 if TurnManager.turn_based_mode else true
|
|
|
|
if not enhanced_gridmap:
|
|
print("[Grab] Failed for %s: enhanced_gridmap is null" % player.name)
|
|
return false
|
|
if not has_ap:
|
|
print("[Grab] Failed for %s: no AP (%d)" % [player.name, player.action_points])
|
|
return false
|
|
|
|
if player.get("is_frozen"):
|
|
print("[Grab] Failed for %s: player is frozen" % player.name)
|
|
return false
|
|
|
|
var cell = Vector3i(grid_position.x, 1, grid_position.y)
|
|
var item = enhanced_gridmap.get_cell_item(cell)
|
|
|
|
# Validate adjacency (unless it's current position)
|
|
if grid_position != player.current_position:
|
|
var neighbors = enhanced_gridmap.get_neighbors(player.current_position, 0)
|
|
var is_adjacent = false
|
|
for neighbor in neighbors:
|
|
if neighbor.position == grid_position:
|
|
is_adjacent = true
|
|
break
|
|
if not is_adjacent:
|
|
print("[Grab] Failed for %s: %s is not adjacent to current %s" % [player.name, grid_position, player.current_position])
|
|
return false
|
|
|
|
if item == -1:
|
|
print("[Grab] Failed for %s: no item at %s Layer 1" % [player.name, grid_position])
|
|
return false
|
|
|
|
# === AUTO-ARRANGE LOGIC (Client-side pre-check) ===
|
|
# If item is powerup (11-14), we don't need a slot.
|
|
var is_powerup = (item >= 11 and item <= 14)
|
|
var target_slot = -1
|
|
|
|
if not is_powerup:
|
|
target_slot = find_best_goal_slot_for_item(item)
|
|
if target_slot == -1:
|
|
print("[Grab] Failed for %s: No valid slot found for item %d." % [player.name, item])
|
|
return false # no space
|
|
|
|
if not player.is_multiplayer_authority():
|
|
print("[Grab] Failed for %s: not authority" % player.name)
|
|
return false
|
|
|
|
print("[Grab] %s SUCCESS! Grabbing item %d at %s into slot %d" % [player.name, item, grid_position, target_slot])
|
|
|
|
# Play pickup animation (synced across network)
|
|
if player.is_multiplayer_authority() and player.has_method("sync_pickup_animation"):
|
|
player.rpc("sync_pickup_animation", item)
|
|
|
|
# === Optimistic Local Update (immediate visual feedback) ===
|
|
# Apply changes locally first, server will validate/sync
|
|
enhanced_gridmap.set_cell_item(cell, -1) # Remove item visually immediately
|
|
|
|
# === Power-Up Consumption (Instant Unlock) ===
|
|
# IDs 11-14 are Ability Power-Ups. They are consumed on pickup, not placed on board.
|
|
if item >= 11 and item <= 14:
|
|
var special_tiles_manager = player.get_node_or_null("SpecialTilesManager")
|
|
if special_tiles_manager:
|
|
special_tiles_manager.add_powerup_from_item(item)
|
|
|
|
# Animation and sound handled via sync_pickup_animation RPC above
|
|
|
|
# Skip adding to playerboard. Just consume.
|
|
else:
|
|
# Normal Tile: Add to playerboard
|
|
player.playerboard[target_slot] = item
|
|
|
|
# Stop n Go Mission Progress
|
|
var sng_main = player.get_tree().root.get_node_or_null("Main")
|
|
if sng_main:
|
|
var sng_manager = sng_main.get_node_or_null("StopNGoManager")
|
|
if sng_manager and sng_manager.has_method("update_mission_progress"):
|
|
sng_manager.update_mission_progress(player.name.to_int(), item)
|
|
|
|
# Update UI immediately for responsiveness
|
|
|
|
# Update UI immediately for responsiveness
|
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
|
if main and main.ui_manager and not (player.is_bot or player.is_in_group("Bots")):
|
|
main.ui_manager.update_playerboard_ui()
|
|
|
|
# Check if goal is completed after grabbing
|
|
_check_goal_completion()
|
|
|
|
# === Server Sync ===
|
|
if multiplayer.is_server():
|
|
# HOST/SERVER: Broadcast to all clients
|
|
main.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1)
|
|
# Use main's RPC which properly looks up player by ID on each client
|
|
var peer_id = player.name.to_int()
|
|
main.rpc("sync_playerboard", peer_id, player.playerboard)
|
|
player.has_performed_action = true
|
|
player.consume_action_points(1)
|
|
player.rpc("force_action_state_none")
|
|
else:
|
|
# CLIENT: Send RPC request to server for validation
|
|
player.rpc_id(1, "request_server_grab", grid_position, cell.x, cell.y, cell.z, item)
|
|
|
|
return true # Action applied locally
|
|
|
|
func _execute_grab(grid_pos: Vector2i, cell: Vector3i, item_id: int):
|
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
|
if not main:
|
|
push_error("Server: Main node not found.")
|
|
return false
|
|
|
|
var server_gridmap = main.get_node("EnhancedGridMap")
|
|
if not server_gridmap:
|
|
push_error("Server: EnhancedGridMap not found.")
|
|
return false
|
|
|
|
# 1. Server-side Validation
|
|
var server_item = server_gridmap.get_cell_item(cell)
|
|
|
|
# Check if item is still there
|
|
if server_item != item_id:
|
|
print("Server: Item mismatch or already taken. Server has ", server_item)
|
|
_force_sync_to_client(cell, server_item)
|
|
return false
|
|
|
|
# Check action points
|
|
if player.action_points <= 0:
|
|
print("Server: Player has no action points.")
|
|
_force_sync_to_client(cell, server_item)
|
|
return false
|
|
|
|
# Check adjacency
|
|
if grid_pos != player.current_position:
|
|
var neighbors = server_gridmap.get_neighbors(player.current_position, 0)
|
|
if not neighbors.any(func(n): return n.position == grid_pos):
|
|
print("Server: Player is not adjacent to item.")
|
|
_force_sync_to_client(cell, server_item)
|
|
return false
|
|
|
|
# 2. Server-side Auto-Arrange
|
|
var is_powerup = (item_id >= 11 and item_id <= 14)
|
|
var target_slot = -1
|
|
if not is_powerup:
|
|
target_slot = find_best_goal_slot_for_item(item_id)
|
|
if target_slot == -1:
|
|
print("Server: Player has no valid slot for item.")
|
|
_force_sync_to_client(cell, server_item)
|
|
return false
|
|
|
|
# 3. Server Executes the Action
|
|
|
|
# 3a. Update gridmap (using Main's RPC, which has authority)
|
|
main.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1)
|
|
|
|
# 3b. Update playerboard state (on this server-side instance)
|
|
if is_powerup:
|
|
var special_tiles_manager = player.get_node_or_null("SpecialTilesManager")
|
|
if special_tiles_manager:
|
|
special_tiles_manager.add_powerup_from_item(item_id)
|
|
# Do not add to playerboard
|
|
else:
|
|
player.playerboard[target_slot] = item_id
|
|
|
|
# Stop n Go Mission Progress
|
|
var sng_manager = main.get_node_or_null("StopNGoManager")
|
|
if sng_manager and sng_manager.has_method("update_mission_progress"):
|
|
sng_manager.update_mission_progress(player.name.to_int(), item_id)
|
|
|
|
# 3c. Broadcast the new playerboard state to all clients
|
|
var peer_id = player.name.to_int()
|
|
main.rpc("sync_playerboard", peer_id, player.playerboard)
|
|
|
|
# 3d. Check if goal is completed (SERVER-SIDE - this triggers goal regeneration for clients!)
|
|
# Logic only runs if board changed, but theoretically powerup pickup shouldn't trigger goal
|
|
if not is_powerup:
|
|
_check_goal_completion()
|
|
|
|
# 3e. Consume action points
|
|
player.has_performed_action = true
|
|
player.consume_action_points(1)
|
|
|
|
# 3f. Reset the UI for the player who acted
|
|
player.rpc("force_action_state_none")
|
|
|
|
# 4. Check if we need to respawn tiles (Scarcity Logic)
|
|
# "during no more tiles" -> Refill if floor is empty
|
|
_check_and_refill_grid_if_needed(server_gridmap)
|
|
|
|
return true
|
|
|
|
func _check_and_refill_grid_if_needed(server_gridmap: Node):
|
|
# Check if there are any items left on floor 1
|
|
var has_items = false
|
|
var item_list = server_gridmap.mesh_library.get_item_list() if server_gridmap.mesh_library else []
|
|
|
|
# Iterate used cells to check for ANY item on floor 1
|
|
var used_cells = server_gridmap.get_used_cells()
|
|
for cell in used_cells:
|
|
if cell.y == 1: # Floor 1
|
|
has_items = true
|
|
break
|
|
|
|
if not has_items:
|
|
if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS):
|
|
# Tekton Doors handles its own wall-aware refill in PortalModeManager
|
|
return
|
|
|
|
print("[PlayerboardManager] Floor 1 empty! Respawning tiles with Scarcity...")
|
|
# Call randomize_floor on floor 1 using ScarcityController
|
|
# ScarcityController is a global class, so we can pass its static function as a Callable
|
|
server_gridmap.randomize_floor(1, ScarcityController.get_random_tile_id)
|
|
|
|
# We need to sync the ENTIRE floor to clients.
|
|
# EnhancedGridMap doesn't have a "Sync Floor" RPC built-in to Main, only single cells or array update.
|
|
# Main.gd has sync_grid_item which handles single cells.
|
|
# Ideally we'd loop and sync all, or add a method to Main to sync floor.
|
|
# Loop is okay for 10x10 (100 RPCs is a bit much but acceptable for a rare event).
|
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
|
if main:
|
|
for x in range(server_gridmap.columns):
|
|
for z in range(server_gridmap.rows):
|
|
var item = server_gridmap.get_cell_item(Vector3i(x, 1, z))
|
|
main.rpc("sync_grid_item", x, 1, z, item)
|
|
|
|
|
|
func _force_sync_to_client(cell: Vector3i, server_item: int):
|
|
"""Force a sync of the specific cell and playerboard to the client who initiated the failed action."""
|
|
# Only meaningful if we are server
|
|
if not multiplayer.is_server():
|
|
return
|
|
|
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
|
if not main: return
|
|
|
|
# Determine client peer ID from player name (standard convention)
|
|
# Note: Bots are ID 1, so we don't need to sync special for them (local function calls)
|
|
# But for Clients...
|
|
var peer_id = player.name.to_int()
|
|
if peer_id == 1: # Server/Bot
|
|
return
|
|
|
|
# Sync the Grid Item (which they thought they took)
|
|
main.rpc_id(peer_id, "sync_grid_item", cell.x, cell.y, cell.z, server_item)
|
|
|
|
# Sync their Playerboard (which they thought they updated)
|
|
main.rpc_id(peer_id, "sync_playerboard", peer_id, player.playerboard)
|
|
|
|
print("Server: Forced sync to client %d due to action failure." % peer_id)
|
|
|
|
func bot_try_grab_item() -> bool:
|
|
if not enhanced_gridmap or player.action_points <= 0:
|
|
return false
|
|
|
|
# First check current position
|
|
var current_cell = Vector3i(player.current_position.x, 1, player.current_position.y)
|
|
var item = enhanced_gridmap.get_cell_item(current_cell)
|
|
|
|
if item != -1:
|
|
# Find empty slot that is NOT hidden
|
|
var empty_slot = -1
|
|
for i in range(player.playerboard.size()):
|
|
if player.playerboard[i] == -1 and not (i in HIDDEN_SLOTS):
|
|
empty_slot = i
|
|
break
|
|
|
|
if empty_slot != -1:
|
|
if player.is_multiplayer_authority():
|
|
# Convert Holo (11-14)
|
|
if item >= 11 and item <= 14:
|
|
item = item - 4
|
|
|
|
# Inventory Add
|
|
if item >= 7 and item <= 10:
|
|
var special_tiles_manager = player.get_node_or_null("SpecialTilesManager")
|
|
if special_tiles_manager:
|
|
special_tiles_manager.add_powerup_from_item(item)
|
|
|
|
player.playerboard[empty_slot] = item
|
|
|
|
# Play pickup animation for bot
|
|
if player.has_method("sync_pickup_animation"):
|
|
player.rpc("sync_pickup_animation", item)
|
|
|
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
|
if main:
|
|
main.rpc("sync_grid_item", current_cell.x, current_cell.y, current_cell.z, -1)
|
|
main.rpc("sync_playerboard", player.name.to_int(), player.playerboard)
|
|
player.has_performed_action = true
|
|
player.action_points -= 1
|
|
_check_goal_completion()
|
|
return true
|
|
|
|
# Check adjacent cells if nothing at current position
|
|
var neighbors = enhanced_gridmap.get_neighbors(player.current_position, 0)
|
|
for neighbor in neighbors:
|
|
if neighbor.is_walkable:
|
|
var cell = Vector3i(neighbor.position.x, 1, neighbor.position.y)
|
|
item = enhanced_gridmap.get_cell_item(cell)
|
|
if item != -1:
|
|
# Find empty slot that is NOT hidden
|
|
var empty_slot = -1
|
|
for i in range(player.playerboard.size()):
|
|
if player.playerboard[i] == -1 and not (i in HIDDEN_SLOTS):
|
|
empty_slot = i
|
|
break
|
|
|
|
if empty_slot != -1:
|
|
if player.is_multiplayer_authority():
|
|
player.playerboard[empty_slot] = item
|
|
|
|
# Play pickup animation for bot
|
|
if player.has_method("sync_pickup_animation"):
|
|
player.rpc("sync_pickup_animation", item)
|
|
|
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
|
if main:
|
|
main.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1)
|
|
main.rpc("sync_playerboard", player.name.to_int(), player.playerboard)
|
|
player.has_performed_action = true
|
|
player.action_points -= 1
|
|
return true
|
|
|
|
return false
|
|
|
|
# =============================================================================
|
|
# PUT Operations
|
|
# =============================================================================
|
|
|
|
func auto_put_item() -> bool:
|
|
# Check AP only if in turn-based mode
|
|
var has_ap = player.action_points > 0 if TurnManager.turn_based_mode else true
|
|
|
|
if not enhanced_gridmap or not has_ap or player.is_bot or player.is_in_group("Bots") or player.get("is_frozen"):
|
|
return false
|
|
|
|
# Step 1: Find empty adjacent (or current) grid cells
|
|
var valid_put_positions = []
|
|
var current_cell_3d = Vector3i(player.current_position.x, 1, player.current_position.y)
|
|
if enhanced_gridmap.get_cell_item(current_cell_3d) == -1:
|
|
valid_put_positions.append(player.current_position)
|
|
|
|
var neighbors = enhanced_gridmap.get_neighbors(player.current_position, 0)
|
|
for neighbor in neighbors:
|
|
if neighbor.is_walkable:
|
|
var pos = neighbor.position
|
|
var cell_3d = Vector3i(pos.x, 1, pos.y)
|
|
if enhanced_gridmap.get_cell_item(cell_3d) == -1 and not player.is_position_occupied(pos):
|
|
# TEKTON DOORS: Avoid portal doors
|
|
var is_on_portal = false
|
|
if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS):
|
|
var doors = get_tree().get_nodes_in_group("PortalDoors")
|
|
for door in doors:
|
|
var door_grid = enhanced_gridmap.local_to_map(enhanced_gridmap.to_local(door.global_position))
|
|
if Vector2i(door_grid.x, door_grid.z) == pos:
|
|
is_on_portal = true
|
|
break
|
|
|
|
if not is_on_portal:
|
|
valid_put_positions.append(pos)
|
|
|
|
if valid_put_positions.is_empty():
|
|
return false
|
|
|
|
# Step 2: Find a tile that should NOT be on the board (prioritize non-goal items)
|
|
var put_slot = -1
|
|
|
|
# Count how many times each goal item is needed in the goals
|
|
var goal_counts = {}
|
|
for i in range(3):
|
|
for j in range(3):
|
|
var g = player.goals[i * 3 + j]
|
|
if g != -1:
|
|
goal_counts[g] = goal_counts.get(g, 0) + 1
|
|
|
|
# Priority 1: Find items that are NOT in goals at all (junk tiles)
|
|
for i in range(player.playerboard.size()):
|
|
var current_item = player.playerboard[i]
|
|
if current_item == -1:
|
|
continue
|
|
|
|
# Normalize item for goal comparison (holo tiles 11-14 = normal tiles 7-10)
|
|
var normalized_item = _normalize_tile(current_item)
|
|
# Item is not in goals at all → definitely junk, put this first
|
|
if not normalized_item in player.goals:
|
|
put_slot = i
|
|
break
|
|
|
|
# Priority 2: Find items outside central 3x3 OR items in central 3x3 that don't match goal position
|
|
if put_slot == -1:
|
|
for i in range(player.playerboard.size()):
|
|
var board_item = player.playerboard[i]
|
|
if board_item == -1:
|
|
continue
|
|
var row = i / 5
|
|
var col = i % 5
|
|
|
|
# If it's outside the central 3x3, it's potentially movable
|
|
if row < 1 or row > 3 or col < 1 or col > 3:
|
|
put_slot = i
|
|
break
|
|
else:
|
|
# It's inside central 3x3 - check if it matches the corresponding goal position
|
|
var goal_row = row - 1
|
|
var goal_col = col - 1
|
|
var expected_goal = player.goals[goal_row * 3 + goal_col]
|
|
# Normalize for comparison (holo tiles 11-14 = normal tiles 7-10)
|
|
var normalized_board = _normalize_tile(board_item)
|
|
# If the item doesn't match what should be in this goal position, it's misplaced
|
|
if normalized_board != expected_goal and expected_goal != -1:
|
|
put_slot = i
|
|
break
|
|
|
|
# Priority 3: Find excess goal items (we have more than needed in central area)
|
|
if put_slot == -1:
|
|
for i in range(player.playerboard.size()):
|
|
var current_item = player.playerboard[i]
|
|
if current_item == -1:
|
|
continue
|
|
if not current_item in player.goals:
|
|
continue
|
|
|
|
# Count how many of this item we have in the correct central positions
|
|
var correctly_placed_count = 0
|
|
for goal_row in range(3):
|
|
for goal_col in range(3):
|
|
if player.goals[goal_row * 3 + goal_col] == current_item:
|
|
var board_idx = (goal_row + 1) * 5 + (goal_col + 1)
|
|
if player.playerboard[board_idx] == current_item:
|
|
correctly_placed_count += 1
|
|
|
|
# Count total of this item on the board
|
|
var total_count = 0
|
|
for board_item in player.playerboard:
|
|
if board_item == current_item:
|
|
total_count += 1
|
|
|
|
# If we have all needed copies already correctly placed, extras can go
|
|
if correctly_placed_count >= goal_counts.get(current_item, 0) and total_count > correctly_placed_count:
|
|
put_slot = i
|
|
break
|
|
|
|
if put_slot == -1:
|
|
return false # Nothing suitable to put
|
|
|
|
# Step 3: Perform the put with optimistic local update
|
|
var target_pos = valid_put_positions[0]
|
|
var item = player.playerboard[put_slot]
|
|
var cell = Vector3i(target_pos.x, 1, target_pos.y)
|
|
|
|
if player.is_multiplayer_authority():
|
|
# Play put animation (synced across network)
|
|
if player.is_multiplayer_authority() and player.has_method("sync_put_animation"):
|
|
player.rpc("sync_put_animation")
|
|
|
|
# === Optimistic Local Update (immediate visual feedback) ===
|
|
enhanced_gridmap.set_cell_item(cell, item) # Add item to grid visually immediately
|
|
player.playerboard[put_slot] = -1 # Remove from playerboard immediately
|
|
|
|
# Update UI immediately for responsiveness
|
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
|
if main and main.ui_manager and not (player.is_bot or player.is_in_group("Bots")):
|
|
main.ui_manager.update_playerboard_ui()
|
|
|
|
# === Server Sync ===
|
|
player.rpc("sync_grid_item", cell.x, cell.y, cell.z, item)
|
|
player.rpc("sync_playerboard", player.playerboard)
|
|
player.has_performed_action = true
|
|
player.consume_action_points(1)
|
|
|
|
return true
|
|
|
|
# =============================================================================
|
|
# ARRANGE Operations
|
|
# =============================================================================
|
|
|
|
# Arrangement mode has been removed along with ActionMenu.
|
|
pass
|
|
|
|
# =============================================================================
|
|
# Helper Functions
|
|
# =============================================================================
|
|
|
|
func find_best_goal_slot_for_item(item: int) -> int:
|
|
if item == -1:
|
|
return -1
|
|
|
|
# Normalize item - treat holo tiles (11-14) the same as normal tiles (7-10)
|
|
var normalized_item = _normalize_tile(item)
|
|
|
|
# Convert goals to 2D (3x3)
|
|
var goals_2d = []
|
|
for i in range(3):
|
|
var row = []
|
|
for j in range(3):
|
|
row.append(player.goals[i * 3 + j])
|
|
goals_2d.append(row)
|
|
|
|
# Search for where this item should go in the central 3x3 (mapped to 5x5 board)
|
|
for i in range(3):
|
|
for j in range(3):
|
|
if goals_2d[i][j] == normalized_item:
|
|
var board_row = i + 1 # offset to center in 5x5
|
|
var board_col = j + 1
|
|
var slot_index = board_row * 5 + board_col
|
|
|
|
# Ensure this slot is not hidden (though 3x3 center should be safe, best to check)
|
|
if slot_index in HIDDEN_SLOTS:
|
|
continue
|
|
|
|
if player.playerboard[slot_index] == -1: # only if empty
|
|
return slot_index
|
|
|
|
# No ideal goal slot? Look for any empty slot that is NOT hidden AND NOT reserved for Goals
|
|
# Goal slots are row 2-3, col 1-3 (indices: 11,12,13, 16,17,18) matching User request
|
|
# Rows 1 (6,7,8) are now storage slots
|
|
var reserved_goal_slots = [11, 12, 13, 16, 17, 18]
|
|
|
|
for i in range(player.playerboard.size()):
|
|
if player.playerboard[i] == -1:
|
|
if i in HIDDEN_SLOTS: continue
|
|
if i in reserved_goal_slots: continue # Skip if it's a goal slot (reserved)
|
|
return i
|
|
|
|
return -1
|
|
|
|
func find_best_put_candidate() -> Dictionary:
|
|
# Convert goals to 2D (3x3)
|
|
var goals_2d = []
|
|
for i in range(3):
|
|
var row = []
|
|
for j in range(3):
|
|
row.append(player.goals[i * 3 + j])
|
|
goals_2d.append(row)
|
|
|
|
# Convert playerboard to 2D (5x5)
|
|
var board_2d = []
|
|
for i in range(5):
|
|
var row = []
|
|
for j in range(5):
|
|
row.append(player.playerboard[i * 5 + j])
|
|
board_2d.append(row)
|
|
|
|
# Step 1: Find misplaced or extra goal-matching items
|
|
var candidate_items = []
|
|
for board_i in range(5):
|
|
for board_j in range(5):
|
|
var item = board_2d[board_i][board_j]
|
|
if item == -1:
|
|
continue
|
|
var board_idx = board_i * 5 + board_j
|
|
|
|
# Is this item part of the goals?
|
|
if item not in player.goals:
|
|
continue
|
|
|
|
# Is it already in the correct central position?
|
|
var is_in_correct_central_spot = false
|
|
if board_i in [1, 2, 3] and board_j in [1, 2, 3]:
|
|
var goal_i = board_i - 1
|
|
var goal_j = board_j - 1
|
|
if goals_2d[goal_i][goal_j] == item:
|
|
is_in_correct_central_spot = true
|
|
|
|
if not is_in_correct_central_spot:
|
|
candidate_items.append({
|
|
"slot": board_idx,
|
|
"item": item
|
|
})
|
|
|
|
# Step 2: Find valid adjacent empty grid cells
|
|
var valid_cells = []
|
|
# Check current position
|
|
var current_cell_3d = Vector3i(player.current_position.x, 1, player.current_position.y)
|
|
if enhanced_gridmap.get_cell_item(current_cell_3d) == -1:
|
|
valid_cells.append(player.current_position)
|
|
# Check neighbors
|
|
var neighbors = enhanced_gridmap.get_neighbors(player.current_position, 0)
|
|
for neighbor in neighbors:
|
|
if neighbor.is_walkable:
|
|
var pos = neighbor.position
|
|
var cell_3d = Vector3i(pos.x, 1, pos.y)
|
|
if enhanced_gridmap.get_cell_item(cell_3d) == -1 and not player.is_position_occupied(pos):
|
|
valid_cells.append(pos)
|
|
|
|
if valid_cells.is_empty() or candidate_items.is_empty():
|
|
return {}
|
|
|
|
# Step 3: Prefer to put an item that *completes* a missing goal
|
|
for goal_i in range(3):
|
|
for goal_j in range(3):
|
|
var needed_item = goals_2d[goal_i][goal_j]
|
|
if needed_item == -1:
|
|
continue
|
|
# Check if central spot is empty
|
|
var board_i = goal_i + 1
|
|
var board_j = goal_j + 1
|
|
var central_slot = board_i * 5 + board_j
|
|
if player.playerboard[central_slot] == -1:
|
|
# Look for this item in candidate_items
|
|
for cand in candidate_items:
|
|
if cand.item == needed_item:
|
|
if not valid_cells.is_empty():
|
|
return {
|
|
"slot_index": cand.slot,
|
|
"grid_position": valid_cells[0] # pick first valid cell
|
|
}
|
|
|
|
# Fallback: just put any candidate item
|
|
return {
|
|
"slot_index": candidate_items[0].slot,
|
|
"grid_position": valid_cells[0]
|
|
}
|
|
|
|
func get_adjacent_playerboard_slots(slot_index) -> Array:
|
|
var adjacent = []
|
|
var row = slot_index / 5
|
|
var col = slot_index % 5
|
|
|
|
if row > 0: adjacent.append(slot_index - 5)
|
|
if row < 4: adjacent.append(slot_index + 5)
|
|
if col > 0: adjacent.append(slot_index - 1)
|
|
if col < 4: adjacent.append(slot_index + 1)
|
|
|
|
return adjacent
|
|
|
|
func is_valid_arrangement_slot(from_slot: int, to_slot: int) -> bool:
|
|
var from_row = int(from_slot / 5)
|
|
var from_col = from_slot % 5
|
|
var to_row = int(to_slot / 5)
|
|
var to_col = to_slot % 5
|
|
|
|
var row_diff = abs(from_row - to_row)
|
|
var col_diff = abs(from_col - to_col)
|
|
|
|
return (row_diff == 1 and col_diff == 0) or (row_diff == 0 and col_diff == 1)
|
|
|
|
func has_item_at_current_position() -> bool:
|
|
var current_cell = Vector3i(player.current_position.x, 1, player.current_position.y)
|
|
return enhanced_gridmap.get_cell_item(current_cell) != -1
|
|
|
|
func has_items_in_playerboard() -> bool:
|
|
return player.playerboard.any(func(item): return item != -1)
|
|
|
|
func playerboard_is_full() -> bool:
|
|
# Check if any NON-HIDDEN slot is empty
|
|
for i in range(player.playerboard.size()):
|
|
if not (i in HIDDEN_SLOTS) and player.playerboard[i] == -1:
|
|
return false
|
|
return true
|
|
|
|
# Slot selection management
|
|
func select_playerboard_slot(slot_index: int):
|
|
selected_playerboard_slot = slot_index
|
|
_update_playerboard_slot_visual(slot_index)
|
|
_highlight_adjacent_playerboard_slots()
|
|
|
|
func deselect_playerboard_slot():
|
|
var old_selected = selected_playerboard_slot
|
|
selected_playerboard_slot = -1
|
|
if old_selected != -1:
|
|
_update_playerboard_slot_visual(old_selected)
|
|
untarget_playerboard_slot()
|
|
_highlight_adjacent_playerboard_slots()
|
|
|
|
func target_playerboard_slot(slot_index: int):
|
|
if targeted_playerboard_slot != -1:
|
|
untarget_playerboard_slot()
|
|
targeted_playerboard_slot = slot_index
|
|
_update_playerboard_slot_visual(slot_index)
|
|
|
|
func untarget_playerboard_slot():
|
|
if targeted_playerboard_slot != -1:
|
|
var old_targeted = targeted_playerboard_slot
|
|
targeted_playerboard_slot = -1
|
|
_update_playerboard_slot_visual(old_targeted)
|
|
|
|
func can_move_to_target_playerboard_slot() -> bool:
|
|
if selected_playerboard_slot == -1 or targeted_playerboard_slot == -1 or selected_playerboard_slot == targeted_playerboard_slot:
|
|
return false
|
|
|
|
var adjacent_slots = get_adjacent_playerboard_slots(selected_playerboard_slot)
|
|
return adjacent_slots.has(targeted_playerboard_slot)
|
|
|
|
func _update_playerboard_slot_visual(slot_index: int):
|
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
|
if not main or not main.playerboard_ui or (player.is_bot or player.is_in_group("Bots")):
|
|
return
|
|
|
|
var slot = main.playerboard_ui.get_child(slot_index)
|
|
if slot:
|
|
if slot.get_child_count() > 0:
|
|
slot.get_child(0).visible = slot_index == selected_playerboard_slot
|
|
if slot.get_child_count() > 1:
|
|
slot.get_child(1).visible = slot_index == targeted_playerboard_slot
|
|
if slot.get_child_count() > 2:
|
|
slot.get_child(2).visible = selected_playerboard_slot != -1 and get_adjacent_playerboard_slots(selected_playerboard_slot).has(slot_index)
|
|
|
|
func _highlight_adjacent_playerboard_slots():
|
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
|
if not main or not main.playerboard_ui or (player.is_bot or player.is_in_group("Bots")):
|
|
return
|
|
|
|
for i in range(25):
|
|
var slot = main.playerboard_ui.get_child(i)
|
|
if slot.get_child_count() > 2:
|
|
slot.get_child(2).hide()
|
|
|
|
if selected_playerboard_slot != -1:
|
|
var adjacent_slots = get_adjacent_playerboard_slots(selected_playerboard_slot)
|
|
for adj_slot in adjacent_slots:
|
|
var slot = main.playerboard_ui.get_child(adj_slot)
|
|
if slot.get_child_count() > 2:
|
|
slot.get_child(2).show()
|
|
|
|
# =============================================================================
|
|
# Goal Completion Check
|
|
# =============================================================================
|
|
|
|
func _check_goal_completion():
|
|
"""Check if playerboard matches goals and trigger completion rewards."""
|
|
if not player.race_manager:
|
|
return
|
|
|
|
# Check if the pattern matches
|
|
if player.race_manager.check_pattern_match():
|
|
print("[PlayerboardManager] Goal completed for player %s!" % player.name)
|
|
|
|
# Level up boost difficulty on goal completion
|
|
var powerup_manager = player.get_node_or_null("PowerUpManager")
|
|
if powerup_manager:
|
|
powerup_manager.add_goal_completion_reward()
|
|
|
|
# Trigger screen shake for goal completion
|
|
if player.is_multiplayer_authority() and player.has_method("trigger_screen_shake"):
|
|
player.trigger_screen_shake("goal")
|
|
|
|
# Notify GoalsCycleManager for scoring
|
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
|
if main:
|
|
var goals_cycle_manager = main.get_node_or_null("GoalsCycleManager")
|
|
if goals_cycle_manager:
|
|
goals_cycle_manager.on_goal_completed(player, goals_cycle_manager.get_time_remaining())
|
|
else:
|
|
# Fallback if manager not initialized yet
|
|
NotificationManager.send_message(player, NotificationManager.MESSAGES.GOAL_COMPLETED, NotificationManager.MessageType.GOAL)
|
|
|
|
func clear_and_convert_to_score() -> int:
|
|
"""Clear playerboard and return score for matching tiles."""
|
|
var matching_score = 0
|
|
var goals = player.goals
|
|
var playerboard = player.playerboard
|
|
|
|
# Check center 3x3 of playerboard against goals
|
|
for i in range(3):
|
|
for j in range(3):
|
|
var goal_idx = i * 3 + j
|
|
var board_idx = (i + 1) * 5 + (j + 1) # Center 3x3 in 5x5 board
|
|
|
|
if goal_idx < goals.size() and board_idx < playerboard.size():
|
|
if goals[goal_idx] != -1 and playerboard[board_idx] == goals[goal_idx]:
|
|
matching_score += 10 # 10 points per matching tile
|
|
|
|
# Clear playerboard
|
|
player.playerboard.fill(-1)
|
|
|
|
return matching_score
|