/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.Inventory { using Opsive.Shared.Events; #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER using Opsive.Shared.Game; #endif using Opsive.Shared.Inventory; using Opsive.UltimateCharacterController.Events; using Opsive.UltimateCharacterController.Items; using Opsive.UltimateCharacterController.Items.Actions; #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER using Opsive.UltimateCharacterController.Networking; using Opsive.UltimateCharacterController.Networking.Character; #endif using System.Collections.Generic; using UnityEngine; /// /// Provides a common base class for any character Inventory. /// public abstract class InventoryBase : MonoBehaviour { [Tooltip("Should all of the IItemIdentifier be removed when the character dies?")] [SerializeField] protected bool m_RemoveAllOnDeath = true; [Tooltip("Should the default loadout be loaded when the character respawns?")] [SerializeField] protected bool m_LoadDefaultLoadoutOnRespawn = true; [Tooltip("The name of the state when the inventory is unequipped.")] [SerializeField] protected string m_UnequippedStateName = "Unequipped"; [Tooltip("Unity event that is invoked when an item is initially added to the inventory.")] [SerializeField] protected UnityItemEvent m_OnAddItemEvent; [Tooltip("Unity event that is invoked when an IItemIdentifier is picked up.")] [SerializeField] protected UnityItemIdentifierFloatBoolBoolEvent m_OnPickupItemIdentifierEvent; [Tooltip("Unity event that is invoked when an item is picked up.")] [SerializeField] protected UnityItemFloatBoolBoolEvent m_OnPickupItemEvent; [Tooltip("Unity event that is invoked when an item is equipped.")] [SerializeField] protected UnityItemIntEvent m_OnEquipItemEvent; [Tooltip("Unity event that is invoked when an IItemIdentifier is adjusted.")] [SerializeField] protected UnityItemIdentifierFloatEvent m_OnAdjustItemIdentifierAmountEvent; [Tooltip("Unity event that is invoked when an item is unequipped.")] [SerializeField] protected UnityItemIntEvent m_OnUnequipItemEvent; [Tooltip("Unity event that is invoked when an item is removed.")] [SerializeField] protected UnityItemIntEvent m_OnRemoveItemEvent; public bool RemoveAllOnDeath { get { return m_RemoveAllOnDeath; } set { m_RemoveAllOnDeath = value; } } public bool LoadDefaultLoadoutOnRespawn { get { return m_LoadDefaultLoadoutOnRespawn; } set { m_LoadDefaultLoadoutOnRespawn = value; } } public string UnequippedStateName { get { return m_UnequippedStateName; } set { m_UnequippedStateName = value; } } public UnityItemEvent OnAddItemEvent { get { return m_OnAddItemEvent; } set { m_OnAddItemEvent = value; } } public UnityItemIdentifierFloatBoolBoolEvent OnPickupItemIdentifierEvent { get { return m_OnPickupItemIdentifierEvent; } set { m_OnPickupItemIdentifierEvent = value; } } public UnityItemFloatBoolBoolEvent OnPickupItemEvent { get { return m_OnPickupItemEvent; } set { m_OnPickupItemEvent = value; } } public UnityItemIntEvent OnEquipItemEvent { get { return m_OnEquipItemEvent; } set { m_OnEquipItemEvent = value; } } public UnityItemIdentifierFloatEvent OnAdjustItemIdentifierAmountEvent { get { return m_OnAdjustItemIdentifierAmountEvent; } set { m_OnAdjustItemIdentifierAmountEvent = value; } } public UnityItemIntEvent OnUnequipItemEvent { get { return m_OnUnequipItemEvent; } set { m_OnUnequipItemEvent = value; } } public UnityItemIntEvent OnRemoveItemEvent { get { return m_OnRemoveItemEvent; } set { m_OnRemoveItemEvent = value; } } protected GameObject m_GameObject; #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER private INetworkInfo m_NetworkInfo; private INetworkCharacter m_NetworkCharacter; #endif protected int m_SlotCount = 1; private List m_AllItems = new List(); private List m_AllItemIdentifiers = new List(); public int SlotCount { get { #if UNITY_EDITOR if (!Application.isPlaying) { DetermineSlotCount(); } #endif return m_SlotCount; } } /// /// Initialize the default values. /// protected virtual void Awake() { m_GameObject = gameObject; #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER m_NetworkInfo = m_GameObject.GetCachedComponent(); m_NetworkCharacter = m_GameObject.GetCachedComponent(); #endif DetermineSlotCount(); EventHandler.RegisterEvent(m_GameObject, "OnDeath", OnDeath); EventHandler.RegisterEvent(m_GameObject, "OnRespawn", OnRespawn); } /// /// Determines the number of slots on the character. /// public void DetermineSlotCount() { // The number of slots depends on the maximum number of ItemSlot IDs. var itemSlots = GetComponentsInChildren(true); for (int i = 0; i < itemSlots.Length; ++i) { if (m_SlotCount <= itemSlots[i].ID) { m_SlotCount = itemSlots[i].ID + 1; } } } /// /// Loads the default loadout. /// protected virtual void Start() { // The character starts out unequipped. if (!string.IsNullOrEmpty(m_UnequippedStateName)) { StateSystem.StateManager.SetState(m_GameObject, m_UnequippedStateName, true); } #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo == null || m_NetworkInfo.IsLocalPlayer()) { if (m_NetworkInfo != null) { // Load the default loadout on the network first to ensure it is received before any equip events. m_NetworkCharacter.LoadDefaultLoadout(); } #endif LoadDefaultLoadout(); #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER } #endif EventHandler.ExecuteEvent(m_GameObject, "OnCharacterSnapAnimator"); } /// /// Pick up each ItemIdentifier within the DefaultLoadout. /// public abstract void LoadDefaultLoadout(); /// /// Determines if the character has the specified item. /// /// The item to check against. /// True if the character has the item. public bool HasItem(Item item) { return HasItemInternal(item); } /// /// Internal method which determines if the character has the specified item. /// /// The item to check against. /// True if the character has the item. protected abstract bool HasItemInternal(Item item); /// /// Adds the item to the inventory. This does not add the actual ItemIdentifier - PickupItem does that. /// /// The Item to add. /// Can the item be equipped immediately? /// Should the item be force equipped? public void AddItem(Item item, bool immediateEquip, bool forceEquip) { if (AddItemInternal(item)) { m_AllItems.Add(item); // Notify those interested that an item has been added. EventHandler.ExecuteEvent(m_GameObject, "OnInventoryAddItem", item); if (m_OnAddItemEvent != null) { m_OnAddItemEvent.Invoke(item); } // The ItemIdentifier event should also be called in cases where the amount is greater than 0. // This allows the ItemIdentifier to be picked up before the item has been added. if (GetItemIdentifierAmount(item.ItemIdentifier) > 0) { ItemIdentifierPickedUp(item.ItemIdentifier, 1, item.SlotID, immediateEquip, forceEquip); } } } /// /// Internal method which adds the item to the Inventory. This does not add the actual IItemIdentifier - PickupItem does that. /// /// The item to add. /// True if the item was added to the inventory. protected abstract bool AddItemInternal(Item item); /// /// Adds the specified amount of the ItemIdentifier to the inventory. /// /// The ItemIdentifier to add. /// The amount of ItemIdentifier to add. /// The slot ID that picked up the item. A -1 value will indicate no specified slot. /// Should the item be picked up immediately? /// Should the item be force equipped? /// True if the ItemIdentifier was picked up. public bool Pickup(IItemIdentifier itemIdentifier, int amount, int slotID, bool immediatePickup, bool forceEquip) { return Pickup(itemIdentifier, amount, slotID, immediatePickup, forceEquip, true); } /// /// Adds the specified amount of the ItemIdentifier to the inventory. /// /// The ItemIdentifier to add. /// The amount of ItemIdentifier to add. /// The slot ID that picked up the item. A -1 value will indicate no specified slot. /// Should the item be picked up immediately? /// Should the item be force equipped? /// Should other objects be notified that the ItemIdentifier was picked up? /// True if the ItemIdentifier was picked up. public bool Pickup(IItemIdentifier itemIdentifier, int amount, int slotID, bool immediatePickup, bool forceEquip, bool notifyOnPickup) { // Prevent pickup when the inventory isn't enabled. if (itemIdentifier == null || !enabled || amount == 0) { return false; } var pickedUp = PickupInternal(itemIdentifier, amount); // Notify those interested that an item has been picked up. if (pickedUp && notifyOnPickup) { if (slotID == -1) { // Find the slot that the item belongs to (if any). for (int i = 0; i < m_SlotCount; ++i) { if (GetItem(itemIdentifier, i) != null) { ItemIdentifierPickedUp(itemIdentifier, amount, i, immediatePickup, forceEquip); slotID = i; } } if (slotID == -1) { // The ItemIdentifier doesn't correspond to an item so execute the event once. ItemIdentifierPickedUp(itemIdentifier, amount, -1, immediatePickup, forceEquip); } } else { ItemIdentifierPickedUp(itemIdentifier, amount, slotID, immediatePickup, forceEquip); } // If the slot ID isn't -1 then AddItem has already run. Add the item if it hasn't already been added. This will occur if the item is removed // and then later added again. if (slotID != -1) { var item = GetItem(itemIdentifier, slotID); if (item != null && !m_AllItems.Contains(item)) { m_AllItems.Add(item); } } } return pickedUp; } /// /// Internal method which adds the specified amount of the ItemIdentifier to the inventory. /// /// The IItemIdentifier to add. /// The amount of ItemIdentifier to add. /// True if the ItemIdentifier was picked up successfully. protected abstract bool PickupInternal(IItemIdentifier itemIdentifier, int amount); /// /// The ItemIdentifier has been picked up. Notify interested objects. /// /// The ItemIdentifier that was picked up. /// The number of ItemIdentifier picked up. /// The ID of the slot which the item belongs to. /// Was the item be picked up immediately? /// Should the item be force equipped? protected void ItemIdentifierPickedUp(IItemIdentifier itemIdentifier, int amount, int slotID, bool immediatePickup, bool forceEquip) { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo != null && m_NetworkInfo.IsLocalPlayer()) { m_NetworkCharacter.ItemIdentifierPickup(itemIdentifier.ID, amount, slotID, immediatePickup, forceEquip); } #endif EventHandler.ExecuteEvent(m_GameObject, "OnInventoryPickupItemIdentifier", itemIdentifier, amount, immediatePickup, forceEquip); if (m_OnPickupItemIdentifierEvent != null) { m_OnPickupItemIdentifierEvent.Invoke(itemIdentifier, amount, immediatePickup, forceEquip); } if (slotID != -1) { var item = GetItem(itemIdentifier, slotID); if (item != null) { item.Pickup(); EventHandler.ExecuteEvent(m_GameObject, "OnInventoryPickupItem", item, amount, immediatePickup, forceEquip); if (m_OnPickupItemEvent != null) { m_OnPickupItemEvent.Invoke(item, amount, immediatePickup, forceEquip); } } } if (!m_AllItemIdentifiers.Contains(itemIdentifier)) { m_AllItemIdentifiers.Add(itemIdentifier); } } /// /// Returns the active item in the specified slot. /// /// The ID of the slot. /// The active item which occupies the specified slot. Can be null. public Item GetActiveItem(int slotID) { return GetActiveItemInternal(slotID); } /// /// Internal method which returns the active item in the specified slot. /// /// The ID of the slot which the item belongs to. /// The active item which occupies the specified slot. Can be null. protected abstract Item GetActiveItemInternal(int slotID); /// /// Returns the item that corresponds to the specified ItemIdentifier. /// /// The ItemIdentifier of the item. /// The ID of the slot which the item belongs to. /// The item which occupies the specified slot. Can be null. public Item GetItem(IItemIdentifier itemIdentifier, int slotID) { return GetItemInternal(itemIdentifier, slotID); } /// /// Internal method which returns the item that corresponds to the specified IItemIdentifier. /// /// The ItemIdentifier of the item. /// The ID of the slot which the item belongs to. /// The item which occupies the specified slot. Can be null. protected abstract Item GetItemInternal(IItemIdentifier itemIdentifier, int slotID); /// /// Returns a list of all of the items in the inventory. /// /// A list of all of the items in the inventory. public List GetAllItems() { return m_AllItems; } /// /// Returns a list of all of the ItemIdentifier in the inventory. Only used by the editor for the inventory inspector. /// /// A list of all of the ItemIdentifier in the inventory. public List GetAllItemIdentifiers() { return m_AllItemIdentifiers; } /// /// Equips the ItemIdentifier in the specified slot. /// /// The ItemIdentifier to equip. /// The ID of the slot. /// Is the item being equipped immediately? Immediate equips will occur from the default loadout or quickly switching to the item. public void EquipItem(IItemIdentifier itemIdentifier, int slotID, bool immediateEquip) { if (itemIdentifier == null) { return; } var currentItem = GetActiveItem(slotID); if (currentItem != null && currentItem.ItemIdentifier != itemIdentifier) { UnequipItem(slotID); } var item = EquipItemInternal(itemIdentifier, slotID); if (item != null) { item.Equip(immediateEquip); // Notify those interested that an item has been equipped. EventHandler.ExecuteEvent(m_GameObject, "OnInventoryEquipItem", item, slotID); if (m_OnEquipItemEvent != null) { m_OnEquipItemEvent.Invoke(item, slotID); } #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo != null && m_NetworkInfo.IsLocalPlayer()) { m_NetworkCharacter.EquipUnequipItem(itemIdentifier.ID, slotID, true); } #endif if (!string.IsNullOrEmpty(m_UnequippedStateName)) { StateSystem.StateManager.SetState(m_GameObject, m_UnequippedStateName, false); } } } /// /// Internal method which equips the ItemIdentifier in the specified slot. /// /// The ItemIdentifier to equip. /// The ID of the slot. /// The item which corresponds to the ItemIdentifier. Can be null. protected abstract Item EquipItemInternal(IItemIdentifier itemIdentifier, int slotID); /// /// Unequips the specified ItemIdentifier in the specified slot. /// /// The ItemIdentifier to unequip. If the ItemIdentifier isn't currently equipped then no changes will be made. /// The ID of the slot. public void UnequipItem(IItemIdentifier itemIdentifier, int slotID) { // No need to unequip if the item is already unequipped or the ItemIdentifier don't match. var currentItem = GetActiveItem(slotID); if (currentItem == null || currentItem.ItemIdentifier != itemIdentifier) { return; } UnequipItem(slotID); } /// /// Unequips the item in the specified slot. /// /// The ID of the slot. public void UnequipItem(int slotID) { // No need to unequip if the item is already unequipped. var currentItem = GetActiveItem(slotID); if (currentItem == null) { return; } var item = UnequipItemInternal(slotID); if (item != null) { item.Unequip(); // Notify those interested that an item has been unequipped. EventHandler.ExecuteEvent(m_GameObject, "OnInventoryUnequipItem", item, slotID); if (m_OnUnequipItemEvent != null) { m_OnUnequipItemEvent.Invoke(item, slotID); } #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo != null && m_NetworkInfo.IsLocalPlayer()) { m_NetworkCharacter.EquipUnequipItem(item.ItemIdentifier.ID, slotID, false); } #endif // Optionally enable a state when the inventory is unequipped. if (!string.IsNullOrEmpty(m_UnequippedStateName)) { var unequipped = true; for (int i = 0; i < m_SlotCount; ++i) { if (i == slotID) { continue; } if (GetActiveItem(i) != null) { unequipped = false; } } if (unequipped) { StateSystem.StateManager.SetState(m_GameObject, m_UnequippedStateName, true); } } } } /// /// Internal method which unequips the item in the specified slot. /// /// The ID of the slot. /// The item that was unequipped. protected abstract Item UnequipItemInternal(int slotID); /// /// Returns the amount of the specified ItemIdentifier. /// /// The ItemIdentifier to get the amount of. /// The amount of the specified ItemIdentifier. public int GetItemIdentifierAmount(IItemIdentifier itemIdentifier) { if (itemIdentifier == null) { return 0; } return GetItemIdentifierAmountInternal(itemIdentifier); } /// /// Internal method which returns the amount of the specified ItemIdentifier. /// /// The ItemIdentifier to get the amount of. /// The amount of the specified ItemIdentifier. protected abstract int GetItemIdentifierAmountInternal(IItemIdentifier itemIdentifier); /// /// Adjusts the amount of the specified ItemIdentifier. /// /// The ItemIdentifier to adjust. /// The amount of ItemIdentifier to adjust. public void AdjustItemIdentifierAmount(IItemIdentifier itemIdentifier, int amount) { if (itemIdentifier == null || amount == 0) { return; } AdjustItemIdentifierAmountInternal(itemIdentifier, amount); // Notify those interested that an item has been adjusted. var remaining = GetItemIdentifierAmount(itemIdentifier); EventHandler.ExecuteEvent(m_GameObject, "OnInventoryAdjustItemIdentifierAmount", itemIdentifier, remaining); if (m_OnAdjustItemIdentifierAmountEvent != null) { m_OnAdjustItemIdentifierAmountEvent.Invoke(itemIdentifier, remaining); } } /// /// Internal method which adjusts the amount of the specified ItemIdentifier. /// /// The ItemIdentifier to adjust. /// The amount of ItemIdentifier to adjust. protected abstract void AdjustItemIdentifierAmountInternal(IItemIdentifier itemIdentifier, int amount); /// /// Removes the ItemIdentifier from the inventory. /// /// The ItemIdentifier to remove. /// The ID of the slot. /// The amount of the ItemIdentnfier that should be removed. /// Should the item be dropped when removed? public void RemoveItem(IItemIdentifier itemIdentifier, int slotID, int amount, bool drop) { var item = GetItem(itemIdentifier, slotID); if (item != null) { // The item should be dropped before unequipped so the drop position will be correct. if (drop) { item.Drop(amount, false); } // An equipped item needs to be unequipped. UnequipItem(itemIdentifier, slotID); // If the item isn't dropped then it is removed immediately. if (!drop) { item.Remove(); } if (item.DropConsumableItems) { var itemActions = item.ItemActions; if (itemActions != null) { IUsableItem usableItem; IItemIdentifier consumableItemIdentifier; for (int i = 0; i < itemActions.Length; ++i) { if (((usableItem = itemActions[i] as IUsableItem) != null) && (consumableItemIdentifier = usableItem.GetConsumableItemIdentifier()) != null) { usableItem.RemoveConsumableItemIdentifierAmount(); // Any consumable ItemIdentifier should also be removed if there are no more of the same items remaining. if (GetItemIdentifierAmount(itemIdentifier) == 1) { RemoveItemIdentifierInternal(consumableItemIdentifier, slotID, amount); m_AllItemIdentifiers.Remove(consumableItemIdentifier); } // Notify those interested of the removed amount. SendItemIdentifierAdjustmentEvents(consumableItemIdentifier); } } } } m_AllItems.Remove(item); } // The ItemIdentifier should be removed from the inventory. RemoveItemIdentifierInternal(itemIdentifier, slotID, amount); if (GetItemIdentifierAmount(itemIdentifier) == 0) { m_AllItemIdentifiers.Remove(itemIdentifier); } // Notify those interested that the item will be removed. if (item != null) { EventHandler.ExecuteEvent(m_GameObject, "OnInventoryRemoveItem", item, slotID); if (m_OnRemoveItemEvent != null) { m_OnRemoveItemEvent.Invoke(item, slotID); } } else { SendItemIdentifierAdjustmentEvents(itemIdentifier); } } /// /// Sends the ItemIdentifier adjustment events. /// /// The ItemIdentifier to remove. private void SendItemIdentifierAdjustmentEvents(IItemIdentifier itemIdentifier) { // Notify those interested of the removed amount. var amount = GetItemIdentifierAmount(itemIdentifier); EventHandler.ExecuteEvent(m_GameObject, "OnInventoryAdjustItemIdentifierAmount", itemIdentifier, amount); if (m_OnAdjustItemIdentifierAmountEvent != null) { m_OnAdjustItemIdentifierAmountEvent.Invoke(itemIdentifier, amount); } if (amount == 0) { m_AllItemIdentifiers.Remove(itemIdentifier); } } /// /// Internal method which removes the ItemIdentifier from the inventory. /// /// The ItemIdentifier to remove. /// The ID of the slot. /// The amount of the ItemIdentifier that should be removed. protected abstract void RemoveItemIdentifierInternal(IItemIdentifier itemIdentifier, int slotID, int amount); /// /// The character has died. /// /// The position of the force. /// The amount of force which killed the character. /// The GameObject that killed the character. private void OnDeath(Vector3 position, Vector3 force, GameObject attacker) { enabled = false; // The item's drop method will call RemoveItem within the inventory. if (m_RemoveAllOnDeath) { RemoveAllItems(true); #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo != null && m_NetworkInfo.IsLocalPlayer()) { m_NetworkCharacter.RemoveAllItems(); } #endif } } /// /// Removes all of the items from the inventory. /// /// Should the item be dropped when removed? public void RemoveAllItems(bool drop) { var allItems = GetAllItems(); for (int i = allItems.Count - 1; i >= 0; --i) { // Multiple items may be dropped at the same time. if (allItems.Count <= i) { continue; } var itemIdentifier = allItems[i].ItemIdentifier; var slotID = allItems[i].SlotID; while (GetItemIdentifierAmount(itemIdentifier) > 0) { RemoveItem(itemIdentifier, slotID, 1, drop); } } } /// /// The character has respawned. /// private void OnRespawn() { enabled = true; if (m_LoadDefaultLoadoutOnRespawn) { LoadDefaultLoadout(); } // Notify others that the inventory has respawned - allows EquipUnequip to equip any previously equipped items. EventHandler.ExecuteEvent(m_GameObject, "OnInventoryRespawned"); } /// /// The object has been destroyed. /// private void OnDestroy() { EventHandler.UnregisterEvent(m_GameObject, "OnDeath", OnDeath); EventHandler.UnregisterEvent(m_GameObject, "OnRespawn", OnRespawn); } } }