523 lines
20 KiB
C#
523 lines
20 KiB
C#
using System.Collections.Generic;
|
||
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>
|
||
/// Saveable data for ItemSlot state
|
||
/// </summary>
|
||
[Serializable]
|
||
public class ItemSlotSaveData
|
||
{
|
||
public ItemSlotState slotState;
|
||
public string slottedItemSaveId;
|
||
public string slottedItemDataId; // ItemId of the PickupItemData (for verification)
|
||
}
|
||
|
||
/// <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;
|
||
|
||
// 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;
|
||
|
||
/// <summary>
|
||
/// Read-only access to the current slotted item state.
|
||
/// </summary>
|
||
public ItemSlotState CurrentSlottedState => currentState;
|
||
|
||
public UnityEvent onItemSlotted;
|
||
public UnityEvent onItemSlotRemoved;
|
||
// Native C# event alternative for code-only subscribers
|
||
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;
|
||
// Native C# event alternative for code-only subscribers
|
||
public event Action<PickupItemData, PickupItemData> OnForbiddenItemSlotted;
|
||
|
||
public GameObject GetSlottedObject()
|
||
{
|
||
return currentlySlottedItemObject;
|
||
}
|
||
|
||
public void SetSlottedObject(GameObject obj)
|
||
{
|
||
currentlySlottedItemObject = obj;
|
||
if (currentlySlottedItemObject != null)
|
||
{
|
||
currentlySlottedItemObject.SetActive(false);
|
||
}
|
||
}
|
||
|
||
protected override void Awake()
|
||
{
|
||
base.Awake(); // 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;
|
||
|
||
// 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<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.
|
||
/// </summary>
|
||
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<Pickup>();
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Helper: Check if the currently slotted item is correct.
|
||
/// </summary>
|
||
private bool IsSlottedItemCorrect()
|
||
{
|
||
return currentState == ItemSlotState.Correct;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Helper: Clear the slot and fire removal events.
|
||
/// </summary>
|
||
private void ClearSlot()
|
||
{
|
||
var previousData = currentlySlottedItemData;
|
||
|
||
// Clear the pickup's OwningSlot reference
|
||
if (currentlySlottedItemObject != null)
|
||
{
|
||
var pickup = currentlySlottedItemObject.GetComponent<Pickup>();
|
||
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
|
||
|
||
/// <summary>
|
||
/// Updates the sprite and scale for the currently slotted item.
|
||
/// </summary>
|
||
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<Pickup>();
|
||
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<ItemSlotSaveData>(serializedData);
|
||
if (data == null)
|
||
{
|
||
Debug.LogWarning($"[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))
|
||
{
|
||
Debug.Log($"[ItemSlot] Restoring slotted item: {data.slottedItemSaveId} (itemId: {data.slottedItemDataId})");
|
||
RestoreSlottedItem(data.slottedItemSaveId, data.slottedItemDataId);
|
||
}
|
||
}
|
||
|
||
/// <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)
|
||
{
|
||
Debug.LogWarning($"[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)
|
||
{
|
||
Debug.LogWarning($"[ItemSlot] ItemId mismatch! Pickup has '{slottedData.itemId}' but expected '{expectedItemDataId}'");
|
||
}
|
||
}
|
||
|
||
if (slottedData == null)
|
||
{
|
||
Debug.LogWarning($"[ItemSlot] Pickup {pickup.gameObject.name} has null itemData! Expected itemId: {expectedItemDataId}");
|
||
return;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning($"[ItemSlot] Slotted object has no Pickup component: {slottedObject.name}");
|
||
return;
|
||
}
|
||
|
||
// Silently slot the item (no events, no interaction completion)
|
||
// Follower state is managed separately during save/load restoration
|
||
ApplySlottedItemState(slottedObject, slottedData, triggerEvents: false);
|
||
|
||
Debug.Log($"[ItemSlot] Successfully restored slotted item: {slottedData.itemName} (itemId: {slottedData.itemId})");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
/// <param name="itemToSlot">The item GameObject to slot (or null to clear)</param>
|
||
/// <param name="itemToSlotData">The PickupItemData for the item</param>
|
||
/// <param name="triggerEvents">Whether to fire events</param>
|
||
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<Pickup>();
|
||
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<Pickup>();
|
||
if (pickup != null)
|
||
{
|
||
pickup.IsPickedUp = true;
|
||
pickup.OwningSlot = this;
|
||
}
|
||
|
||
// Determine if correct
|
||
var config = interactionSettings?.GetSlotItemConfig(itemData);
|
||
var allowed = config?.allowedItems ?? new List<PickupItemData>();
|
||
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Public API for slotting items during gameplay.
|
||
/// Caller is responsible for managing follower's held item state.
|
||
/// </summary>
|
||
public void SlotItem(GameObject itemToSlot, PickupItemData itemToSlotData)
|
||
{
|
||
ApplySlottedItemState(itemToSlot, itemToSlotData, triggerEvents: true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public bool TryClaimSlottedItem(Pickup pickup)
|
||
{
|
||
if (pickup == null)
|
||
return false;
|
||
|
||
// If slot already has an item, reject the claim
|
||
if (currentlySlottedItemObject != null)
|
||
{
|
||
Debug.LogWarning($"[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);
|
||
|
||
Debug.Log($"[ItemSlot] Successfully claimed slotted item: {pickup.itemData?.itemName}");
|
||
return true;
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
}
|