using System.Collections.Generic; using System.Linq; // for Count() on List using UnityEngine; using UnityEngine.Events; using System; // for Action 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 } /// /// Maps a sprite renderer to an optional item assignment /// [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; } /// /// Saveable data for ItemSlot state /// [Serializable] public class ItemSlotSaveData { public ItemSlotState slotState; 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 } /// /// Interaction that allows slotting, swapping, or picking up items in a slot. /// ItemSlot is a CONTAINER, not a Pickup itself. /// 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 slottedItemsData = new List(); public SlotRendererMapping[] slottedItemRenderers; // Array of renderers with optional item assignments private List slottedItemObjects = new List(); private List slottedItemCorrectness = new List(); // 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; /// /// 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(obj => obj != null); /// /// Number of CORRECT items currently slotted /// 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; } } /// /// 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 alternatives for code-only subscribers public event Action OnItemSlotted; // (slotData, slottedItemData) public event Action OnItemSlotRemoved; public UnityEvent onCorrectItemSlotted; // Native C# event alternative to the UnityEvent for code-only subscribers public event Action OnCorrectItemSlotted; public UnityEvent onIncorrectItemSlotted; // Native C# event alternative for code-only subscribers public event Action OnIncorrectItemSlotted; public UnityEvent onForbiddenItemSlotted; /// /// Get the first (or only) slotted object - for backward compatibility /// public GameObject GetSlottedObject() { return slottedItemObjects.Count > 0 ? slottedItemObjects[0] : null; } /// /// Set a slotted object - for backward compatibility, replaces first item or adds /// 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(); ApplyItemData(); // Initialize settings references interactionSettings = GameManager.GetSettingsObject(); playerFollowerSettings = GameManager.GetSettingsObject(); } #if UNITY_EDITOR /// /// Unity OnValidate callback. Ensures icon and data are up to date in editor. /// void OnValidate() { if (iconRenderer == null) iconRenderer = GetComponent(); ApplyItemData(); } #endif /// /// Applies the item data to the slot (icon, name, etc). /// public void ApplyItemData() { if (itemData != null) { if (iconRenderer != null && itemData.mapSprite != null) { iconRenderer.sprite = itemData.mapSprite; } gameObject.name = itemData.itemName + "_Slot"; } } #region Interaction Logic /// /// Validation: Check if interaction can proceed based on held item and slot state. /// 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(); if (PickupItemData.ListContainsEquivalent(forbidden, heldItem)) return (false, "Can't place that here."); } return (true, null); } /// /// Main interaction logic: Slot, pickup, swap, or combine items. /// Returns true only if correct item was slotted AND slot is now complete. /// 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(); 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; } /// /// Helper: Clear the slot and fire removal events. /// 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(); 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); } /// /// Helper: Remove a specific item from the slot by index. /// 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(); 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 /// /// Determines the best slot index for an item based on assignments and availability. /// Returns -1 if no mappings are configured (uses append behavior). /// 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; } /// /// Updates the sprite and scale for all slotted items. /// 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(); 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(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(); } /// /// Restore a slotted item from save data. /// This is called during load restoration and should NOT trigger events. /// 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(); 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(); 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})"); } /// /// 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. /// 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(); 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(); 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 is full or wrong pickup. /// 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(); 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 } }