Strange behavior with allocated point when validate

Hello everyone.
Another little trouble :slight_smile:
At start I’ve 1 point to allocate.
I apply it where I want, confirm, and everything goes well.
I hit E key to level up to level 2.
I’ve 2 point to allocate, exactly like in my progression table.
And when I validate, it propose me another 5 points to allocate ???
It’s a little bit eractic bug, because sometime it works and bug only at level 3.
Sometime the allocated amount is 10, sometime 5…
Lol, i don’t understand too much.
Here are my TraitStore and proression table:

image

My BaseStats.cs

using System;
using GameDevTV.Utils;
using Unity.VisualScripting;
using UnityEngine;

namespace RPG.Stats
{
        public class BaseStats : MonoBehaviour,IPredicateEvaluator
    {
        [Range(1,99)]//Bornage du niveau min et max
        [SerializeField] int startingLevel = 1;
        [SerializeField] CharacterClass characterClass;
        [SerializeField] Progression progression = null;
        [SerializeField] GameObject levelupParticleEffect = null;
        [SerializeField] bool shouldUseModifiers = false;// Pour ne faire bénéficer des bonus que le joueur
        public event Action onLevelUp;//Création du pointeur vers l'evenement onLevelUp
        LazyValue<int> currentLevel;
        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;//Sans parenthèse, on ajoute la méthode UpdateLevel dans la liste de onExperienceGained
            }
        }
        private void OnDisable()
        {
            if(experience != null)
            {
                experience.onExperienceGained -= UpdateLevel;//Sans parenthèse, on retire la méthode UpdateLevel dans la liste de onExperienceGained
            }
        }
        private void UpdateLevel()
        {
            int newLevel = CalculateLevel();//On créé la variable local NewLevel
            if(newLevel > currentLevel.value)
            {
                currentLevel.value = newLevel;
                LevelupEffect();
                onLevelUp();
                print("Niveau gagné");
            }
        }
        private void LevelupEffect()
        {
            Instantiate(levelupParticleEffect, transform);//Cela lie l'effet au porteur du script.
        }

//Methode pour recupérer la stat de base et lui ajouter des modificateurs.
        public float GetStat(Stat stat)
        {
            return (GetBaseStat(stat) + GetAdditiveModifier(stat)) * (1 + GetPercentageModifier(stat)/100);
        }
        private float GetBaseStat(Stat stat)
        {
            return progression.GetStat(stat, characterClass, GetLevel());
        }

        public int GetLevel()
        {
            return currentLevel.value;
        }
        private float GetAdditiveModifier(Stat stat)
        {
            if(!shouldUseModifiers) return 0;//Si le personnage ne devrait pas bénéficer d'avantages modifieurs, le bonus est nul.
            
            float total = 0;//Création de la variable de point de modification de stat total
            foreach(IModifierProvider provider in GetComponents<IModifierProvider>())
            {
                //Pour chaque modificateur de stat présent dans la liste récupérée ci-dessus:
                foreach(float modifier in provider.GetAdditiveModifiers(stat))
                {
                    total += modifier;
                }
            }
                return total;
        }
        private float GetPercentageModifier(Stat stat)
        {
            if(!shouldUseModifiers) return 0;//Si le personnage ne devrait pas bénéficer d'avantages modifieurs, le bonus est nul.
            float total = 0;//Créationd e la variable de point de modification de stat total
            foreach(IModifierProvider provider in GetComponents<IModifierProvider>())
            {
                //Pour chaque modificateur de stat présent dans la liste récupérée ci-dessus:
                foreach(float modifier in provider.GetPercentageModifiers(stat))
                {
                    total += modifier;
                }
            }
                return total;
        }
        private int CalculateLevel()//Pour récupérer l'information du niveau actuel
        {
            Experience experience = GetComponent<Experience>();

            if(experience == null) return startingLevel;

            float currentXP = experience.GetPoints();
            int penUltimateLevel = progression.GetLevels(Stat.ExperienceToLevelUp, characterClass);//PenultimateLevel = avant dernier niveau Ultime
            for(int level=1; level<=penUltimateLevel; level++)
            {
                float XPToLevelUP = progression.GetStat(Stat.ExperienceToLevelUp, characterClass, level);
                if(XPToLevelUP > currentXP)//Si l'XP pour UP est supèrieure à l'XP actuelle on renvoie le niveau actuel
                {
                    return level;
                }
            }//Sinon on est au niveau max soit l'avantdernier +1
            return penUltimateLevel + 1;
        }
        public bool? Evaluate(EPredicate predicate, string[] parameters)
        {
            if (predicate == EPredicate.HasLevel)
            {
                if (int.TryParse(parameters[0], out int testLevel))
                {
                    Debug.Log($"Testing HasLevel({testLevel}) against character level {currentLevel.value} : {currentLevel.value == testLevel}");
                    return currentLevel.value == testLevel;
                }
            }            
            if(predicate == EPredicate.UnderLevel)
            {
                if(int.TryParse(parameters[0], out int testLevel))
                Debug.Log($"Testing UnderLevel({testLevel}) against character level {currentLevel.value} : {currentLevel.value<testLevel}");
                {
                    return currentLevel.value < testLevel;
                }
            }
            if(predicate == EPredicate.OverLevel)
            {
                if(int.TryParse(parameters[0], out int testLevel))
                {
                    Debug.Log($"Testing OverLevel({testLevel}) against character level {currentLevel.value} : {currentLevel.value > testLevel}");
                    return currentLevel.value > testLevel;
                }
            }
            return null;
        }
        
    }
}

