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 # === Branching Logic: Host vs Client === if multiplayer.is_server(): # HOST/SERVER: Call the logic directly _execute_grab(grid_position, cell, item) else: # CLIENT: Send RPC request to server (peer 1) player.rpc_id(1, "request_server_grab", grid_position, cell.x, cell.y, cell.z, item) return true # Request was sent or processed 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(): 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 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 var put_slot = -1 # Count how many times each goal item appears in central 3x3 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 # Now scan playerboard for i in range(player.playerboard.size()): var current_item = player.playerboard[i] if current_item == -1: continue # Case 1: Item is not in goals at all → definitely junk if not current_item in player.goals: put_slot = i break # Case 2: Item is in goals, but we already have enough in correct spots var current_count = 0 for r in range(1, 4): # central rows 1-3 (5x5 board) for c in range(1, 4): # central cols 1-3 var idx = r * 5 + c if player.playerboard[idx] == current_item: current_count += 1 # If we already have all needed copies in central area, this is extra if current_count >= goal_counts.get(current_item, 0): put_slot = i break # If no junk found, fall back to any non-goal-matching tile outside center 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 shouldn't be there if row < 1 or row > 3 or col < 1 or col > 3: if not board_item in player.goals or player.playerboard[i] != player.goals[(row - 1) * 3 + (col - 1)]: put_slot = i break if put_slot == -1: return false # Nothing suitable to put # Step 3: Perform the put 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(): player.rpc("sync_grid_item", cell.x, cell.y, cell.z, item) player.playerboard[put_slot] = -1 player.rpc("sync_playerboard", player.playerboard) player.has_performed_action = true player.consume_action_points(1) var main = player.get_tree().get_root().get_node_or_null("Main") if main: main.set_action_state(main.ActionState.NONE) 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()