Files
AppleHillsProduction/Assets/Scripts/Interactions/ItemSlot.cs
2025-12-16 17:19:01 +01:00

717 lines
28 KiB
C#

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using System; // for Action<T>
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
}
/// <summary>
/// Display behavior for slot renderers
/// </summary>
[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)
}
/// <summary>
/// Maps a sprite renderer to an optional item assignment
/// </summary>
[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;
}
/// <summary>
/// Saveable data for ItemSlot state
/// </summary>
[Serializable]
public class ItemSlotSaveData
{
public ItemSlotState slotState;
public List<string> slottedItemSaveIds = new List<string>(); // Changed to list for multi-slot support
public List<string> slottedItemDataIds = new List<string>(); // Changed to list for multi-slot support
public bool isLocked; // Track if slot is completed and locked
}
/// <summary>
/// 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).
/// </summary>
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<PickupItemData> slottedItemsData = new List<PickupItemData>();
public SlotRendererMapping[] slottedItemRenderers; // Array of renderers with optional item assignments
private List<GameObject> slottedItemObjects = new List<GameObject>();
// 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;
/// <summary>
/// Read-only access to the current slotted item state.
/// </summary>
public ItemSlotState CurrentSlottedState => currentState;
/// <summary>
/// Number of items currently slotted (all are correct in new system)
/// </summary>
public int CurrentSlottedCount => slottedItemObjects.Count;
/// <summary>
/// Number of CORRECT items currently slotted (all items are correct now)
/// </summary>
public int CurrentCorrectCount => slottedItemObjects.Count;
/// <summary>
/// Number of items required to complete this slot
/// </summary>
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
}
}
/// <summary>
/// Whether this slot has all required CORRECT items
/// </summary>
public bool IsComplete => CurrentCorrectCount >= RequiredItemCount && RequiredItemCount > 0;
/// <summary>
/// Whether this slot has space for more items
/// </summary>
public bool HasSpace => slottedItemRenderers != null && CurrentSlottedCount < slottedItemRenderers.Length;
/// <summary>
/// Whether this is a multi-slot (more than one renderer)
/// </summary>
private bool IsMultiSlot => slottedItemRenderers != null && slottedItemRenderers.Length > 1;
// Event dispatchers (only 3 events needed)
public UnityEvent onItemSlotted;
public event Action<PickupItemData, PickupItemData> OnItemSlotted; // (slotData, slottedItemData)
public UnityEvent onCorrectItemSlotted;
public event Action<PickupItemData, PickupItemData> OnCorrectItemSlotted; // (slotData, slottedItemData)
public UnityEvent onWrongItemAttempted;
public event Action<PickupItemData, PickupItemData> OnWrongItemAttempted; // (slotData, attemptedItem)
/// <summary>
/// Get the first (or only) slotted object - for backward compatibility
/// </summary>
public GameObject GetSlottedObject()
{
return slottedItemObjects.Count > 0 ? slottedItemObjects[0] : null;
}
/// <summary>
/// Set a slotted object - for backward compatibility, replaces first item or adds
/// </summary>
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<SpriteRenderer>();
ApplyItemData();
// Initialize settings references
interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
// 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
/// <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.
/// Now allows all items to proceed - wrong items will trigger reactions without slotting.
/// No unslotting or swapping allowed.
/// </summary>
protected override (bool canProceed, string errorMessage) CanProceedWithInteraction()
{
// TODO: Fuck this but it's late, check if the soundbird can sill be interacted with
var soundGenerator = FindFirstObjectByType<SoundGenerator>(FindObjectsInactive.Exclude);
if (soundGenerator)
return (soundGenerator.soundBirdSMRef.currentState.name.ToLower().Contains("soundbird_slot"), "Sound Bird!");
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);
}
/// <summary>
/// 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.
/// </summary>
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<PickupItemData>();
// 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;
}
/// <summary>
/// Triggers a "wrong item" reaction without slotting.
/// Integrates with DialogueComponent for NPC feedback.
/// Item stays in player's hand.
/// </summary>
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
/// <summary>
/// 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.
/// </summary>
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)
}
/// <summary>
/// Updates the sprite and scale for all slotted items.
/// Handles both UseConfig (assign sprite) and RevealExisting (just enable renderer) display modes.
/// </summary>
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<IPlayerMovementConfigs>();
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<Pickup>();
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<ItemSlotSaveData>(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();
}
/// <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 && !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<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)
{
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})");
}
/// <summary>
/// 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.
/// </summary>
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<Pickup>();
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);
}
}
/// <summary>
/// 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.
/// </summary>
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
}
}