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 func initialize(p_player: Node3D, p_gridmap: Node): player = p_player enhanced_gridmap = p_gridmap # ============================================================================= # GRAB Operations # ============================================================================= func grab_item(grid_position: Vector2i) -> bool: if not enhanced_gridmap or player.action_points <= 0: 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: return false if item == -1: return false # === AUTO-ARRANGE LOGIC (Client-side pre-check) === var target_slot = find_best_goal_slot_for_item(item) if target_slot == -1: print("Player: No valid slot found for item.") return false # no space if not player.is_multiplayer_authority(): return false # === 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 # Check if grabbed item is a holo tile (11-14) - add to powerup instead of triggering effect var is_holo = item >= 11 and item <= 14 if is_holo: # Add holo pickup to power-up manager (4 pickups = 1 bar) var powerup_manager = player.get_node_or_null("PowerUpManager") if powerup_manager: powerup_manager.add_holo_pickup() # Convert holo tile to normal tile (11->7, 12->8, 13->9, 14->10) item = item - 4 player.playerboard[target_slot] = item # Add to 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: 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) player.rpc("sync_playerboard", 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) return false # Check action points if player.action_points <= 0: print("Server: Player has no action points.") 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.") return false # 2. Server-side Auto-Arrange var target_slot = find_best_goal_slot_for_item(item_id) if target_slot == -1: print("Server: Player has no valid slot for 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) player.playerboard[target_slot] = item_id # 3c. Broadcast the new playerboard state to all clients player.rpc("sync_playerboard", player.playerboard) # 3d. Consume action points player.has_performed_action = true player.consume_action_points(1) # 3e. Reset the UI for the player who acted player.rpc("force_action_state_none") return true 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: var empty_slot = player.playerboard.find(-1) if empty_slot != -1: if player.is_multiplayer_authority(): # Check if grabbed item is a holo tile (11-14) - add to powerup if item >= 11 and item <= 14: var powerup_manager = player.get_node_or_null("PowerUpManager") if powerup_manager: powerup_manager.add_holo_pickup() item = item - 4 # Convert to normal tile 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: var empty_slot = player.playerboard.find(-1) 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: if not enhanced_gridmap or player.action_points <= 0 or player.is_bot or player.is_in_group("Bots"): 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 # Item is not in goals at all → definitely junk, put this first if not current_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] # If the item doesn't match what should be in this goal position, it's misplaced if board_item != 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(): # === 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: 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 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: # 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 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 # 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] == item: var board_row = i + 1 # offset to center in 5x5 var board_col = j + 1 var slot_index = board_row * 5 + board_col if player.playerboard[slot_index] == -1: # only if empty return slot_index # No ideal slot? Return any empty slot return player.playerboard.find(-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: return player.playerboard.find(-1) == -1 # 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: 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: 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) # Award power-up bar for goal completion var powerup_manager = player.get_node_or_null("PowerUpManager") if powerup_manager: powerup_manager.add_goal_completion_reward() # 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 player.rpc("display_message", "Goal completed!") 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