diff --git a/Runtime/INotifyStatModifierSet.cs b/Runtime/INotifyStatModifierSet.cs new file mode 100644 index 0000000..2e05bc1 --- /dev/null +++ b/Runtime/INotifyStatModifierSet.cs @@ -0,0 +1,22 @@ +namespace Gameframe.StatSheet +{ + public enum StatModifierSetActionType + { + Add, + Set + } + + public struct StatModifierSetChangedArgs + { + public StatModifierSetActionType Action; + public StatModifier Modifier; + public StatModifier Previous; + } + + public delegate void StatModifierSetChangedEventHandler(StatModifierSet modifierSet, StatModifierSetChangedArgs args); + + public interface INotifyStatModifierSet : IStatModifierSet + { + event StatModifierSetChangedEventHandler ModifiersChanged; + } +} diff --git a/Runtime/INotifyStatModifierSet.cs.meta b/Runtime/INotifyStatModifierSet.cs.meta new file mode 100644 index 0000000..1158510 --- /dev/null +++ b/Runtime/INotifyStatModifierSet.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4fc0900bb1a4472e9d41b823482c7366 +timeCreated: 1662529099 \ No newline at end of file diff --git a/Runtime/IStatModifierSet.cs b/Runtime/IStatModifierSet.cs index 83f2db7..5d0d7d4 100644 --- a/Runtime/IStatModifierSet.cs +++ b/Runtime/IStatModifierSet.cs @@ -9,6 +9,8 @@ namespace Gameframe.StatSheet /// Stat key type (Usually an enum) public interface IStatModifierSet : IEnumerable> { - float Modify(TKey statType, float inValue); + StatModifier Get(TKey statName, StatMode mode); + + IEnumerable> Get(StatMode mode); } } diff --git a/Runtime/ListStatSet.cs b/Runtime/ListStatSet.cs index fb052d0..89ca9dc 100644 --- a/Runtime/ListStatSet.cs +++ b/Runtime/ListStatSet.cs @@ -66,6 +66,8 @@ public struct StatValue : IStatValue /// Duplicate stat names should only be possible if manually added via the Unity inspector /// [SerializeField] + // ReSharper disable once FieldCanBeMadeReadOnly.Global + // If we make this readonly it seems to become unserializable so don't do that pls protected List stats = new List(); /// diff --git a/Runtime/StatModel.cs b/Runtime/StatModel.cs index eb4cfb2..b3f43bc 100644 --- a/Runtime/StatModel.cs +++ b/Runtime/StatModel.cs @@ -1,19 +1,28 @@ using System.Collections; using System.Collections.Generic; +using System.Linq; namespace Gameframe.StatSheet { - public class StatModel : StatModel { } + /// + /// + /// + public class StatModel : StatModel + { + } public class StatModel : IReadOnlyStatSet { protected IStatSet _baseStats; + public IStatSet BaseStats { get => _baseStats; set => _baseStats = value; } + public bool IsDirty { get; private set; } + protected ListStatSet _statTotals = new ListStatSet(); public IStatSet StatTotals => _statTotals; @@ -22,15 +31,43 @@ public IStatSet BaseStats public virtual void AddModifierSet(IStatModifierSet modifierSet) { _modifiers.Add(modifierSet); + //If this modifier set is a notify set then subscribe for changes + if (modifierSet is INotifyStatModifierSet notifySet) + { + notifySet.ModifiersChanged += NotifySetOnModifiersChanged; + } + + IsDirty = true; } public virtual void RemoveModifier(IStatModifierSet modifierSet) { _modifiers.Remove(modifierSet); + if (modifierSet is INotifyStatModifierSet notifySet) + { + notifySet.ModifiersChanged -= NotifySetOnModifiersChanged; + } + + IsDirty = true; + } + + private void NotifySetOnModifiersChanged(StatModifierSet set, StatModifierSetChangedArgs args) + { + IsDirty = true; } - public void UpdateTotals() + /// + /// Update all stat totals + /// This method must be called to ensure all stats affected by modifiers are up to date + /// + /// When true totals will update only if IsDirty property is true + public virtual void UpdateTotals(bool checkDirty = false) { + if (checkDirty && !IsDirty) + { + return; + } + //Add Base Stats _statTotals.Clear(); if (_baseStats != null) @@ -38,14 +75,19 @@ public void UpdateTotals() _statTotals.Add(_baseStats); } - // - foreach (var modifierSet in _modifiers) + //Apply Adds + foreach (var mod in _modifiers.SelectMany(modifierSet => modifierSet.Get(StatMode.Add))) { - foreach (var mod in modifierSet) - { - _statTotals[mod.statType] = mod.Modify(_statTotals[mod.statType]); - } + _statTotals[mod.statType] = mod.Modify(_statTotals[mod.statType]); } + + //Apply Multipliers + foreach (var mod in _modifiers.SelectMany(modifierSet => modifierSet.Get(StatMode.Multiply))) + { + _statTotals[mod.statType] = mod.Modify(_statTotals[mod.statType]); + } + + IsDirty = false; } #region IReadonlyStatSet Implementation @@ -63,7 +105,5 @@ IEnumerator IEnumerable.GetEnumerator() } #endregion - } - } diff --git a/Runtime/StatModifierSet.cs b/Runtime/StatModifierSet.cs index a4231ae..3f5d3bf 100644 --- a/Runtime/StatModifierSet.cs +++ b/Runtime/StatModifierSet.cs @@ -1,5 +1,7 @@ using System.Collections; using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; namespace Gameframe.StatSheet { @@ -8,61 +10,99 @@ namespace Gameframe.StatSheet /// Stat modifiers are adds or multipliers that can be applied to a stat sheet /// /// - public class StatModifierSet : IStatModifierSetIndexed + public class StatModifierSet : INotifyStatModifierSet { private readonly List> _mods = new List>(); + /// + /// Set a modifier + /// + /// Stat modifier value + public void Set(StatModifier modifier) + { + Set(modifier.statType,modifier.value, modifier.mode); + } + + /// + /// Set a modifier + /// + /// Stat type the modifier will affect + /// value of the stat modifier + /// how the value will be applied to the stat public void Set(TKey statType, float value, StatMode mode) { for (int i = 0; i < _mods.Count; i++) { if (_mods[i].statType.Equals(statType) && _mods[i].mode == mode) { - var val = _mods[i]; - val.value = value; - _mods[i] = val; + //If there is no change in value then just return and do nothing + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (_mods[i].value == value) + { + return; + } + + var prevValue = _mods[i]; + var newVal = prevValue; + newVal.value = value; + _mods[i] = newVal; + NotifyChanged(StatModifierSetActionType.Set, newVal, prevValue); return; } } //Adding a new modifier - _mods.Add(new StatModifier() + var modifier = new StatModifier() { statType = statType, value = value, mode = mode - }); + }; + _mods.Add(modifier); + NotifyChanged(StatModifierSetActionType.Add, modifier); } - public StatModifier Get(TKey statName, StatMode mode) + /// + /// Get modifier for a given stat type and mode + /// + /// Stat that is being affected + /// how the stat is applied: Add or Multiply + /// StatModifier value + public StatModifier Get(TKey statType, StatMode mode) { for (var i = 0; i < _mods.Count; i++) { - if (_mods[i].statType.Equals(statName) && _mods[i].mode == mode) + if (_mods[i].statType.Equals(statType) && _mods[i].mode == mode) { return _mods[i]; } } - if (mode == StatMode.Multiply) + switch (mode) { - return new StatModifier - { - statType = statName, - value = 1, - mode = StatMode.Multiply - }; - } - else - { - return new StatModifier - { - statType = statName, - value = 0, - mode = StatMode.Add - }; + case StatMode.Multiply: + return new StatModifier + { + statType = statType, + value = 1, + mode = StatMode.Multiply + }; + case StatMode.Add: + return new StatModifier + { + statType = statType, + value = 0, + mode = StatMode.Add + }; + default: + throw new InvalidEnumArgumentException($"StatMode {mode} not implemented"); } } + public IEnumerable> Get(StatMode mode) + { + return _mods.Where(x => x.mode == mode); + } + public int Count => _mods.Count; public StatModifier GetIndex(int index) @@ -70,15 +110,6 @@ public StatModifier GetIndex(int index) return _mods[index]; } - public float Modify(TKey statType, float value) - { - var adds = Get(statType, StatMode.Add); - var mul = Get(statType, StatMode.Multiply); - value += adds.value; - value *= mul.value; - return value; - } - public IEnumerator> GetEnumerator() { return _mods.GetEnumerator(); @@ -88,5 +119,17 @@ IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + + public event StatModifierSetChangedEventHandler ModifiersChanged; + + private void NotifyChanged(StatModifierSetActionType action, StatModifier modifier, StatModifier previous = new StatModifier()) + { + ModifiersChanged?.Invoke(this, new StatModifierSetChangedArgs + { + Action = action, + Modifier = modifier, + Previous = previous + }); + } } } diff --git a/Runtime/StatSet.cs b/Runtime/StatSet.cs index 753972f..3a6a9ed 100644 --- a/Runtime/StatSet.cs +++ b/Runtime/StatSet.cs @@ -3,14 +3,6 @@ namespace Gameframe.StatSheet { - // List of base stats - // List of adds from n sources - // List of multipliers from n sources - // Generate/Calculate List of final stats - // Need to Serialize just base stats - // Equipment is just a list of modifiers - // Need to be able to show stat delta (unequip modifier set A, equip set B and show how stats change) - /// /// Abstract implementation of IStatSet /// Provides Add and Subtract methods @@ -20,7 +12,7 @@ public abstract class StatSet : IStatSet { protected StatSet() { } - protected StatSet(IStatSet set) + protected StatSet(IReadOnlyStatSet set) { foreach (var statValue in set) { @@ -38,7 +30,7 @@ protected StatSet(IStatSet set) /// Loops the given set and adds the value to this set. /// /// a stat set - public void Add(IStatSet statSet) + public void Add(IReadOnlyStatSet statSet) { foreach (var stat in statSet) { @@ -50,7 +42,7 @@ public void Add(IStatSet statSet) /// Loops over the given set and subtracts the values from the stats in this set /// /// a stat set - public void Subtract(IStatSet statSet) + public void Subtract(IReadOnlyStatSet statSet) { foreach (var stat in statSet) { diff --git a/Runtime/StatSetUtility.cs b/Runtime/StatSetUtility.cs index f3c8cb8..cab042c 100644 --- a/Runtime/StatSetUtility.cs +++ b/Runtime/StatSetUtility.cs @@ -11,7 +11,7 @@ public static class StatSetUtility /// /// The type of the Stat. Usually an enum. /// IStatSetIndexed - public static IStatSetIndexed Sum(params IStatSet[] sets) + public static IStatSetIndexed Sum(params IReadOnlyStatSet[] sets) { var newSet = new ListStatSet(); for (var i = 0; i < sets.Length; i++) diff --git a/Tests/Editor/StatSheetTests.cs b/Tests/Editor/ListStatSetTests.cs similarity index 51% rename from Tests/Editor/StatSheetTests.cs rename to Tests/Editor/ListStatSetTests.cs index 66d55d2..c003920 100644 --- a/Tests/Editor/StatSheetTests.cs +++ b/Tests/Editor/ListStatSetTests.cs @@ -3,97 +3,7 @@ namespace Gameframe.StatSheet.Tests { - public class StatSheetTests - { - [Test] - public void CanCreate() - { - var statSheet = new ListStatSet(); - Assert.IsTrue(statSheet != null); - } - - [Test] - public void StatUtility_Sum() - { - var set1 = new ListStatSet - { - ["sta"] = 10, - ["int"] = 5, - }; - - var set2 = new ListStatSet - { - ["sta"] = 10, - }; - - var set3 = new ListStatSet - { - ["sta"] = 10, - }; - - var sumSet = StatSetUtility.Sum(set1, set2, set3); - Assert.IsFalse(set1 == sumSet); - Assert.IsFalse(set2 == sumSet); - Assert.IsFalse(set3 == sumSet); - - Assert.IsTrue(Mathf.Approximately(set1["sta"],10)); - Assert.IsTrue(Mathf.Approximately(set2["sta"],10)); - Assert.IsTrue(Mathf.Approximately(set3["sta"],10)); - - Assert.NotNull(sumSet); - Assert.IsTrue(Mathf.Approximately(sumSet["sta"],30)); - Assert.IsTrue(Mathf.Approximately(sumSet["int"],5)); - } - - [Test] - public void StatUtility__Diff() - { - var set1 = new ListStatSet - { - ["sta"] = 10, - ["int"] = 5, - }; - - var set2 = new ListStatSet - { - ["sta"] = 10, - }; - - var diffSet = StatSetUtility.Diff(set1, set2); - Assert.IsFalse(set1 == diffSet); - Assert.IsFalse(set2 == diffSet); - - Assert.IsTrue(Mathf.Approximately(set1["sta"],10)); - Assert.IsTrue(Mathf.Approximately(set1["int"],5)); - Assert.IsTrue(Mathf.Approximately(set2["sta"],10)); - - Assert.NotNull(diffSet); - Assert.IsTrue(Mathf.Approximately(diffSet["sta"],0)); - Assert.IsTrue(Mathf.Approximately(diffSet["int"],5)); - } - - [Test] - public void StatValue_Add() - { - var stat1 = new StatValue() {StatType = "v1", Value = 1}; - var stat2 = new StatValue() {StatType = "v1", Value = 2}; - var stat3 = stat1 + stat2; - Assert.IsTrue(Mathf.Approximately(stat3.Value,3) && stat3.StatType == "v1",$"Expected 3 but got {stat3.Value}"); - Assert.IsTrue(Mathf.Approximately(stat1.Value,1) && stat1.StatType == "v1"); - } - - [Test] - public void StatValue_Subtract() - { - var stat1 = new StatValue() {StatType = "v1", Value = 1}; - var stat2 = new StatValue() {StatType = "v1", Value = 2}; - var stat3 = stat1 - stat2; - Assert.IsTrue(Mathf.Approximately(stat3.Value,-1) && stat3.StatType == stat1.StatType); - } - - } - - public class ListStatTests + public class ListStatSetTests { public enum StatType { diff --git a/Tests/Editor/ListStatSetTests.cs.meta b/Tests/Editor/ListStatSetTests.cs.meta new file mode 100644 index 0000000..94111be --- /dev/null +++ b/Tests/Editor/ListStatSetTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 71fa0e09217a4ee4ae3f780ac3a79614 +timeCreated: 1662531895 \ No newline at end of file diff --git a/Tests/Editor/StatModelTests.cs b/Tests/Editor/StatModelTests.cs index 5bc1537..6dd402b 100644 --- a/Tests/Editor/StatModelTests.cs +++ b/Tests/Editor/StatModelTests.cs @@ -1,5 +1,3 @@ -using System.Collections; -using System.Collections.Generic; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; @@ -15,7 +13,6 @@ public enum TestStatType Will } - // A Test behaves as an ordinary method [Test] public void CanCreate() { @@ -42,7 +39,7 @@ public void BaseStats() } [Test] - public void Modifiers() + public void Modifiers_Add_and_Multiply() { var model = new StatModel { @@ -68,5 +65,27 @@ public void Modifiers() Assert.IsTrue(Mathf.Approximately(model[TestStatType.Intelligence],30), $"Expected 30 but got {model[TestStatType.Intelligence]}"); } + [Test] + public void IsDirty() + { + var model = new StatModel + { + BaseStats = new ListStatSet() + }; + model.BaseStats[TestStatType.Intelligence] = 10; + + //This works because StatModifierSet is a INotifyStatModifierSet + var modifierSet = new StatModifierSet(); + model.AddModifierSet(modifierSet); + Assert.IsTrue(model.IsDirty); + model.UpdateTotals(); + Assert.IsFalse(model.IsDirty); + modifierSet.Set(TestStatType.Intelligence, 10, StatMode.Add); + Assert.IsTrue(model.IsDirty); + model.UpdateTotals(); + Assert.IsFalse(model.IsDirty); + Assert.IsTrue(Mathf.Approximately(model[TestStatType.Intelligence],20)); + } + } } diff --git a/Tests/Editor/StatModifierSetTests.cs b/Tests/Editor/StatModifierSetTests.cs new file mode 100644 index 0000000..b036c78 --- /dev/null +++ b/Tests/Editor/StatModifierSetTests.cs @@ -0,0 +1,127 @@ +using NUnit.Framework; +using UnityEngine; + +namespace Gameframe.StatSheet.Tests +{ + public class StatModifierSetTests + { + public enum StatType + { + Str, + Int + } + + [Test] + public void CanCreate() + { + var modSet = new StatModifierSet(); + Assert.NotNull(modSet); + } + + [Test] + public void Set_and_Get_Add_Modify() + { + var modSet = new StatModifierSet(); + //Add additive Str modifier + modSet.Set(StatType.Str, 1, StatMode.Add); + + var modifier = modSet.Get(StatType.Str, StatMode.Add); + Assert.IsTrue(modifier.mode == StatMode.Add); + Assert.IsTrue(modifier.statType == StatType.Str); + Assert.IsTrue(Mathf.Approximately(1, modifier.value)); + + var moddedValue = modifier.Modify(1); + Assert.IsTrue(Mathf.Approximately(2, moddedValue)); + } + + [Test] + public void Set_and_Get_Mul_Modify() + { + var modSet = new StatModifierSet(); + //Add additive Str modifier + modSet.Set(StatType.Str, 5, StatMode.Multiply); + + var modifier = modSet.Get(StatType.Str, StatMode.Multiply); + Assert.IsTrue(modifier.mode == StatMode.Multiply); + Assert.IsTrue(modifier.statType == StatType.Str); + Assert.IsTrue(Mathf.Approximately(5, modifier.value)); + + var moddedValue = modifier.Modify(2); + Assert.IsTrue(Mathf.Approximately(10, moddedValue)); + } + + [Test] + public void ModifiersChangedEvent_Action_Add() + { + var modSet = new StatModifierSet(); + + bool notified = false; + + modSet.ModifiersChanged += (set, args) => + { + notified = true; + Assert.IsTrue(set == modSet); + Assert.IsTrue(args.Action == StatModifierSetActionType.Add); + Assert.IsTrue(args.Modifier.statType == StatType.Int); + // ReSharper disable once CompareOfFloatsByEqualityOperator + Assert.IsTrue(args.Modifier.value == 2); + Assert.IsTrue(args.Modifier.mode == StatMode.Multiply); + }; + + modSet.Set(StatType.Int, 2, StatMode.Multiply); + + //Ensure callback was actually called + Assert.IsTrue(notified); + } + + [Test] + public void ModifiersChangedEvent_Action_Set() + { + var modSet = new StatModifierSet(); + + modSet.Set(StatType.Int, 2, StatMode.Multiply); + + bool notified = false; + + modSet.ModifiersChanged += (set, args) => + { + notified = true; + Assert.IsTrue(set == modSet); + Assert.IsTrue(args.Action == StatModifierSetActionType.Set); + Assert.IsTrue(args.Modifier.statType == StatType.Int); + // ReSharper disable once CompareOfFloatsByEqualityOperator + Assert.IsTrue(args.Modifier.value == 3); + // ReSharper disable once CompareOfFloatsByEqualityOperator + Assert.IsTrue(args.Previous.value == 2); + Assert.IsTrue(args.Previous.statType == StatType.Int); + Assert.IsTrue(args.Previous.mode == StatMode.Multiply); + Assert.IsTrue(args.Modifier.mode == StatMode.Multiply); + }; + + modSet.Set(StatType.Int, 3, StatMode.Multiply); + + //Ensure callback was actually called + Assert.IsTrue(notified); + } + + [Test] + public void ModifiersChangedEvent_Action_Set_Unchanged() + { + var modSet = new StatModifierSet(); + + modSet.Set(StatType.Int, 2, StatMode.Multiply); + + bool notified = false; + + modSet.ModifiersChanged += (set, args) => + { + notified = true; + }; + + modSet.Set(StatType.Int, 2, StatMode.Multiply); + + //Value didn't change so we should NOT be notified of anything + Assert.IsFalse(notified); + } + } +} diff --git a/Tests/Editor/StatModifierSetTests.cs.meta b/Tests/Editor/StatModifierSetTests.cs.meta new file mode 100644 index 0000000..552ba46 --- /dev/null +++ b/Tests/Editor/StatModifierSetTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 14b9561257d24a7fbe2deefee8a9f556 +timeCreated: 1662531886 \ No newline at end of file diff --git a/Tests/Editor/StatUtilityTests.cs b/Tests/Editor/StatUtilityTests.cs new file mode 100644 index 0000000..69827b6 --- /dev/null +++ b/Tests/Editor/StatUtilityTests.cs @@ -0,0 +1,88 @@ +using NUnit.Framework; +using UnityEngine; + +namespace Gameframe.StatSheet.Tests +{ + public class StatUtilityTests + { + [Test] + public void StatUtility_Sum() + { + var set1 = new ListStatSet + { + ["sta"] = 10, + ["int"] = 5, + }; + + var set2 = new ListStatSet + { + ["sta"] = 10, + }; + + var set3 = new ListStatSet + { + ["sta"] = 10, + }; + + var sumSet = StatSetUtility.Sum(set1, set2, set3); + Assert.IsFalse(set1 == sumSet); + Assert.IsFalse(set2 == sumSet); + Assert.IsFalse(set3 == sumSet); + + Assert.IsTrue(Mathf.Approximately(set1["sta"],10)); + Assert.IsTrue(Mathf.Approximately(set2["sta"],10)); + Assert.IsTrue(Mathf.Approximately(set3["sta"],10)); + + Assert.NotNull(sumSet); + Assert.IsTrue(Mathf.Approximately(sumSet["sta"],30)); + Assert.IsTrue(Mathf.Approximately(sumSet["int"],5)); + } + + [Test] + public void StatUtility__Diff() + { + var set1 = new ListStatSet + { + ["sta"] = 10, + ["int"] = 5, + }; + + var set2 = new ListStatSet + { + ["sta"] = 10, + }; + + var diffSet = StatSetUtility.Diff(set1, set2); + Assert.IsFalse(set1 == diffSet); + Assert.IsFalse(set2 == diffSet); + + Assert.IsTrue(Mathf.Approximately(set1["sta"],10)); + Assert.IsTrue(Mathf.Approximately(set1["int"],5)); + Assert.IsTrue(Mathf.Approximately(set2["sta"],10)); + + Assert.NotNull(diffSet); + Assert.IsTrue(Mathf.Approximately(diffSet["sta"],0)); + Assert.IsTrue(Mathf.Approximately(diffSet["int"],5)); + } + + [Test] + public void StatValue_Operator_Add() + { + var stat1 = new StatValue() {StatType = "v1", Value = 1}; + var stat2 = new StatValue() {StatType = "v1", Value = 2}; + var stat3 = stat1 + stat2; + Assert.IsTrue(Mathf.Approximately(stat3.Value,3) && stat3.StatType == "v1",$"Expected 3 but got {stat3.Value}"); + Assert.IsTrue(Mathf.Approximately(stat1.Value,1) && stat1.StatType == "v1"); + } + + [Test] + public void StatValue_Operator_Subtract() + { + var stat1 = new StatValue() {StatType = "v1", Value = 1}; + var stat2 = new StatValue() {StatType = "v1", Value = 2}; + var stat3 = stat1 - stat2; + Assert.IsTrue(Mathf.Approximately(stat3.Value,-1) && stat3.StatType == stat1.StatType); + } + + } +} diff --git a/Tests/Editor/StatSheetTests.cs.meta b/Tests/Editor/StatUtilityTests.cs.meta similarity index 100% rename from Tests/Editor/StatSheetTests.cs.meta rename to Tests/Editor/StatUtilityTests.cs.meta