John C. Jensen

Unity Developer

Unity Game Developer specializing in C# programming

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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; attack.source != null &amp;&amp; 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 &amp;&amp; myCharacter.characterType != CharacterType.Enemy))
            {
                if (!myCharacter.inCombat &amp;&amp; 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);
    }
}