using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using System; // for Action using Core; // register with ItemManager using Core.Settings; // Added for IInteractionSettings namespace Interactions { // New enum describing possible states for the slotted item public enum ItemSlotState { None, Correct, Incorrect, Forbidden } /// /// Display behavior for slot renderers /// [Serializable] public enum SlotRendererDisplayType { UseConfig = 0, // Assign sprite from slotted item's config (default) RevealExisting = 1 // Just make the renderer visible (sprite already on renderer) } /// /// 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; [Tooltip("Display behavior: UseConfig assigns sprite from item config, RevealExisting just makes renderer visible")] public SlotRendererDisplayType displayType = SlotRendererDisplayType.UseConfig; } /// /// 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 items in a slot. /// ItemSlot is a CONTAINER, not a Pickup itself. /// Items cannot be removed or swapped once slotted (except via combinations). /// 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 - now only stores correct items (dense arrays) private List slottedItemsData = new List(); public SlotRendererMapping[] slottedItemRenderers; // Array of renderers with optional item assignments private List slottedItemObjects = new List(); // 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; /// /// Read-only access to the current slotted item state. /// public ItemSlotState CurrentSlottedState => currentState; /// /// Number of items currently slotted (all are correct in new system) /// public int CurrentSlottedCount => slottedItemObjects.Count; /// /// Number of CORRECT items currently slotted (all items are correct now) /// public int CurrentCorrectCount => slottedItemObjects.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; // Event dispatchers (only 3 events needed) public UnityEvent onItemSlotted; public event Action OnItemSlotted; // (slotData, slottedItemData) public UnityEvent onCorrectItemSlotted; public event Action OnCorrectItemSlotted; // (slotData, slottedItemData) public UnityEvent onWrongItemAttempted; public event Action OnWrongItemAttempted; // (slotData, attemptedItem) /// /// 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(); // Hide all RevealExisting renderers at start if (slottedItemRenderers != null) { foreach (var mapping in slottedItemRenderers) { if (mapping != null && mapping.renderer != null && mapping.displayType == SlotRendererDisplayType.RevealExisting) { mapping.renderer.enabled = false; } } } } #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. /// Now allows all items to proceed - wrong items will trigger reactions without slotting. /// No unslotting or swapping allowed. /// 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."); // Scenario: Nothing held + Has items = Can't remove (no unslotting) if (heldItem == null && CurrentSlottedCount > 0) return (false, "I can't remove these items."); // Scenario: Holding item + Slot full = Error (no swapping) if (heldItem != null && !HasSpace) return (false, "This slot is full."); // All other cases proceed (holding item + has space) return (true, null); } /// /// Main interaction logic: Slot correct items or trigger reactions for wrong items. /// No swapping, unslotting, or combinations allowed - items stay once slotted. /// 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(); // Only scenario: Held item + Has space = Check if allowed, then slot or react if (heldItemData != null && HasSpace) { var config = interactionSettings?.GetSlotItemConfig(itemData); var allowed = config?.allowedItems ?? new List(); // Check if item is allowed if (PickupItemData.ListContainsEquivalent(allowed, heldItemData)) { // CORRECT ITEM - Slot it 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 } else { // WRONG ITEM - Trigger reaction, don't slot, item stays in hand TriggerWrongItemReaction(heldItemData); return false; } } // No other scenarios return false; } /// /// Triggers a "wrong item" reaction without slotting. /// Integrates with DialogueComponent for NPC feedback. /// Item stays in player's hand. /// private void TriggerWrongItemReaction(PickupItemData wrongItem) { DebugUIMessage.Show($"{wrongItem.itemName} doesn't belong here.", Color.yellow); // Fire event for DialogueComponent integration onWrongItemAttempted?.Invoke(); OnWrongItemAttempted?.Invoke(itemData, wrongItem); // Item stays in player's hand - no state change } #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). /// Simplified for dense arrays. /// private int GetTargetSlotIndexForItem(PickupItemData itemData) { if (slottedItemRenderers == null || slottedItemRenderers.Length == 0) return -1; // Append behavior // 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; // 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; } } // Item is not assigned to a specific slot - return next available index // Dense arrays: just return current count if there's space return slottedItemObjects.Count < slottedItemRenderers.Length ? slottedItemObjects.Count : -1; // No space, use append (will trigger swap logic) } /// /// Updates the sprite and scale for all slotted items. /// Handles both UseConfig (assign sprite) and RevealExisting (just enable renderer) display modes. /// 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]; // Handle display mode if (mapping.displayType == SlotRendererDisplayType.RevealExisting) { // RevealExisting: Just make the renderer visible slotRenderer.enabled = true; } else // UseConfig (default) { // UseConfig: Assign sprite from item config if (slottedData.mapSprite != null) { slotRenderer.sprite = slottedData.mapSprite; // Scale sprite to desired height, preserve aspect ratio, compensate for parent scale var configs = GameManager.GetSettingsObject(); float desiredHeight = configs?.FollowerMovement?.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 { // No item at this index - clear/hide renderer if (mapping.displayType == SlotRendererDisplayType.RevealExisting) { // RevealExisting: Hide the renderer slotRenderer.enabled = false; } else // UseConfig (default) { // UseConfig: Clear sprite 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 // All saved items are correct (wrong items never get slotted) slottedItemObjects.Add(slottedObject); slottedItemsData.Add(slottedData); // 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})"); } /// /// Public API for slotting items during gameplay. /// Adds item to the slot (multi-slot support with positional awareness). /// Only called for correct items - wrong items trigger reactions instead. /// 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; } // NOTE: Only called for CORRECT items now (validation done in DoInteraction) // Determine target slot index int targetIndex = GetTargetSlotIndexForItem(itemToSlotData); if (targetIndex >= 0 && targetIndex < slottedItemObjects.Count) { // Insert at specific position (shifts items if needed) slottedItemObjects.Insert(targetIndex, itemToSlot); slottedItemsData.Insert(targetIndex, itemToSlotData); } else { // Append to end (backward compatible or when all slots full) slottedItemObjects.Add(itemToSlot); slottedItemsData.Add(itemToSlotData); } // 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 success events (all items are correct now) DebugUIMessage.Show($"Slotted {itemToSlotData.itemName}", Color.green); onItemSlotted?.Invoke(); OnItemSlotted?.Invoke(itemData, itemToSlotData); // Check if slot is complete if (IsComplete) { currentState = ItemSlotState.Correct; DebugUIMessage.Show($"Completed: {itemData.itemName}", Color.green); onCorrectItemSlotted?.Invoke(); OnCorrectItemSlotted?.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 (all items are correct in new system) slottedItemObjects.Add(pickup.gameObject); slottedItemsData.Add(pickup.itemData); pickup.gameObject.SetActive(false); pickup.IsPickedUp = true; pickup.OwningSlot = this; Logging.Debug($"[ItemSlot] Successfully claimed slotted item: {pickup.itemData?.itemName}"); return true; } #endregion } }