918 lines
36 KiB
C#
918 lines
36 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq; // for Count() on List<bool>
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
using System; // for Action<T>
|
|
using Core; // register with ItemManager
|
|
using AppleHills.Core.Settings; // Added for IInteractionSettings
|
|
|
|
namespace Interactions
|
|
{
|
|
// New enum describing possible states for the slotted item
|
|
public enum ItemSlotState
|
|
{
|
|
None,
|
|
Correct,
|
|
Incorrect,
|
|
Forbidden
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps a sprite renderer to an optional item assignment
|
|
/// </summary>
|
|
[Serializable]
|
|
public class SlotRendererMapping
|
|
{
|
|
public SpriteRenderer renderer;
|
|
[Tooltip("Optional: If set, this renderer slot is dedicated to this specific item. Leave null for flexible slots.")]
|
|
public PickupItemData assignedItem;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saveable data for ItemSlot state
|
|
/// </summary>
|
|
[Serializable]
|
|
public class ItemSlotSaveData
|
|
{
|
|
public ItemSlotState slotState;
|
|
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>
|
|
/// Interaction that allows slotting, swapping, or picking up items in a slot.
|
|
/// ItemSlot is a CONTAINER, not a Pickup itself.
|
|
/// </summary>
|
|
public class ItemSlot : SaveableInteractable
|
|
{
|
|
// Slot visual data (for the slot itself, not the item in it)
|
|
public PickupItemData itemData;
|
|
public SpriteRenderer iconRenderer;
|
|
|
|
// Multi-slot item tracking
|
|
private List<PickupItemData> slottedItemsData = new List<PickupItemData>();
|
|
public SlotRendererMapping[] slottedItemRenderers; // Array of renderers with optional item assignments
|
|
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(s)
|
|
private ItemSlotState currentState = ItemSlotState.None;
|
|
|
|
// Lock flag to prevent removal after successful completion
|
|
private bool isLockedAfterCompletion;
|
|
|
|
// Settings reference
|
|
private IInteractionSettings interactionSettings;
|
|
private IPlayerFollowerSettings playerFollowerSettings;
|
|
|
|
/// <summary>
|
|
/// 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(obj => obj != null);
|
|
|
|
/// <summary>
|
|
/// Number of CORRECT items currently slotted
|
|
/// </summary>
|
|
public int CurrentCorrectCount
|
|
{
|
|
get
|
|
{
|
|
int count = 0;
|
|
for (int i = 0; i < slottedItemCorrectness.Count; i++)
|
|
{
|
|
if (i < slottedItemObjects.Count && slottedItemObjects[i] != null && slottedItemCorrectness[i])
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
}
|
|
|
|
/// <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 alternatives for code-only subscribers
|
|
public event Action<PickupItemData, PickupItemData> OnItemSlotted; // (slotData, slottedItemData)
|
|
public event Action<PickupItemData> OnItemSlotRemoved;
|
|
|
|
public UnityEvent onCorrectItemSlotted;
|
|
// Native C# event alternative to the UnityEvent for code-only subscribers
|
|
public event Action<PickupItemData, PickupItemData> OnCorrectItemSlotted;
|
|
|
|
public UnityEvent onIncorrectItemSlotted;
|
|
// Native C# event alternative for code-only subscribers
|
|
public event Action<PickupItemData, PickupItemData> OnIncorrectItemSlotted;
|
|
|
|
public UnityEvent onForbiddenItemSlotted;
|
|
|
|
/// <summary>
|
|
/// Get the first (or only) slotted object - for backward compatibility
|
|
/// </summary>
|
|
public GameObject GetSlottedObject()
|
|
{
|
|
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)
|
|
{
|
|
if (obj != null)
|
|
{
|
|
if (slottedItemObjects.Count == 0)
|
|
{
|
|
slottedItemObjects.Add(obj);
|
|
}
|
|
else
|
|
{
|
|
slottedItemObjects[0] = obj;
|
|
}
|
|
obj.SetActive(false);
|
|
}
|
|
}
|
|
|
|
internal override void OnManagedAwake()
|
|
{
|
|
base.OnManagedAwake(); // SaveableInteractable registration
|
|
|
|
// Setup visuals
|
|
if (iconRenderer == null)
|
|
iconRenderer = GetComponent<SpriteRenderer>();
|
|
|
|
ApplyItemData();
|
|
|
|
// Initialize settings references
|
|
interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
|
|
playerFollowerSettings = GameManager.GetSettingsObject<IPlayerFollowerSettings>();
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
/// <summary>
|
|
/// Unity OnValidate callback. Ensures icon and data are up to date in editor.
|
|
/// </summary>
|
|
void OnValidate()
|
|
{
|
|
if (iconRenderer == null)
|
|
iconRenderer = GetComponent<SpriteRenderer>();
|
|
ApplyItemData();
|
|
}
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Applies the item data to the slot (icon, name, etc).
|
|
/// </summary>
|
|
public void ApplyItemData()
|
|
{
|
|
if (itemData != null)
|
|
{
|
|
if (iconRenderer != null && itemData.mapSprite != null)
|
|
{
|
|
iconRenderer.sprite = itemData.mapSprite;
|
|
}
|
|
gameObject.name = itemData.itemName + "_Slot";
|
|
}
|
|
}
|
|
|
|
#region Interaction Logic
|
|
|
|
/// <summary>
|
|
/// Validation: Check if interaction can proceed based on held item and slot state.
|
|
/// </summary>
|
|
protected override (bool canProceed, string errorMessage) CanProceedWithInteraction()
|
|
{
|
|
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 && CurrentSlottedCount == 0)
|
|
return (false, "This requires an item.");
|
|
|
|
// 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>();
|
|
|
|
if (PickupItemData.ListContainsEquivalent(forbidden, heldItem))
|
|
return (false, "Can't place that here.");
|
|
}
|
|
|
|
return (true, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Main interaction logic: Slot, pickup, swap, or combine items.
|
|
/// Returns true only if correct item was slotted AND slot is now complete.
|
|
/// </summary>
|
|
protected override bool DoInteraction()
|
|
{
|
|
Logging.Debug("[ItemSlot] DoInteraction");
|
|
|
|
var heldItemData = FollowerController.CurrentlyHeldItemData;
|
|
var heldItemObj = FollowerController.GetHeldPickupObject();
|
|
|
|
// Scenario 1: Held item + Has space = Slot it
|
|
if (heldItemData != null && HasSpace)
|
|
{
|
|
SlotItem(heldItemObj, heldItemData);
|
|
FollowerController.ClearHeldItem(); // Clear follower's hand after slotting
|
|
|
|
// 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 (CurrentSlottedCount > 0)
|
|
{
|
|
// Try combination if both items present (only for single slots)
|
|
if (heldItemData != null && !IsMultiSlot)
|
|
{
|
|
var slottedPickup = slottedItemObjects[0].GetComponent<Pickup>();
|
|
if (slottedPickup != null)
|
|
{
|
|
var comboResult = FollowerController.TryCombineItems(slottedPickup, out _);
|
|
|
|
if (comboResult == FollowerController.CombinationResult.Successful)
|
|
{
|
|
// Combination succeeded - clear slot and return false (not a "slot success")
|
|
ClearSlot();
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Swap behavior when slot is full (single slots OR multi-slots that aren't complete)
|
|
if (heldItemData != null && !HasSpace)
|
|
{
|
|
// For single slots: always allow swap
|
|
// For multi-slots: only allow swap if not complete yet (allows fixing mistakes)
|
|
if (!IsMultiSlot || !IsComplete)
|
|
{
|
|
// Determine target index for the new item
|
|
int targetIndex = GetTargetSlotIndexForItem(heldItemData);
|
|
|
|
int indexToSwap;
|
|
if (targetIndex >= 0 && targetIndex < slottedItemObjects.Count && slottedItemObjects[targetIndex] != null)
|
|
{
|
|
// Swap with the item in the assigned slot
|
|
indexToSwap = targetIndex;
|
|
}
|
|
else
|
|
{
|
|
// LIFO swap - swap with the last non-null item
|
|
indexToSwap = -1;
|
|
for (int i = slottedItemObjects.Count - 1; i >= 0; i--)
|
|
{
|
|
if (slottedItemObjects[i] != null)
|
|
{
|
|
indexToSwap = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (indexToSwap < 0)
|
|
{
|
|
// No items to swap (shouldn't happen, but fallback)
|
|
SlotItem(heldItemObj, heldItemData);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
var itemToReturn = slottedItemObjects[indexToSwap];
|
|
var itemDataToReturn = slottedItemsData[indexToSwap];
|
|
|
|
// Step 1: Give old item to follower
|
|
FollowerController.TryPickupItem(itemToReturn, itemDataToReturn, dropItem: false);
|
|
|
|
// Step 2: Remove old item from slot
|
|
RemoveItemAtIndex(indexToSwap);
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// Pickup from slot (empty hands) - LIFO removal
|
|
if (heldItemData == null)
|
|
{
|
|
// Find last non-null item
|
|
int lastIndex = -1;
|
|
for (int i = slottedItemObjects.Count - 1; i >= 0; i--)
|
|
{
|
|
if (slottedItemObjects[i] != null)
|
|
{
|
|
lastIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (lastIndex >= 0)
|
|
{
|
|
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: Clear the slot and fire removal events.
|
|
/// </summary>
|
|
private void ClearSlot()
|
|
{
|
|
// Find first non-null data for event
|
|
PickupItemData previousData = null;
|
|
foreach (var data in slottedItemsData)
|
|
{
|
|
if (data != null)
|
|
{
|
|
previousData = data;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Clear all pickup's OwningSlot references
|
|
foreach (var itemObj in slottedItemObjects)
|
|
{
|
|
if (itemObj != null)
|
|
{
|
|
var pickup = itemObj.GetComponent<Pickup>();
|
|
if (pickup != null)
|
|
{
|
|
pickup.OwningSlot = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
slottedItemObjects.Clear();
|
|
slottedItemsData.Clear();
|
|
slottedItemCorrectness.Clear(); // Also clear correctness tracking
|
|
currentState = ItemSlotState.None;
|
|
isLockedAfterCompletion = false;
|
|
UpdateSlottedSprite();
|
|
|
|
// Fire removal events
|
|
onItemSlotRemoved?.Invoke();
|
|
OnItemSlotRemoved?.Invoke(previousData);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper: Remove a specific item from the slot by index.
|
|
/// </summary>
|
|
private void RemoveItemAtIndex(int index)
|
|
{
|
|
if (index < 0 || index >= slottedItemObjects.Count)
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Set to null instead of removing to maintain sparse storage
|
|
slottedItemObjects[index] = null;
|
|
slottedItemsData[index] = null;
|
|
slottedItemCorrectness[index] = false;
|
|
|
|
if (CurrentSlottedCount == 0)
|
|
{
|
|
currentState = ItemSlotState.None;
|
|
isLockedAfterCompletion = false;
|
|
}
|
|
|
|
UpdateSlottedSprite();
|
|
|
|
// Fire removal events
|
|
onItemSlotRemoved?.Invoke();
|
|
OnItemSlotRemoved?.Invoke(removedItemData);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Visual Updates
|
|
|
|
/// <summary>
|
|
/// Determines the best slot index for an item based on assignments and availability.
|
|
/// Returns -1 if no mappings are configured (uses append behavior).
|
|
/// </summary>
|
|
private int GetTargetSlotIndexForItem(PickupItemData itemData)
|
|
{
|
|
if (slottedItemRenderers == null || slottedItemRenderers.Length == 0)
|
|
return -1;
|
|
|
|
// Check if any renderer has assignments configured
|
|
bool hasAnyAssignments = false;
|
|
for (int i = 0; i < slottedItemRenderers.Length; i++)
|
|
{
|
|
if (slottedItemRenderers[i]?.assignedItem != null)
|
|
{
|
|
hasAnyAssignments = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If no assignments configured, use append behavior (backward compatible)
|
|
if (!hasAnyAssignments)
|
|
return -1;
|
|
|
|
// Step 1: Check if this item has a dedicated assigned slot
|
|
for (int i = 0; i < slottedItemRenderers.Length; i++)
|
|
{
|
|
var mapping = slottedItemRenderers[i];
|
|
if (mapping?.assignedItem != null && PickupItemData.AreEquivalent(mapping.assignedItem, itemData))
|
|
{
|
|
// This is the assigned slot for this item
|
|
return i;
|
|
}
|
|
}
|
|
|
|
// Step 2: Item is not assigned to a specific slot - find best available slot
|
|
// Ensure lists are large enough to check
|
|
while (slottedItemObjects.Count < slottedItemRenderers.Length)
|
|
{
|
|
slottedItemObjects.Add(null);
|
|
slottedItemsData.Add(null);
|
|
slottedItemCorrectness.Add(false);
|
|
}
|
|
|
|
// Prefer unassigned empty slots first
|
|
for (int i = 0; i < slottedItemRenderers.Length; i++)
|
|
{
|
|
var mapping = slottedItemRenderers[i];
|
|
if (mapping?.assignedItem == null && slottedItemObjects[i] == null)
|
|
{
|
|
return i; // Empty unassigned slot
|
|
}
|
|
}
|
|
|
|
// Then try assigned but empty slots
|
|
for (int i = 0; i < slottedItemRenderers.Length; i++)
|
|
{
|
|
var mapping = slottedItemRenderers[i];
|
|
if (mapping?.assignedItem != null && slottedItemObjects[i] == null)
|
|
{
|
|
return i; // Empty assigned slot
|
|
}
|
|
}
|
|
|
|
// All slots full - return -1 to trigger swap logic
|
|
return -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the sprite and scale for all slotted items.
|
|
/// </summary>
|
|
private void UpdateSlottedSprite()
|
|
{
|
|
if (slottedItemRenderers == null || slottedItemRenderers.Length == 0)
|
|
return;
|
|
|
|
// Update each renderer based on slotted items
|
|
for (int i = 0; i < slottedItemRenderers.Length; i++)
|
|
{
|
|
var mapping = slottedItemRenderers[i];
|
|
if (mapping == null || mapping.renderer == null)
|
|
continue;
|
|
|
|
var slotRenderer = mapping.renderer;
|
|
|
|
// If we have an item at this index, show it
|
|
if (i < slottedItemsData.Count && slottedItemsData[i] != null)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
// Register with ItemManager when enabled
|
|
private void OnEnable()
|
|
{
|
|
// Register as ItemSlot
|
|
ItemManager.Instance?.RegisterItemSlot(this);
|
|
}
|
|
|
|
internal override void OnManagedDestroy()
|
|
{
|
|
// Unregister from slot manager
|
|
ItemManager.Instance?.UnregisterItemSlot(this);
|
|
}
|
|
|
|
#region Save/Load Implementation
|
|
|
|
protected override object GetSerializableState()
|
|
{
|
|
var saveData = new ItemSlotSaveData
|
|
{
|
|
slotState = currentState,
|
|
isLocked = isLockedAfterCompletion
|
|
};
|
|
|
|
// Save all slotted items
|
|
foreach (var itemObj in slottedItemObjects)
|
|
{
|
|
if (itemObj != null)
|
|
{
|
|
var slottedPickup = itemObj.GetComponent<Pickup>();
|
|
if (slottedPickup is SaveableInteractable saveablePickup)
|
|
{
|
|
saveData.slottedItemSaveIds.Add(saveablePickup.SaveId);
|
|
}
|
|
else
|
|
{
|
|
saveData.slottedItemSaveIds.Add("");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
saveData.slottedItemSaveIds.Add("");
|
|
}
|
|
}
|
|
|
|
// Save all item data IDs for verification
|
|
foreach (var slottedData in slottedItemsData)
|
|
{
|
|
if (slottedData != null)
|
|
{
|
|
saveData.slottedItemDataIds.Add(slottedData.itemId);
|
|
}
|
|
else
|
|
{
|
|
saveData.slottedItemDataIds.Add("");
|
|
}
|
|
}
|
|
|
|
return saveData;
|
|
}
|
|
|
|
protected override void ApplySerializableState(string serializedData)
|
|
{
|
|
ItemSlotSaveData data = JsonUtility.FromJson<ItemSlotSaveData>(serializedData);
|
|
if (data == null)
|
|
{
|
|
Logging.Warning($"[ItemSlot] Failed to deserialize save data for {gameObject.name}");
|
|
return;
|
|
}
|
|
|
|
// Restore slot state
|
|
currentState = data.slotState;
|
|
isLockedAfterCompletion = data.isLocked;
|
|
|
|
// Restore all slotted items if there were any
|
|
if (data.slottedItemSaveIds != null && data.slottedItemSaveIds.Count > 0)
|
|
{
|
|
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>
|
|
/// Restore a slotted item from save data.
|
|
/// This is called during load restoration and should NOT trigger events.
|
|
/// </summary>
|
|
private void RestoreSlottedItem(string slottedItemSaveId, string expectedItemDataId)
|
|
{
|
|
// Try to find the item in the scene by its save ID via ItemManager
|
|
GameObject slottedObject = ItemManager.Instance?.FindPickupBySaveId(slottedItemSaveId);
|
|
|
|
if (slottedObject == null && !string.IsNullOrEmpty(expectedItemDataId))
|
|
{
|
|
// Item not found in scene - it might be a dynamically spawned combined item
|
|
// Try to spawn it from the itemDataId
|
|
Logging.Debug($"[ItemSlot] Slotted item not found in scene: {slottedItemSaveId}, attempting to spawn from itemId: {expectedItemDataId}");
|
|
|
|
GameObject prefab = interactionSettings?.FindPickupPrefabByItemId(expectedItemDataId);
|
|
if (prefab != null)
|
|
{
|
|
// Spawn the item (inactive, since it will be slotted)
|
|
slottedObject = Instantiate(prefab, transform.position, Quaternion.identity);
|
|
slottedObject.SetActive(false);
|
|
Logging.Debug($"[ItemSlot] Successfully spawned combined item for slot: {expectedItemDataId}");
|
|
}
|
|
else
|
|
{
|
|
Logging.Warning($"[ItemSlot] Could not find prefab for itemId: {expectedItemDataId}");
|
|
return;
|
|
}
|
|
}
|
|
else if (slottedObject == null)
|
|
{
|
|
Logging.Warning($"[ItemSlot] Could not find slotted item with save ID: {slottedItemSaveId}");
|
|
return;
|
|
}
|
|
|
|
// Get the item data from the pickup component
|
|
PickupItemData slottedData = null;
|
|
var pickup = slottedObject.GetComponent<Pickup>();
|
|
if (pickup != null)
|
|
{
|
|
slottedData = pickup.itemData;
|
|
|
|
// Verify itemId matches if we have it (safety check)
|
|
if (slottedData != null && !string.IsNullOrEmpty(expectedItemDataId))
|
|
{
|
|
if (slottedData.itemId != expectedItemDataId)
|
|
{
|
|
Logging.Warning($"[ItemSlot] ItemId mismatch! Pickup has '{slottedData.itemId}' but expected '{expectedItemDataId}'");
|
|
}
|
|
}
|
|
|
|
if (slottedData == null)
|
|
{
|
|
Logging.Warning($"[ItemSlot] Pickup {pickup.gameObject.name} has null itemData! Expected itemId: {expectedItemDataId}");
|
|
if (slottedObject != null)
|
|
Destroy(slottedObject);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logging.Warning($"[ItemSlot] Slotted object has no Pickup component: {slottedObject.name}");
|
|
if (slottedObject != null)
|
|
Destroy(slottedObject);
|
|
return;
|
|
}
|
|
|
|
// Add to slotted items list (no events, no interaction completion)
|
|
// Follower state is managed separately during save/load restoration
|
|
slottedItemObjects.Add(slottedObject);
|
|
slottedItemsData.Add(slottedData);
|
|
|
|
// 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)
|
|
{
|
|
pickup.IsPickedUp = true;
|
|
pickup.OwningSlot = this;
|
|
}
|
|
|
|
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 with positional awareness).
|
|
/// Caller is responsible for managing follower's held item state.
|
|
/// </summary>
|
|
public void SlotItem(GameObject itemToSlot, PickupItemData itemToSlotData)
|
|
{
|
|
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);
|
|
|
|
// Determine target slot index
|
|
int targetIndex = GetTargetSlotIndexForItem(itemToSlotData);
|
|
|
|
if (targetIndex >= 0)
|
|
{
|
|
// Positional slotting - ensure lists are large enough
|
|
while (slottedItemObjects.Count <= targetIndex)
|
|
{
|
|
slottedItemObjects.Add(null);
|
|
slottedItemsData.Add(null);
|
|
slottedItemCorrectness.Add(false);
|
|
}
|
|
|
|
// Place at specific index
|
|
slottedItemObjects[targetIndex] = itemToSlot;
|
|
slottedItemsData[targetIndex] = itemToSlotData;
|
|
slottedItemCorrectness[targetIndex] = isCorrectItem;
|
|
}
|
|
else
|
|
{
|
|
// Append behavior (backward compatible or when all slots full)
|
|
slottedItemObjects.Add(itemToSlot);
|
|
slottedItemsData.Add(itemToSlotData);
|
|
slottedItemCorrectness.Add(isCorrectItem);
|
|
}
|
|
|
|
// 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 is full or wrong pickup.
|
|
/// </summary>
|
|
public bool TryClaimSlottedItem(Pickup pickup)
|
|
{
|
|
if (pickup == null)
|
|
return false;
|
|
|
|
// If slot is full, reject the claim
|
|
if (!HasSpace)
|
|
{
|
|
Logging.Warning($"[ItemSlot] Slot is full, rejecting claim from {pickup.gameObject.name}");
|
|
return false;
|
|
}
|
|
|
|
// Verify this pickup's SaveId matches what we expect (from our save data)
|
|
// 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
|
|
|
|
// Add the item to lists
|
|
slottedItemObjects.Add(pickup.gameObject);
|
|
slottedItemsData.Add(pickup.itemData);
|
|
|
|
// 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;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|