Healthy Hat Not Adding Health, Doesn't Seem to Be For Reasons Already Discussed

I set up a healthy hat-esque item that should give 20 Health. The Player is set to use modifiers. The player can equip this item but it doesn’t actually add any HP to their maximum from what I can tell; neither the HP bar nor current or max HP counter I have set up change at all. I could use advice on where I may have gone wrong.

I will provide my base stats script here and can provide other scripts as needed:

using System.Reflection.Emit;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using RPG.Saving.Utils;

namespace RPG.Attributes
{
    public class BaseStats : MonoBehaviour//Chracter's base stats before item bonuses and penalties
    {
        [Range(1, 99)][SerializeField] int startingLevel = 1;//Max level is set at 99. Shouldn't be an issue in most games.
        [SerializeField] CharacterClass characterClass;
        [SerializeField] Progression progression = null;
        [SerializeField] GameObject levelUpParticleEffect = null;
        [SerializeField] Transform levelTarget = null;
        [SerializeField] bool shouldUseModifiers = false;//By default, entities ignore modifiers, since only the player really needs them and it makes tuning easier

        public event Action onLevelUp;

        LazyValue<int> currentLevel; //Intentionally invalid by default
        Experience experience;

        private void Awake()
        {
            experience = GetComponent<Experience>();
            currentLevel = new LazyValue<int>(CalculateLevel);
        }

        private void Start()
        {
            currentLevel.ForceInit();
        }

        private void OnEnable()
        {
            if (experience != null)
            {
                experience.onExperienceGained += UpdateLevel;
            }
        }

        private void OnDisable()
        {
            if (experience != null)
            {
                experience.onExperienceGained -= UpdateLevel;
            }
        }

        private void UpdateLevel()
        {
            int newLevel = CalculateLevel();
            if(newLevel > currentLevel.value)//will need tweaking if deleveling is ever introduced
            {
                currentLevel.value = newLevel;
                LevelUpEffect();
                onLevelUp();
            }
        }

        private void LevelUpEffect()
        {
            Instantiate(levelUpParticleEffect, levelTarget.transform);
        }

        public float GetStat(Stats stat)
        {
            return (GetBaseStat(stat) + GetAdditiveModifier(stat)) * (1 + GetPercentageModifier(stat)/100);
        }

        public float GetBaseStat(Stats stat)
        {
            return progression.GetStat(stat, characterClass, GetLevel());
        }

        public int GetLevel()//helps avoid calculating level a bunch of times each frame
        {
            return currentLevel.value;
        }

        public int CalculateLevel()
        {
            Experience experience = GetComponent<Experience>();
            if (experience == null) return startingLevel;
            float currentXP = experience.GetExperience();
            int penultimateLevel = progression.GetLevels(Stats.ExperienceToLevelUp, characterClass);
            for (int level = 1; level <= penultimateLevel; level++)
            {
                float XPToLevelUp = progression.GetStat(Stats.ExperienceToLevelUp, characterClass, level);
                if (XPToLevelUp > currentXP)
                {
                    return level;
                }
            }
            return penultimateLevel + 1;
        }

        private float GetAdditiveModifier(Stats stat)
        {
            if (!shouldUseModifiers) return 0;
            float total = 0;
            foreach (IModifierProvider provider in GetComponents<IModifierProvider>())
            {
                foreach (float modifier in provider.GetAdditiveModifiers(stat))
                {
                    total += modifier;
                }
            }
            return total;
        }

        private float GetPercentageModifier(Stats stat)
        {
            if (!shouldUseModifiers) return 0;
            float total = 0;
            foreach (IModifierProvider provider in GetComponents<IModifierProvider>())
            {
                foreach (float modifier in provider.GetPercentageModifiers(stat))
                {
                    total += modifier;
                }
            }
            return total;
        }
    }
}

I will also include my StatsEquipableItem script:

using System.Collections;
using System.Collections.Generic;
using RPG.Attributes;
using UnityEngine;
namespace RPG.Saving.Inventories
{
    [CreateAssetMenu(menuName = ("RPG/Inventory/Equipable Item"))]
    public class StatsEquipableItem : EquipableItem, IModifierProvider
    {
        [SerializeField] Modifier[] additiveModifers;
        [SerializeField] Modifier[] percentageModifiers;

        [System.Serializable] struct Modifier
        {
            public Stats stat;
            public float value;
        }

        public IEnumerable<float> GetAdditiveModifiers(Stats stat)
        {
            foreach (var modifier in additiveModifers)
            {
                if(modifier.stat == stat)
                {
                    yield return modifier.value;
                }
            }
        }

