/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.Items.Actions { using Opsive.Shared.Events; using Opsive.Shared.Game; using Opsive.Shared.Utility; using Opsive.UltimateCharacterController.Character; using Opsive.UltimateCharacterController.Character.Abilities; using Opsive.UltimateCharacterController.Character.Abilities.Items; using Opsive.UltimateCharacterController.Game; using Opsive.UltimateCharacterController.Items.Actions.PerspectiveProperties; using Opsive.UltimateCharacterController.Items.Actions.Magic.CastActions; using Opsive.UltimateCharacterController.Items.Actions.Magic.ImpactActions; using Opsive.UltimateCharacterController.Items.Actions.Magic.BeginEndActions; #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER using Opsive.UltimateCharacterController.Networking; using Opsive.UltimateCharacterController.Networking.Character; #endif using Opsive.UltimateCharacterController.Objects.ItemAssist; using Opsive.UltimateCharacterController.SurfaceSystem; using Opsive.UltimateCharacterController.Utility; using UnityEngine; /// /// The Magic Item uses Cast Actions to perform different magical actions when used. Impact Actions will then perform an action from the resulting cast impact. /// public class MagicItem : UsableItem { /// /// Specifies the direction of the cast. /// public enum CastDirection { None, // The cast has no movement. Forward, // The cast should move in the forward direction. Target, // The cast should move towards a target. Indicate // The cast should move towards an indicated position. } /// /// Specifies how often the magic item does its cast. /// public enum CastUseType { Single, // The cast should occur once per use. Continuous // The cast should occur every use update. } /// /// Specifies if the cast should interrupted. /// public enum CastInterruptSource { Movement = 1, // The cast should be interrupted when the character moves. Damage = 2 // The cast should be interrupted when the character takes damage. } [Tooltip("Is the character required to be on the ground?")] [SerializeField] protected bool m_RequireGrounded = true; [Tooltip("The direction of the cast.")] [SerializeField] protected CastDirection m_Direction = CastDirection.Forward; [Tooltip("Should the look source be used when determining the cast direction?")] [SerializeField] protected bool m_UseLookSource = true; [Tooltip("The maximum distance of the movement cast direction.")] [SerializeField] protected float m_MaxDistance = 100; [Tooltip("The radius of the movement cast direction.")] [SerializeField] protected float m_Radius = 0.1f; [Tooltip("The layers that the movement directions can collide with.")] [SerializeField] protected LayerMask m_DetectLayers = ~(1 << LayerManager.IgnoreRaycast | 1 << LayerManager.UI | 1 << LayerManager.SubCharacter | 1 << LayerManager.Overlay | 1 << LayerManager.VisualEffect); [Tooltip("The maximum angle that the target object can be compared to the character's forward direction.")] [SerializeField] protected float m_MaxAngle = 30; [Tooltip("The maximum number of colliders that can be detected by the target cast.")] [SerializeField] protected int m_MaxCollisionCount = 100; [Tooltip("The number of objects that a single cast should cast.")] [SerializeField] protected int m_TargetCount = 1; [Tooltip("The transform used to indicate the surface. Can be null.")] [SerializeField] protected Transform m_SurfaceIndicator; [Tooltip("The offset when positioning the surface indicator.")] [SerializeField] protected Vector3 m_SurfaceIndicatorOffset = new Vector3(0, 0.1f, 0); [Tooltip("Specifies how often the cast is used.")] [SerializeField] protected CastUseType m_UseType; [Tooltip("The minimum duration of the continuous use type. If a value of -1 is set then the item will be stopped when the stop is requested.")] [SerializeField] protected float m_MinContinuousUseDuration = 1; [Tooltip("Should the continuous use type cast every update?")] [SerializeField] protected bool m_ContinuousCast; [Tooltip("The amount of the ItemIdentifier that should be used each cast.")] [SerializeField] protected int m_UseAmount; [Tooltip("Specifies when the cast should be interrupted.")] [SerializeField] protected CastInterruptSource m_InterruptSource; [Tooltip("The SurfaceImpact of the cast.")] [SerializeField] protected SurfaceImpact m_SurfaceImpact; [Tooltip("The value to add to the Item Substate Index when the item can stop.")] [SerializeField] protected int m_CanStopSubstateIndexAddition = 10; [Tooltip("The serialization data for the BeginActions.")] [SerializeField] [ForceSerialized] protected Serialization[] m_BeginActionData; [Tooltip("The serialization data for the CastActions.")] [SerializeField] [ForceSerialized] protected Serialization[] m_CastActionData; [Tooltip("The serialization data for the MagicImpactActions.")] [SerializeField] [ForceSerialized] protected Serialization[] m_ImpactActionData; [Tooltip("The serialization data for the EndActions.")] [SerializeField] [ForceSerialized] protected Serialization[] m_EndActionData; public bool RequireGrounded { get { return m_RequireGrounded; } set { m_RequireGrounded = value; } } public CastDirection Direction { get { return m_Direction; } set { m_Direction = value; } } public bool UseLookSource { get { return m_UseLookSource; } set { m_UseLookSource = value; } } public float MaxDistance { get { return m_MaxDistance; } set { m_MaxDistance = value; } } public float Radius { get { return m_Radius; } set { m_Radius = value; } } public LayerMask DetectLayers { get { return m_DetectLayers; } set { m_DetectLayers = value; } } public float MaxAngle { get { return m_MaxAngle; } set { m_MaxAngle = value; } } public int TargetCount { get { return m_TargetCount; } set { m_TargetCount = value; } } public Transform SurfaceIndicator { get { return m_SurfaceIndicator; } } public Vector3 SurfaceIndicatorOffset { get { return m_SurfaceIndicatorOffset; } set { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER // The local surface indicator should not show for remote players. if (m_NetworkInfo != null && !m_NetworkInfo.IsLocalPlayer()) { return; } #endif m_SurfaceIndicatorOffset = value; } } public CastUseType UseType { get { return m_UseType; } set { m_UseType = value; } } public float MinContinuousUseDuration { get { return m_MinContinuousUseDuration; } set { m_MinContinuousUseDuration = value; } } public bool ContinuousCast { get { return m_ContinuousCast; } set { m_ContinuousCast = value; } } public int UseAmount { get { return m_UseAmount; } set { m_UseAmount = value; } } public CastInterruptSource InterruptSource { get { return m_InterruptSource; } set { m_InterruptSource = value; } } public SurfaceImpact SurfaceImpact { get { return m_SurfaceImpact; } set { m_SurfaceImpact = value; } } public int CanStopSubstateIndexAddition { get { return m_CanStopSubstateIndexAddition; } set { m_CanStopSubstateIndexAddition = value; } } public Serialization[] BeginActionData { get { return m_BeginActionData; } set { m_BeginActionData = value; if (!Application.isPlaying) { DeserializeBeginActions(false); } } } public Serialization[] CastActionData { get { return m_CastActionData; } set { m_CastActionData = value; if (!Application.isPlaying) { DeserializeCastActions(false); } } } public Serialization[] ImpactActionData { get { return m_ImpactActionData; } set { m_ImpactActionData = value; if (!Application.isPlaying) { DeserializeImpactActions(false); } } } public Serialization[] EndActionData { get { return m_EndActionData; } set { m_EndActionData = value; if (!Application.isPlaying) { DeserializeEndActions(false); } } } private Transform m_CharacterTransform; private UltimateCharacterLocomotion m_CharacterLocomotion; private CharacterLayerManager m_CharacterLayerManager; private IMagicItemPerspectiveProperties m_MagicItemPerspectiveProperties; private BeginEndAction[] m_BeginActions; private CastAction[] m_CastActions; private ImpactAction[] m_ImpactActions; private BeginEndAction[] m_EndActions; private ItemAbility m_StartAbility; private Collider[] m_TargetColliders; private float[] m_TargetAngles; private float m_StartUseTime; private float m_StartCastTime; private Vector3 m_CastDirection; private Vector3 m_CastPosition; private Vector3 m_CastNormal; private bool m_Used; private uint m_CastID; private bool[] m_CastActionUsed; private bool[] m_CastActionCasted; private bool m_InterruptImpact; private bool m_StopRequested; private bool m_Stopping; private bool m_ForceStop; public GameObject Character { get { return m_Character; } } #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER public INetworkInfo NetworkInfo { get { return m_NetworkInfo; } } public INetworkCharacter NetworkCharacter { get { return m_NetworkCharacter; } } #endif public IMagicItemPerspectiveProperties MagicItemPerspectiveProperties { get { return m_MagicItemPerspectiveProperties; } } public BeginEndAction[] BeginActions { get { if (!Application.isPlaying && m_BeginActions == null) { DeserializeBeginActions(false); } return m_BeginActions; } set { m_BeginActions = value; } } public CastAction[] CastActions { get { if (!Application.isPlaying && m_CastActions == null) { DeserializeCastActions(false); } return m_CastActions; } set { m_CastActions = value; } } public ImpactAction[] ImpactActions { get { if (!Application.isPlaying && m_ImpactActions == null) { DeserializeImpactActions(false); } return m_ImpactActions; } set { m_ImpactActions = value; } } public BeginEndAction[] EndActions { get { if (!Application.isPlaying && m_EndActions == null) { DeserializeEndActions(false); } return m_EndActions; } set { m_EndActions = value; } } /// /// Initialize the default values. /// protected override void Awake() { base.Awake(); m_CharacterTransform = m_Character.transform; m_CharacterLocomotion = m_Character.GetCachedComponent(); m_CharacterLayerManager = m_Character.GetCachedComponent(); m_MagicItemPerspectiveProperties = m_ActivePerspectiveProperties as IMagicItemPerspectiveProperties; DeserializeBeginActions(false); DeserializeCastActions(false); DeserializeImpactActions(false); DeserializeEndActions(false); if (m_CastActions != null) { m_CastActionUsed = new bool[m_CastActions.Length]; m_CastActionCasted = new bool[m_CastActions.Length]; } if (m_SurfaceIndicator != null) { m_SurfaceIndicator.gameObject.SetActive(false); } #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER // The local surface indicator should not show for remote players. if (m_NetworkInfo != null && !m_NetworkInfo.IsLocalPlayer()) { m_SurfaceIndicator = null; } #endif } /// /// Deserialize the start actions. /// /// Should the actions be force deserialized? public void DeserializeBeginActions(bool forceDeserialization) { // The begin actions only need to be deserialized once. if (m_BeginActions != null && !forceDeserialization) { return; } if (m_BeginActionData != null) { m_BeginActions = new BeginEndAction[m_BeginActionData.Length]; for (int i = 0; i < m_BeginActions.Length; ++i) { m_BeginActions[i] = m_BeginActionData[i].DeserializeFields(MemberVisibility.Public) as BeginEndAction; if (Application.isPlaying) { m_BeginActions[i].Initialize(m_Character, this, true, i); } } } } /// /// Deserialize the cast actions. /// /// Should the actions be force deserialized? public void DeserializeCastActions(bool forceDeserialization) { // The cast actions only need to be deserialized once. if (m_CastActions != null && !forceDeserialization) { return; } if (m_CastActionData != null) { m_CastActions = new CastAction[m_CastActionData.Length]; for (int i = 0; i < m_CastActions.Length; ++i) { m_CastActions[i] = m_CastActionData[i].DeserializeFields(MemberVisibility.Public) as CastAction; if (Application.isPlaying) { m_CastActions[i].Initialize(m_Character, this, i); } } } } /// /// Deserialize the impact actions. /// /// Should the actions be force deserialized? public void DeserializeImpactActions(bool forceDeserialization) { // The impact actions only need to be deserialized once. if (m_ImpactActions != null && !forceDeserialization) { return; } if (m_ImpactActionData != null) { m_ImpactActions = new ImpactAction[m_ImpactActionData.Length]; for (int i = 0; i < m_ImpactActions.Length; ++i) { m_ImpactActions[i] = m_ImpactActionData[i].DeserializeFields(MemberVisibility.Public) as ImpactAction; if (Application.isPlaying) { m_ImpactActions[i].Initialize(m_Character, this, i); } } } } /// /// Deserialize the end actions. /// /// Should the actions be force deserialized? public void DeserializeEndActions(bool forceDeserialization) { // The end actions only need to be deserialized once. if (m_EndActions != null && !forceDeserialization) { return; } if (m_EndActionData != null) { m_EndActions = new BeginEndAction[m_EndActionData.Length]; for (int i = 0; i < m_EndActions.Length; ++i) { m_EndActions[i] = m_EndActionData[i].DeserializeFields(MemberVisibility.Public) as BeginEndAction; if (Application.isPlaying) { m_EndActions[i].Initialize(m_Character, this, false, i); } } } } /// /// Initializes any values that require on other components to first initialize. /// protected override void Start() { base.Start(); if (m_CastActions != null) { // Awake is called when the item is ready to go (within start). for (int i = 0; i < m_CastActions.Length; ++i) { m_CastActions[i].Awake(); } } if (m_MagicItemPerspectiveProperties == null) { m_MagicItemPerspectiveProperties = m_ActivePerspectiveProperties as IMagicItemPerspectiveProperties; if (m_MagicItemPerspectiveProperties == null) { Debug.LogError($"Error: The First/Third Person Magic Item Properties component cannot be found for the Item {name}." + $"Ensure the component exists and the component's Action ID matches the Action ID of the Item ({m_ID})."); } } } /// /// The item has been equipped by the character. /// public override void Equip() { base.Equip(); EventHandler.RegisterEvent(m_Character, "OnCharacterMoving", OnMoving); EventHandler.RegisterEvent(m_Character, "OnCharacterAbilityActive", OnAbilityActive); EventHandler.RegisterEvent(m_Character, "OnHealthDamage", OnDamage); } /// /// Can the item be used? /// /// The itemAbility that is trying to use the item. /// The state of the Use ability when calling CanUseItem. /// True if the item can be used. public override bool CanUseItem(ItemAbility itemAbility, UseAbilityState abilityState) { if (!base.CanUseItem(itemAbility, abilityState)) { return false; } if (abilityState == UseAbilityState.Update) { // Don't allow the item to continue to be reused if it can no longer be used. if (m_Used && CanStopItemUse()) { return false; } } else if (abilityState == UseAbilityState.Start) { // Certain items require the character to be grounded. if (m_RequireGrounded && !m_CharacterLocomotion.Grounded) { return false; } // The item can't start if it is in the process of being stopped. if (m_Stopping) { return false; } // If the cast isn't valid then the item shouldn't start. if (m_Direction == CastDirection.Target) { DetermineTargetColliders(); } if (!DetermineCastValues(0, ref m_CastDirection, ref m_CastPosition, ref m_CastNormal)) { return false; } } return true; } /// /// Can the ability be started? /// /// The ability that is trying to start. /// True if the ability can be started. public override bool CanStartAbility(Ability ability) { if (!m_StartAbility.AllowPositionalInput && ability is StoredInputAbilityBase) { return false; } if (!(ability is Jump)) { return true; } return !m_RequireGrounded; } /// /// Starts the item use. /// /// The item ability that is using the item. public override void StartItemUse(ItemAbility itemAbility) { base.StartItemUse(itemAbility); m_StartUseTime = Time.time; m_StartCastTime = 0; m_StartAbility = itemAbility; // The Begin Actions allows the effect to play any starting effect. StartStopBeginEndActions(true, true, false); // Reset the cast action used time. if (m_CastActionUsed != null) { for (int i = 0; i < m_CastActionUsed.Length; ++i) { m_CastActionUsed[i] = m_CastActionCasted[i] = false; } } } /// /// Starts or stops the begin or end actions. /// /// Should the begin actions be started? /// Should the actions be started? /// Should the event be sent over the network? public void StartStopBeginEndActions(bool beginActions, bool start, bool networkEvent) { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (networkEvent && m_NetworkInfo != null && m_NetworkInfo.IsLocalPlayer()) { m_NetworkCharacter.StartStopBeginEndMagicActions(this, beginActions, start); } #endif var actions = beginActions ? m_BeginActions : m_EndActions; if (actions != null) { for (int i = 0; i < actions.Length; ++i) { if (start) { actions[i].Start(m_MagicItemPerspectiveProperties.OriginLocation); } else { actions[i].Stop(); } } } } /// /// Uses the item. /// public override void UseItem() { base.UseItem(); // If the item hasn't been used yet the begin actions stop. if (m_StartCastTime == 0) { m_StartCastTime = Time.time; StartStopBeginEndActions(true, false, true); } if (m_CastActions != null) { // Only cast the actions that haven't been casted yet. var actionsCasted = !m_ContinuousCast; if (!m_ContinuousCast) { for (int i = 0; i < m_CastActionUsed.Length; ++i) { if (!m_CastActionUsed[i]) { actionsCasted = false; break; } } } if (!actionsCasted) { var useCount = m_Direction == CastDirection.Target ? m_TargetCount : 1; for (int i = 0; i < useCount; ++i) { // The values should be updated for the current cast. if (m_Used || i > 0) { if (!DetermineCastValues(i, ref m_CastDirection, ref m_CastPosition, ref m_CastNormal)) { continue; } } for (int j = 0; j < m_CastActions.Length; ++j) { if (!m_ContinuousCast && m_CastActionUsed[j]) { continue; } // The action may not need to cast if it's not time yet. if (m_StartCastTime + m_CastActions[j].Delay > Time.time) { continue; } if (m_CastActions[j].CastID == 0) { m_CastActions[j].CastID = ++m_CastID; } m_CastActions[j].Cast(m_MagicItemPerspectiveProperties.OriginLocation, m_CastDirection, m_CastPosition); m_CastActionCasted[j] = !m_ContinuousCast; } } // Synchronize the used array with the casted array. This will allow the same action to be performed for multiple targets. if (!m_ContinuousCast) { for (int i = 0; i < m_CastActionUsed.Length; ++i) { m_CastActionUsed[i] = m_CastActionCasted[i]; } } } } if (!m_Used) { // The item isn't done being used until all actions have been used. if (!m_ContinuousCast) { for (int i = 0; i < m_CastActionUsed.Length; ++i) { if (!m_CastActionUsed[i]) { return; } } } // If the item was just used the end actions should start. if (m_UseType == CastUseType.Single) { StartStopBeginEndActions(false, true, true); } m_Inventory.AdjustItemIdentifierAmount(m_Item.ItemIdentifier, m_UseAmount); m_Used = true; } } /// /// Determines the colliders that are hit by the target direction. /// private void DetermineTargetColliders() { if (m_TargetColliders == null) { m_TargetColliders = new Collider[m_MaxCollisionCount]; m_TargetAngles = new float[m_MaxCollisionCount]; // Initialize to the max value. for (int i = 0; i < m_TargetAngles.Length; ++i) { m_TargetAngles[i] = int.MaxValue; } } var hitCount = Physics.OverlapSphereNonAlloc(m_CharacterTransform.position, m_MaxDistance, m_TargetColliders, m_DetectLayers, QueryTriggerInteraction.Ignore); if (hitCount == 0) { return; } #if UNITY_EDITOR if (hitCount == m_TargetColliders.Length) { Debug.LogWarning("Warning: The hit count is equal to the max collider array size. This will cause objects to be missed. Consider increasing the max collision count size."); } #endif for (int i = 0; i < hitCount; ++i) { if (m_TargetColliders[i].transform.IsChildOf(m_CharacterTransform)) { m_TargetAngles[i] = int.MaxValue; continue; } // The target object needs to be within the field of view of the current object var direction = m_TargetColliders[i].transform.position - m_CharacterTransform.position; var angle = Vector3.Angle(direction, transform.forward); if (angle < m_MaxAngle * 0.5f) { // The target must be within sight. var hitTransform = false; if (Physics.Linecast(m_CharacterTransform.position, m_TargetColliders[i].transform.position, out var raycastHit, m_CharacterLayerManager.IgnoreInvisibleCharacterWaterLayers, QueryTriggerInteraction.Ignore)) { if (raycastHit.transform.IsChildOf(m_TargetColliders[i].transform) || raycastHit.transform.IsChildOf(m_CharacterTransform)) { hitTransform = true; } } // Find the target that is most in front of the character. m_TargetAngles[i] = hitTransform ? angle : int.MaxValue; } else { m_TargetAngles[i] = int.MaxValue; } } // Sort by the angle. Return the min angle. System.Array.Sort(m_TargetAngles, m_TargetColliders); } /// /// Determines the values of the cast. /// /// The index of the target position to retrieve. /// A reference to the target direction. /// A reference to the target position. /// A reference to the target normal. /// True if the cast is valid. private bool DetermineCastValues(int index, ref Vector3 direction, ref Vector3 position, ref Vector3 normal) { if (m_Direction == CastDirection.Forward || m_Direction == CastDirection.Indicate) { Vector3 castPosition; if (m_Direction == CastDirection.Forward) { direction = m_UseLookSource ? m_LookSource.LookDirection(false) : m_CharacterTransform.forward; castPosition = m_UseLookSource ? m_LookSource.LookPosition() : m_CharacterTransform.position; } else { // Indicate. direction = m_LookSource.LookDirection(false); castPosition = m_LookSource.LookPosition(); } m_CharacterLocomotion.EnableColliderCollisionLayer(false); var validCast = Physics.SphereCast(castPosition - direction * m_Radius, m_Radius, direction, out var raycastHit, m_MaxDistance, m_DetectLayers, QueryTriggerInteraction.Ignore); if (validCast) { // The Cast Actions may indicate that the position is invalid. if (m_CastActions != null) { for (int i = 0; i < m_CastActions.Length; ++i) { if (!m_CastActions[i].IsValidTargetPosition(raycastHit.point, raycastHit.normal)) { return false; } } } position = raycastHit.point; normal = raycastHit.normal; } m_CharacterLocomotion.EnableColliderCollisionLayer(true); return validCast; } else if (m_Direction == CastDirection.Target) { if (index >= m_TargetAngles.Length || m_TargetAngles[index] == int.MaxValue) { return false; } var targetTransform = m_TargetColliders[index].transform; PivotOffset pivotOffset; if ((pivotOffset = targetTransform.gameObject.GetCachedComponent()) != null) { position = targetTransform.TransformPoint(pivotOffset.Offset); } else { position = targetTransform.position; } direction = (position - m_MagicItemPerspectiveProperties.OriginLocation.position).normalized; normal = targetTransform.up; return true; } // None direction. direction = m_CharacterTransform.forward; position = m_CharacterTransform.position; normal = m_CharacterTransform.up; return true; } /// /// Allows the item to update while it is being used. /// public override void UseItemUpdate() { base.UseItemUpdate(); var beginEndActions = (m_Used ? m_EndActions : m_BeginActions); if (beginEndActions != null) { for (int i = 0; i < beginEndActions.Length; ++i) { beginEndActions[i].Update(); } } } /// /// Is the item waiting to be used? This will return true if the item is waiting to be charged or pulled back. /// /// Returns true if the item is waiting to be used. public override bool IsItemUsePending() { return !CanStopItemUse(); } /// /// Returns the substate index that the item should be in. /// /// the substate index that the item should be in. public override int GetItemSubstateIndex() { return base.GetItemSubstateIndex() + (CanStopItemUse() ? m_CanStopSubstateIndexAddition : 0); } /// /// The item has been used. /// public override void ItemUseComplete() { base.ItemUseComplete(); for (int i = 0; i < m_CastActions.Length; ++i) { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo != null && m_NetworkInfo.IsLocalPlayer()) { m_NetworkCharacter.StopMagicCast(this, i, m_CastActions[i].CastID); } #endif m_CastActions[i].Stop(); } } /// /// Draws an indicator when the direction is indicate. /// public void LateUpdate() { var direction = Vector3.zero; var position = Vector3.zero; var normal = Vector3.zero; if (m_Direction != CastDirection.Indicate || !DetermineCastValues(-1, ref direction, ref position, ref normal)) { if (m_SurfaceIndicator != null && m_SurfaceIndicator.gameObject.activeSelf) { m_SurfaceIndicator.gameObject.SetActive(false); } return; } // The position is valid. Show an optional indicator. if (m_SurfaceIndicator != null) { m_SurfaceIndicator.rotation = Quaternion.LookRotation(Vector3.ProjectOnPlane(direction, normal)); m_SurfaceIndicator.position = position + m_SurfaceIndicator.TransformDirection(m_SurfaceIndicatorOffset); if (!m_SurfaceIndicator.gameObject.activeSelf) { m_SurfaceIndicator.gameObject.SetActive(true); } } } /// /// A cast has caused a collision. Perform the impact actions. /// /// The ID of the cast. /// The object that caused the cast. /// The object that was hit by the cast. /// The raycast that caused the impact. public void PerformImpact(uint castID, GameObject source, GameObject target, RaycastHit hit) { if (!IsValidCollisionObject(target)) { return; } #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo != null && m_NetworkInfo.IsLocalPlayer()) { m_NetworkCharacter.MagicImpact(this, castID, source, target, hit.point, hit.normal); } #endif m_InterruptImpact = false; if (m_ImpactActions != null) { for (int i = 0; i < m_ImpactActions.Length; ++i) { // Stop calling interrupt if InteruptImpact was triggered. if (m_InterruptImpact) { break; } m_ImpactActions[i].Impact(castID, source, target, hit); } } // Execute an event to allow interested objects know about the collision. EventHandler.ExecuteEvent(target, "OnMagicCastCollision", hit, m_SurfaceImpact); } /// /// Returns true if the object can be collided with. /// /// The object that may be able to be collided with. /// True if the object can be collided with. private bool IsValidCollisionObject(GameObject other) { if (m_Direction == CastDirection.Target) { return MathUtility.InLayerMask(other.layer, m_DetectLayers); } return true; } /// /// The impact actions should be interrupted. Consider the case where the first action deducts health and the second action plays a particle effect. /// The particle effect should not be played if the object does not have the Health component. /// public void InterruptImpact() { m_InterruptImpact = true; } /// /// Tries to stop the item use. /// public override void TryStopItemUse() { base.TryStopItemUse(); // The end actions aren't called until the continuous use item stops. m_StopRequested = true; if (!m_Stopping && CanStopItemUse()) { m_Stopping = true; m_CharacterLocomotion.UpdateItemAbilityAnimatorParameters(); if (m_CastActions != null) { for (int i = 0; i < m_CastActions.Length; ++i) { m_CastActions[i].WillStop(); } } if (m_UseType == CastUseType.Continuous) { StartStopBeginEndActions(false, true, true); } } } /// /// Can the item use be stopped? /// /// True if the item use can be stopped. public override bool CanStopItemUse() { // The item can always stop when the attributes are no longer valid. if ((m_UseAttribute != null && !m_UseAttribute.IsValid(-m_UseAttributeAmount)) || (m_CharacterUseAttribute != null && !m_CharacterUseAttribute.IsValid(-m_CharacterUseAttributeAmount))) { return true; } return m_UseType == CastUseType.Single || (m_StopRequested && (m_MinContinuousUseDuration == -1 || (m_MinContinuousUseDuration > 0 && m_StartUseTime + m_MinContinuousUseDuration < Time.time))); } /// /// Stops the item use. /// public override void StopItemUse() { base.StopItemUse(); // If Force Stop is true then the cast was interrupted. Reset the objects. if (m_ForceStop) { ItemUseComplete(); StartStopBeginEndActions(true, false, false); } m_Used = false; m_Stopping = m_StopRequested = false; m_ForceStop = false; StartStopBeginEndActions(false, false, false); } /// /// The item has started to be unequipped by the character. /// public override void StartUnequip() { base.StartUnequip(); if (m_SurfaceIndicator != null && m_SurfaceIndicator.gameObject.activeSelf) { m_SurfaceIndicator.gameObject.SetActive(false); } } /// /// The item has been unequipped by the character. /// public override void Unequip() { base.Unequip(); EventHandler.UnregisterEvent(m_Character, "OnCharacterMoving", OnMoving); EventHandler.UnregisterEvent(m_Character, "OnCharacterAbilityActive", OnAbilityActive); EventHandler.UnregisterEvent(m_Character, "OnHealthDamage", OnDamage); } /// /// The character has started to or stopped moving. /// /// Is the character moving? private void OnMoving(bool moving) { // Stop the item if the character starts to move and the cast should be interrupted on movement. if (moving && (m_InterruptSource & CastInterruptSource.Movement) != 0 && IsItemInUse()) { m_ForceStop = true; m_StartAbility.StopAbility(true); } } /// /// The character's ability has been started or stopped. /// /// The ability which was started or stopped. /// True if the ability was started, false if it was stopped. private void OnAbilityActive(Ability ability, bool active) { if (!active || (!(ability is Jump) && !(ability is Fall))) { return; } // Stop the item if the character starts to jump or fall and the cast should be interrupted on movement. if ((m_InterruptSource & CastInterruptSource.Movement) != 0 && IsItemInUse()) { m_ForceStop = true; m_StartAbility.StopAbility(true); } } /// /// The character has taken damage. /// /// The amount of damage taken. /// The position of the damage. /// The amount of force applied to the object while taking the damage. /// The GameObject that did the damage. /// The Collider that was hit. private void OnDamage(float amount, Vector3 position, Vector3 force, GameObject attacker, Collider hitCollider) { // The item can stop the use ability when the character takes damage. if ((m_InterruptSource & CastInterruptSource.Damage) != 0 && IsItemInUse()) { m_ForceStop = true; m_StartAbility.StopAbility(true); } } /// /// The camera perspective between first and third person has changed. /// /// Is the character in a first person perspective? protected override void OnChangePerspectives(bool firstPersonPerspective) { base.OnChangePerspectives(firstPersonPerspective); var targetMagicItemPerspectiveProperties = m_ActivePerspectiveProperties as IMagicItemPerspectiveProperties; // The OriginLocation cannot be null. if (targetMagicItemPerspectiveProperties.OriginLocation == null) { Debug.LogError($"Error: The OriginLocation is null on the {name} MagicItemPerspectiveProperties."); return; } m_MagicItemPerspectiveProperties = targetMagicItemPerspectiveProperties; if (m_BeginActions != null) { for (int i = 0; i < m_BeginActions.Length; ++i) { m_BeginActions[i].OnChangePerspectives(m_MagicItemPerspectiveProperties.OriginLocation); } } if (m_CastActions != null) { for (int i = 0; i < m_CastActions.Length; ++i) { m_CastActions[i].OnChangePerspectives(m_MagicItemPerspectiveProperties.OriginLocation); } } if (m_EndActions != null) { for (int i = 0; i < m_EndActions.Length; ++i) { m_EndActions[i].OnChangePerspectives(m_MagicItemPerspectiveProperties.OriginLocation); } } } /// /// The item has been destroyed. /// protected override void OnDestroy() { base.OnDestroy(); if (m_BeginActions != null) { for (int i = 0; i < m_BeginActions.Length; ++i) { m_BeginActions[i].OnDestroy(); } } if (m_CastActions != null) { for (int i = 0; i < m_CastActions.Length; ++i) { m_CastActions[i].OnDestroy(); } } if (m_ImpactActions != null) { for (int i = 0; i < m_ImpactActions.Length; ++i) { m_ImpactActions[i].OnDestroy(); } } if (m_EndActions != null) { for (int i = 0; i < m_EndActions.Length; ++i) { m_EndActions[i].OnDestroy(); } } } } }