using System.Collections; using System.Collections.Generic; using UnityEngine; using System; using EmeraldAI.Utility; namespace EmeraldAI { /// /// Handles how an receives damage and track health. The Damage function is called through the IDamageable interface script. /// [HelpURL("https://black-horizon-studios.gitbook.io/emerald-ai-wiki/emerald-components-required/health-component")] public class EmeraldHealth : MonoBehaviour, IDamageable { #region Health variables public int CurrentHealth = 50; public int StartingHealth = 50; public int HealRate = 0; public bool AttachHitEffects = false; public bool Immortal = false; public List CurrentActiveEffects; public bool HitEffectFoldout; public YesOrNo UseHitEffect = YesOrNo.No; public Vector3 HitEffectPosOffset; public float HitEffectTimeoutSeconds = 3f; public List HitEffectsList = new List(); public delegate void DamageHandler(); public event DamageHandler OnTakeDamage; public delegate void TakeCritDamageHandler(); public event TakeCritDamageHandler OnTakeCritDamage; public delegate void AnyDamageHandler(); public event DamageHandler OnTakeAnyDamage; public delegate void BlockHandler(); public event BlockHandler OnBlock; public delegate void DodgeHandler(); public event DodgeHandler OnDodge; public delegate void DeathHandler(); public event DeathHandler OnDeath; public delegate void HealRateTickHandler(); public event HealRateTickHandler OnHealRateTick; public delegate void OnHealingReceivedHandler(); public event OnHealingReceivedHandler OnHealingReceived; public delegate void HealthChangeHandler(); public event HealthChangeHandler OnHealthChange; EmeraldSystem EmeraldComponent; public List ActiveEffects { get => CurrentActiveEffects; set => CurrentActiveEffects = value; } public int Health { get => CurrentHealth; set => CurrentHealth = value; } public int StartHealth { get => StartingHealth; set => StartingHealth = value; } #endregion #region Editor Variables public bool HideSettingsFoldout; public bool HealthFoldout; #endregion void Start () { CurrentHealth = StartingHealth; EmeraldComponent = GetComponent(); EmeraldComponent.CombatComponent.OnExitCombat += RecoverHealth; //Subscribe to the OnExitCombat event for RecoverHealth } /// /// Damages the AI and allows it to block and mitigate damage, if enabled. To use the ragdoll feature, all /// parameters need to be used where AttackerTransform is the current attacker. /// /// Amount of damage caused during attack. /// The transform of the current attacker. /// The amount of force to apply to this AI when they die. (Use Ragdoll must be enabled on this AI) public void Damage(int DamageAmount, Transform AttackerTransform = null, int RagdollForce = 100, bool CriticalHit = false) { if (EmeraldComponent.AnimationComponent.IsDead || transform.localScale == Vector3.one * 0.003f || AttackerTransform && AttackerTransform == EmeraldComponent.TargetToFollow || AttackerTransform && EmeraldComponent.DetectionComponent.GetTargetFactionRelation(AttackerTransform) == "Friendly") return; //Check for an attacker if there's no current target. if (AttackerTransform != null) CheckForAttacker(AttackerTransform); //Cache the reference to the newest/current attacker. EmeraldComponent.CombatComponent.LastAttacker = AttackerTransform; //Get the angle from the current attacker to determine if the incoming hit can be blocked or dodge. float AttackerAngle = EmeraldCombatManager.TransformAngle(EmeraldComponent, AttackerTransform); //Check to see if the current attack is being blocked bool Blocked = (EmeraldComponent.AnimationComponent.IsBlocking && AttackerAngle <= EmeraldComponent.CombatComponent.MaxMitigationAngle && EmeraldComponent.AIAnimator.GetBool("Blocking")); //Check to see if the current attack is being dodged bool Dodged = (EmeraldComponent.AnimationComponent.IsDodging && AttackerAngle <= EmeraldComponent.CombatComponent.MaxMitigationAngle); //Set the base calculated damage equal to DamageAmount. If it is dodged or blocked, the amount will be adjusted below. int CalculatedDamage = DamageAmount; if (Blocked) CalculatedDamage = Mathf.FloorToInt(Mathf.Abs((DamageAmount * ((EmeraldComponent.CombatComponent.MitigationAmount) * 0.01f)) - DamageAmount)); //Mitigate the damage from blocking if (Dodged) CalculatedDamage = Mathf.FloorToInt(Mathf.Abs((DamageAmount * ((EmeraldComponent.CombatComponent.MitigationAmount) * 0.01f)) - DamageAmount)); //Mitigate the damage from dodging //Don't reduce an AI's health if Immortal is enabled if (!Immortal) Health -= CalculatedDamage; //Display the damage dealt through the Combat Text System, given that it's enabled. if (CalculatedDamage > 0) CombatTextSystem.Instance.CreateCombatTextAI(CalculatedDamage, EmeraldComponent.CombatComponent.DamagePosition(), CriticalHit, false); //In order to have the most reliable On Do Damage events, simply invoke the attacker's OnDoDoamage callback through a public function, given it is an Emerald AI agent. if (AttackerTransform != null) { EmeraldSystem AttackEmeraldComponent = AttackerTransform.GetComponent(); if (AttackEmeraldComponent != null) AttackEmeraldComponent.CombatComponent.InvokeDoDamage(); if (AttackEmeraldComponent != null && CriticalHit) AttackEmeraldComponent.CombatComponent.InvokeDoCritDamage(); } //Invoke the damage delegates if (!CriticalHit && CalculatedDamage > 0) OnTakeDamage?.Invoke(); else if (CriticalHit && CalculatedDamage > 0) OnTakeCritDamage?.Invoke(); OnTakeAnyDamage?.Invoke(); //Create hit effect, if it's enabled, the AI is not blocking or dodging, and the damage is greater than 0. if (!Blocked && !Dodged && CalculatedDamage > 0) CreateHitEffect(); if (Blocked) OnBlock?.Invoke(); //Invoke the block delegate else if (Dodged) OnDodge?.Invoke(); //Invoke the dodge delegate //The AI has died, initialize its death state. if (Health <= 0 && !EmeraldComponent.AnimationComponent.IsDead) { EmeraldComponent.CombatComponent.ReceivedRagdollForceAmount = RagdollForce; Health = 0; Death(); } } /// /// Called when an AI receives damage, but has no current target (typically from an unseen attacker). When this happenss, assign the /// attacker as the current target (if the have a Neutral Relation Type or higher) or search for any visible targets within the AI's Line of Sight. /// void CheckForAttacker (Transform AttackerTransform) { if (EmeraldComponent.CombatTarget == null && !EmeraldComponent.CombatComponent.CombatState) { StartCoroutine(DelaySetDetectedTarget(AttackerTransform)); } } /// /// Delay SetDetectedTarget to give the AI time to play its non-combat hit animation and transfer to its combat state. /// IEnumerator DelaySetDetectedTarget(Transform AttackerTransform) { string RelationName = EmeraldComponent.DetectionComponent.GetTargetFactionRelation(AttackerTransform); yield return new WaitForSeconds(0.6f); if (RelationName == "Neutral" || RelationName == "Enemy") { EmeraldComponent.DetectionComponent.SetDetectedTarget(AttackerTransform); } else { EmeraldComponent.DetectionComponent.SearchForTarget(PickTargetTypes.Closest); } } /// /// Called when the AI's health reaches 0. The OnDeath delegate is also invoked and is responsible for triggering any death functionality to external subscribers. /// void Death() { OnDeath?.Invoke(); //Invoke the AI death event. EmeraldComponent.AnimationComponent.IsDead = true; EmeraldCombatManager.DisableComponents(EmeraldComponent); EmeraldCombatManager.EnableRagdoll(EmeraldComponent); } /// /// Refills the AI's health to full instantly /// public void InstantlyRefillAIHealth() { Health = StartHealth; CurrentHealth = StartHealth; OnHealthChange?.Invoke(); } /// /// Instantly kills this AI. /// public void KillAI() { EmeraldComponent.CombatComponent.ReceivedRagdollForceAmount = 1; Health = 0; Death(); } /// /// Called through the OnExitCombat callback when an AI exits combat. This heals the AI over time according to its HealRate. /// void RecoverHealth () { StartCoroutine(RecoverHealthInternal()); } /// /// Called through the StartHealing function which increases the AI's health each HealRate. This gets canceled if the target enters combat. /// IEnumerator RecoverHealthInternal() { float t = 0; while (CurrentHealth < StartingHealth) { t += Time.deltaTime; if (t >= 1) { CurrentHealth = CurrentHealth + HealRate; OnHealRateTick?.Invoke(); t = 0; } if (EmeraldComponent.CombatComponent.CombatState) yield break; yield return null; } CurrentHealth = StartingHealth; } public void UpdateHealTick () { OnHealRateTick?.Invoke(); } public void UpdateHealingReceived() { OnHealingReceived?.Invoke(); } /// /// Updates the AI's Max Health and Current Health. /// public void UpdateHealth(int MaxHealth, int CurrentHealth) { Health = CurrentHealth; StartHealth = MaxHealth; OnHealthChange?.Invoke(); } /// /// Creates (an optional) hit effect (outside of an AI's Ability Objects) when an AI takes damage. /// void CreateHitEffect() { if (EmeraldComponent.HealthComponent.UseHitEffect == YesOrNo.Yes && !EmeraldComponent.LBDComponent && EmeraldComponent.HealthComponent.HitEffectsList.Count > 0 && !EmeraldComponent.AnimationComponent.IsDead) { GameObject RandomBloodEffect = EmeraldComponent.HealthComponent.HitEffectsList[UnityEngine.Random.Range(0, EmeraldComponent.HealthComponent.HitEffectsList.Count)]; if (RandomBloodEffect != null) { GameObject SpawnedBlood = EmeraldObjectPool.SpawnEffect(RandomBloodEffect, Vector3.zero, EmeraldComponent.transform.rotation, EmeraldComponent.HealthComponent.HitEffectTimeoutSeconds) as GameObject; if (AttachHitEffects) SpawnedBlood.transform.SetParent(EmeraldComponent.transform); SpawnedBlood.transform.position = EmeraldComponent.CombatComponent.DamagePosition() + EmeraldComponent.HealthComponent.HitEffectPosOffset; } } } } }