/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.Character.Abilities.Items { using Opsive.Shared.Events; using Opsive.Shared.Game; using Opsive.Shared.Inventory; using Opsive.UltimateCharacterController.Items; using Opsive.UltimateCharacterController.Items.Actions; using Opsive.UltimateCharacterController.Utility; using System.Collections.Generic; using UnityEngine; /// /// ItemAbility which will reload the item. There are two parts to a reload: /// - The first part will take the reload amount from the inventory and add it to the item. /// - The second part can wait for a small amount of time after the first part to ensure the reload animation is complete before ending the ability. /// [DefaultStartType(AbilityStartType.ButtonDown)] [DefaultInputName("Reload")] [DefaultItemStateIndex(3)] [AllowDuplicateTypes] public class Reload : ItemAbility { /// /// Specifies when the item should automatically be reloaded. /// public enum AutoReloadType { Pickup = 1, // The item should be reloaded upon pickup for the first time. Empty = 2 // Automatically reload when the item is empty. } [Tooltip("The slot that should be reloaded. -1 will use all of the slots.")] [SerializeField] protected int m_SlotID = -1; [Tooltip("The ID of the ItemAction component that can be reloaded.")] [SerializeField] protected int m_ActionID; public int SlotID { get { return m_SlotID; } set { if (m_SlotID != value) { UnregisterSlotEvents(m_SlotID); m_SlotID = value; RegisterSlotEvents(m_SlotID); } } } public int ActionID { get { return m_ActionID; } set { m_ActionID = value; } } private IReloadableItem[] m_ReloadableItems; private ScheduledEventBase[] m_ReloadEvents; private HashSet m_InventoryItems = new HashSet(); private HashSet m_EquippedItems = new HashSet(); private IReloadableItem[] m_CanReloadItems; private bool[] m_Reloaded; public IReloadableItem[] ReloadableItems { get { return m_ReloadableItems; } } #if UNITY_EDITOR public override string AbilityDescription { get { if (m_SlotID != -1) { return "Slot " + m_SlotID; } return string.Empty; } } #endif /// /// Initialize the default values. /// public override void Awake() { base.Awake(); m_ReloadableItems = new IReloadableItem[m_SlotID == -1 ? m_Inventory.SlotCount : 1]; m_ReloadEvents = new ScheduledEventBase[m_ReloadableItems.Length]; m_Reloaded = new bool[m_ReloadableItems.Length]; EventHandler.RegisterEvent(m_GameObject, "OnItemPickupStartPickup", OnStartPickup); EventHandler.RegisterEvent(m_GameObject, "OnInventoryPickupItemIdentifier", OnPickupItemIdentifier); EventHandler.RegisterEvent(m_GameObject, "OnItemTryReload", OnTryReload); EventHandler.RegisterEvent(m_GameObject, "OnAnimatorItemReload", OnItemReload); EventHandler.RegisterEvent(m_GameObject, "OnAnimatorItemReloadComplete", OnItemReloadComplete); // Register for the interested slot events. RegisterSlotEvents(m_SlotID); } /// /// Registers for the interested events according to the slot id. /// /// The slot id to register for. private void RegisterSlotEvents(int slotID) { if (slotID == 0) { EventHandler.RegisterEvent(m_GameObject, "OnAnimatorItemReloadFirstSlot", OnItemReloadFirstSlot); EventHandler.RegisterEvent(m_GameObject, "OnAnimatorItemReloadCompleteFirstSlot", OnItemReloadCompleteFirstSlot); } else if (slotID == 1) { EventHandler.RegisterEvent(m_GameObject, "OnAnimatorItemReloadSecondSlot", OnItemReloadSecondSlot); EventHandler.RegisterEvent(m_GameObject, "OnAnimatorItemReloadCompleteSecondSlot", OnItemReloadCompleteSecondSlot); } else if (slotID == 2) { EventHandler.RegisterEvent(m_GameObject, "OnAnimatorItemReloadThirdSlot", OnItemReloadThirdSlot); EventHandler.RegisterEvent(m_GameObject, "OnAnimatorItemReloadCompleteThirdSlot", OnItemReloadCompleteThirdSlot); } else if (slotID != -1) { Debug.LogError("Error: The Reload ability does not listen to slot " + m_SlotID); } } /// /// Unregisters from the interested events according to the slot id. /// /// The slot id to unregister from. private void UnregisterSlotEvents(int slotID) { if (slotID == 0) { EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorItemReloadFirstSlot", OnItemReloadFirstSlot); EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorItemReloadCompleteFirstSlot", OnItemReloadCompleteFirstSlot); } else if (slotID == 1) { EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorItemReloadSecondSlot", OnItemReloadSecondSlot); EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorItemReloadCompleteSecondSlot", OnItemReloadCompleteSecondSlot); } else if (slotID == 2) { EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorItemReloadThirdSlot", OnItemReloadThirdSlot); EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorItemReloadCompleteThirdSlot", OnItemReloadCompleteThirdSlot); } } /// /// Can the item be reloaded? /// /// True if the item can be reloaded. public override bool CanStartAbility() { // An attribute may prevent the ability from starting. if (!base.CanStartAbility()) { return false; } var canReload = false; // If the SlotID is -1 then the ability should reload every equipped item at the same time. If only one slot has a ReloadableItem then the // ability can start. If the SlotID is not -1 then the ability should reload the item in the specified slot. if (m_SlotID == -1) { for (int i = 0; i < m_ReloadableItems.Length; ++i) { var item = m_Inventory.GetActiveItem(i); if (item == null) { continue; } var itemAction = item.GetItemAction(m_ActionID); if (itemAction == null) { Debug.LogWarning("Warning: The item " + item.name + " must have an ItemAction component attached to it in order to be reloaded."); continue; } m_ReloadableItems[i] = itemAction as IReloadableItem; // The item can't be reloaded if it isn't a reloadable item. if (m_ReloadableItems[i] != null && m_ReloadableItems[i].CanReloadItem(true)) { canReload = true; } else { // The ability should not attempt to reload the item if IReloadableItem says that it cannot reload. m_ReloadableItems[i] = null; } } } else { var item = m_Inventory.GetActiveItem(m_SlotID); if (item != null) { var itemAction = item.GetItemAction(m_ActionID); if (itemAction == null) { Debug.LogWarning("Warning: The item " + item.name + " must have an ItemAction component attached to it in order to be used."); } else { m_ReloadableItems[0] = itemAction as IReloadableItem; canReload = m_ReloadableItems[0] != null && m_ReloadableItems[0].CanReloadItem(true); } } } return canReload; } /// /// The ability has started. /// protected override void AbilityStarted() { base.AbilityStarted(false); for (int i = 0; i < m_ReloadableItems.Length; ++i) { if (m_ReloadableItems[i] != null) { m_Reloaded[i] = false; m_ReloadableItems[i].StartItemReload(); if (!m_ReloadableItems[i].ReloadEvent.WaitForAnimationEvent) { m_ReloadEvents[i] = Scheduler.ScheduleFixed(m_ReloadableItems[i].ReloadEvent.Duration, ReloadItem, i); } } } } /// /// Stops reloading the item in the specified slot. /// /// The ID of the slot to stop reloading the item at. public void StopItemReload(int slotID) { if (m_ReloadableItems[slotID] == null) { return; } m_ReloadableItems[slotID].ItemReloadComplete(false, false); m_ReloadableItems[slotID] = null; m_Reloaded[slotID] = false; Scheduler.Cancel(m_ReloadEvents[slotID]); m_ReloadEvents[slotID] = null; m_CharacterLocomotion.UpdateItemAbilityAnimatorParameters(); // The ability won't be active if CanStartAbility filled in the ReloadableItem but the ability hasn't started yet. if (!IsActive) { return; } // The ability should stop if no more items can be reloaded. var canStop = true; for (int i = 0; i < m_ReloadableItems.Length; ++i) { if (m_ReloadableItems[i] != null) { canStop = false; } } if (canStop) { StopAbility(true); } } /// /// Called when another ability is attempting to start and the current ability is active. /// Returns true or false depending on if the new ability should be blocked from starting. /// /// The ability that is starting. /// True if the ability should be blocked. public override bool ShouldBlockAbilityStart(Ability startingAbility) { if (base.ShouldBlockAbilityStart(startingAbility)) { return true; } if (startingAbility is Use) { // The ability should be able to be used unless the dominant item state doesn't match. This will prevent a secondary grenade throw // from being started when the primary item is being used. It will not prevent two independent items from being used at the same time. var dominantItem = true; for (int i = 0; i < m_ReloadableItems.Length; ++i) { if (m_ReloadableItems[i] == null) { continue; } if (!m_ReloadableItems[i].Item.DominantItem) { dominantItem = false; break; } } var useAbility = startingAbility as Use; for (int i = 0; i < useAbility.UsableItems.Length; ++i) { if (useAbility.UsableItems[i] == null) { continue; } if (dominantItem != useAbility.UsableItems[i].Item.DominantItem) { return true; } } } // Equip/Unequip cannot be active at the same time as Reload. return startingAbility is EquipUnequip; } /// /// Returns the Item State Index which corresponds to the slot ID. /// /// The ID of the slot that corresponds to the Item State Index. /// The Item State Index which corresponds to the slot ID. public override int GetItemStateIndex(int slotID) { // Return the ItemStateIndex if the SlotID matches the requested slotID. if (m_SlotID == -1) { if (m_ReloadableItems[slotID] != null) { return m_ItemStateIndex; } } else if (m_SlotID == slotID && m_ReloadableItems[0] != null) { return m_ItemStateIndex; } return -1; } /// /// Returns the Item Substate Index which corresponds to the slot ID. /// /// The ID of the slot that corresponds to the Item Substate Index. /// The Item Substate Index which corresponds to the slot ID. public override int GetItemSubstateIndex(int slotID) { if (m_SlotID == -1) { if (m_ReloadableItems[slotID] != null) { if (m_Reloaded[slotID]) { return 0; } return m_ReloadableItems[slotID].ReloadAnimatorAudioStateSet.GetItemSubstateIndex(); } } else if (m_SlotID == slotID && m_ReloadableItems[0] != null) { if (m_Reloaded[0]) { return 0; } return m_ReloadableItems[0].ReloadAnimatorAudioStateSet.GetItemSubstateIndex(); } return -1; } /// /// The animation has reloaded all of the items. /// private void OnItemReload() { for (int i = 0; i < m_ReloadableItems.Length; ++i) { if (m_ReloadableItems[i] != null && !m_Reloaded[i]) { ReloadItem(i); } } } /// /// The animation has reloaded the first item slot. /// private void OnItemReloadFirstSlot() { ReloadItem(0); } /// /// The animation has reloaded the second item slot. /// private void OnItemReloadSecondSlot() { ReloadItem(1); } /// /// The animation has reloaded the third item slot. /// private void OnItemReloadThirdSlot() { ReloadItem(2); } /// /// The animation has reloaded the item. /// /// The slot that is reloading the item. private void ReloadItem(int slotID) { var reloadableItem = m_ReloadableItems[slotID]; if (reloadableItem == null) { return; } reloadableItem.ReloadItem(false); // Each reload should update the attribute. if (m_AttributeModifier != null) { m_AttributeModifier.EnableModifier(true); } var canReload = true; // An attribute may prevent the reload from being able to continue. if (m_AttributeModifier != null && !m_AttributeModifier.IsValid()) { canReload = false; } // The item may need to be reloaded again if the reload type is single and the inventory still has ammo. if (canReload && reloadableItem.CanReloadItem(true)) { m_CharacterLocomotion.UpdateItemAbilityAnimatorParameters(); if (!reloadableItem.ReloadEvent.WaitForAnimationEvent) { m_ReloadEvents[slotID] = Scheduler.ScheduleFixed(reloadableItem.ReloadEvent.Duration, ReloadItem, slotID); } } else { m_Reloaded[slotID] = true; m_CharacterLocomotion.UpdateItemAbilityAnimatorParameters(); // The reload ability isn't done until the ReloadItemComplete method is called. if (!reloadableItem.ReloadCompleteEvent.WaitForAnimationEvent) { m_ReloadEvents[slotID] = Scheduler.ScheduleFixed(reloadableItem.ReloadCompleteEvent.Duration, ReloadItemComplete, slotID); } } } /// /// The reload animation has completed for all of the items. /// private void OnItemReloadComplete() { for (int i = 0; i < m_ReloadableItems.Length; ++i) { if (m_ReloadableItems[i] != null) { ReloadItemComplete(i); } } } /// /// The reload animation has completed for the first item slot. /// private void OnItemReloadCompleteFirstSlot() { ReloadItemComplete(0); } /// /// The reload animation has completed for the second item slot. /// private void OnItemReloadCompleteSecondSlot() { ReloadItemComplete(1); } /// /// The reload animation has completed for the third item slot. /// private void OnItemReloadCompleteThirdSlot() { ReloadItemComplete(2); } /// /// The animator has finished playing the reload animation. /// /// The slot that is reloading the item. private void ReloadItemComplete(int slotID) { var reloadableItem = m_ReloadableItems[slotID]; if (reloadableItem == null) { return; } m_ReloadableItems[slotID].ItemReloadComplete(true, false); m_ReloadableItems[slotID] = null; if (m_ReloadEvents[slotID] != null) { Scheduler.Cancel(m_ReloadEvents[slotID]); m_ReloadEvents[slotID] = null; } // Don't stop the ability unless all slots have been reloaded. var stopAbility = true; for (int i = 0; i < m_ReloadableItems.Length; ++i) { if (m_ReloadableItems[i] != null) { stopAbility = false; break; } } if (stopAbility) { StopAbility(); } } /// /// The ItemPickup component is starting to pick up ItemIdentifier. /// private void OnStartPickup() { // Remember the initial item inventory list to be able to determine if an item has been added. m_InventoryItems.Clear(); var allItems = m_Inventory.GetAllItems(); for (int i = 0; i < allItems.Count; ++i) { if (m_Inventory.GetItemIdentifierAmount(allItems[i].ItemIdentifier) == 0) { continue; } m_InventoryItems.Add(allItems[i]); } m_EquippedItems.Clear(); Item item; for (int i = 0; i < m_Inventory.SlotCount; ++i) { if ((item = m_Inventory.GetActiveItem(i)) != null) { m_EquippedItems.Add(item); } } } /// /// An ItemIdentifier has been picked up within the inventory. /// /// The ItemIdentifier that has been equipped. /// The amount of ItemIdentifier picked up. /// Was the item be picked up immediately? /// Should the item be force equipped? private void OnPickupItemIdentifier(IItemIdentifier itemIdentifier, int amount, bool immediatePickup, bool forceEquip) { // Determine if the equipped item should be reloaded. OnTryReload(-1, itemIdentifier, immediatePickup, true); } /// /// Tries the reload the item with the specified ItemIdentifier. /// /// The SlotID of the item trying to reload. /// The ItemIdentifier which should be reloaded. /// Should the item be reloaded immediately? /// Should the equipped items be checked. private void OnTryReload(int slotID, IItemIdentifier itemIdentifier, bool immediateReload, bool equipCheck) { if (m_SlotID != -1 && slotID != -1 && m_SlotID != slotID) { return; } var allItems = m_Inventory.GetAllItems(); var canReloadCount = 0; for (int i = 0; i < allItems.Count; ++i) { var item = allItems[i]; if (slotID != -1 && item.SlotID != slotID) { continue; } IReloadableItem reloadableItem; if ((reloadableItem = ShouldReload(item, itemIdentifier, slotID == -1)) != null) { // -1 indicates that the item is being picked up. if (m_CanReloadItems == null || m_CanReloadItems.Length == canReloadCount) { System.Array.Resize(ref m_CanReloadItems, canReloadCount + 1); } m_CanReloadItems[canReloadCount] = reloadableItem; canReloadCount++; } } if (canReloadCount > 0) { var startAbility = false; for (int i = 0; i < canReloadCount; ++i) { var reloadableItem = m_CanReloadItems[i]; // The item should automatically be reloaded if: // - The item is being reloaded automatically. // - The item isn't currently equipped. Non-equipped items don't need to play an animation. if (immediateReload || (equipCheck && !m_EquippedItems.Contains(reloadableItem.Item))) { reloadableItem.ReloadItem(true); reloadableItem.ItemReloadComplete(true, immediateReload); } else { startAbility = true; if (m_SlotID == -1) { m_ReloadableItems[reloadableItem.Item.SlotID] = reloadableItem; } else { m_ReloadableItems[0] = reloadableItem; } } } if (startAbility) { StartAbility(); } } } /// /// Should the item be reloaded? An IReloadableItem reference will be returned if the item can be reloaded. /// /// The item which may need to be reloaded. /// The ItemIdentifier that is being reloaded. /// Is the item being reloaded from a pickup? /// A reference to the IReloadableItem if the item can be reloaded. Null if the item cannot be reloaded. private IReloadableItem ShouldReload(Item item, IItemIdentifier itemIdentifier, bool fromPickup) { var itemAction = item.GetItemAction(m_ActionID); // Don't reload if the item isn't a IReloadableItem. var reloadableItem = itemAction as IReloadableItem; if (reloadableItem == null) { return null; } // Don't reload if the ItemIdentifier doesn't match. if (reloadableItem.GetReloadableItemIdentifier() != itemIdentifier) { return null; } var autoReload = false; if ((reloadableItem.AutoReload & AutoReloadType.Empty) != 0 && (reloadableItem is IUsableItem && (reloadableItem as IUsableItem).GetConsumableItemIdentifierAmount() == 0)) { // The item is empty. autoReload = true; } else if ((reloadableItem.AutoReload & AutoReloadType.Pickup) != 0 && fromPickup) { // The item was just picked up for the first time. autoReload = true; } // Don't automatically reload if the item says that it shouldn't. if (!autoReload) { return null; } // Don't reload if the reloadable item can't reload. if (!reloadableItem.CanReloadItem(true)) { return null; } // Reload. return reloadableItem; } /// /// Can the camera zoom while the ability is active? /// /// True if the camera can zoom while the ability is active. public override bool CanCameraZoom() { for (int i = 0; i < m_ReloadableItems.Length; ++i) { if (m_ReloadableItems[i] != null && !m_ReloadableItems[i].CanCameraZoom) { return false; } } return true; } /// /// The ability has stopped running. /// /// Was the ability force stopped? protected override void AbilityStopped(bool force) { base.AbilityStopped(force); // Ensure the arrays are set to null for the next run. for (int i = 0; i < m_ReloadableItems.Length; ++i) { if (m_ReloadableItems[i] != null) { m_ReloadableItems[i].ItemReloadComplete(!force, force); m_ReloadableItems[i] = null; Scheduler.Cancel(m_ReloadEvents[i]); m_ReloadEvents[i] = null; } } } /// /// Called when the character is destroyed. /// public override void OnDestroy() { base.OnDestroy(); EventHandler.UnregisterEvent(m_GameObject, "OnItemPickupStartPickup", OnStartPickup); EventHandler.UnregisterEvent(m_GameObject, "OnInventoryPickupItemIdentifier", OnPickupItemIdentifier); EventHandler.UnregisterEvent(m_GameObject, "OnItemTryReload", OnTryReload); EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorItemReload", OnItemReload); EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorItemReloadComplete", OnItemReloadComplete); UnregisterSlotEvents(m_SlotID); } } }