My StateStore.cs

using System;
using System.Collections.Generic;
using GameDevTV.Saving;
using GameDevTV.Utils;
using Newtonsoft.Json.Linq;
using UnityEngine;

namespace RPG.Stats
{
    public class TraitStore : MonoBehaviour, IModifierProvider, ISaveable, IPredicateEvaluator, IJsonSaveable
    {//Création du dictionnaire où l'in stock les valeurs actuelles des caractéristiques

        [SerializeField] TraitBonus[] bonusConfig;
        [System.Serializable]//Permet de rendre visible dans l'inspector les caractéristiques suivantes de la class TraitBonus:

        class TraitBonus
        {
            public Trait trait;
            public Stat stat;
            public float additiveBonusPerPoint = 0;
            public float percentageBonusPerPoint = 0;
        }

        Dictionary<Trait, int> assignedPoints = new Dictionary<Trait, int>();//Pour enregistrer les points qu'on est en train d'assigner
        Dictionary<Trait, int> stagedPoints = new Dictionary<Trait, int>();//Pour enregistrer les points déjà assignés, après validation.
        Dictionary<Stat, Dictionary<Trait, float>> additiveBonusCache;
        Dictionary<Stat, Dictionary<Trait, float>> percentageBonusCache;

        private void Awake()
        {
            additiveBonusCache = new Dictionary<Stat, Dictionary<Trait,float>>();
            percentageBonusCache = new Dictionary<Stat, Dictionary<Trait,float>>();
            foreach(var bonus in bonusConfig)
            {
                if(!additiveBonusCache.ContainsKey(bonus.stat))
                {
                    additiveBonusCache[bonus.stat] = new Dictionary<Trait, float>();
                }
                if(!percentageBonusCache.ContainsKey(bonus.stat))
                {
                    percentageBonusCache[bonus.stat] = new Dictionary<Trait, float>();
                }
                additiveBonusCache[bonus.stat][bonus.trait] = bonus.additiveBonusPerPoint;
                percentageBonusCache[bonus.stat][bonus.trait] = bonus.percentageBonusPerPoint;
            }
        }

        public int GetProposedPoints(Trait trait)//Pour crééer la valeur des points éxistants + les point alloués
        {
            return GetPoints(trait) + GetStagedPoints(trait);
        }

        public int GetPoints(Trait trait)
        {
            return assignedPoints.ContainsKey(trait) ? assignedPoints[trait] : 0;//S'il y a des points assignés aux traits, on renvoie la somme de tous ces point sinon on renvoie 0
        }

        public int GetStagedPoints(Trait trait)
        {
            return stagedPoints.ContainsKey(trait)? stagedPoints[trait] : 0;//S'il y a des points déjà assignés, les enregistre.
        }

        public void AssignPoints(Trait trait, int points)//points représente le nombre de point restant à allouer
        {
            if (!CanAssignPoints(trait,points)) return;//Si on ne peut pas assigner de point, on ne lit pas la suite du texte.
        
            stagedPoints[trait] = GetStagedPoints(trait) + points;
        }

        public bool CanAssignPoints(Trait trait, int points)
        {
            if (GetStagedPoints(trait) + points <0) return false;
            if (GetUnassignedPoints() < points) return false;
            return true;
        }

        public int GetUnassignedPoints()
        {
            return GetAssignablePoints() - GetTotalProposedPoints();
        }

