Hello everyone.
Another little trouble 
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:

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
