Working mulit-slot items

This commit is contained in:
Michal Pikulski
2025-11-21 15:50:06 +01:00
parent e9320c6d03
commit 3f847508be
7 changed files with 464 additions and 186 deletions

View File

@@ -23,5 +23,8 @@ namespace AppleHills.Core.Settings
public PickupItemData slotItem; // The slot object (SO reference)
public List<PickupItemData> allowedItems;
public List<PickupItemData> forbiddenItems; // Items that cannot be placed in this slot
[Tooltip("Number of items required to complete this slot. If 0, requires ALL allowed items.")]
public int requiredItemCount; // 0 = require all allowed items (backward compatible)
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq; // for Count() on List<bool>
using UnityEngine;
using UnityEngine.Events;
using System; // for Action<T>
@@ -23,8 +24,9 @@ namespace Interactions
public class ItemSlotSaveData
{
public ItemSlotState slotState;
public string slottedItemSaveId;
public string slottedItemDataId; // ItemId of the PickupItemData (for verification)
public List<string> slottedItemSaveIds = new List<string>(); // Changed to list for multi-slot support
public List<string> slottedItemDataIds = new List<string>(); // Changed to list for multi-slot support
public bool isLocked; // Track if slot is completed and locked
}
/// <summary>
@@ -37,13 +39,17 @@ namespace Interactions
public PickupItemData itemData;
public SpriteRenderer iconRenderer;
// Slotted item tracking
private PickupItemData currentlySlottedItemData;
public SpriteRenderer slottedItemRenderer;
private GameObject currentlySlottedItemObject;
// Multi-slot item tracking
private List<PickupItemData> slottedItemsData = new List<PickupItemData>();
public SpriteRenderer[] slottedItemRenderers; // Array of renderers for multiple items
private List<GameObject> slottedItemObjects = new List<GameObject>();
private List<bool> slottedItemCorrectness = new List<bool>(); // Track which items are correct
// Tracks the current state of the slotted item
// Tracks the current state of the slotted item(s)
private ItemSlotState currentState = ItemSlotState.None;
// Lock flag to prevent removal after successful completion
private bool isLockedAfterCompletion;
// Settings reference
private IInteractionSettings interactionSettings;
@@ -53,10 +59,53 @@ namespace Interactions
/// Read-only access to the current slotted item state.
/// </summary>
public ItemSlotState CurrentSlottedState => currentState;
/// <summary>
/// Number of items currently slotted (correct or incorrect)
/// </summary>
public int CurrentSlottedCount => slottedItemObjects.Count;
/// <summary>
/// Number of CORRECT items currently slotted
/// </summary>
public int CurrentCorrectCount => slottedItemCorrectness.Count(correct => correct);
/// <summary>
/// Number of items required to complete this slot
/// </summary>
public int RequiredItemCount
{
get
{
var config = interactionSettings?.GetSlotItemConfig(itemData);
if (config != null)
{
// If requiredItemCount is set (> 0), use it; otherwise require all allowed items
return config.requiredItemCount > 0 ? config.requiredItemCount : (config.allowedItems?.Count ?? 0);
}
return 1; // Default to 1 for backward compatibility
}
}
/// <summary>
/// Whether this slot has all required CORRECT items
/// </summary>
public bool IsComplete => CurrentCorrectCount >= RequiredItemCount && RequiredItemCount > 0;
/// <summary>
/// Whether this slot has space for more items
/// </summary>
public bool HasSpace => slottedItemRenderers != null && CurrentSlottedCount < slottedItemRenderers.Length;
/// <summary>
/// Whether this is a multi-slot (more than one renderer)
/// </summary>
private bool IsMultiSlot => slottedItemRenderers != null && slottedItemRenderers.Length > 1;
public UnityEvent onItemSlotted;
public UnityEvent onItemSlotRemoved;
// Native C# event alternative for code-only subscribers
// Native C# event alternatives for code-only subscribers
public event Action<PickupItemData, PickupItemData> OnItemSlotted; // (slotData, slottedItemData)
public event Action<PickupItemData> OnItemSlotRemoved;
public UnityEvent onCorrectItemSlotted;
@@ -69,17 +118,30 @@ namespace Interactions
public UnityEvent onForbiddenItemSlotted;
/// <summary>
/// Get the first (or only) slotted object - for backward compatibility
/// </summary>
public GameObject GetSlottedObject()
{
return currentlySlottedItemObject;
return slottedItemObjects.Count > 0 ? slottedItemObjects[0] : null;
}
/// <summary>
/// Set a slotted object - for backward compatibility, replaces first item or adds
/// </summary>
public void SetSlottedObject(GameObject obj)
{
currentlySlottedItemObject = obj;
if (currentlySlottedItemObject != null)
if (obj != null)
{
currentlySlottedItemObject.SetActive(false);
if (slottedItemObjects.Count == 0)
{
slottedItemObjects.Add(obj);
}
else
{
slottedItemObjects[0] = obj;
}
obj.SetActive(false);
}
}
@@ -134,12 +196,32 @@ namespace Interactions
{
var heldItem = FollowerController?.CurrentlyHeldItemData;
// Check if slot is locked after completion
if (isLockedAfterCompletion)
{
if (heldItem != null)
return (false, "This is already complete.");
else
return (false, "I can't remove these items.");
}
// Scenario: Nothing held + Empty slot = Error
if (heldItem == null && currentlySlottedItemObject == null)
if (heldItem == null && CurrentSlottedCount == 0)
return (false, "This requires an item.");
// Check forbidden items if trying to slot into empty slot
if (heldItem != null && currentlySlottedItemObject == null)
// If holding an item and slot is full but not complete, allow swap
if (heldItem != null && !HasSpace)
{
// Allow swap for fixing mistakes (single-slot or multi-slot not complete)
if (!IsMultiSlot || !IsComplete)
return (true, null); // Allow swap
// Multi-slot is complete - can't swap
return (false, "This slot is full.");
}
// Check forbidden items if trying to slot
if (heldItem != null)
{
var config = interactionSettings?.GetSlotItemConfig(itemData);
var forbidden = config?.forbiddenItems ?? new List<PickupItemData>();
@@ -153,7 +235,7 @@ namespace Interactions
/// <summary>
/// Main interaction logic: Slot, pickup, swap, or combine items.
/// Returns true only if correct item was slotted.
/// Returns true only if correct item was slotted AND slot is now complete.
/// </summary>
protected override bool DoInteraction()
{
@@ -162,24 +244,33 @@ namespace Interactions
var heldItemData = FollowerController.CurrentlyHeldItemData;
var heldItemObj = FollowerController.GetHeldPickupObject();
// Scenario 1: Held item + Empty slot = Slot it
if (heldItemData != null && currentlySlottedItemObject == null)
// Scenario 1: Held item + Has space = Slot it
if (heldItemData != null && HasSpace)
{
SlotItem(heldItemObj, heldItemData);
FollowerController.ClearHeldItem(); // Clear follower's hand after slotting
return IsSlottedItemCorrect();
// Check if we completed the slot
if (IsComplete)
{
isLockedAfterCompletion = true;
currentState = ItemSlotState.Correct;
return true; // Completed!
}
return false; // Slotted but not complete yet
}
// Scenario 2 & 3: Slot is full
if (currentlySlottedItemObject != null)
if (CurrentSlottedCount > 0)
{
// Try combination if both items present
if (heldItemData != null)
// Try combination if both items present (only for single slots)
if (heldItemData != null && !IsMultiSlot)
{
var slottedPickup = currentlySlottedItemObject.GetComponent<Pickup>();
var slottedPickup = slottedItemObjects[0].GetComponent<Pickup>();
if (slottedPickup != null)
{
var comboResult = FollowerController.TryCombineItems(slottedPickup, out var combinationResultItem);
var comboResult = FollowerController.TryCombineItems(slottedPickup, out _);
if (comboResult == FollowerController.CombinationResult.Successful)
{
@@ -190,55 +281,88 @@ namespace Interactions
}
}
// No combination or unsuccessful - perform swap
// Step 1: Pickup from slot (follower now holds the old slotted item)
FollowerController.TryPickupItem(currentlySlottedItemObject, currentlySlottedItemData, dropItem: false);
ClearSlot();
// Step 2: If we had a held item, slot it (follower already holding picked up item, don't clear!)
if (heldItemData != null)
// Swap behavior when slot is full (single slots OR multi-slots that aren't complete)
if (heldItemData != null && !HasSpace)
{
SlotItem(heldItemObj, heldItemData);
// Don't clear follower - they're holding the item they picked up from the slot
return IsSlottedItemCorrect();
// For single slots: always allow swap
// For multi-slots: only allow swap if not complete yet (allows fixing mistakes)
if (!IsMultiSlot || !IsComplete)
{
// LIFO swap - swap with the last item
int lastIndex = CurrentSlottedCount - 1;
var itemToReturn = slottedItemObjects[lastIndex];
var itemDataToReturn = slottedItemsData[lastIndex];
// Step 1: Give old item to follower
FollowerController.TryPickupItem(itemToReturn, itemDataToReturn, dropItem: false);
// Step 2: Remove old item from slot
RemoveItemAtIndex(lastIndex);
// Step 3: Slot the new item
SlotItem(heldItemObj, heldItemData);
// Check if we completed the slot with this swap
if (IsComplete)
{
isLockedAfterCompletion = true;
currentState = ItemSlotState.Correct;
return true; // Completed!
}
return false; // Swapped but not complete
}
}
// Just picked up from slot - not a success
return false;
// Pickup from slot (empty hands) - LIFO removal
if (heldItemData == null)
{
int lastIndex = CurrentSlottedCount - 1;
var itemToPickup = slottedItemObjects[lastIndex];
var itemDataToPickup = slottedItemsData[lastIndex];
// Try to give item to follower
FollowerController.TryPickupItem(itemToPickup, itemDataToPickup, dropItem: false);
// Remove from slot
RemoveItemAtIndex(lastIndex);
// Just picked up from slot - not a success
return false;
}
}
// Shouldn't reach here (validation prevents empty + no held)
return false;
}
/// <summary>
/// Helper: Check if the currently slotted item is correct.
/// </summary>
private bool IsSlottedItemCorrect()
{
return currentState == ItemSlotState.Correct;
}
/// <summary>
/// Helper: Clear the slot and fire removal events.
/// </summary>
private void ClearSlot()
{
var previousData = currentlySlottedItemData;
var previousData = slottedItemsData.Count > 0 ? slottedItemsData[0] : null;
// Clear the pickup's OwningSlot reference
if (currentlySlottedItemObject != null)
// Clear all pickup's OwningSlot references
foreach (var itemObj in slottedItemObjects)
{
var pickup = currentlySlottedItemObject.GetComponent<Pickup>();
if (pickup != null)
if (itemObj != null)
{
pickup.OwningSlot = null;
var pickup = itemObj.GetComponent<Pickup>();
if (pickup != null)
{
pickup.OwningSlot = null;
}
}
}
currentlySlottedItemObject = null;
currentlySlottedItemData = null;
slottedItemObjects.Clear();
slottedItemsData.Clear();
slottedItemCorrectness.Clear(); // Also clear correctness tracking
currentState = ItemSlotState.None;
isLockedAfterCompletion = false;
UpdateSlottedSprite();
// Fire removal events
@@ -246,35 +370,92 @@ namespace Interactions
OnItemSlotRemoved?.Invoke(previousData);
}
/// <summary>
/// Helper: Remove a specific item from the slot by index.
/// </summary>
private void RemoveItemAtIndex(int index)
{
if (index < 0 || index >= CurrentSlottedCount)
return;
var itemObj = slottedItemObjects[index];
var removedItemData = slottedItemsData[index];
// Clear the pickup's OwningSlot reference
if (itemObj != null)
{
var pickup = itemObj.GetComponent<Pickup>();
if (pickup != null)
{
pickup.OwningSlot = null;
}
}
slottedItemObjects.RemoveAt(index);
slottedItemsData.RemoveAt(index);
slottedItemCorrectness.RemoveAt(index); // Also remove correctness tracking
if (CurrentSlottedCount == 0)
{
currentState = ItemSlotState.None;
isLockedAfterCompletion = false;
}
UpdateSlottedSprite();
// Fire removal events
onItemSlotRemoved?.Invoke();
OnItemSlotRemoved?.Invoke(removedItemData);
}
#endregion
#region Visual Updates
/// <summary>
/// Updates the sprite and scale for the currently slotted item.
/// Updates the sprite and scale for all slotted items.
/// </summary>
private void UpdateSlottedSprite()
{
if (slottedItemRenderer != null && currentlySlottedItemData != null && currentlySlottedItemData.mapSprite != null)
if (slottedItemRenderers == null || slottedItemRenderers.Length == 0)
return;
// Update each renderer based on slotted items
for (int i = 0; i < slottedItemRenderers.Length; i++)
{
slottedItemRenderer.sprite = currentlySlottedItemData.mapSprite;
// Scale sprite to desired height, preserve aspect ratio, compensate for parent scale
float desiredHeight = playerFollowerSettings?.HeldIconDisplayHeight ?? 2.0f;
var sprite = currentlySlottedItemData.mapSprite;
float spriteHeight = sprite.bounds.size.y;
Vector3 parentScale = slottedItemRenderer.transform.parent != null
? slottedItemRenderer.transform.parent.localScale
: Vector3.one;
if (spriteHeight > 0f)
var slotRenderer = slottedItemRenderers[i];
if (slotRenderer == null)
continue;
// If we have an item at this index, show it
if (i < slottedItemsData.Count && slottedItemsData[i] != null)
{
float uniformScale = desiredHeight / spriteHeight;
float scale = uniformScale / Mathf.Max(parentScale.x, parentScale.y);
slottedItemRenderer.transform.localScale = new Vector3(scale, scale, 1f);
var slottedData = slottedItemsData[i];
if (slottedData.mapSprite != null)
{
slotRenderer.sprite = slottedData.mapSprite;
// Scale sprite to desired height, preserve aspect ratio, compensate for parent scale
float desiredHeight = playerFollowerSettings?.HeldIconDisplayHeight ?? 2.0f;
var sprite = slottedData.mapSprite;
float spriteHeight = sprite.bounds.size.y;
Vector3 parentScale = slotRenderer.transform.parent != null
? slotRenderer.transform.parent.localScale
: Vector3.one;
if (spriteHeight > 0f)
{
float uniformScale = desiredHeight / spriteHeight;
float scale = uniformScale / Mathf.Max(parentScale.x, parentScale.y);
slotRenderer.transform.localScale = new Vector3(scale, scale, 1f);
}
}
}
else
{
// Clear renderer if no item at this index
slotRenderer.sprite = null;
}
}
else if (slottedItemRenderer != null)
{
slottedItemRenderer.sprite = null;
}
}
@@ -297,31 +478,47 @@ namespace Interactions
protected override object GetSerializableState()
{
// Get slotted item save ID if there's a slotted item
string slottedSaveId = "";
string slottedDataId = "";
if (currentlySlottedItemObject != null)
var saveData = new ItemSlotSaveData
{
var slottedPickup = currentlySlottedItemObject.GetComponent<Pickup>();
if (slottedPickup is SaveableInteractable saveablePickup)
slotState = currentState,
isLocked = isLockedAfterCompletion
};
// Save all slotted items
foreach (var itemObj in slottedItemObjects)
{
if (itemObj != null)
{
slottedSaveId = saveablePickup.SaveId;
var slottedPickup = itemObj.GetComponent<Pickup>();
if (slottedPickup is SaveableInteractable saveablePickup)
{
saveData.slottedItemSaveIds.Add(saveablePickup.SaveId);
}
else
{
saveData.slottedItemSaveIds.Add("");
}
}
else
{
saveData.slottedItemSaveIds.Add("");
}
}
// Also save the itemData ID for verification
if (currentlySlottedItemData != null)
// Save all item data IDs for verification
foreach (var slottedData in slottedItemsData)
{
slottedDataId = currentlySlottedItemData.itemId;
if (slottedData != null)
{
saveData.slottedItemDataIds.Add(slottedData.itemId);
}
else
{
saveData.slottedItemDataIds.Add("");
}
}
return new ItemSlotSaveData
{
slotState = currentState,
slottedItemSaveId = slottedSaveId,
slottedItemDataId = slottedDataId
};
return saveData;
}
protected override void ApplySerializableState(string serializedData)
@@ -335,13 +532,26 @@ namespace Interactions
// Restore slot state
currentState = data.slotState;
isLockedAfterCompletion = data.isLocked;
// Restore slotted item if there was one
if (!string.IsNullOrEmpty(data.slottedItemSaveId))
// Restore all slotted items if there were any
if (data.slottedItemSaveIds != null && data.slottedItemSaveIds.Count > 0)
{
Logging.Debug($"[ItemSlot] Restoring slotted item: {data.slottedItemSaveId} (itemId: {data.slottedItemDataId})");
RestoreSlottedItem(data.slottedItemSaveId, data.slottedItemDataId);
for (int i = 0; i < data.slottedItemSaveIds.Count; i++)
{
string saveId = data.slottedItemSaveIds[i];
string dataId = i < data.slottedItemDataIds.Count ? data.slottedItemDataIds[i] : "";
if (!string.IsNullOrEmpty(saveId))
{
Logging.Debug($"[ItemSlot] Restoring slotted item {i}: {saveId} (itemId: {dataId})");
RestoreSlottedItem(saveId, dataId);
}
}
}
// Update all renderers after restoration
UpdateSlottedSprite();
}
/// <summary>
@@ -411,118 +621,107 @@ namespace Interactions
return;
}
// Silently slot the item (no events, no interaction completion)
// Add to slotted items list (no events, no interaction completion)
// Follower state is managed separately during save/load restoration
ApplySlottedItemState(slottedObject, slottedData, triggerEvents: false);
slottedItemObjects.Add(slottedObject);
slottedItemsData.Add(slottedData);
Logging.Debug($"[ItemSlot] Successfully restored slotted item: {slottedData.itemName} (itemId: {slottedData.itemId})");
}
/// <summary>
/// Core logic for slotting an item. Can be used both for normal slotting and silent restoration.
/// NOTE: Does NOT call CompleteInteraction - the template method handles that via DoInteraction return value.
/// NOTE: Does NOT manage follower state - caller is responsible for clearing follower's hand if needed.
/// </summary>
/// <param name="itemToSlot">The item GameObject to slot (or null to clear)</param>
/// <param name="itemToSlotData">The PickupItemData for the item</param>
/// <param name="triggerEvents">Whether to fire events</param>
private void ApplySlottedItemState(GameObject itemToSlot, PickupItemData itemToSlotData, bool triggerEvents)
{
if (itemToSlot == null)
// Determine if this item is correct for correctness tracking
var config = interactionSettings?.GetSlotItemConfig(itemData);
var allowed = config?.allowedItems ?? new List<PickupItemData>();
bool isCorrectItem = PickupItemData.ListContainsEquivalent(allowed, slottedData);
slottedItemCorrectness.Add(isCorrectItem);
// Deactivate the item and set pickup state
slottedObject.SetActive(false);
if (pickup != null)
{
// Clear slot - also clear the pickup's OwningSlot reference
if (currentlySlottedItemObject != null)
{
var oldPickup = currentlySlottedItemObject.GetComponent<Pickup>();
if (oldPickup != null)
{
oldPickup.OwningSlot = null;
}
}
var previousData = currentlySlottedItemData;
currentlySlottedItemObject = null;
currentlySlottedItemData = null;
currentState = ItemSlotState.None;
// Fire native event for slot clearing (only if triggering events)
if (previousData != null && triggerEvents)
{
onItemSlotRemoved?.Invoke();
OnItemSlotRemoved?.Invoke(previousData);
}
}
else
{
// Slot the item
itemToSlot.SetActive(false);
itemToSlot.transform.SetParent(null);
SetSlottedObject(itemToSlot);
currentlySlottedItemData = itemToSlotData;
// Mark the pickup as picked up and track slot ownership for save/load
var pickup = itemToSlot.GetComponent<Pickup>();
if (pickup != null)
{
pickup.IsPickedUp = true;
pickup.OwningSlot = this;
}
// Determine if correct
var config = interactionSettings?.GetSlotItemConfig(itemData);
var allowed = config?.allowedItems ?? new List<PickupItemData>();
if (itemToSlotData != null && PickupItemData.ListContainsEquivalent(allowed, itemToSlotData))
{
currentState = ItemSlotState.Correct;
// Fire events if requested
if (triggerEvents)
{
DebugUIMessage.Show($"You correctly slotted {itemToSlotData.itemName} into: {itemData.itemName}", Color.green);
onCorrectItemSlotted?.Invoke();
OnCorrectItemSlotted?.Invoke(itemData, currentlySlottedItemData);
}
}
else
{
currentState = ItemSlotState.Incorrect;
// Fire events if requested
if (triggerEvents)
{
DebugUIMessage.Show("I'm not sure this works.", Color.yellow);
onIncorrectItemSlotted?.Invoke();
OnIncorrectItemSlotted?.Invoke(itemData, currentlySlottedItemData);
}
}
pickup.IsPickedUp = true;
pickup.OwningSlot = this;
}
UpdateSlottedSprite();
Logging.Debug($"[ItemSlot] Successfully restored slotted item: {slottedData.itemName} (itemId: {slottedData.itemId}, correct: {isCorrectItem})");
}
/// <summary>
/// Public API for slotting items during gameplay.
/// Adds item to the slot (multi-slot support).
/// Caller is responsible for managing follower's held item state.
/// </summary>
public void SlotItem(GameObject itemToSlot, PickupItemData itemToSlotData)
{
ApplySlottedItemState(itemToSlot, itemToSlotData, triggerEvents: true);
if (itemToSlot == null || itemToSlotData == null)
{
Logging.Warning($"[ItemSlot] Attempted to slot null item or data");
return;
}
// Determine if this item is correct (allowed)
var config = interactionSettings?.GetSlotItemConfig(itemData);
var allowed = config?.allowedItems ?? new List<PickupItemData>();
bool isCorrectItem = PickupItemData.ListContainsEquivalent(allowed, itemToSlotData);
// Add to lists
slottedItemObjects.Add(itemToSlot);
slottedItemsData.Add(itemToSlotData);
slottedItemCorrectness.Add(isCorrectItem); // Track correctness
// Deactivate item and set pickup state
itemToSlot.SetActive(false);
itemToSlot.transform.SetParent(null);
var pickup = itemToSlot.GetComponent<Pickup>();
if (pickup != null)
{
pickup.IsPickedUp = true;
pickup.OwningSlot = this;
}
// Update visuals
UpdateSlottedSprite();
// Fire events based on correctness
if (isCorrectItem)
{
DebugUIMessage.Show($"You slotted {itemToSlotData.itemName} into: {itemData.itemName}", Color.green);
// Fire generic slot event
onItemSlotted?.Invoke();
OnItemSlotted?.Invoke(itemData, itemToSlotData);
// Only fire correct completion event if ALL required CORRECT items are now slotted
if (IsComplete)
{
currentState = ItemSlotState.Correct;
DebugUIMessage.Show($"Completed: {itemData.itemName}", Color.green);
onCorrectItemSlotted?.Invoke();
OnCorrectItemSlotted?.Invoke(itemData, itemToSlotData);
}
}
else
{
// Incorrect item slotted
DebugUIMessage.Show($"Slotted {itemToSlotData.itemName}, but it might not be right...", Color.yellow);
onItemSlotted?.Invoke(); // Still fire generic event
OnItemSlotted?.Invoke(itemData, itemToSlotData);
onIncorrectItemSlotted?.Invoke();
OnIncorrectItemSlotted?.Invoke(itemData, itemToSlotData);
}
}
/// <summary>
/// Bilateral restoration entry point: Pickup calls this to offer itself to the Slot.
/// Returns true if claim was successful, false if slot already has an item or wrong pickup.
/// Returns true if claim was successful, false if slot is full or wrong pickup.
/// </summary>
public bool TryClaimSlottedItem(Pickup pickup)
{
if (pickup == null)
return false;
// If slot already has an item, reject the claim
if (currentlySlottedItemObject != null)
// If slot is full, reject the claim
if (!HasSpace)
{
Logging.Warning($"[ItemSlot] Already has a slotted item, rejecting claim from {pickup.gameObject.name}");
Logging.Warning($"[ItemSlot] Slot is full, rejecting claim from {pickup.gameObject.name}");
return false;
}
@@ -530,10 +729,21 @@ namespace Interactions
// Note: We don't have easy access to the expected SaveId here, so we just accept it
// The Pickup's bilateral restoration ensures it only claims the correct slot
// Claim the pickup
ApplySlottedItemState(pickup.gameObject, pickup.itemData, triggerEvents: false);
// Add the item to lists
slottedItemObjects.Add(pickup.gameObject);
slottedItemsData.Add(pickup.itemData);
Logging.Debug($"[ItemSlot] Successfully claimed slotted item: {pickup.itemData?.itemName}");
// Determine correctness for tracking
var config = interactionSettings?.GetSlotItemConfig(itemData);
var allowed = config?.allowedItems ?? new List<PickupItemData>();
bool isCorrectItem = PickupItemData.ListContainsEquivalent(allowed, pickup.itemData);
slottedItemCorrectness.Add(isCorrectItem);
pickup.gameObject.SetActive(false);
pickup.IsPickedUp = true;
pickup.OwningSlot = this;
Logging.Debug($"[ItemSlot] Successfully claimed slotted item: {pickup.itemData?.itemName} (correct: {isCorrectItem})");
return true;
}