        public int GetTotalProposedPoints()
        {
            int total = 0;
            foreach(int points in assignedPoints.Values)
            {
                total += points;
            }
            foreach(int points in stagedPoints.Values)
            {
                total += points;
            }

            return total;
        }

       

        public void Commit()
        {
            foreach(Trait trait in stagedPoints.Keys)
            {  
                assignedPoints[trait] = GetProposedPoints(trait);
            }
            stagedPoints.Clear();
        }

        public int GetAssignablePoints()
        {
            return (int)GetComponent<BaseStats>().GetStat(Stat.CumulatedTotalTraitPoints);
        }

        public IEnumerable<float> GetAdditiveModifiers(Stat stat)
        {
            if (!additiveBonusCache.ContainsKey(stat)) yield break;//Ici pour les traits nnous n'avons pas de modificateurs additif en fonction des traits mais multiplicatif (%)
                       
            foreach(Trait trait in additiveBonusCache[stat].Keys)
            {
                float bonus = additiveBonusCache[stat][trait];
                yield return bonus * GetPoints(trait);
            }
        }

        public IEnumerable<float> GetPercentageModifiers(Stat stat)
        {
            if (!percentageBonusCache.ContainsKey(stat)) yield break;//Ici pour les traits nnous n'avons pas de modificateurs additif en fonction des traits mais multiplicatif (%)
            foreach(Trait trait in percentageBonusCache[stat].Keys)
            {
                float bonus = percentageBonusCache[stat][trait];
                yield return bonus * GetPoints(trait);
            }
        }

        public object CaptureState()
        {
            return assignedPoints;
        }

        public void RestoreState(object state)
        {
            assignedPoints = new Dictionary<Trait, int>((IDictionary<Trait, int>) state);
        }

        public bool? Evaluate(EPredicate predicate, string[] parameters)
        {
            if (predicate == EPredicate.MinimumTrait)
            {
                if (Enum.TryParse<Trait>(parameters[0], out Trait trait))
                {
                    return GetPoints(trait) >= Int32.Parse(parameters[1]);
                }
            }
            return null;    
        }

        public JToken CaptureAsJToken()
        {
            JObject state = new JObject();
            IDictionary<string, JToken> stateDict = state;
            foreach (KeyValuePair<Trait,int> pair in assignedPoints)
            {
                stateDict[pair.Key.ToString()] = JToken.FromObject(pair.Value);
            }
            return state;
        }

        public void RestoreFromJToken(JToken state)
        {
            if (state is JObject stateObject)
            {
                assignedPoints.Clear();
                IDictionary<string, JToken> stateDict= stateObject;
                foreach (KeyValuePair<string, JToken> pair in stateDict)
                {
                    if (Enum.TryParse(pair.Key, true, out Trait trait))
                    {
                        assignedPoints[trait] = pair.Value.ToObject<int>();
                    }
                }
            }
        }
    }
}

And my TraitUI.cs

using RPG.Stats;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

namespace RPG.UI

{
    public class TraitUI : MonoBehaviour
    {
        [SerializeField] TextMeshProUGUI unassignedPointsText;
        [SerializeField] Button commitButton;

        TraitStore playerTraitStore = null;

            void Start()
            {
                playerTraitStore = GameObject.FindGameObjectWithTag("Player").GetComponent<TraitStore>();
                commitButton.onClick.AddListener(playerTraitStore.Commit);//Ajout d'une surveilllance lorsqu'on appui sur le bouton valider.
            }

            void Update()
            {
                unassignedPointsText.text = playerTraitStore.GetUnassignedPoints().ToString();//Pour mettre à jour en temps réel les points restants à allouer
            }
    }
}

Any Idea ?
Thanks.
François

Everything looks right (I reformatted the TraitStore to get rid of the extra blank lines).

Silly thing to check, but can you show me the bonusConfig expanded out? One of the first things that came to mind was if the TraitStore was increasing the Stat.CumulatedTotalTraits via the IModifierProvider…

Arff :slight_smile:
it was that…
Sorry for the disturb…


When I put 1 point in empathie, it gave me 5 another point …
When I set this part i clicked too quick wanted to use dicount bonus , just above Cumulated point…
There ther is no problem :slight_smile:
Have a good day.

François

No worries. I’ve done something similar myself. In my own project, I actually made the Traits themselves Stat.Strength, etc instead of Trait.Strength… so that I could have armor that increased the traits, like World of Warcraft.
All that’s great until you accidentally make an armor with a boost to total trait points, LOL. I had a feeling this was what was happening here.

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