Files
BABA_YAGA/Assets/Opsive/UltimateCharacterController/Scripts/Character/Abilities/Items/Reload.cs
2026-06-09 02:05:00 +07:00

641 lines
27 KiB
C#

/// ---------------------------------------------
/// 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;
/// <summary>
/// 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.
/// </summary>
[DefaultStartType(AbilityStartType.ButtonDown)]
[DefaultInputName("Reload")]
[DefaultItemStateIndex(3)]
[AllowDuplicateTypes]
public class Reload : ItemAbility
{
/// <summary>
/// Specifies when the item should automatically be reloaded.
/// </summary>
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<Item> m_InventoryItems = new HashSet<Item>();
private HashSet<Item> m_EquippedItems = new HashSet<Item>();
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
/// <summary>
/// Initialize the default values.
/// </summary>
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<IItemIdentifier, int, bool, bool>(m_GameObject, "OnInventoryPickupItemIdentifier", OnPickupItemIdentifier);
EventHandler.RegisterEvent<int, IItemIdentifier, bool, bool>(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);
}
/// <summary>
/// Registers for the interested events according to the slot id.
/// </summary>
/// <param name="slotID">The slot id to register for.</param>
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);
}
}
/// <summary>
/// Unregisters from the interested events according to the slot id.
/// </summary>
/// <param name="slotID">The slot id to unregister from.</param>
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);
}
}
/// <summary>
/// Can the item be reloaded?
/// </summary>
/// <returns>True if the item can be reloaded.</returns>
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;
}
/// <summary>
/// The ability has started.
/// </summary>
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);
}
}
}
}
/// <summary>
/// Stops reloading the item in the specified slot.
/// </summary>
/// <param name="slotID">The ID of the slot to stop reloading the item at.</param>
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);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="startingAbility">The ability that is starting.</param>
/// <returns>True if the ability should be blocked.</returns>
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;
}
/// <summary>
/// Returns the Item State Index which corresponds to the slot ID.
/// </summary>
/// <param name="slotID">The ID of the slot that corresponds to the Item State Index.</param>
/// <returns>The Item State Index which corresponds to the slot ID.</returns>
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;
}
/// <summary>
/// Returns the Item Substate Index which corresponds to the slot ID.
/// </summary>
/// <param name="slotID">The ID of the slot that corresponds to the Item Substate Index.</param>
/// <returns>The Item Substate Index which corresponds to the slot ID.</returns>
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;
}
/// <summary>
/// The animation has reloaded all of the items.
/// </summary>
private void OnItemReload()
{
for (int i = 0; i < m_ReloadableItems.Length; ++i) {
if (m_ReloadableItems[i] != null && !m_Reloaded[i]) {
ReloadItem(i);
}
}
}
/// <summary>
/// The animation has reloaded the first item slot.
/// </summary>
private void OnItemReloadFirstSlot()
{
ReloadItem(0);
}
/// <summary>
/// The animation has reloaded the second item slot.
/// </summary>
private void OnItemReloadSecondSlot()
{
ReloadItem(1);
}
/// <summary>
/// The animation has reloaded the third item slot.
/// </summary>
private void OnItemReloadThirdSlot()
{
ReloadItem(2);
}
/// <summary>
/// The animation has reloaded the item.
/// </summary>
/// <param name="slotID">The slot that is reloading the item.</param>
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);
}
}
}
/// <summary>
/// The reload animation has completed for all of the items.
/// </summary>
private void OnItemReloadComplete()
{
for (int i = 0; i < m_ReloadableItems.Length; ++i) {
if (m_ReloadableItems[i] != null) {
ReloadItemComplete(i);
}
}
}
/// <summary>
/// The reload animation has completed for the first item slot.
/// </summary>
private void OnItemReloadCompleteFirstSlot()
{
ReloadItemComplete(0);
}
/// <summary>
/// The reload animation has completed for the second item slot.
/// </summary>
private void OnItemReloadCompleteSecondSlot()
{
ReloadItemComplete(1);
}
/// <summary>
/// The reload animation has completed for the third item slot.
/// </summary>
private void OnItemReloadCompleteThirdSlot()
{
ReloadItemComplete(2);
}
/// <summary>
/// The animator has finished playing the reload animation.
/// </summary>
/// <param name="slotID">The slot that is reloading the item.</param>
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();
}
}
/// <summary>
/// The ItemPickup component is starting to pick up ItemIdentifier.
/// </summary>
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);
}
}
}
/// <summary>
/// An ItemIdentifier has been picked up within the inventory.
/// </summary>
/// <param name="itemIdentifier">The ItemIdentifier that has been equipped.</param>
/// <param name="amount">The amount of ItemIdentifier picked up.</param>
/// <param name="immediatePickup">Was the item be picked up immediately?</param>
/// <param name="forceEquip">Should the item be force equipped?</param>
private void OnPickupItemIdentifier(IItemIdentifier itemIdentifier, int amount, bool immediatePickup, bool forceEquip)
{
// Determine if the equipped item should be reloaded.
OnTryReload(-1, itemIdentifier, immediatePickup, true);
}
/// <summary>
/// Tries the reload the item with the specified ItemIdentifier.
/// </summary>
/// <param name="slotID">The SlotID of the item trying to reload.</param>
/// <param name="itemIdentifier">The ItemIdentifier which should be reloaded.</param>
/// <param name="immediateReload">Should the item be reloaded immediately?</param>
/// <param name="equipCheck">Should the equipped items be checked.</param>
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();
}
}
}
/// <summary>
/// Should the item be reloaded? An IReloadableItem reference will be returned if the item can be reloaded.
/// </summary>
/// <param name="item">The item which may need to be reloaded.</param>
/// <param name="itemIdentifier">The ItemIdentifier that is being reloaded.</param>
/// <param name="fromPickup">Is the item being reloaded from a pickup?</param>
/// <returns>A reference to the IReloadableItem if the item can be reloaded. Null if the item cannot be reloaded.</returns>
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;
}
/// <summary>
/// Can the camera zoom while the ability is active?
/// </summary>
/// <returns>True if the camera can zoom while the ability is active.</returns>
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;
}
/// <summary>
/// The ability has stopped running.
/// </summary>
/// <param name="force">Was the ability force stopped?</param>
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;
}
}
}
/// <summary>
/// Called when the character is destroyed.
/// </summary>
public override void OnDestroy()
{
base.OnDestroy();
EventHandler.UnregisterEvent(m_GameObject, "OnItemPickupStartPickup", OnStartPickup);
EventHandler.UnregisterEvent<IItemIdentifier, int, bool, bool>(m_GameObject, "OnInventoryPickupItemIdentifier", OnPickupItemIdentifier);
EventHandler.UnregisterEvent<int, IItemIdentifier, bool, bool>(m_GameObject, "OnItemTryReload", OnTryReload);
EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorItemReload", OnItemReload);
EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorItemReloadComplete", OnItemReloadComplete);
UnregisterSlotEvents(m_SlotID);
}
}
}