# 2. Modifiers and Timed Buffs Part of [[Stat-Driven RPG Systems]]. Requires Unity 6000.5+. Previous: [[1.Stat Definitions and Base vs Derived Stats]]. Next: [[3.Leveling and XP Curves]]. ## 2.1 Three modifier types, and why ordering matters | Type | Example | Combines as | |---|---|---| | **Flat** | +10 Strength from a ring | Added to the base | | **PercentAdd** | +10% and +15% damage from two talents | **Summed** (+25%), applied once | | **PercentMult** | ×1.2 from a rage buff, ×1.1 from a potion | **Multiplied** (×1.32) | Formula: `final = (base + Σflat) × (1 + ΣpercentAdd) × Π(1 + percentMult)` Why separate PercentAdd from PercentMult? Additive percentages grow linearly, so ten +10% bonuses give +100%. Multiplicative ones compound, so ten ×1.1 bonuses give ×2.59. Most games make common bonuses additive and keep multiplicative ones for rare, powerful effects, which keeps balance predictable. Each modifier also has an `Order` for exceptions, such as a flat bonus that applies *after* percentages ("+5 damage, not scaled"). The math sorts by `Order` and uses each type's default order unless you override it. ```csharp // StatModifier.cs using System; public enum ModType { Flat = 100, PercentAdd = 200, PercentMult = 300 } [Serializable] public class StatModifier { public float Value; public ModType Type; public int Order; [NonSerialized] public object Source; // item, buff, talent… used for removal public StatModifier(float value, ModType type, object source = null, int? order = null) { Value = value; Type = type; Source = source; Order = order ?? (int)type; } } ``` ```csharp // StatModifierMath.cs using System.Collections.Generic; public static class StatModifierMath { static readonly List<StatModifier> Sorted = new(); public static float Apply(float baseValue, IReadOnlyList<StatModifier> mods) { Sorted.Clear(); Sorted.AddRange(mods); Sorted.Sort((a, b) => a.Order.CompareTo(b.Order)); // stable enough for game use; use OrderBy for strict stability float value = baseValue; float percentAddSum = 0f; for (int i = 0; i < Sorted.Count; i++) { var m = Sorted[i]; switch (m.Type) { case ModType.Flat: value += m.Value; break; case ModType.PercentAdd: percentAddSum += m.Value; // apply the summed group when the next mod isn't PercentAdd at the same order if (i + 1 >= Sorted.Count || Sorted[i + 1].Type != ModType.PercentAdd) { value *= 1f + percentAddSum; percentAddSum = 0f; } break; case ModType.PercentMult: value *= 1f + m.Value; break; } } // Round away float noise (e.g. 12.0000001) so UI and comparisons stay clean. return (float)System.Math.Round(value, 4); } } ``` Worked example: base STR 20, ring +10 flat, talent +10% add, talent +15% add, rage +20% mult. `(20 + 10) × 1.25 × 1.2 = 45`. ## 2.2 Sources and removal Every modifier records its `Source`. When you unequip a sword, call `stat.RemoveAllFromSource(swordInstance)`, so you never have to track the individual modifier objects. Use the **item instance** as the source, not the `ItemDefinition`. Otherwise two copies of the same ring would remove each other's bonuses. ## 2.3 Timed buffs ```csharp // BuffDefinition.cs using System; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu(menuName = "RPG/Buff")] public class BuffDefinition : ScriptableObject { [Serializable] public struct Entry { public StatDefinition stat; public float value; public ModType type; } public string displayName = "Rage"; public Sprite icon; public float duration = 10f; public enum StackMode { RefreshDuration, StackIntensity, Ignore } public StackMode stacking = StackMode.RefreshDuration; [Min(1)] public int maxStacks = 1; public List<Entry> modifiers = new(); } ``` ```csharp // BuffController.cs using System; using System.Collections.Generic; using UnityEngine; [RequireComponent(typeof(CharacterStats))] public class BuffController : MonoBehaviour { public class ActiveBuff { public BuffDefinition Def; public float Remaining; public int Stacks = 1; } readonly List<ActiveBuff> _active = new(); CharacterStats _stats; public event Action<ActiveBuff> BuffAdded, BuffRemoved; public IReadOnlyList<ActiveBuff> Active => _active; void Awake() => _stats = GetComponent<CharacterStats>(); public void Apply(BuffDefinition def) { var existing = _active.Find(b => b.Def == def); if (existing != null) { switch (def.stacking) { case BuffDefinition.StackMode.Ignore: return; case BuffDefinition.StackMode.RefreshDuration: existing.Remaining = def.duration; return; case BuffDefinition.StackMode.StackIntensity: existing.Remaining = def.duration; if (existing.Stacks >= def.maxStacks) return; existing.Stacks++; AddMods(existing); // each stack adds another copy, all with the same source return; } } var buff = new ActiveBuff { Def = def, Remaining = def.duration }; _active.Add(buff); AddMods(buff); BuffAdded?.Invoke(buff); } void AddMods(ActiveBuff buff) { foreach (var e in buff.Def.modifiers) _stats.Get(e.stat)?.AddModifier(new StatModifier(e.value, e.type, buff)); } public void Remove(ActiveBuff buff) { foreach (var s in _stats.All) s.RemoveAllFromSource(buff); _active.Remove(buff); BuffRemoved?.Invoke(buff); } void Update() { // Use scaled time so buffs pause with the game; iterate backwards because Remove mutates the list. for (int i = _active.Count - 1; i >= 0; i--) { _active[i].Remaining -= Time.deltaTime; if (_active[i].Remaining <= 0f) Remove(_active[i]); } } } ``` > [!tip] Permanent vs timed > Equipment and talents are permanent modifiers whose source is the item or talent. Buffs are timed modifiers whose source is the `ActiveBuff`. All of them go through the same `Stat` pipeline, so UI tooltips can list every modifier and its source in one place.