diff --git a/Assets/Prefabs/Environment/LureSpotA.prefab b/Assets/Prefabs/Environment/LureSpotA.prefab index bee98d4c..a873370a 100644 --- a/Assets/Prefabs/Environment/LureSpotA.prefab +++ b/Assets/Prefabs/Environment/LureSpotA.prefab @@ -262,7 +262,7 @@ MonoBehaviour: m_Calls: [] itemData: {fileID: 11400000, guid: aaf36cd26cf74334e9c7db6c1b03b3fb, type: 2} iconRenderer: {fileID: 6258593095132504700} - slottedItemRenderer: {fileID: 4110666412151536905} + slottedItemRenderers: [] onItemSlotted: m_PersistentCalls: m_Calls: [] diff --git a/Assets/Prefabs/Environment/LuringSpotB.prefab b/Assets/Prefabs/Environment/LuringSpotB.prefab index 3218e932..11c5bdf6 100644 --- a/Assets/Prefabs/Environment/LuringSpotB.prefab +++ b/Assets/Prefabs/Environment/LuringSpotB.prefab @@ -1170,7 +1170,7 @@ MonoBehaviour: m_Calls: [] itemData: {fileID: 11400000, guid: f97b9e24d6dceb145b56426c1152ebeb, type: 2} iconRenderer: {fileID: 2343214996212089369} - slottedItemRenderer: {fileID: 7990414055343410434} + slottedItemRenderers: [] onItemSlotted: m_PersistentCalls: m_Calls: [] diff --git a/Assets/Prefabs/Environment/LuringSpotC.prefab b/Assets/Prefabs/Environment/LuringSpotC.prefab index 40f56333..efcfa71c 100644 --- a/Assets/Prefabs/Environment/LuringSpotC.prefab +++ b/Assets/Prefabs/Environment/LuringSpotC.prefab @@ -348,7 +348,7 @@ MonoBehaviour: m_Calls: [] itemData: {fileID: 11400000, guid: c68dea945fecbf44094359769db04f31, type: 2} iconRenderer: {fileID: 2825253017896168654} - slottedItemRenderer: {fileID: 3806274462998212361} + slottedItemRenderers: [] onItemSlotted: m_PersistentCalls: m_Calls: [] diff --git a/Assets/Prefabs/Environment/SoundBird.prefab b/Assets/Prefabs/Environment/SoundBird.prefab index d998a67e..8d87a7c6 100644 --- a/Assets/Prefabs/Environment/SoundBird.prefab +++ b/Assets/Prefabs/Environment/SoundBird.prefab @@ -203,7 +203,7 @@ MonoBehaviour: m_Calls: [] itemData: {fileID: 11400000, guid: d28f5774afad9d14f823601707150700, type: 2} iconRenderer: {fileID: 8875860401447896107} - slottedItemRenderer: {fileID: 6941190210788968874} + slottedItemRenderers: [] onItemSlotted: m_PersistentCalls: m_Calls: [] diff --git a/Assets/Scenes/Levels/Quarry.unity b/Assets/Scenes/Levels/Quarry.unity index 8003ffba..e56cbf76 100644 --- a/Assets/Scenes/Levels/Quarry.unity +++ b/Assets/Scenes/Levels/Quarry.unity @@ -465965,7 +465965,8 @@ MonoBehaviour: m_Calls: [] itemData: {fileID: 11400000, guid: d28f5774afad9d14f823601707150700, type: 2} iconRenderer: {fileID: 1399567344} - slottedItemRenderer: {fileID: 1707349194} + slottedItemRenderers: + - {fileID: 1707349194} onItemSlotted: m_PersistentCalls: m_Calls: [] @@ -471861,6 +471862,14 @@ PrefabInstance: propertyPath: bushAnimator value: objectReference: {fileID: 1476225951} + - target: {fileID: 3093816592344978065, guid: 3346526f3046f424196615241a307104, type: 3} + propertyPath: slottedItemRenderers.Array.size + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 3093816592344978065, guid: 3346526f3046f424196615241a307104, type: 3} + propertyPath: 'slottedItemRenderers.Array.data[0]' + value: + objectReference: {fileID: 3708074769586677214} - target: {fileID: 3093816592344978065, guid: 3346526f3046f424196615241a307104, type: 3} propertyPath: onCorrectItemSlotted.m_PersistentCalls.m_Calls.Array.data[1].m_Target value: @@ -471939,6 +471948,11 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 95e46aacea5b42888ee7881894193c11, type: 3} m_Name: m_EditorClassIdentifier: AppleHillsScripts::Core.SaveLoad.AppleState +--- !u!212 &3708074769586677214 stripped +SpriteRenderer: + m_CorrespondingSourceObject: {fileID: 7990414055343410434, guid: 3346526f3046f424196615241a307104, type: 3} + m_PrefabInstance: {fileID: 3708074769586677211} + m_PrefabAsset: {fileID: 0} --- !u!1001 &3917799031583628180 PrefabInstance: m_ObjectHideFlags: 0 @@ -472016,6 +472030,14 @@ PrefabInstance: serializedVersion: 3 m_TransformParent: {fileID: 1007550749} m_Modifications: + - target: {fileID: 106497079666291966, guid: df01157608cce6447b7ccde0bfa290e1, type: 3} + propertyPath: slottedItemRenderers.Array.size + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 106497079666291966, guid: df01157608cce6447b7ccde0bfa290e1, type: 3} + propertyPath: 'slottedItemRenderers.Array.data[0]' + value: + objectReference: {fileID: 3978117984697153446} - target: {fileID: 106497079666291966, guid: df01157608cce6447b7ccde0bfa290e1, type: 3} propertyPath: onCorrectItemSlotted.m_PersistentCalls.m_Calls.Array.data[1].m_Target value: @@ -472081,6 +472103,11 @@ PrefabInstance: m_AddedGameObjects: [] m_AddedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: df01157608cce6447b7ccde0bfa290e1, type: 3} +--- !u!212 &3978117984697153446 stripped +SpriteRenderer: + m_CorrespondingSourceObject: {fileID: 3806274462998212361, guid: df01157608cce6447b7ccde0bfa290e1, type: 3} + m_PrefabInstance: {fileID: 3978117984697153445} + m_PrefabAsset: {fileID: 0} --- !u!1001 &4596770314561390347 PrefabInstance: m_ObjectHideFlags: 0 @@ -472839,6 +472866,22 @@ PrefabInstance: propertyPath: playerToPlaceDistance value: 30 objectReference: {fileID: 0} + - target: {fileID: 4110666412151536905, guid: 3144c6bbac26fbd49a1608152821cc5f, type: 3} + propertyPath: m_Size.x + value: 5.75 + objectReference: {fileID: 0} + - target: {fileID: 4110666412151536905, guid: 3144c6bbac26fbd49a1608152821cc5f, type: 3} + propertyPath: m_Size.y + value: 2.78 + objectReference: {fileID: 0} + - target: {fileID: 4110666412151536905, guid: 3144c6bbac26fbd49a1608152821cc5f, type: 3} + propertyPath: m_Sprite + value: + objectReference: {fileID: 0} + - target: {fileID: 4110666412151536905, guid: 3144c6bbac26fbd49a1608152821cc5f, type: 3} + propertyPath: m_WasSpriteAssigned + value: 0 + objectReference: {fileID: 0} - target: {fileID: 5375394469162727687, guid: 3144c6bbac26fbd49a1608152821cc5f, type: 3} propertyPath: m_LocalPosition.x value: 5.28 @@ -472911,6 +472954,18 @@ PrefabInstance: propertyPath: m_Name value: LureSpotA_Slot objectReference: {fileID: 0} + - target: {fileID: 8578055200319571631, guid: 3144c6bbac26fbd49a1608152821cc5f, type: 3} + propertyPath: iconRenderer + value: + objectReference: {fileID: 8013274907828598646} + - target: {fileID: 8578055200319571631, guid: 3144c6bbac26fbd49a1608152821cc5f, type: 3} + propertyPath: slottedItemRenderers.Array.size + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 8578055200319571631, guid: 3144c6bbac26fbd49a1608152821cc5f, type: 3} + propertyPath: 'slottedItemRenderers.Array.data[0]' + value: + objectReference: {fileID: 8013274907828598645} - target: {fileID: 8578055200319571631, guid: 3144c6bbac26fbd49a1608152821cc5f, type: 3} propertyPath: onCorrectItemSlotted.m_PersistentCalls.m_Calls.Array.size value: 2 @@ -472965,6 +473020,16 @@ Transform: m_CorrespondingSourceObject: {fileID: 2045549771447434109, guid: 3144c6bbac26fbd49a1608152821cc5f, type: 3} m_PrefabInstance: {fileID: 8013274907828598643} m_PrefabAsset: {fileID: 0} +--- !u!212 &8013274907828598645 stripped +SpriteRenderer: + m_CorrespondingSourceObject: {fileID: 4110666412151536905, guid: 3144c6bbac26fbd49a1608152821cc5f, type: 3} + m_PrefabInstance: {fileID: 8013274907828598643} + m_PrefabAsset: {fileID: 0} +--- !u!212 &8013274907828598646 stripped +SpriteRenderer: + m_CorrespondingSourceObject: {fileID: 6258593095132504700, guid: 3144c6bbac26fbd49a1608152821cc5f, type: 3} + m_PrefabInstance: {fileID: 8013274907828598643} + m_PrefabAsset: {fileID: 0} --- !u!1001 &8058740013708592448 PrefabInstance: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/Core/Settings/ItemConfigTypes.cs b/Assets/Scripts/Core/Settings/ItemConfigTypes.cs index a97c4269..73b9ed58 100644 --- a/Assets/Scripts/Core/Settings/ItemConfigTypes.cs +++ b/Assets/Scripts/Core/Settings/ItemConfigTypes.cs @@ -23,5 +23,8 @@ namespace AppleHills.Core.Settings public PickupItemData slotItem; // The slot object (SO reference) public List allowedItems; public List 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) } } diff --git a/Assets/Scripts/Interactions/ItemSlot.cs b/Assets/Scripts/Interactions/ItemSlot.cs index 659a601a..25c1c74c 100644 --- a/Assets/Scripts/Interactions/ItemSlot.cs +++ b/Assets/Scripts/Interactions/ItemSlot.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; // for Count() on List using UnityEngine; using UnityEngine.Events; using System; // for Action @@ -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 slottedItemSaveIds = new List(); // Changed to list for multi-slot support + public List slottedItemDataIds = new List(); // Changed to list for multi-slot support + public bool isLocked; // Track if slot is completed and locked } /// @@ -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 slottedItemsData = new List(); + public SpriteRenderer[] slottedItemRenderers; // Array of renderers for multiple items + private List slottedItemObjects = new List(); + private List slottedItemCorrectness = new List(); // 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. /// public ItemSlotState CurrentSlottedState => currentState; + + /// + /// Number of items currently slotted (correct or incorrect) + /// + public int CurrentSlottedCount => slottedItemObjects.Count; + + /// + /// Number of CORRECT items currently slotted + /// + public int CurrentCorrectCount => slottedItemCorrectness.Count(correct => correct); + + /// + /// Number of items required to complete this slot + /// + 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 + } + } + + /// + /// Whether this slot has all required CORRECT items + /// + public bool IsComplete => CurrentCorrectCount >= RequiredItemCount && RequiredItemCount > 0; + + /// + /// Whether this slot has space for more items + /// + public bool HasSpace => slottedItemRenderers != null && CurrentSlottedCount < slottedItemRenderers.Length; + + /// + /// Whether this is a multi-slot (more than one renderer) + /// + 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 OnItemSlotted; // (slotData, slottedItemData) public event Action OnItemSlotRemoved; public UnityEvent onCorrectItemSlotted; @@ -69,17 +118,30 @@ namespace Interactions public UnityEvent onForbiddenItemSlotted; + /// + /// Get the first (or only) slotted object - for backward compatibility + /// public GameObject GetSlottedObject() { - return currentlySlottedItemObject; + return slottedItemObjects.Count > 0 ? slottedItemObjects[0] : null; } + /// + /// Set a slotted object - for backward compatibility, replaces first item or adds + /// 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(); @@ -153,7 +235,7 @@ namespace Interactions /// /// 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. /// 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(); + var slottedPickup = slottedItemObjects[0].GetComponent(); 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; } - /// - /// Helper: Check if the currently slotted item is correct. - /// - private bool IsSlottedItemCorrect() - { - return currentState == ItemSlotState.Correct; - } + /// /// Helper: Clear the slot and fire removal events. /// 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(); - if (pickup != null) + if (itemObj != null) { - pickup.OwningSlot = null; + var pickup = itemObj.GetComponent(); + 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); } + /// + /// Helper: Remove a specific item from the slot by index. + /// + 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(); + 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 /// - /// Updates the sprite and scale for the currently slotted item. + /// Updates the sprite and scale for all slotted items. /// 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(); - 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(); + 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(); } /// @@ -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})"); - } - - /// - /// 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. - /// - /// The item GameObject to slot (or null to clear) - /// The PickupItemData for the item - /// Whether to fire events - 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(); + 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(); - 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(); - if (pickup != null) - { - pickup.IsPickedUp = true; - pickup.OwningSlot = this; - } - - // Determine if correct - var config = interactionSettings?.GetSlotItemConfig(itemData); - var allowed = config?.allowedItems ?? new List(); - - 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})"); } /// /// 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. /// 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(); + 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(); + 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); + } } /// /// 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. /// 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(); + 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; }