using MalbersAnimations.Events; using MalbersAnimations.Scriptables; using UnityEngine; using MalbersAnimations.Utilities; using System.Collections; using MalbersAnimations.Controller.Reactions; #if UNITY_EDITOR using UnityEditor; #endif namespace MalbersAnimations.Controller { /// Core Class to cause damage to the stats // [AddComponentMenu("Malbers/Damage/Damager")] public abstract class MDamager : MonoBehaviour, IMDamager, IInteractor { #region Public Variables /// ID of the Damager. This is called on the Animator to wakes up the Damager [SerializeField, Tooltip("Index of the Damager, used by the Animator know which damager to enable/disable")] protected int index = 1; /// Enable/Disable the Damager [SerializeField, Tooltip("Enable/Disable the Damager")] protected BoolReference m_Active = new BoolReference(true); /// Hit Layer to interact with Objects in case RayCast is used [SerializeField, Tooltip("Hit Layer to interact with Objects"), ContextMenuItem("Get Layer from Root", "GetLayerFromRoot")] protected LayerReference m_hitLayer = new LayerReference(-1); /// What to do with Triggers [SerializeField, Tooltip("What to do with Triggers")] protected QueryTriggerInteraction triggerInteraction = QueryTriggerInteraction.Ignore; /// Owner. usually the Character Owns the Damager [SerializeField, Tooltip("Owner. usually the Character Owns the Damager")] [ContextMenuItem("Find Owner", "Find_Owner")] protected GameObject owner; [SerializeField, Tooltip("This Gameobject will be enabled on Impact, if its a Prefab it will be instantiated")] internal GameObjectReference m_HitEffect; public GameObject HitEffect { get => m_HitEffect.Value; set => m_HitEffect.Value = value; } [Tooltip("The HitEffect will be destroyed after this time has elapsed, if it is a prefab. if = to zero, will be ignored")] [Min(0)] public float DestroyHitEffect; // protected IInteractor interactor; public IntReference interactorID = new IntReference(0); [Tooltip("Dont Hit any objects on the Owner's hierarchy")] public BoolReference dontHitOwner = new BoolReference( true); [Tooltip("Don't use the Default Reaction of the Damageable Component")] [CreateScriptableAsset] public MReaction CustomReaction; /// Extra Transform to Ignore Damage. E.g. The Mount Animal public virtual Transform IgnoreTransform { get; set; } /// Damager can activate interactables [Tooltip("Damager can activate interactables")] public BoolReference interact = new BoolReference(true); /// Damager allows the Damagee to apply a reaction [Tooltip("Damager allows the Damagee to apply an animal reaction")] public BoolReference react = new BoolReference(true); /// Ignores Damagee Multiplier [Tooltip("If true the Damage Receiver will not apply its Default Multiplier")] public BoolReference pureDamage = new BoolReference(false); /// Stat to modify on the Damagee [Tooltip("Stat to modify on the Damagee")] [ContextMenuItem("Set Default Damage", "Set_DefaultDamage")] public StatModifier statModifier = new StatModifier(); /// Critical Change (0 - 1) [SerializeField, Tooltip("Critical Change (0 - 1)\n1 means it will be always critical")] protected FloatReference m_cChance = new FloatReference(0); /// If the Damage is critical, the Stat modifier value will be multiplied by the Critical Multiplier [SerializeField, Tooltip("If the Damage is critical, the Stat modifier value will be multiplied by the Critical Multiplier")] protected FloatReference cMultiplier = new FloatReference(2); [SerializeField, Tooltip("Force to Apply to RigidBodies when the Damager hit them")] protected FloatReference m_Force = new FloatReference(50f); [Tooltip("Force mode to apply to the Object that the Damager Hits")] public ForceMode forceMode = ForceMode.Force; [Tooltip("Direction of the Attack. Used to apply the Force and to know the Direction of the Hit from the Damager")] protected Vector3 Direction = Vector3.forward; public TransformEvent OnHit = new TransformEvent(); public Vector3Event OnHitPosition = new Vector3Event(); public IntEvent OnHitInteractable = new IntEvent(); //[Tooltip("When the Attack Trigger Touches a valid collider, it will stop the animator to give an extra effect")] //public BoolReference StopAnimator = new BoolReference(false); [Tooltip("If there's an Animator Controller it will be stopped")] [ContextMenuItem("Find Animator", "Find_Animator")] public Animator animator; [Tooltip("Value of the Animator Speed when its stopped")] public FloatReference AnimatorSpeed = new FloatReference(0.1f); [Tooltip("Time the Animator will be stopped. If its zero, stopping the animator is ignored")] public FloatReference AnimatorStopTime = new FloatReference(0.2f); #endregion #region Properties /// Owner of the Damager public virtual GameObject Owner { get => owner; set => owner = value; } /// Force of the Damager public float Force { get => m_Force; set => m_Force = value; } public LayerMask Layer { get => m_hitLayer.Value; set => m_hitLayer.Value = value; } public QueryTriggerInteraction TriggerInteraction { get => triggerInteraction; set => triggerInteraction = value; } /// Does the hit was Critical public bool IsCritical { get; set; } public bool debug; /// If the Damage is critical, the Stat modifier value will be multiplied by the Critical Multiplier public float CriticalMultiplier { get => cMultiplier.Value; set => cMultiplier.Value = value; } /// >Critical Change (0 - 1) public float CriticalChance { get => m_cChance.Value; set => m_cChance.Value = value; } /// >Index of the Damager public virtual int Index => index; public virtual int ID => interactorID.Value; /// Set/Get the Damager Active public virtual bool Active { get => m_Active.Value; set => m_Active.Value = enabled = value; } /// Point of of Contact public Vector3 HitPosition { get; private set; } /// Rotation of the Point of contact (Normal) public Quaternion HitRotation { get; private set; } #endregion /// The Damagee does not have all the conditions to apply the Damage public virtual bool IsInvalid(Collider damagee) { if (damagee.isTrigger && TriggerInteraction == QueryTriggerInteraction.Ignore) return true; //just collapse when is a collider what we are hitting if (!MTools.Layer_in_LayerMask(damagee.gameObject.layer, Layer)) { return true; } //Just hit what is on the HitMask Layer if (dontHitOwner && Owner != null && damagee.transform.IsChildOf(Owner.transform)) { return true; } //Dont hit yourself! // if (damagee.gameObject.isStatic) return true; return false; } /// Applies the Damage to the Game object /// is False if the other gameobject didn't had a IMDamage component attached protected virtual bool TryDamage(IMDamage damagee, StatModifier stat) { if (damagee != null && !stat.IsNull) { var criticalStat = IsCriticalStatModifier(stat); damagee.ReceiveDamage(Direction, Owner, criticalStat, IsCritical, react.Value, CustomReaction , pureDamage.Value); Debugging($"Do Damage to [{damagee.Damagee.name}]", damagee.Damagee); return true; } return false; } protected void TryHit(Collider col, Vector3 DamageCenter) { if (col is MeshCollider && !(col as MeshCollider).convex) return; //Do not hit NonConvex Collider if (col is TerrainCollider) return; //Do not hit a Terrain Collider HitPosition = col.ClosestPoint(DamageCenter); //Find the closest point on the Collider hitted HitRotation = Quaternion.FromToRotation(Vector3.up, col.bounds.center - DamageCenter); OnHitPosition.Invoke(HitPosition); MTools.DrawWireSphere(HitPosition, Color.red, 0.2f, 1); if (HitEffect != null) { if (HitEffect.IsPrefab()) { var instance = Instantiate(HitEffect, HitPosition, HitRotation); // instance.transform.parent = col.transform; //Reset the gameobject visibility CheckHitEffect(instance); if (DestroyHitEffect > 0) Destroy(instance, DestroyHitEffect); } else { HitEffect.transform.position = HitPosition; HitEffect.transform.rotation = HitRotation; CheckHitEffect(HitEffect); } } OnHit.Invoke(col.transform); } protected void CheckHitEffect(GameObject hit) { var isDamager = hit.GetComponent(); if (isDamager) { isDamager.Owner = Owner; isDamager.Layer = Layer; isDamager.TriggerInteraction = TriggerInteraction; } //Next Frame Reset the GameObject visibility this.Delay_Action(() => { hit.SetActive(false); hit.SetActive(true); } ); } protected virtual bool TryDamage(GameObject other, StatModifier stat) => TryDamage(other.FindInterface(), stat); /// Activates the Damager in case the Damager uses a Trigger public virtual void DoDamage(bool value) { } protected void TryStopAnimator() { if (animator && C_StopAnim == null) { C_StopAnim = C_StopAnimator(); StartCoroutine(C_StopAnim); } } protected IEnumerator C_StopAnim; protected float defaultAnimatorSpeed = 1; protected IEnumerator C_StopAnimator() { animator.speed = AnimatorSpeed; yield return new WaitForSeconds(AnimatorStopTime.Value); animator.speed = defaultAnimatorSpeed; C_StopAnim = null; } /// Damager can Activate Interactables protected bool TryInteract(GameObject damagee) { if (interact) { var interactable = damagee.FindInterface(); if (interactable != null && interactable.Active) { return Interact(interactable); //if we have an Local Interactor then use it instead of this Damager } } return false; } public void Focus(IInteractable item) { if (item.Active) //Ignore One Disable Interactors { item.CurrentInteractor = this; item.Focused = true; if (item.Auto) Interact(item); //Interact if the interacter is on Auto } } public void UnFocus(IInteractable item) { if (item != null) { item.CurrentInteractor = this; item.Focused = false; item.CurrentInteractor = null; item = null; } } /// Interact locally public bool Interact(IInteractable interactable) { if (interactable != null) { Debugging($"Interact with [{interactable.Owner.name}]", interactable.Owner); if (interactable.Interact(this)) { OnHitInteractable.Invoke(interactable.Index); return true; } return false; } return false; } /// Restart method from Interactor public virtual void Restart() { } /// Apply Physics to the Damageee protected virtual bool TryPhysics(Rigidbody rb, Collider col,Vector3 Origin ,Vector3 Direction, float force) { if (rb && force > 0) { if (col) //When using collider { var HitPoint = col.ClosestPoint(Origin); rb.AddForceAtPosition(Direction * force, HitPoint, forceMode); MTools.DrawWireSphere(HitPoint, Color.red, 0.1f, 2f); MTools.Draw_Arrow(HitPoint, Direction * force, Color.red, 2f); } else rb.AddForce(Direction * force, forceMode); Debugging($"Apply Force to [{rb.name}]", this); return true; } return false; } public void SetOwner(GameObject owner) => Owner = owner; public void SetOwner(Transform owner) => Owner = owner.gameObject; /// Prepare the modifier value to change it if is critical protected StatModifier IsCriticalStatModifier(StatModifier mod) { IsCritical = m_cChance > Random.value; //Calculate if is critical var modifier = new StatModifier(mod); if (IsCritical && CriticalChance > 0) { modifier.Value = mod.Value * CriticalMultiplier; //apply the Critical Damage } return modifier; } //protected void GetLayerFromRoot() //{ // var HitMaskOwner = m_Owner.GetComponentInParent(); // if (HitMaskOwner != null) // { // Layer = HitMaskOwner.Layer; // Debugging($"{name} Layer set by its Root: {transform.root}",null); // } //} protected void Find_Owner() { if (Owner == null) Owner = transform.root.gameObject; MTools.SetDirty(this); } protected void Find_Animator() { if (animator == null) animator = gameObject.FindComponent(); MTools.SetDirty(this); } #if UNITY_EDITOR protected virtual void Reset() { statModifier = new StatModifier() { ID = MTools.GetInstance("Health"), modify = StatOption.SubstractValue, Value = new FloatReference(10) }; m_hitLayer.Variable = MTools.GetInstance("Hit Layer"); m_hitLayer.UseConstant = false; owner = transform.root.gameObject; } #endif public void Debugging(string value, Object obj) { #if UNITY_EDITOR if (debug) Debug.Log($"[{Owner.name} - {name}] → {value} " ,obj); #endif } } ///--------------------------------INSPECTOR------------------- /// #if UNITY_EDITOR [CustomEditor(typeof(MDamager)),CanEditMultipleObjects] public class MDamagerEd : Editor { public static GUIStyle StyleBlue => MTools.Style(new Color(0, 0.5f, 1f, 0.3f)); protected MonoScript script; protected MDamager MD; protected SerializedProperty Force, forceMode, index, statModifier, onhit, OnHitPosition, OnHitInteractable, dontHitOwner, owner, m_Active, debug, hitLayer, triggerInteraction, m_cChance, cMultiplier, pureDamage, react, CustomReaction, interact , m_HitEffect, interactorID, DestroyHitEffect, StopAnimator, AnimatorSpeed, AnimatorStopTime, animator; private void OnEnable() => FindBaseProperties(); protected virtual void FindBaseProperties() { script = MonoScript.FromMonoBehaviour((MonoBehaviour)target); MD = (MDamager)target; index = serializedObject.FindProperty("index"); m_HitEffect = serializedObject.FindProperty("m_HitEffect"); OnHitPosition = serializedObject.FindProperty("OnHitPosition"); m_Active = serializedObject.FindProperty("m_Active"); hitLayer = serializedObject.FindProperty("m_hitLayer"); triggerInteraction = serializedObject.FindProperty("triggerInteraction"); dontHitOwner = serializedObject.FindProperty("dontHitOwner"); owner = serializedObject.FindProperty("owner"); interactorID = serializedObject.FindProperty("interactorID"); DestroyHitEffect = serializedObject.FindProperty("DestroyHitEffect"); react = serializedObject.FindProperty("react"); CustomReaction = serializedObject.FindProperty("CustomReaction"); interact = serializedObject.FindProperty("interact"); pureDamage = serializedObject.FindProperty("pureDamage"); m_cChance = serializedObject.FindProperty("m_cChance"); cMultiplier = serializedObject.FindProperty("cMultiplier"); Force = serializedObject.FindProperty("m_Force"); forceMode = serializedObject.FindProperty("forceMode"); statModifier = serializedObject.FindProperty("statModifier"); onhit = serializedObject.FindProperty("OnHit"); OnHitInteractable = serializedObject.FindProperty("OnHitInteractable"); debug = serializedObject.FindProperty("debug"); StopAnimator = serializedObject.FindProperty("StopAnimator"); animator = serializedObject.FindProperty("animator"); AnimatorSpeed = serializedObject.FindProperty("AnimatorSpeed"); AnimatorStopTime = serializedObject.FindProperty("AnimatorStopTime"); } public override void OnInspectorGUI() { serializedObject.Update(); DrawDescription("Damager Core Logic"); // DrawScript(); DrawGeneral(); DrawPhysics(); DrawCriticalDamage(); DrawStatModifier(); DrawMisc(); DrawEvents(); serializedObject.ApplyModifiedProperties(); } protected void DrawEvents() { EditorGUILayout.BeginVertical(EditorStyles.helpBox); { EditorGUILayout.PropertyField(onhit); EditorGUILayout.PropertyField(OnHitPosition); EditorGUILayout.PropertyField(OnHitInteractable); DrawCustomEvents(); } EditorGUILayout.EndVertical(); } protected virtual void DrawCustomEvents() { } protected virtual void DrawMisc(bool drawbox = true) { if (drawbox) EditorGUILayout.BeginVertical(EditorStyles.helpBox); react.isExpanded = MalbersEditor.Foldout(react.isExpanded, "Interactions"); if (react.isExpanded) { var p = " [Prefab]"; if (MD.HitEffect == null || !MD.HitEffect.IsPrefab()) p = ""; EditorGUILayout.PropertyField(m_HitEffect, new GUIContent(m_HitEffect.displayName + p)); if (MD.HitEffect != null) { if (MD.HitEffect.IsPrefab()) EditorGUILayout.PropertyField(DestroyHitEffect); EditorGUILayout.HelpBox( MD.HitEffect.IsPrefab() ? "The Hit Effect its a Prefab. The Effect will be instantiated as a child of the hitted collider, positioned and oriented using the hit position" : "The Hit Effect its a NOT a Prefab. The Effect will be positioned and oriented using the hit position. It will be enabled and disabled", MessageType.Info); } EditorGUILayout.PropertyField(react); EditorGUILayout.PropertyField(CustomReaction); EditorGUILayout.PropertyField(interact); if (MD.interact.Value) EditorGUILayout.PropertyField(interactorID); } // EditorGUILayout.Space(); AnimatorStopTime.isExpanded = MalbersEditor.Foldout(AnimatorStopTime.isExpanded, "Stop Animator"); if (AnimatorStopTime.isExpanded) { EditorGUILayout.PropertyField(AnimatorStopTime); if (MD.AnimatorStopTime.Value > 0) { EditorGUILayout.PropertyField(AnimatorSpeed); EditorGUILayout.PropertyField(animator); } } if (drawbox) EditorGUILayout.EndVertical(); } protected virtual void DrawGeneral(bool drawbox = true) { if (drawbox) EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginHorizontal(); EditorGUILayout.PropertyField(m_Active); MalbersEditor.DrawDebugIcon(debug); EditorGUILayout.EndHorizontal(); EditorGUILayout.PropertyField(index); EditorGUILayout.PropertyField(hitLayer); EditorGUILayout.PropertyField(triggerInteraction); EditorGUILayout.PropertyField(dontHitOwner, new GUIContent("Don't hit Owner")); if (MD.dontHitOwner.Value) { EditorGUILayout.PropertyField(owner); // Debug.Log("MD = " + MD.Owner); } if (drawbox) EditorGUILayout.EndVertical(); } protected virtual void DrawPhysics(bool drawbox = true) { if (drawbox) EditorGUILayout.BeginVertical(EditorStyles.helpBox); Force.isExpanded = MalbersEditor.Foldout(Force.isExpanded, "Physics"); if (Force.isExpanded) { //EditorGUILayout.LabelField("Physics", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(); EditorGUILayout.PropertyField(Force); EditorGUILayout.PropertyField(forceMode, GUIContent.none, GUILayout.MaxWidth(90), GUILayout.MinWidth(20)); EditorGUILayout.EndHorizontal(); } if (drawbox) EditorGUILayout.EndVertical(); } protected virtual void DrawCriticalDamage(bool drawbox = true) { if (drawbox) EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.LabelField("Critical Damage", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(); EditorGUILayout.PropertyField(m_cChance, new GUIContent("Chance [0-1]"), GUILayout.MinWidth(50)); EditorGUIUtility.labelWidth = 47; EditorGUILayout.PropertyField(cMultiplier, new GUIContent("Mult"), GUILayout.MinWidth(50)); EditorGUIUtility.labelWidth = 0; EditorGUILayout.EndHorizontal(); if (drawbox) EditorGUILayout.EndVertical(); } protected virtual void DrawStatModifier(bool drawbox = true) { if (drawbox) EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.PropertyField(statModifier, new GUIContent("Stat Modifier","Which Stat will be affected on the Object to hit after Impact"), true); EditorGUILayout.PropertyField(pureDamage); if (drawbox) EditorGUILayout.EndVertical(); } protected void DrawDescription(string desc) => MalbersEditor.DrawDescription(desc); } #endif }