        public IEnumerable<float> GetPercentageModifiers(Stats stat)
        {
            foreach (var modifier in percentageModifiers)
            {
                if(modifier.stat == stat)
                {
                    yield return modifier.value;
                }
            }
        }
    }
}

Thanks for any help!

As an update to this, it appears it DOES update maxHealth or at least does upon saving, ending the play session, and then hitting play again. So I suspect the issue is with the way my Health script updates maybe? I will put that script here, as it has some changes (and some code I honestly know is probably redundant, but my focus is mostly on making things work at the moment):

using System.Reflection;
using System;
using System.Collections;
using System.Collections.Generic;
using RPG.Saving;
using UnityEngine;
using RPG.Core;
using RPG.Saving.Utils;
using UnityEngine.Events;

namespace RPG.Attributes
{
    public class Health : MonoBehaviour, ISaveable
    {
        [SerializeField] UnityEvent<float> takeDamage;

        public LazyValue<float> maxHealth;
        public LazyValue<float> health;

        bool isDead = false;
        bool isHurt = false;
        bool hasAwardedExperience = false;

        private void Awake()
        {
            health = new LazyValue<float>(GetInitialHealth);
            maxHealth = new LazyValue<float>(GetCurrentMaxHealth);
            GetComponent<BaseStats>().onLevelUp += RegenerateHealth;
        }

        private void Start()
        {
            health.ForceInit();
            maxHealth.ForceInit();
        }

        public bool IsDead()
        {
            return isDead;
        }

        public bool IsHurt()
        {
            return isHurt;
        }

        public void Heal(float heal)
        {
            if(health.value >= 0)
            {
                 health.value = Mathf.Min(health.value + heal, maxHealth.value);
            }
        }

        public void TakeDamage(GameObject instigator, float damage)
        {
            health.value = Mathf.Max(health.value - damage, 0);          
            {
                takeDamage.Invoke(damage);//Can place in an else statement after if statement to make text not appear on death
                if(health.value <= 0)
                {
                    Die();
                    AwardExperience(instigator);
                }
            }
        
            isHurt = true;
        }

        private float GetInitialHealth()
        {
            return GetComponent<BaseStats>().GetStat(Stats.Health);
        }

        public float GetCurrentMaxHealth()
        {
            return GetComponent<BaseStats>().GetStat(Stats.Health);
        }

        public float GetCurrentHealth()
        {
            return health.value;
        }

        public float GetMaxHealth()
        {
            return maxHealth.value;
        }

        private void RegenerateHealth()
        {
            health.value =  Mathf.Max(maxHealth.value * .8f, health.value);
            maxHealth.value = GetComponent<BaseStats>().GetStat(Stats.Health);//Forces maxHP to update upon a level up
        }

        private void Die()
        {
            if(isDead) return;
            isDead = true;
            GetComponent<Animator>().SetTrigger("Die");
            GetComponent<ActionScheduler>().CancelCurrentAction();
        }

        private void AwardExperience(GameObject instigator)
        {
            Experience experience = instigator.GetComponent<Experience>();
            if(experience == null) return;
          
            if(hasAwardedExperience == false)
            {
                hasAwardedExperience = true;
                experience.GainExperience(GetComponent<BaseStats>().GetStat(Stats.ExperienceReward));
            }
        }

        public object CaptureState()
        {
            return health.value;
        }

        public void RestoreState(object state)
        {
            health.value = (float)state;
            if(health.value <= 0)
            {
                Die();
            }
        }
    }
}

I suspect the issue may be here in the order of the way things are done.
You’re regenerating to the cached maxHealth.value, and then setting a new maxHealth. These should, at the very least be reversed.

I would like to discourage caching maxHealth to begin with. The problem with caching maxHealth is that there are a number of things that could change during your gameplay that could affect any given stat through the IModifierProvider interface. For example: Equipping a Healthy Hat should be changing your MaxHealth when it’s equipped, but it won’t be noticed by the maxHealth Lazy Value until you’ve levelled up. It makes more sense instead of caching the maxHealth and hoping to subscribe to everything where it may change (StatsInventory now, but add Traits in the final course, and you may come up with other things) to calculate the MaxHealth whenever it is needed. For this, we really only need

public float GetMaxHealth()
{
    return GetComponent<BaseStats>().GetStat(Stats.Health);
}

By doing this it will always be valid and current.

I’ve formatted your code pastes above for you. See the link below for how to format the code pastes in the future so that they aren’t messed up by the forums:

Sorry about the formatting issues, I was wondering how you were able to fix them. I will take a look at the link you posted.

I did manage to fix the issue with a janky solution involving just getting the MaxHealth on update but I will look at implementing the solution you suggested because you’re right, this has caused me a few issues.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms