Files
tekton/scripts/managers/playerboard_manager.gd
T

837 lines
29 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")
# === 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 for powerup?
# ...
# 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
player.rpc("sync_grid_item", current_cell.x, current_cell.y, current_cell.z, -1)
player.rpc("sync_playerboard", 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
player.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1)
player.rpc("sync_playerboard", 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):
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()
main.ui_manager.current_action_state = main.ui_manager.ActionState.NONE
# === 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
# =============================================================================
func arrange_playerboard_item(slot_index: int):
if player.action_points < 2 or player.playerboard[slot_index] == -1:
return
#var selected_item = player.playerboard[slot_index]
var adjacent_slots = get_adjacent_playerboard_slots(slot_index)
var main = player.get_tree().get_root().get_node_or_null("Main")
if not main or not main.ui_manager.playerboard_ui:
return
# Store the selected slot
selected_playerboard_slot = slot_index
# Highlight selected slot
var selected_slot_ui = main.ui_manager.playerboard_ui.get_child(slot_index)
if selected_slot_ui.get_child_count() > 1:
selected_slot_ui.get_child(1).show()
# Highlight valid adjacent slots
for adj_slot in adjacent_slots:
if player.playerboard[adj_slot] == -1: # Only highlight empty adjacent slots
if not (player.is_bot or player.is_in_group("Bots")):
var adj_slot_ui = main.ui_manager.playerboard_ui.get_child(adj_slot)
if adj_slot_ui.get_child_count() > 2:
adj_slot_ui.get_child(2).show()
player.action_manager.highlighted_cells.append(adj_slot)
# Connect to slot click signals
for i in range(player.playerboard.size()):
var slot = main.ui_manager.playerboard_ui.get_child(i)
if not slot.gui_input.is_connected(player._on_slot_clicked):
slot.gui_input.connect(player._on_slot_clicked.bind(i))
func handle_slot_clicked(slot_index: int):
var main = player.get_tree().get_root().get_node_or_null("Main")
if not main or main.ui_manager.current_action_state != main.ui_manager.ActionState.ARRANGING:
return
if selected_playerboard_slot == -1 or slot_index == selected_playerboard_slot:
return
var adjacent_slots = get_adjacent_playerboard_slots(selected_playerboard_slot)
if slot_index in adjacent_slots and player.playerboard[slot_index] == -1 and not (slot_index in HIDDEN_SLOTS):
# Move item to empty target slot
var selected_item = player.playerboard[selected_playerboard_slot]
player.playerboard[slot_index] = selected_item
player.playerboard[selected_playerboard_slot] = -1
if player.is_multiplayer_authority():
player.rpc("sync_playerboard", player.playerboard)
player.consume_action_points(2)
player.has_performed_action = true
# Clear highlights
player.clear_highlights()
player.clear_playerboard_highlights()
# Reset selection
selected_playerboard_slot = -1
# Update the visual representation
if not (player.is_bot or player.is_in_group("Bots")):
main.ui_manager.update_playerboard_ui()
main.ui_manager.current_action_state = main.ui_manager.ActionState.NONE
# =============================================================================
# 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