using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using NeoFPS.Constants; using NeoSaveGames.Serialization; using NeoSaveGames; using UnityEngine.Serialization; namespace NeoFPS { [HelpURL("https://docs.neofps.com/manual/weaponsref-mb-meleeweapon.html")] [RequireComponent(typeof(AudioSource))] public class MeleeWeapon : BaseMeleeWeapon { [Header("Melee Properties")] [SerializeField, Tooltip("The damage the weapon does.")] private float m_Damage = 50f; [SerializeField, Tooltip("The force to impart on the hit object. Requires either a [Rigidbody][unity-rigidbody] or an impact handler on the hit object.")] private float m_ImpactForce = 15f; [SerializeField, Tooltip("The layers the attack will collide with.")] private LayerMask m_Layers = PhysicsFilter.Masks.BulletBlockers; [SerializeField, Tooltip("Should the attack be tested against trigger colliders.")] private bool m_QueryTriggerColliders = false; [SerializeField, Tooltip("The range that the melee weapon can reach.")] private float m_Range = 1f; [SerializeField, Tooltip("The delay from starting the attack to checking for an impact. Should be synced with the striking point in the animation.")] private float m_Delay = 0.6f; [SerializeField, Tooltip("The recovery time after a hit.")] private float m_RecoverTime = 1f; [Header("Melee Animation")] [SerializeField, AnimatorParameterKey("m_Animator", AnimatorControllerParameterType.Trigger), Tooltip("The animation trigger for the attack animation.")] private string m_TriggerAttack = "Attack"; [SerializeField, AnimatorParameterKey("m_Animator", AnimatorControllerParameterType.Trigger), Tooltip("The animation trigger for the attack hit animation.")] private string m_TriggerAttackHit = "AttackHit"; [SerializeField, AnimatorParameterKey("m_Animator", AnimatorControllerParameterType.Bool), Tooltip("The animation bool parameter for the block animation."), FormerlySerializedAs("m_TriggerBlock")] private string m_BoolBlock = "Block"; [Header("Melee Audio")] [SerializeField, Tooltip("The audio clip when attacking.")] private AudioClip m_AudioAttack = null; [SerializeField, Tooltip("The audio clip when bringing the weapon into block position.")] private AudioClip m_AudioBlockRaise = null; [SerializeField, Tooltip("The audio clip when bringing the weapon out of block position.")] private AudioClip m_AudioBlockLower = null; private static List s_DamageHandlers = new List(4); private int m_AnimHashBlock = 0; private int m_AnimHashAttack = 0; private int m_AnimHashAttackHit = 0; private IMeleeHitExtension[] m_HitExtensions = null; private float m_CooldownTimer = 0f; private float m_DelayTimer = 0f; public enum HitDetectType { Raycast, Spherecast, Custom } #if UNITY_EDITOR protected override void OnValidate () { base.OnValidate(); // Limit values if (m_Damage < 0f) m_Damage = 0f; if (m_ImpactForce < 0f) m_ImpactForce = 0f; if (m_Delay < 0f) m_Delay = 0f; if (m_RecoverTime < 0f) m_RecoverTime = 0f; m_Range = Mathf.Clamp(m_Range, 0.1f, 5f); } #endif protected override void Awake () { base.Awake(); m_AnimHashBlock = Animator.StringToHash (m_BoolBlock); m_AnimHashAttack = Animator.StringToHash (m_TriggerAttack); m_AnimHashAttackHit = Animator.StringToHash (m_TriggerAttackHit); m_HitExtensions = GetComponents(); } protected override void OnDisable() { base.OnDisable(); blocking = false; // Clear coroutines if (m_CooldownCoroutine != null) { StopCoroutine(m_CooldownCoroutine); m_CooldownCoroutine = null; } if (m_DoRaycastCoroutine != null) { StopCoroutine(m_DoRaycastCoroutine); m_DoRaycastCoroutine = null; } } public override void PrimaryPress () { if (!blocking && m_CooldownCoroutine == null) { animator.SetTrigger (m_AnimHashAttack); m_DoRaycastCoroutine = StartCoroutine (DoRaycast (m_Delay)); m_CooldownCoroutine = StartCoroutine (Cooldown (m_RecoverTime)); if (m_AudioAttack != null) audioSource.PlayOneShot(m_AudioAttack); attacking = true; } } public override void PrimaryRelease() { // Current implementation are all one-shots } public override void SecondaryPress() { blocking = true; } public override void SecondaryRelease() { blocking = false; } Coroutine m_CooldownCoroutine = null; IEnumerator Cooldown (float timer) { m_CooldownTimer = timer; while (m_CooldownTimer > 0f) { yield return null; m_CooldownTimer -= Time.deltaTime; } attacking = false; m_CooldownCoroutine = null; } Coroutine m_DoRaycastCoroutine = null; IEnumerator DoRaycast (float timer) { m_DelayTimer = timer; while (m_DelayTimer > 0f) { yield return null; m_DelayTimer -= Time.deltaTime; } Vector3 direction = transform.forward; // Get root game object to prevent impacts with body Transform ignoreRoot = null; if (wielder != null) ignoreRoot = wielder.gameObject.transform; RaycastHit hit; if (PhysicsExtensions.RaycastNonAllocSingle ( new Ray (transform.position, direction), out hit, m_Range, m_Layers, ignoreRoot, m_QueryTriggerColliders ? QueryTriggerInteraction.Collide : QueryTriggerInteraction.Ignore )) { OnMeleeHit(hit, direction); } m_DoRaycastCoroutine = null; } protected virtual void OnMeleeHit(RaycastHit hit, Vector3 attackDirection) { // Show effect SurfaceManager.ShowBulletHit(hit, attackDirection, 1f, hit.rigidbody != null); // Apply damage hit.transform.GetComponents(s_DamageHandlers); for (int i = 0; i < s_DamageHandlers.Count; ++i) s_DamageHandlers[i].AddDamage(m_Damage, hit, this); s_DamageHandlers.Clear(); // Apply force if (hit.rigidbody != null) hit.rigidbody.AddForceAtPosition(attackDirection * m_ImpactForce, hit.point, ForceMode.Impulse); else { IImpactHandler impactHandler = hit.transform.GetComponent(); if (impactHandler != null) impactHandler.HandlePointImpact(hit.point, attackDirection * m_ImpactForce); } // Trigger hit animation animator.SetTrigger(m_AnimHashAttackHit); // Process hit extensions for (int i = 0; i < m_HitExtensions.Length; ++i) m_HitExtensions[i].OnMeleeHit(hit); } protected override void OnBlockStateChange(bool to) { base.OnBlockStateChange(to); // Set animator parameter animator.SetBool(m_AnimHashBlock, to); // Play audio if (to) { if (m_AudioBlockRaise != null) audioSource.PlayOneShot(m_AudioBlockRaise); } else { if (m_AudioBlockLower != null) audioSource.PlayOneShot(m_AudioBlockLower); } } #region INeoSerializableComponent IMPLEMENTATION private static readonly NeoSerializationKey k_CooldownKey = new NeoSerializationKey("cooldown"); private static readonly NeoSerializationKey k_DelayTimerKey = new NeoSerializationKey("delayTimer"); public override void WriteProperties(INeoSerializer writer, NeoSerializedGameObject nsgo, SaveMode saveMode) { base.WriteProperties(writer, nsgo, saveMode); if (saveMode == SaveMode.Default) { // Write coroutines if relevant if (m_CooldownCoroutine != null) writer.WriteValue(k_CooldownKey, m_CooldownTimer); if (m_DoRaycastCoroutine != null) writer.WriteValue(k_DelayTimerKey, m_DelayTimer); } } public override void ReadProperties(INeoDeserializer reader, NeoSerializedGameObject nsgo) { base.ReadProperties(reader, nsgo); // Read and start coroutines if relevant float floatResult = 0f; if (reader.TryReadValue(k_DelayTimerKey, out floatResult, 0f)) m_DoRaycastCoroutine = StartCoroutine(DoRaycast(floatResult)); if (reader.TryReadValue(k_CooldownKey, out floatResult, 0f)) m_CooldownCoroutine = StartCoroutine(Cooldown(floatResult)); } #endregion } }