quot;F{decimals}") + "%" : v.ToString(quot;F{decimals}"); } ``` ## 1.3 The runtime Stat A `Stat` stores a **base value** and a list of modifiers (added in part 2), and caches its final `Value` until something changes. The dirty flag matters because UI and combat read stats far more often than they change. ```csharp // Stat.cs using System; using System.Collections.Generic; using UnityEngine; public class Stat { public StatDefinition Definition { get; } public event Action<Stat> Changed; float _baseValue; float _cached; bool _dirty = true; readonly List<StatModifier> _modifiers = new(); // For derived stats: a function that supplies the base from other stats. Func<float> _baseProvider; public Stat(StatDefinition def, float baseValue) { Definition = def; _baseValue = baseValue; } public float BaseValue { get => _baseProvider != null ? _baseProvider() : _baseValue; set { _baseValue = value; MarkDirty(); } } public float Value { get { if (_dirty) { _cached = Calculate(); _dirty = false; } return _cached; } } public IReadOnlyList<StatModifier> Modifiers => _modifiers; public void SetBaseProvider(Func<float> provider) { _baseProvider = provider; MarkDirty(); } public void MarkDirty() { if (_dirty) { Changed?.Invoke(this); return; } _dirty = true; Changed?.Invoke(this); } public void AddModifier(StatModifier mod) { _modifiers.Add(mod); MarkDirty(); } public bool RemoveModifier(StatModifier mod) { if (!_modifiers.Remove(mod)) return false; MarkDirty(); return true; } public int RemoveAllFromSource(object source) { int n = _modifiers.RemoveAll(m => ReferenceEquals(m.Source, source)); if (n > 0) MarkDirty(); return n; } float Calculate() { float v = StatModifierMath.Apply(BaseValue, _modifiers); return Mathf.Clamp(v, Definition.minValue, Definition.maxValue); } } ``` ## 1.4 Base vs derived stats - **Base (primary) stats** come from the class plus level growth plus points the player spends: STR, DEX, INT, VIT. - **Derived stats** are calculated from other stats, and items can still add modifiers on top: `MaxHealth = 50 + VIT × 10`, `AttackPower = STR × 2 + DEX × 0.5`, `CritChance = 0.05 + DEX × 0.002`. Derived stats must update whenever a stat they read from changes. The `DerivedStatFormula` asset declares its inputs, so `CharacterStats` can subscribe to their `Changed` events. ```csharp // DerivedStatFormula.cs using System; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu(menuName = "RPG/Derived Stat Formula")] public class DerivedStatFormula : ScriptableObject { [Serializable] public struct Term { public StatDefinition input; public float coefficient; // value contributed per point of input } public StatDefinition output; public float constant; public List<Term> terms = new(); // Evaluates a linear formula: constant + sum(input * coefficient). // For non-linear formulas, subclass and override. public virtual float Evaluate(Func<StatDefinition, float> getValue) { float v = constant; foreach (var t in terms) if (t.input) v += getValue(t.input) * t.coefficient; return v; } } ``` > [!note] Avoid cycles > If `Armor` derives from `Strength` and `Strength` also derives from `Armor`, `Value` recurses forever. The validator in [[7.RPG Editor Tooling Tips]] detects cycles at edit time. ## 1.5 CharacterStats component This is the single owner of a character's stats. Later parts add level, equipment, and buffs to it. ```csharp // CharacterStats.cs using System; using System.Collections.Generic; using UnityEngine; public class CharacterStats : MonoBehaviour { [SerializeField] CharacterClass characterClass; // part 4 [SerializeField] List<DerivedStatFormula> derivedFormulas = new(); readonly Dictionary<StatDefinition, Stat> _stats = new(); public event Action<Stat> AnyStatChanged; public CharacterClass Class => characterClass; public IEnumerable<Stat> All => _stats.Values; void Awake() => Build(); public void Build() { _stats.Clear(); if (characterClass) foreach (var entry in characterClass.baseStats) Ensure(entry.stat).BaseValue = entry.value; foreach (var f in derivedFormulas) { if (!f || !f.output) continue; var stat = Ensure(f.output); var formula = f; stat.SetBaseProvider(() => formula.Evaluate(GetValue)); foreach (var t in f.terms) if (t.input) Ensure(t.input).Changed += _ => stat.MarkDirty(); } } Stat Ensure(StatDefinition def) { if (!_stats.TryGetValue(def, out var s)) { s = new Stat(def, 0); s.Changed += st => AnyStatChanged?.Invoke(st); _stats.Add(def, s); } return s; } public Stat Get(StatDefinition def) => def ? Ensure(def) : null; public float GetValue(StatDefinition def) => def && _stats.TryGetValue(def, out var s) ? s.Value : 0f; } ``` `CharacterClass` is defined in part 4. Until you get there, a stub with a `baseStats` list is enough, or comment out the class lines.