using Pathfinding;
using Sirenix.OdinInspector;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using ScrollingText;
[RequireComponent(typeof(Character), typeof(Stats), typeof(CapsuleCollider2D))]
public class Health : MonoBehaviour
{
[ShowInInspector]
public int MaxHP { get; private set; } = 100;
[ShowInInspector]
public int CurrentHP { get; private set; } = 0;
[ShowInInspector]
public int CurrentAbsorb { get; private set; }
public int MaxAbsorb { get; private set; }
[ShowInInspector]
public int CurrentHealAbsorb { get; private set; }
public int MaxHealAbsorb { get; private set; }
private int hpRegen;
private int outOfCombatRegenMultiplier = 1;
public bool IsAlive { get; private set; } = true;
private WaitForSeconds tickLength;
public Dictionary<Resource, ResourcePool> resources = new Dictionary<Resource, ResourcePool>();
public delegate void OnDamage();
public event OnDamage onDamage;
public delegate void OnHeal();
public event OnHeal onHeal;
public delegate void OnApplyAbsorb();
public event OnApplyAbsorb onApplyAbsorb;
public delegate void OnApplyHealAbsorb();
public event OnApplyHealAbsorb onApplyHealAbsorb;
public delegate void OnDeath(Character character);
public event OnDeath onDeath;
public delegate void OnResurrect(Character character);
public event OnResurrect onResurrect;
private Character myCharacter;
private EnemyAggro myAggro;
private Stats myStats;
private CapsuleCollider2D myCollider;
void Awake()
{
myCharacter = GetComponent<Character>();
myStats = myCharacter.myStats;
myAggro = GetComponent<EnemyAggro>();
myCollider = GetComponent<CapsuleCollider2D>();
}
void Start()
{
tickLength = new WaitForSeconds(Static.tickRate);
StartCoroutine(Regen());
}
//Debug fully restore HP/Mana pools
public void MaxHPMana()
{
if (HasResourcePool(Resource.Mana))
{
resources[Resource.Mana].SetToMax();
}
CurrentHP = MaxHP;
}
//Calculate incoming damage or healing
public void ChangeHealth(AttackInfo attack)
{
if (!IsAlive)
{
return;
}
//Process harmful physical attack and calculate physical damage reduction
if (attack.hpDelta < 0 && attack.damageType == DamageType.Physical)
{
//Roll for dodge
if (Random.Range(1f, 100f) <= myStats.dodge)
{
FloatText.NewMessage(MessageType.Default, "Dodge", transform.position);
CombatLog.LogDodge(attack, myCharacter);
return;
}
OnHitByMeleeProc(attack.source);
attack.hpDelta = Mathf.RoundToInt((float)attack.hpDelta * (1f - myStats.physicalDamageReduction));
}
//Process harmful magic attack and calculate spell damage reduction
else if (attack.hpDelta < 0 && attack.damageType == DamageType.Magic)
{
//Roll for spell resistance
if (Random.Range(1f, 100f) + attack.resistFactor <= myStats.spellResistChance)
{
FloatText.NewMessage(MessageType.Default, "Resist", transform.position);
CombatLog.LogResist(attack, myCharacter);
return;
}
OnHitBySpellProc(attack.source);
attack.hpDelta = Mathf.RoundToInt((float)attack.hpDelta * (1f - myStats.spellDamageReduction));
}
//Calculate general damage reduction modifier
if (attack.hpDelta < 0)
{
attack.hpDelta = Mathf.RoundToInt((float)attack.hpDelta * myStats.damageTakenMod);
//If any Damage Absorb effects are active, damage those first
if (CurrentAbsorb > 0)
{
attack.hpDelta = myCharacter.myEffects.UpdateAbsorb(attack.hpDelta);
FloatText.NewMessage(MessageType.Absorb, "Absorb", transform.position);
}
//Final damage is applied to HP pool
CombatLog.LogDamage(attack, myCharacter);
CurrentHP = Mathf.Clamp(CurrentHP + attack.hpDelta, 0, MaxHP);
onDamage?.Invoke();
if (attack.delayCast)
{
myCharacter.mySpellCast.DelayCast();
}
//If I'm an Enemy, add to my Aggro/Hate list
if (myAggro != null && attack.source != null && attack.aggroMultiplier != 0)
{
myAggro.AddAggro(Mathf.RoundToInt(attack.hpDelta * -1 * attack.aggroMultiplier), attack.source);
}
}
//If incoming is a Heal, calculate healing
if (attack.hpDelta > 0)
{
//If any Healing Absorb effects are active, they will reduce healing received.
if (CurrentHealAbsorb > 0)
{
attack.hpDelta = myCharacter.myEffects.UpdateHealAbsorb(attack.hpDelta);
FloatText.NewMessage(MessageType.Absorb, "Absorb", transform.position);
}
//Final healing is applied to HP pool
CombatLog.LogSpellHeal(attack, myCharacter);
CurrentHP = Mathf.Clamp(CurrentHP + attack.hpDelta, 0, MaxHP);
onHeal?.Invoke();
}
//Create floating combat text
FloatText.HPMessage(attack.hpDelta, transform.position, attack.source.characterType, attack.isCrit);
if (CurrentHP <= 0)
{
Die();
return;
}
UpdateHPThresholds();
}
//Roll for and process any active On Hit by Melee effects
private void OnHitByMeleeProc(Character source)
{
foreach (Proc proc in myCharacter.myEffects.onHitByMeleeProcs)
{
if (proc.RollProc())
{
myCharacter.spellTarget = source;
myCharacter.mySpellCast.StartCast(proc.spell, proc.targetOverride, true);
}
}
}
//Roll for and process any active On Hit by Spell effects
private void OnHitBySpellProc(Character source)
{
foreach (Proc proc in myCharacter.myEffects.onHitBySpellProcs)
{
if (proc.RollProc())
{
myCharacter.spellTarget = source;
myCharacter.mySpellCast.StartCast(proc.spell, proc.targetOverride, true);
}
}
}
//Update low/critical health threshold status. Used by AI. TODO: Move to Character.cs
public void UpdateHPThresholds()
{
if (GetHPPercent() <= .15f)
{
myCharacter.isLowHealth = true;
myCharacter.isCriticalHealth = true;
}
else if (GetHPPercent() <= .3f)
{
myCharacter.isLowHealth = true;
myCharacter.isCriticalHealth = false;
}
else
{
myCharacter.isLowHealth = false;
myCharacter.isCriticalHealth = false;
}
}
public void UpdateBar()
{
onHeal?.Invoke();
}
public float GetHPPercent()
{
float pct = CurrentHP / (float)MaxHP;
return pct;
}
public float GetAbsorbPercent()
{
return (float)CurrentAbsorb / (float)MaxHP;
}
public float GetHealAbsorbPercent()
{
return (float)CurrentHealAbsorb / (float)MaxHP;
}
public void DebugSetCurrentHP(int hp) {
CurrentHP = hp;
}
public void SetMaxHP(int maxhp)
{
MaxHP = maxhp;
CurrentHP = Mathf.Clamp(CurrentHP, 0, MaxHP);
MaxAbsorb = Mathf.RoundToInt(MaxHP * .5f);
MaxHealAbsorb = Mathf.RoundToInt(MaxHP * .75f);
}
public void SetHPRegen(int hpregen)
{
hpRegen = hpregen;
}
private void Die()
{
if (!IsAlive)
{
return;
}
IsAlive = false;
myCollider.enabled = false;
EmptyResources();
onDeath?.Invoke(myCharacter);
if (myCharacter.mySpellCast)
{
myCharacter.mySpellCast.InterruptCast();
}
GetComponent<AIPath>().enabled = false;
myCharacter.DisableBehavior();
myCharacter.ChangeTarget(null);
myCharacter.myEffects.RemoveAllEffects();
}
[Button]
public void Resurrect(bool restoreHealth = false, bool restoreMana = false)
{
if (IsAlive)
{
return;
}
if (restoreHealth)
{
CurrentHP = MaxHP;
}
else { CurrentHP = 1; }
if (HasResourcePool(Resource.Mana))
{
if (restoreMana)
{
resources[Resource.Mana].SetToMax();
}
else resources[Resource.Mana].resourceValue = 0;
}
IsAlive = true;
myCollider.enabled = true;
GetComponent<Character>().EnableBehavior();
GetComponent<AIPath>().enabled = true;
GetComponent<AIPath>().maxSpeed = 0f;
StartCoroutine(Regen());
onHeal?.Invoke();
onResurrect?.Invoke(myCharacter);
}
#region Resources
//Resources include Mana and any other secondary resources that a character may spend to use abilities.
//Initialize a resource pool
public void InitializeResource(ResourceInfo resource)
{
if (!resources.ContainsKey(resource.resource))
{
resources.Add(resource.resource, new ResourcePool(resource));
}
}
//Has enough of all required resource costs to use an ability?
public bool ValidateCosts(Spell spell)
{
foreach (ResourceCost cost in spell.resourceCosts)
{
if (!ValidateResourceCost(cost.resource, cost.cost))
{
myCharacter.mySpellCast.errorMessage = CastErrorMessages.NoResource;
myCharacter.mySpellCast.customErrorString = cost.resource.ToString();
return false;
}
}
return true;
}
//Return the current value of a resource pool
public int GetResourceValue(Resource resource)
{
if (resources.ContainsKey(resource))
{
return resources[resource].resourceValue;
}
else return 0;
}
//Return the maximum value of a resource pool
public int GetResourceMax(Resource resource)
{
if (resources.ContainsKey(resource))
{
return resources[resource].maxResource;
}
else return 0;
}
//Return the current percentage value of a resource pool
public float GetResourcePercent(Resource resource)
{
if (resources.ContainsKey(resource))
{
return (float)resources[resource].resourceValue / (float)resources[resource].maxResource;
}
else return 0;
}
//Check if a character uses a specific resource pool
public bool HasResourcePool(Resource resource)
{
if (resources.ContainsKey(resource))
{
return true;
}
else return false;
}
//Has enough of a resource to use an ability?
public bool ValidateResourceCost(Resource resource, int resourceDelta)
{
if (resources.ContainsKey(resource))
{
return resources[resource].ValidateCost(resourceDelta);
}
else return false;
}
//Increase or decrease a secondary resource pool. Used if an ability modifies the resource as part of its effect. ("Mana Burn" or generating a secondary resource)
public void ModifyResource(Resource resource, int resourceDelta)
{
if (resources.ContainsKey(resource))
{
resources[resource].Modify(resourceDelta);
return;
}
}
//Used when an ability requires the consumption of a resource to use
public void SpendResources(Spell spell)
{
if (spell == null)
{
return;
}
foreach (ResourceCost cost in spell.resourceCosts)
{
resources[cost.resource].Modify(cost.cost * -1);
}
}
//Reduce all resources to 0
private void EmptyResources()
{
foreach (KeyValuePair<Resource, ResourcePool> kvp in resources)
{
kvp.Value.EmptyResource();
}
}
#endregion Resources
public void UpdateAbsorb(int absorb)
{
CurrentAbsorb = Mathf.Clamp(absorb, 0, MaxAbsorb);
UpdateBar();
}
public void UpdateHealAbsorb(int healabsorb)
{
CurrentHealAbsorb = healabsorb;
}
//Periodic automatic recovery of health/mana/secondary resources
IEnumerator Regen()
{
while (IsAlive)
{
//Calculation for player/friendly NPCs
if (myCharacter.inCombat || (GroupAI.navigating && myCharacter.characterType != CharacterType.Enemy))
{
if (!myCharacter.inCombat && GroupAI.navigating)
{
outOfCombatRegenMultiplier = 10;
}
else
{
outOfCombatRegenMultiplier = 1;
}
if (resources.ContainsKey(Resource.Mana))
{
ModifyResource(Resource.Mana, Mathf.RoundToInt(resources[Resource.Mana].regen * outOfCombatRegenMultiplier));
}
CurrentHP = Mathf.Clamp(CurrentHP + hpRegen, 0, MaxHP);
onHeal?.Invoke();
UpdateHPThresholds();
}
//Calculation for enemies
else if (myCharacter.characterType == CharacterType.Enemy)
{
if (resources.ContainsKey(Resource.Mana))
{
ModifyResource(Resource.Mana, Mathf.RoundToInt(resources[Resource.Mana].maxResource * .2f));
}
CurrentHP = Mathf.Clamp(CurrentHP + Mathf.RoundToInt(MaxHP * .2f), 0, MaxHP);
onHeal?.Invoke();
}
yield return tickLength;
}
}
}
public class AttackInfo
{
public int hpDelta;
public float aggroMultiplier;
public Character source;
public DamageType damageType;
public bool isCrit;
public bool delayCast;
public float resistFactor;
public Spell spell;
public SpellEffect effect;
public AttackInfo(int hpDelta, float aggro, Character source, DamageType type, float resistFactor = 0f, bool crit = false, bool delayCast = true, Spell spell = null, SpellEffect effect = null)
{
this.hpDelta = hpDelta;
aggroMultiplier = aggro;
this.source = source;
damageType = type;
isCrit = crit;
this.delayCast = delayCast;
this.resistFactor = resistFactor;
this.spell = spell;
this.effect = effect;
}
public void Crit(int critMultiplier)
{
if (critMultiplier > 1)
{
isCrit = true;
}
hpDelta *= critMultiplier;
}
}
[System.Serializable]
public class ResourcePool
{
[HideLabel, ReadOnly]
public string resourceName;
[HideInInspector]
public ResourceInfo resourceInfo;
[HideInInspector]
public Resource resource;
[HideInInspector]
public int maxResource;
public int resourceValue;
public int regen, regenBase;
private int regenDelta;
public delegate void OnResourceChange(ResourcePool resourcePool, Resource resource);
public event OnResourceChange onResourceChange;
private float regenPct = 1f;
public ResourcePool(ResourceInfo info)
{
resourceInfo = info;
maxResource = info.max;
resourceValue = Mathf.RoundToInt(maxResource * info.startingPct);
regen = info.regen;
regenBase = info.regen;
regenPct = 1f;
}
public void SetToMax()
{
resourceValue = maxResource;
}
//Has enough of a resource to use an ability?
public bool ValidateCost(int cost)
{
if (resourceValue >= cost)
{
return true;
}
else return false;
}
public void SetMaxValue(int max)
{
maxResource = max;
resourceValue = Mathf.Clamp(resourceValue, 0, maxResource);
onResourceChange?.Invoke(this, resource);
}
public void Modify(int delta)
{
resourceValue = Mathf.Clamp(resourceValue + delta, 0, maxResource);
onResourceChange?.Invoke(this, resource);
}
public void UpdateRegen(int delta)
{
regenDelta = regenBase + delta;
regen = Mathf.RoundToInt(regenDelta * regenPct);
}
public void UpdateRegenPct(float pct)
{
regenPct = regenPct + pct;
regen = Mathf.RoundToInt(regenDelta * regenPct);
}
public void EmptyResource()
{
resourceValue = 0;
onResourceChange?.Invoke(this, resource);
}
}