using System.Collections.Generic; 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 } /// /// Saveable data for ItemSlot state /// [Serializable] public class ItemSlotSaveData { public ItemSlotState slotState; public string slottedItemSaveId; public string slottedItemDataId; // ItemId of the PickupItemData (for verification) } /// /// 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; // Slotted item tracking private PickupItemData currentlySlottedItemData; public SpriteRenderer slottedItemRenderer; private GameObject currentlySlottedItemObject; // Tracks the current state of the slotted item private ItemSlotState currentState = ItemSlotState.None; // Settings reference private IInteractionSettings interactionSettings; private IPlayerFollowerSettings playerFollowerSettings; /// /// Read-only access to the current slotted item state. /// public ItemSlotState CurrentSlottedState => currentState; public UnityEvent onItemSlotted; public UnityEvent onItemSlotRemoved; // Native C# event alternative for code-only subscribers 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; public GameObject GetSlottedObject() { return currentlySlottedItemObject; } public void SetSlottedObject(GameObject obj) { currentlySlottedItemObject = obj; if (currentlySlottedItemObject != null) { currentlySlottedItemObject.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; // Scenario: Nothing held + Empty slot = Error if (heldItem == null && currentlySlottedItemObject == null) return (false, "This requires an item."); // Check forbidden items if trying to slot into empty slot if (heldItem != null && currentlySlottedItemObject == 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. /// protected override bool DoInteraction() { Logging.Debug("[ItemSlot] DoInteraction"); var heldItemData = FollowerController.CurrentlyHeldItemData; var heldItemObj = FollowerController.GetHeldPickupObject(); // Scenario 1: Held item + Empty slot = Slot it if (heldItemData != null && currentlySlottedItemObject == null) { SlotItem(heldItemObj, heldItemData); FollowerController.ClearHeldItem(); // Clear follower's hand after slotting return IsSlottedItemCorrect(); } // Scenario 2 & 3: Slot is full if (currentlySlottedItemObject != null) { // Try combination if both items present if (heldItemData != null) { var slottedPickup = currentlySlottedItemObject.GetComponent(); if (slottedPickup != null) { var comboResult = FollowerController.TryCombineItems(slottedPickup, out var combinationResultItem); if (comboResult == FollowerController.CombinationResult.Successful) { // Combination succeeded - clear slot and return false (not a "slot success") ClearSlot(); return false; } } } // 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) { SlotItem(heldItemObj, heldItemData); // Don't clear follower - they're holding the item they picked up from the slot return IsSlottedItemCorrect(); } // 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; // Clear the pickup's OwningSlot reference if (currentlySlottedItemObject != null) { var pickup = currentlySlottedItemObject.GetComponent(); if (pickup != null) { pickup.OwningSlot = null; } } currentlySlottedItemObject = null; currentlySlottedItemData = null; currentState = ItemSlotState.None; UpdateSlottedSprite(); // Fire removal events onItemSlotRemoved?.Invoke(); OnItemSlotRemoved?.Invoke(previousData); } #endregion #region Visual Updates /// /// Updates the sprite and scale for the currently slotted item. /// private void UpdateSlottedSprite() { if (slottedItemRenderer != null && currentlySlottedItemData != null && currentlySlottedItemData.mapSprite != null) { 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) { float uniformScale = desiredHeight / spriteHeight; float scale = uniformScale / Mathf.Max(parentScale.x, parentScale.y); slottedItemRenderer.transform.localScale = new Vector3(scale, scale, 1f); } } else if (slottedItemRenderer != null) { slottedItemRenderer.sprite = null; } } #endregion // Register with ItemManager when enabled private void OnEnable() { // Register as ItemSlot ItemManager.Instance?.RegisterItemSlot(this); } protected override void OnDestroy() { base.OnDestroy(); // Unregister from slot manager ItemManager.Instance?.UnregisterItemSlot(this); } #region Save/Load Implementation protected override object GetSerializableState() { // Get slotted item save ID if there's a slotted item string slottedSaveId = ""; string slottedDataId = ""; if (currentlySlottedItemObject != null) { var slottedPickup = currentlySlottedItemObject.GetComponent(); if (slottedPickup is SaveableInteractable saveablePickup) { slottedSaveId = saveablePickup.SaveId; } } // Also save the itemData ID for verification if (currentlySlottedItemData != null) { slottedDataId = currentlySlottedItemData.itemId; } return new ItemSlotSaveData { slotState = currentState, slottedItemSaveId = slottedSaveId, slottedItemDataId = slottedDataId }; } 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; // Restore slotted item if there was one if (!string.IsNullOrEmpty(data.slottedItemSaveId)) { Logging.Debug($"[ItemSlot] Restoring slotted item: {data.slottedItemSaveId} (itemId: {data.slottedItemDataId})"); RestoreSlottedItem(data.slottedItemSaveId, data.slottedItemDataId); } } /// /// 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; } // Silently slot the item (no events, no interaction completion) // Follower state is managed separately during save/load restoration ApplySlottedItemState(slottedObject, slottedData, triggerEvents: false); 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) { // 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); } } } UpdateSlottedSprite(); } /// /// Public API for slotting items during gameplay. /// Caller is responsible for managing follower's held item state. /// public void SlotItem(GameObject itemToSlot, PickupItemData itemToSlotData) { ApplySlottedItemState(itemToSlot, itemToSlotData, triggerEvents: true); } /// /// 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. /// public bool TryClaimSlottedItem(Pickup pickup) { if (pickup == null) return false; // If slot already has an item, reject the claim if (currentlySlottedItemObject != null) { Logging.Warning($"[ItemSlot] Already has a slotted item, 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 // Claim the pickup ApplySlottedItemState(pickup.gameObject, pickup.itemData, triggerEvents: false); Logging.Debug($"[ItemSlot] Successfully claimed slotted item: {pickup.itemData?.itemName}"); return true; } #endregion } }