Browse Source

Initial commit

jamesperet 2 years ago
commit
0bf60ce262
51 changed files with 2174 additions and 0 deletions
  1. 71 0
      .gitignore
  2. 5 0
      ChangeLog.md
  3. 7 0
      ChangeLog.md.meta
  4. 8 0
      Editor.meta
  5. 8 0
      Editor/Icons.meta
  6. BIN
      Editor/Icons/Fire.png
  7. 92 0
      Editor/Icons/Fire.png.meta
  8. 92 0
      Readme.md
  9. 7 0
      Readme.md.meta
  10. 8 0
      Runtime.meta
  11. 8 0
      Runtime/GameActions.meta
  12. 8 0
      Runtime/GameActions/Actions.meta
  13. 129 0
      Runtime/GameActions/Actions/StatChangeValueGameAction.cs
  14. 11 0
      Runtime/GameActions/Actions/StatChangeValueGameAction.cs.meta
  15. 160 0
      Runtime/GameActions/Actions/StatModifierOperationsGameAction.cs
  16. 11 0
      Runtime/GameActions/Actions/StatModifierOperationsGameAction.cs.meta
  17. 156 0
      Runtime/GameActions/Actions/StatusEffectOperationsGameAction.cs
  18. 11 0
      Runtime/GameActions/Actions/StatusEffectOperationsGameAction.cs.meta
  19. 8 0
      Runtime/GameActions/Variables.meta
  20. 100 0
      Runtime/GameActions/Variables/GameActionContextStatsController.cs
  21. 11 0
      Runtime/GameActions/Variables/GameActionContextStatsController.cs.meta
  22. 18 0
      Runtime/KairoEngine.Stats.asmdef
  23. 7 0
      Runtime/KairoEngine.Stats.asmdef.meta
  24. 213 0
      Runtime/Stat.cs
  25. 11 0
      Runtime/Stat.cs.meta
  26. 13 0
      Runtime/StatGroup.cs
  27. 11 0
      Runtime/StatGroup.cs.meta
  28. 63 0
      Runtime/StatModifier.cs
  29. 11 0
      Runtime/StatModifier.cs.meta
  30. 96 0
      Runtime/StatModifiersController.cs
  31. 11 0
      Runtime/StatModifiersController.cs.meta
  32. 77 0
      Runtime/StatTemplate.cs
  33. 11 0
      Runtime/StatTemplate.cs.meta
  34. 42 0
      Runtime/StatsComponent.cs
  35. 11 0
      Runtime/StatsComponent.cs.meta
  36. 130 0
      Runtime/StatsController.cs
  37. 11 0
      Runtime/StatsController.cs.meta
  38. 8 0
      Runtime/StatusEffects.meta
  39. 96 0
      Runtime/StatusEffects/StatusEffect.cs
  40. 11 0
      Runtime/StatusEffects/StatusEffect.cs.meta
  41. 79 0
      Runtime/StatusEffects/StatusEffectTemplate.cs
  42. 11 0
      Runtime/StatusEffects/StatusEffectTemplate.cs.meta
  43. 108 0
      Runtime/StatusEffects/StatusEffectsController.cs
  44. 11 0
      Runtime/StatusEffects/StatusEffectsController.cs.meta
  45. 8 0
      Tests.meta
  46. 8 0
      Tests/Editor.meta
  47. 22 0
      Tests/Editor/KairoEngine.Stats.EditorTests.asmdef
  48. 7 0
      Tests/Editor/KairoEngine.Stats.EditorTests.asmdef.meta
  49. 130 0
      Tests/Editor/StatsControllerTests.cs
  50. 11 0
      Tests/Editor/StatsControllerTests.cs.meta
  51. 17 0
      package.json

+ 71 - 0
.gitignore

@@ -0,0 +1,71 @@
+# This .gitignore file should be placed at the root of your Unity project directory
+#
+# Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore
+#
+/[Ll]ibrary/
+/[Tt]emp/
+/[Oo]bj/
+/[Bb]uild/
+/[Bb]uilds/
+/[Ll]ogs/
+/[Uu]ser[Ss]ettings/
+
+# MemoryCaptures can get excessive in size.
+# They also could contain extremely sensitive data
+/[Mm]emoryCaptures/
+
+# Asset meta data should only be ignored when the corresponding asset is also ignored
+!/[Aa]ssets/**/*.meta
+
+# Uncomment this line if you wish to ignore the asset store tools plugin
+# /[Aa]ssets/AssetStoreTools*
+
+# Autogenerated Jetbrains Rider plugin
+/[Aa]ssets/Plugins/Editor/JetBrains*
+
+# Visual Studio cache directory
+.vs/
+
+# Gradle cache directory
+.gradle/
+
+# Autogenerated VS/MD/Consulo solution and project files
+ExportedObj/
+.consulo/
+*.csproj
+*.unityproj
+*.sln
+*.suo
+*.tmp
+*.user
+*.userprefs
+*.pidb
+*.booproj
+*.svd
+*.pdb
+*.mdb
+*.opendb
+*.VC.db
+
+# Unity3D generated meta files
+*.pidb.meta
+*.pdb.meta
+*.mdb.meta
+
+# Unity3D generated file on crash reports
+sysinfo.txt
+
+# Builds
+*.apk
+*.aab
+*.unitypackage
+
+# Crashlytics generated file
+crashlytics-build.properties
+
+# Packed Addressables
+/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*
+
+# Temporary auto-generated Android Assets
+/[Aa]ssets/[Ss]treamingAssets/aa.meta
+/[Aa]ssets/[Ss]treamingAssets/aa/*

+ 5 - 0
ChangeLog.md

@@ -0,0 +1,5 @@
+# KairoEngine.Stats Change Log
+
+### v0.1.1
+
+- Moved the ``StatusEffectOperationsGameAction`` icon ``Fire.png``  to a folder inside the package: ``Assets/Plugins/KairoEngine/Stats/Editor/Icons``. *06/23/21 - 16:30*

+ 7 - 0
ChangeLog.md.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 3d8b3c3d78d2cf647b2390a1f4804d9b
+TextScriptImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Editor.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: da5fd38f18b64f84a9a057f447c5ba88
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Editor/Icons.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3c53f2eceaff3d64e87dae9a6112b13e
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
Editor/Icons/Fire.png


+ 92 - 0
Editor/Icons/Fire.png.meta

@@ -0,0 +1,92 @@
+fileFormatVersion: 2
+guid: 36ec7608bc69e7d4abac6175887d0423
+TextureImporter:
+  internalIDToNameTable: []
+  externalObjects: {}
+  serializedVersion: 11
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 1
+    sRGBTexture: 1
+    linearTexture: 0
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapsPreserveCoverage: 0
+    alphaTestReferenceValue: 0.5
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  streamingMipmaps: 0
+  streamingMipmapsPriority: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 6
+  cubemapConvolution: 0
+  seamlessCubemap: 0
+  textureFormat: 1
+  maxTextureSize: 2048
+  textureSettings:
+    serializedVersion: 2
+    filterMode: -1
+    aniso: -1
+    mipBias: -100
+    wrapU: -1
+    wrapV: -1
+    wrapW: -1
+  nPOTScale: 1
+  lightmap: 0
+  compressionQuality: 50
+  spriteMode: 0
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spritePixelsToUnits: 100
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spriteGenerateFallbackPhysicsShape: 1
+  alphaUsage: 1
+  alphaIsTransparency: 0
+  spriteTessellationDetail: -1
+  textureType: 0
+  textureShape: 1
+  singleChannelComponent: 0
+  maxTextureSizeSet: 0
+  compressionQualitySet: 0
+  textureFormatSet: 0
+  applyGammaDecoding: 0
+  platformSettings:
+  - serializedVersion: 3
+    buildTarget: DefaultTexturePlatform
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+    forceMaximumCompressionQuality_BC6H_BC7: 0
+  spriteSheet:
+    serializedVersion: 2
+    sprites: []
+    outline: []
+    physicsShape: []
+    bones: []
+    spriteID: 
+    internalID: 0
+    vertices: []
+    indices: 
+    edges: []
+    weights: []
+    secondaryTextures: []
+  spritePackingTag: 
+  pSDRemoveMatte: 0
+  pSDShowRemoveMatteOption: 0
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 92 - 0
Readme.md

@@ -0,0 +1,92 @@
+# 📦 KairoEngine.Stats v0.1.1
+
+The Stats system adds tools for creating lists of linked smart values. Stats can be used for many ends like an RPG rule system, weapon statistics or scoreboards.
+
+Stats store a number or resolve to an equation that can access values from other stats. A stat holds data for current, initial, minimum and maximum values. It also holds a list of modifiers that can change any of the stored value. Stats are stored in controllers that also hold.
+
+Stat Controllers store and manage lists for stats, traits and status effects. A Stat Controller can be addd as variable to any class in Unity and will display all stored stats correctly in the editor and handle them in any context.
+
+Traits are bundles of stat modifiers for different stats. They can be added on the editor or at run time and usualy aren't removed. 
+
+Status Effects also add stat modifiers to different stats and handles other functions like timeout, stacking and effects.
+
+### 🛑Required packages
+
+- `KairoEngine.Core`
+- `UniRX`
+- ``Sirenix.OdinInspector``
+
+### 📄Namespaces
+
+- `KairoEngine.Stats`
+- ``KairoEngine.Stats.EditorTests``
+
+### 🔷Components
+
+- ``StatComponent`` – A generic component that holds stats data.
+
+### ✔Getting Started
+
+```
+name: hitpoints
+value: 75
+max: max_hitpoints
+```
+
+```
+name: max_hitpoints
+value: (strength + endurance) * 10 = 100
+max: 10000
+```
+
+```
+name: strength
+value: 4
+```
+
+```
+name: endurance
+value: 6
+```
+
+### 🧰Functions
+
+### 🎈Back Log
+
+- [x] Create ``StatTemplate`` class
+
+- [x] Create ``StatGroup`` class
+
+- [x] Create ``StatsController`` class
+
+- [x] Create ``Stat`` class
+
+- [x] Stats Controller tests
+
+- [x] Create ``StatModifier`` and ``StatModifiersController``
+
+- [x] Extend Stat Modifier system to work for *min* and *max* values
+
+- [x] Functions for adding and removing stat modifiers
+
+- [x] Tests for stat modifiers
+
+- [x] Create `StatusEffect` and `StatusEffectsController`
+
+- [ ] Create `Trait` and `TraitsController`
+
+- [ ] Stat Controller events
+
+- [ ] Draw `StatTemplate` Icon
+
+- [ ] Draw `StatsConponent` Icon
+
+- [ ] Draw `StatGroup` Icon
+
+- [ ] Draw `StatusEffect` Icon
+
+- [ ] Function to duplicate ``StatController`` data
+
+- [ ] Random value equation function
+
+- [ ] Show and hide stats

+ 7 - 0
Readme.md.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 0eb5193e297987746b0944bc760e121c
+TextScriptImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Runtime.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 708c02b133d0a6944aff7b4f1af4c487
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Runtime/GameActions.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 50a38a20fd2bf93408dad8b4e7dbaf32
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Runtime/GameActions/Actions.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d011595482a244c44a4f804340d74559
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 129 - 0
Runtime/GameActions/Actions/StatChangeValueGameAction.cs

@@ -0,0 +1,129 @@
+using System;
+using System.Collections;
+using System.Linq;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+using KairoEngine.Core.GameActions;
+
+namespace KairoEngine.Stats.GameActions
+{
+
+    [System.Serializable, HideReferenceObjectPicker]
+    public class StatChangeValueGameAction : GameActionBase
+    {
+        #region DefaultVariables
+        public override string name { get => $"Change stat \'{statName}\' value by {statValueChange}"; }
+
+        public override GameActionsController controller { 
+            get => _controller; 
+            set 
+            {
+                _controller = value;
+                typeName = "StatChangeValueGameAction";
+            }
+        }
+        public override string GetTypeName() => "StatChangeValueGameAction";
+        public override string GetActionName() => "Stats/Change Stat Value";
+
+        #endregion
+
+        #region ActionVariables
+
+        [FoldoutGroup("@name")] public string statName = "";
+
+        [FoldoutGroup("@name")] public int statValueChange = +1;
+
+        [ValueDropdown("statsControllers", IsUniqueList = false), OnInspectorInit("GetCompatibleVariablenames"), LabelText("Target StatsController")]
+        [FoldoutGroup("@name")]
+        public string statsControllerVariable;
+
+        private IEnumerable statsControllers = new ValueDropdownList<string>();
+
+        [FoldoutGroup("@name")] public bool debugWarnings = false;
+
+        #endregion
+
+        #region ActionFunctions
+
+        public StatChangeValueGameAction(GameActionsController controller) : base(controller)
+        {
+            this.controller = controller;
+            className =  this.GetType().AssemblyQualifiedName;
+        }
+
+        private void GetCompatibleVariablenames()
+        {
+            statsControllers = _controller.context.variables
+                .Where(x => x.GetVariableName() == "StatsController")
+                .Select(x => new ValueDropdownItem(x.name, x.name)); 
+        }
+
+        #endregion
+
+        #region Flow
+
+        public override void Start()
+        {
+            StatsController stats = GetVariable<StatsController>(statsControllerVariable, null);
+            if(stats != null)
+            {
+                Stat stat = stats.GetStat(statName);
+                if(stat != null)
+                {
+                    stat.value += statValueChange;
+                }
+                else if(debugWarnings) Debug.LogWarning($"Stat \'{statName}\' could not be found in \'{statsControllerVariable}\'.");
+            }
+            else if(debugWarnings) Debug.LogWarning($"Could not find StatsController with GameActionContextVariable named \'{statsControllerVariable}\'.");
+            _done = true;
+            _started = true;
+        }
+
+        public override void Update() { }
+
+        public override void Restart()
+        {
+            _done = false;
+            _started = false;
+        }
+
+        #endregion
+
+        #region utilities
+
+        private StatChangeValueGameAction Duplicate(GameActionsController controller = null)
+        {
+            StatChangeValueGameAction action = new StatChangeValueGameAction(controller == null ? this.controller : controller);
+            action.controller = controller;
+            return action;
+        }
+
+        private T GetVariable<T>(string title, T defaultValue)
+        {
+            for (int i = 0; i < controller.context.variables.Count; i++)
+            {
+                if(controller.context.variables[i].name == title)
+                {
+                    return controller.context.variables[i].GetValue<T>(defaultValue);
+                }
+            }
+            return defaultValue;
+        }
+
+        #endregion
+
+        #region Serialization
+
+        public static StatChangeValueGameAction JSONToStatChangeValueGameAction(string data)
+        {
+            return JsonUtility.FromJson<StatChangeValueGameAction>(data);
+        }
+
+        public override void OnBeforeSerialize(GameActionObjectSerializer serializer, int n, int depth) { }
+        public override void OnBeforeDeserialize(GameActionObjectSerializer serializer, int n, int depth) { }
+
+        #endregion
+    }
+}
+

+ 11 - 0
Runtime/GameActions/Actions/StatChangeValueGameAction.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e9b214fbd1ece734faebdbb3f31cf58c
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 160 - 0
Runtime/GameActions/Actions/StatModifierOperationsGameAction.cs

@@ -0,0 +1,160 @@
+using System;
+using System.Collections;
+using System.Linq;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+using KairoEngine.Core;
+using KairoEngine.Core.GameActions;
+
+namespace KairoEngine.Stats.GameActions
+{
+    public enum StatOperationType
+    {
+        Add,
+        Remove
+    }
+
+    public enum StatModifierContextType
+    {
+        New,
+        Variable
+    }
+
+    [System.Serializable, HideReferenceObjectPicker]
+    public class StatModifierOperationsGameAction : GameActionBase
+    {
+        #region DefaultVariables
+        public override string name { get => $"{operationType.ToString()} StatModifier \'{GetStatModifierText()}\'"; }
+
+        public override GameActionsController controller { 
+            get => _controller; 
+            set 
+            {
+                _controller = value;
+                typeName = "StatModifierOperationsGameAction";
+            }
+        }
+        public override string GetTypeName() => "StatModifierOperationsGameAction";
+        public override string GetActionName() => "Stats/ Stat Modifier Operation";
+
+        #endregion
+
+        #region ActionVariables
+
+        
+        [LabelText("Context"), HideInInspector] public StatModifierContextType statModifierContext = StatModifierContextType.New;
+
+        [InlineProperty, HideLabel, HideReferenceObjectPicker, ShowIf("@statModifierContext == StatModifierContextType.New")] 
+        [IconFoldoutGroup("@name", "Assets/Packages/Simple Vector Icons/PNG icons/Game PNG/Diamonds.png")] 
+        public StatModifier statModifier = new StatModifier();
+
+        [IconFoldoutGroup("@name"), LabelText("operation")] public StatOperationType operationType = StatOperationType.Add;
+
+        [ValueDropdown("statsControllers", IsUniqueList = false), OnInspectorInit("GetCompatibleVariablenames"), LabelText("Target StatsController")]
+        [IconFoldoutGroup("@name")]
+        public string statsControllerVariable;
+
+        private IEnumerable statsControllers = new ValueDropdownList<string>();
+
+        [IconFoldoutGroup("@name")] public bool debugWarnings = false;
+
+        
+
+        #endregion
+
+        #region ActionFunctions
+
+        public StatModifierOperationsGameAction(GameActionsController controller) : base(controller)
+        {
+            this.controller = controller;
+            className =  this.GetType().AssemblyQualifiedName;
+        }
+
+        private void GetCompatibleVariablenames()
+        {
+            statsControllers = _controller.context.variables
+                .Where(x => x.GetVariableName() == "StatsController")
+                .Select(x => new ValueDropdownItem(x.name, x.name)); 
+        }
+
+        public string GetStatModifierText()
+        {
+            if(statModifier == null) return "";
+            else return statModifier.text;
+        }
+
+        #endregion
+
+        #region Flow
+
+        public override void Start()
+        {
+            StatsController stats = GetVariable<StatsController>(statsControllerVariable, null);
+            if(stats != null)
+            {
+                if(statModifier != null)
+                {
+                    Stat stat = stats.GetStat(statModifier.statName);
+                    if(stat == null && debugWarnings) Debug.LogWarning($"Could not find Stat \'{statModifier.statName}\' in StatsController.");
+                    else
+                    {
+                        statModifier.enabled = true;
+                        if(operationType == StatOperationType.Add) stat.modifiers.Add(statModifier);
+                        else stat.modifiers.Remove(statModifier);
+                    }
+                }
+                else if(debugWarnings) Debug.LogWarning($"StatModifier is empty in StatModifier operations Game Action.");
+            }
+            else if(debugWarnings) Debug.LogWarning($"Could not find StatsController with GameActionContextVariable named \'{statsControllerVariable}\'.");
+            _done = true;
+            _started = true;
+        }
+
+        public override void Update() { }
+
+        public override void Restart()
+        {
+            _done = false;
+            _started = false;
+        }
+
+        #endregion
+
+        #region utilities
+
+        private StatModifierOperationsGameAction Duplicate(GameActionsController controller = null)
+        {
+            StatModifierOperationsGameAction action = new StatModifierOperationsGameAction(controller == null ? this.controller : controller);
+            action.controller = controller;
+            return action;
+        }
+
+        private T GetVariable<T>(string title, T defaultValue)
+        {
+            for (int i = 0; i < controller.context.variables.Count; i++)
+            {
+                if(controller.context.variables[i].name == title)
+                {
+                    return controller.context.variables[i].GetValue<T>(defaultValue);
+                }
+            }
+            return defaultValue;
+        }
+
+        #endregion
+
+        #region Serialization
+
+        public static StatModifierOperationsGameAction JSONToStatModifierOperationsGameAction(string data)
+        {
+            return JsonUtility.FromJson<StatModifierOperationsGameAction>(data);
+        }
+
+        public override void OnBeforeSerialize(GameActionObjectSerializer serializer, int n, int depth) { }
+        public override void OnBeforeDeserialize(GameActionObjectSerializer serializer, int n, int depth) { }
+
+        #endregion
+    }
+}
+

+ 11 - 0
Runtime/GameActions/Actions/StatModifierOperationsGameAction.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0b42d50c67e608542a507bb6a3f7da52
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 156 - 0
Runtime/GameActions/Actions/StatusEffectOperationsGameAction.cs

@@ -0,0 +1,156 @@
+using System;
+using System.Collections;
+using System.Linq;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+using KairoEngine.Core.GameActions;
+using KairoEngine.Core;
+
+namespace KairoEngine.Stats.GameActions
+{
+
+    [System.Serializable, HideReferenceObjectPicker]
+    public class StatusEffectOperationsGameAction : GameActionBase
+    {
+        #region DefaultVariables
+        public override string name { get => $"{operationType.ToString()} StatusEffect \'{GetStatuseffectName()}\'"; }
+
+        public override GameActionsController controller { 
+            get => _controller; 
+            set 
+            {
+                _controller = value;
+                typeName = "StatusEffectOperationsGameAction";
+            }
+        }
+        public override string GetTypeName() => "StatusEffectOperationsGameAction";
+        public override string GetActionName() => "Stats/Status Effect Operation";
+
+        #endregion
+
+        #region ActionVariables
+
+        [IconFoldoutGroup("@name", "Assets/Plugins/KairoEngine/Stats/Editor/Icons/Fire.png")]  public StatusEffectTemplate statusEffect;
+
+        [IconFoldoutGroup("@name"), LabelText("Operation")] public StatOperationType operationType = StatOperationType.Add;
+
+        [ValueDropdown("statsControllers", IsUniqueList = false), OnInspectorInit("GetCompatibleVariablenames"), LabelText("Target StatsController")]
+        [IconFoldoutGroup("@name")]
+        public string statsControllerVariable;
+
+        [IconFoldoutGroup("@name"), ValueDropdown("sourcesList", IsUniqueList = false), OnInspectorInit("GetCompatibleGameObjects"), LabelText("Source")] 
+        public string sourceVariableName;
+
+        private IEnumerable statsControllers = new ValueDropdownList<string>();
+        private IEnumerable sourcesList = new ValueDropdownList<string>();
+
+        [IconFoldoutGroup("@name")] public bool debugWarnings = false;
+
+        #endregion
+
+        #region ActionFunctions
+
+        public StatusEffectOperationsGameAction(GameActionsController controller) : base(controller)
+        {
+            this.controller = controller;
+            className =  this.GetType().AssemblyQualifiedName;
+        }
+
+        private void GetCompatibleVariablenames()
+        {
+            statsControllers = _controller.context.variables
+                .Where(x => x.GetVariableName() == "StatsController")
+                .Select(x => new ValueDropdownItem(x.name, x.name)); 
+        }
+
+        private void GetCompatibleGameObjects()
+        {
+            sourcesList = _controller.context.variables
+                .Where(x => x.GetVariableName() == "GameObject")
+                .Select(x => new ValueDropdownItem(x.name, x.name)); 
+        }
+
+        public string GetStatuseffectName()
+        {
+            if(statusEffect != null) return statusEffect.title;
+            else return "";
+        }
+
+        #endregion
+
+        #region Flow
+
+        public override void Start()
+        {
+            StatsController stats = GetVariable<StatsController>(statsControllerVariable, null);
+            if(stats != null)
+            {
+                if(statusEffect != null)
+                {
+                    GameObject source = GetVariable<GameObject>(sourceVariableName, null);
+                    Transform sourceTransform = null;
+                    if (source != null) sourceTransform = source.transform;
+                    stats.statusEffects.Add(statusEffect, sourceTransform);
+                }
+                else if(debugWarnings) Debug.LogWarning($"StatusEffect is missing in StatusEffectOperation GameAction.");
+            }
+            else if(debugWarnings) Debug.LogWarning($"Could not find StatsController with GameActionContextVariable named \'{statsControllerVariable}\'.");
+            _done = true;
+            _started = true;
+        }
+
+        public override void Update() { }
+
+        public override void Restart()
+        {
+            _done = false;
+            _started = false;
+        }
+
+        #endregion
+
+        #region utilities
+
+        private StatusEffectOperationsGameAction Duplicate(GameActionsController controller = null)
+        {
+            StatusEffectOperationsGameAction action = new StatusEffectOperationsGameAction(controller == null ? this.controller : controller);
+            action.controller = controller;
+            return action;
+        }
+
+        private T GetVariable<T>(string title, T defaultValue)
+        {
+            for (int i = 0; i < controller.context.variables.Count; i++)
+            {
+                if(controller.context.variables[i].name == title)
+                {
+                    return controller.context.variables[i].GetValue<T>(defaultValue);
+                }
+            }
+            return defaultValue;
+        }
+
+        #endregion
+
+        #region Serialization
+
+        public static StatusEffectOperationsGameAction JSONToStatusEffectOperationsGameAction(string data)
+        {
+            return JsonUtility.FromJson<StatusEffectOperationsGameAction>(data);
+        }
+
+        public override void OnBeforeSerialize(GameActionObjectSerializer serializer, int n, int depth) 
+        { 
+            serializer.SerializeScriptableObject($"{depth}-{n}-statusEffect", statusEffect);
+        }
+
+        public override void OnBeforeDeserialize(GameActionObjectSerializer serializer, int n, int depth) 
+        { 
+            statusEffect = (StatusEffectTemplate)serializer.DeserializeScriptableObject($"{depth}-{n}-statusEffect");
+        }
+
+        #endregion
+    }
+}
+

+ 11 - 0
Runtime/GameActions/Actions/StatusEffectOperationsGameAction.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0f5456cfc6215fe429df95033642e8d9
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Runtime/GameActions/Variables.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 2735fb3893898604685b0a1a4d2ec0f0
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 100 - 0
Runtime/GameActions/Variables/GameActionContextStatsController.cs

@@ -0,0 +1,100 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+using KairoEngine.Stats;
+using KairoEngine.Core.GameActions;
+
+namespace KairoEngine.Stats.GameActions
+{
+    public class GameActionContextStatsController : GameActionContextVariableBase
+    {
+        public StatsController value { 
+            get 
+            {
+                if(_value == null && searchTarget != null) UpdateSearchTarget();
+                return _value;
+            }
+            set => _value = value; }
+
+        [HorizontalGroup("value"), LabelText("@name"), ShowInInspector, PropertyOrder(1), ReadOnly, OnInspectorInit("UpdateSearchTarget")] 
+        private string result = "";
+
+        [HorizontalGroup("value", 0.14f), Button("@editButtonName"), ShowIf("@canEdit"), PropertyOrder(2)]
+        private void EditValue()
+        {
+            if(showEdit == false)
+            {
+                showEdit = true;
+                editButtonName = "Done";
+            }
+            else
+            {
+                showEdit = false;
+                editButtonName = "Edit";
+            }
+        }
+
+        private string editButtonName = "Edit";
+
+        [NonSerialized] internal StatsController _value;
+
+        [NonSerialized, ShowInInspector, ShowIf("@showEdit && canEdit"), PropertyOrder(4), LabelText("Search in"), OnValueChanged("UpdateSearchTarget")] 
+        internal GameObject searchTarget;
+
+        private void UpdateSearchTarget()
+        {
+            if(searchTarget == null) return;
+            var statsComponent = searchTarget.GetComponent<StatsComponent>();
+            if(statsComponent == null)
+            {
+                statsComponent = searchTarget.GetComponentInChildren<StatsComponent>();
+            }
+            if(statsComponent != null) 
+            {
+                _value = statsComponent.statsController;
+                result = $"{statsComponent.gameObject.name} StatsComponent";
+            }
+            else
+            {
+                if(_value == null) result = "";
+                else result = "StatController";
+            }
+
+        }
+
+        public override string GetTypeName() => "GameActionContextStatsController";
+        public override string GetVariableName() => "StatsController";
+
+        public override T GetValue<T>(T defaultValue)
+        {
+            var result = value;
+            try
+            {
+                return (T)System.Convert.ChangeType(value, typeof(T));
+            }
+            catch
+            {
+                return defaultValue;
+            }
+        }
+
+        public static GameActionContextStatsController JSONToGameActionContextStatsController(string data)
+        {
+            return JsonUtility.FromJson<GameActionContextStatsController>(data);
+        }
+
+        public override void OnBeforeSerialize(GameActionObjectSerializer serializer) 
+        { 
+            serializer.SerializeGameObject($"{name}-searchTarget", searchTarget);
+        }
+        public override void OnBeforeDeserialize(GameActionObjectSerializer serializer) 
+        { 
+            searchTarget = serializer.DeserializeGameObject($"{name}-searchTarget");
+            //UpdateSearchTarget();
+        }
+    }
+}
+
+

+ 11 - 0
Runtime/GameActions/Variables/GameActionContextStatsController.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 929312a7f5772d44c884b3e449978bcd
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 18 - 0
Runtime/KairoEngine.Stats.asmdef

@@ -0,0 +1,18 @@
+{
+    "name": "KairoEngine.Stats",
+    "references": [
+        "GUID:7e5ae6a38d1532248b4c890eca668b06",
+        "GUID:560b04d1a97f54a4e82edc0cbbb69285",
+        "GUID:5f03fc37b95cb644599751ca563336b2",
+        "GUID:8bb76041527da2b43baf46cb672097ab"
+    ],
+    "includePlatforms": [],
+    "excludePlatforms": [],
+    "allowUnsafeCode": false,
+    "overrideReferences": false,
+    "precompiledReferences": [],
+    "autoReferenced": true,
+    "defineConstraints": [],
+    "versionDefines": [],
+    "noEngineReferences": false
+}

+ 7 - 0
Runtime/KairoEngine.Stats.asmdef.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: bc1ac81aedfa56c4083dea5332ed3810
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 213 - 0
Runtime/Stat.cs

@@ -0,0 +1,213 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+using NCalc;
+
+namespace KairoEngine.Stats
+{
+    [System.Serializable]
+    public class Stat
+    {
+        [HideInInspector] public string title;
+        private string description;
+
+        [LabelText("@title"), PropertyOrder(1)]
+        [ProgressBar("@_minValue + modifiers.minValue", "@_maxValue + modifiers.maxValue", ColorGetter="GetDisplayColor", Height=18)]
+        [PropertyTooltip("@description"), ShowInInspector, InlineButton("ShowModifiers", "@modifiers.pretyCount()")]
+        public int value
+        {
+            get 
+            {
+                if(template.currentValueType == StatValueType.Equation) this._value = SolveEquation(template.currentValueEquation);
+                int finalValue = this._value;
+                finalValue += modifiers.value;
+                if(finalValue > maxValue) finalValue = maxValue;
+                if(finalValue < minValue) finalValue = minValue;
+                return finalValue;
+            }
+            set 
+            {
+                if(template.currentValueType == StatValueType.Value)  this._value = value;
+                else _value = SolveEquation(template.currentValueEquation);
+                if(this._value > maxValue) this._value = maxValue;
+                if(this._value < minValue) this._value = minValue;
+                if(this._value != value) Changed();
+            }
+        }
+        [SerializeField, HideInInspector] private int _value;
+        public int GetValueRaw() => _value;
+        
+        public int minValue
+        {
+            get
+            {
+                if(template.minValueType == StatValueType.Equation) this._minValue = SolveEquation(template.minValueEquation);
+                int finalValue = this._minValue;
+                finalValue += modifiers.minValue;
+                return finalValue;
+            }
+            set 
+            {
+                if(template.minValueType == StatValueType.Value)  this._minValue = value;
+                else this._minValue = SolveEquation(template.minValueEquation);
+            }
+        }
+        [SerializeField, HideInInspector] private int _minValue = 0;
+        public int GetMinValueRaw() => _minValue;
+
+        public int maxValue
+        {
+            get
+            {
+                if(template.maxValueType == StatValueType.Equation) this._maxValue = SolveEquation(template.maxValueEquation);
+                int finalValue = this._maxValue;
+                finalValue += modifiers.maxValue;
+                return finalValue;
+            }
+            set 
+            {
+                if(template.maxValueType == StatValueType.Value)  this._maxValue = value;
+                else this._maxValue = SolveEquation(template.maxValueEquation);
+            }
+        }
+        [SerializeField, HideInInspector] private int _maxValue = 1;
+        public int GetMaxValueRaw() => _maxValue;
+
+        private bool showModifiersEditor = false;
+        [PropertyOrder(2), InlineProperty, HideLabel, ShowIf("@showModifiersEditor"), Space] public StatModifiersController modifiers;
+
+        [SerializeField, HideInInspector] public StatTemplate template;
+
+        [HideInInspector, System.NonSerialized] public StatsController controller;
+
+        public Stat(StatTemplate template, StatsController controller, SerializedStat data = null)
+        {
+            this.title = template.title;
+            this.template = template;
+            this.controller = controller;
+            this.description = template.description;
+            this.modifiers = new StatModifiersController(this);
+            if(data != null)
+            {
+                _value = data.value;
+                _minValue = data.minValue;
+                _maxValue = data.maxValue;
+                for (int i = 0; i < data.modifiers.Count; i++) modifiers.list.Add(data.modifiers[i]);
+            }
+            this.modifiers.Calculate();
+        }
+
+        public void Initialize()
+        {
+            if(template.currentValueType == StatValueType.Value)
+            {
+                if(template.initialValueType == StatValueType.Value) this.value = template.initialValue;
+                else this.value = SolveEquation(template.initialValueEquation);
+            }
+            else this._value = value;
+            this.minValue = template.minValue;
+            this.maxValue = template.maxValue;
+            modifiers.stat = this;
+        }
+
+        public void Update()
+        {
+            title = template.title;
+            description = template.description;
+            value = _value;
+            minValue = _minValue;
+            maxValue =_maxValue;
+            //Debug.Log($"Updating {StatLog()}");
+        }
+
+        public void Changed() => controller.UpdateStats();
+
+        public int SolveEquation(string equation)
+        {
+            int result = 0;
+            if(equation == "") 
+            {
+                Debug.LogError(StatLog());
+                return result;
+            }
+            if(controller == null) return result;
+            var expr = new Expression(equation);
+            Func<StatEquationContext, int> f = expr.ToLambda<StatEquationContext, int>();
+            var context = new StatEquationContext { value = _value, minValue = _minValue, maxValue = _maxValue, controller = controller};
+            result = f(context);
+            //Debug.Log($"{title}: {equation} = {result}");
+            return result;
+        }
+
+        public string StatLog()
+        {
+            string s = $" {title}: ";
+            s += $" value={this._value} minValue={this._minValue} maxValue={this._maxValue} ";
+            if(template.currentValueType == StatValueType.Equation) s += $"currentEquation='{template.currentValueEquation}' ";
+            if(template.initialValueType == StatValueType.Equation) s += $"initialEquation='{template.initialValueEquation}' ";
+            if(template.minValueType == StatValueType.Equation) s += $"minEquation='{template.minValueEquation}' ";
+            if(template.maxValueType == StatValueType.Equation) s += $"maxEquation='{template.maxValueEquation}' ";
+            return s;
+        }
+
+        private Color GetDisplayColor()
+        {
+            if(template.currentValueType == StatValueType.Equation) return Color.gray;
+            else 
+            {
+                Color color;
+                if ( !ColorUtility.TryParseHtmlString("#3e649e", out color)) color = Color.cyan;
+                return color;
+            }
+        }
+
+        public SerializedStat Serialize() => new SerializedStat(title, _value, _minValue, _maxValue, modifiers.list);
+
+        public static Stat Deserialize(SerializedStat data, StatsController controller)
+        {
+            Stat stat = new Stat(controller.GetStatTemplate(data.title), controller, data);
+            return stat;
+        }
+
+        public void ShowModifiers()
+        {
+            if(showModifiersEditor) showModifiersEditor = false;
+            else showModifiersEditor = true;
+            modifiers.Calculate();
+        }
+    }
+
+    public class StatEquationContext
+    {
+        public int value { get; set; }
+        public int minValue { get; set; }
+        public int maxValue { get; set; }
+        public StatsController controller { get; set; }
+        public int s(string title) => controller.GetStatValue(title);
+    }
+
+    [System.Serializable]
+    public class SerializedStat
+    {
+        public string title;
+        public int value;
+        public int minValue;
+        public int maxValue;
+
+        public List<StatModifier> modifiers = new List<StatModifier>();
+
+        public SerializedStat(string title, int value, int minValue, int maxValue, List<StatModifier> modifiers)
+        {
+            this.title = title;
+            this.value = value;
+            this.minValue = minValue;
+            this.maxValue = maxValue;
+            for (int i = 0; i < modifiers.Count; i++)
+            {
+                this.modifiers.Add(modifiers[i]);
+            }
+        }
+    }
+}

+ 11 - 0
Runtime/Stat.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bb8b9c06bc514bb4c8a290b1cd114936
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 13 - 0
Runtime/StatGroup.cs

@@ -0,0 +1,13 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace KairoEngine.Stats
+{
+    [CreateAssetMenu(fileName = "StatGroup", menuName = "KairoEngine/Stat Group", order = 7)]
+    public class StatGroup : ScriptableObject
+    {
+        public List<StatTemplate> statTemplates = new List<StatTemplate>();
+    }
+}
+

+ 11 - 0
Runtime/StatGroup.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 53b65a11ca7b8b04faa6ea6c3972fab8
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 63 - 0
Runtime/StatModifier.cs

@@ -0,0 +1,63 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+
+namespace KairoEngine.Stats
+{
+    [System.Serializable]
+    public class StatModifier
+    {
+        [ShowInInspector, HideLabel, ShowIf("@enabled == true")] public string text => $"{op}{value}{endOp} {valueType} {statName} - {from}";
+        [ShowInInspector, HideLabel, HorizontalGroup("m",Width=0.02f), ShowIf("@enabled == false")] public bool enabled = false;
+        [HideLabel, HorizontalGroup("m", Width=0.1f), ValueDropdown("GetOperators"), ShowIf("@enabled == false")] public string op = "+";
+        [HideLabel, HorizontalGroup("m", Width=0.1f), ShowIf("@enabled == false")] public int value = 1;
+        [HideLabel, HorizontalGroup("m", Width=0.1f), ValueDropdown("GetEndOperators"), ShowIf("@enabled == false")] public string endOp = "";
+        [HideLabel, HorizontalGroup("m", Width=0.16f), ValueDropdown("GetValueTypes"), ShowIf("@enabled == false")] public string valueType = "";
+        [HideLabel, HorizontalGroup("m", Width=0.2f), ShowIf("@enabled == false")] public string statName = "Stat Name";
+        [HideLabel, HorizontalGroup("m", Width=0.32f), ShowIf("@enabled == false")] public string from = "From Entity";
+        
+        private IEnumerable GetOperators = new ValueDropdownList<string>()
+        {
+            { "+", "+" },
+            { "-", "-" }
+        };
+        private IEnumerable GetEndOperators = new ValueDropdownList<string>()
+        {
+            { " ", "" },
+            { "%", "%" }
+        };
+
+        private IEnumerable GetValueTypes = new ValueDropdownList<string>()
+        {
+            { " ", "" },
+            { "min", "min" },
+            { "max", "max" }
+        };
+
+        public bool Equals(StatModifier statModifier) => statModifier.text == text ? true : false;
+
+        public StatModifier()
+        {
+            this.op = "+";
+            this.value = 1;
+            this.endOp = "";
+            this.valueType = "";
+            this.statName = "Stat Name";
+            this.from = "From Entity";
+            this.enabled = false;
+        }
+
+        public StatModifier(string op = "+", int value = 1, string endOp = "", string valueType = "", string statName = "Stat Name", string from = "From Entity", bool enabled = true)
+        {
+            this.op = op;
+            this.value = value;
+            this.endOp = endOp;
+            this.valueType = valueType;
+            this.statName = statName;
+            this.from = from;
+            this.enabled = enabled;
+        }
+    }
+}
+

+ 11 - 0
Runtime/StatModifier.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0841d3662e5f3b04aa732e428e8a179b
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 96 - 0
Runtime/StatModifiersController.cs

@@ -0,0 +1,96 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+
+namespace KairoEngine.Stats
+{
+    [System.Serializable]
+    public class StatModifiersController
+    {
+        [ListDrawerSettings(HideAddButton = false, HideRemoveButton = false, DraggableItems = false, Expanded = true, ShowPaging = false, ShowItemCount = false)]
+        [PropertyOrder(1), LabelText("Modifiers"), OnCollectionChanged("Calculate")] public List<StatModifier> list = new List<StatModifier>();
+
+        [HideInInspector] public int value = 0;
+        [HideInInspector] public int minValue = 0;
+        [HideInInspector] public int maxValue = 0;
+        [System.NonSerialized] public Stat stat;
+
+        public string pretyValue()
+        {
+             if(value > 0) return $"+{value}";
+             else if (value == 0) return " 0 ";
+             else return $"{value}";
+        }
+
+        public string pretyCount()
+        {
+            if(list.Count > 99) return $" {list.Count} ";
+            else if (list.Count > 9) return $" {list.Count} ";
+            else return $"  {list.Count}  ";
+        }
+
+        public StatModifiersController(Stat stat)
+        {
+            this.stat = stat;
+        }
+
+        public void Add(StatModifier statModifier)
+        {
+            list.Add(statModifier);
+            Calculate();
+        }
+
+        public void Remove(StatModifier statModifier)
+        {
+            for (int i = 0; i < list.Count; i++)
+            {
+                if(statModifier.Equals(list[i]))
+                {
+                    list.Remove(list[i]);
+                    Calculate();
+                    return;
+                }
+            }
+        }
+
+        public bool HasModifier(StatModifier statModifier)
+        {
+            for (int i = 0; i < list.Count; i++)
+            {
+                if(statModifier.Equals(list[i])) return true;
+            }
+            return false;
+        }
+
+        public void Calculate()
+        {
+            if(stat == null) return;
+            value = CalculateModifier("", stat.GetValueRaw());
+            minValue = CalculateModifier("min", stat.GetMinValueRaw());
+            maxValue = CalculateModifier("max", stat.GetMaxValueRaw());
+            //Debug.Log($"{stat.title} Modifiers: value={value} minValue={minValue} maxValue={maxValue}");
+        }
+
+        private int CalculateModifier(string valueType, int statValue)
+        {
+            int values = 0;
+            int percentage = 0;
+            for (int i = 0; i < list.Count; i++)
+            {
+                if(list[i].endOp == "%" && list[i].valueType == valueType) 
+                {
+                    if(list[i].op == "+") percentage += list[i].value;
+                    else percentage -= list[i].value;
+                }
+                else if(list[i].endOp == "" && list[i].valueType == valueType) 
+                {
+                     if(list[i].op == "+") values += list[i].value;
+                     else values -= list[i].value;
+                }
+            }
+            if(percentage != 0) percentage = ((statValue + values) * percentage) /100;
+            return percentage + values;
+        }
+    }
+}

+ 11 - 0
Runtime/StatModifiersController.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7752098e8aaff1144a0139ee81bda320
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 77 - 0
Runtime/StatTemplate.cs

@@ -0,0 +1,77 @@
+using System;
+using System.Collections;
+using System.Linq;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+
+namespace KairoEngine.Stats
+{
+    public enum StatValueType
+    {
+        Value,
+        Equation
+    }
+
+    [HideMonoScript]
+    [CreateAssetMenu(fileName = "Stat", menuName = "KairoEngine/Stat", order = 6)]
+    public class StatTemplate : ScriptableObject
+    {
+        public string title = "Stat Name";
+
+        public string description = "";
+
+        [Header("Current Value")]
+        [LabelText("Type")] public StatValueType currentValueType = StatValueType.Value;
+        [LabelText("Equation"), ShowIf("@currentValueType == StatValueType.Equation")] public string currentValueEquation;
+
+        [Header("Initial Value")]
+        [LabelText("Type"), HideIf("@currentValueType == StatValueType.Equation")] public StatValueType initialValueType = StatValueType.Value;
+        [LabelText("Value"), ShowIf("@initialValueType == StatValueType.Value && currentValueType != StatValueType.Equation")] public int initialValue = 100;
+        [LabelText("Equation"), ShowIf("@initialValueType == StatValueType.Equation && currentValueType != StatValueType.Equation")] public string initialValueEquation;
+        
+        [Header("Minimum Value")]
+        [LabelText("Type")] public StatValueType minValueType = StatValueType.Value;
+        [LabelText("Value"), ShowIf("@minValueType == StatValueType.Value")] public int minValue = 0;
+        [LabelText("Equation"), ShowIf("@minValueType == StatValueType.Equation")] public string minValueEquation;
+
+        [Header("Maximum Value")]
+        [LabelText("Type")] public StatValueType maxValueType = StatValueType.Value;
+        [LabelText("Value"), ShowIf("@maxValueType == StatValueType.Value")] public int maxValue = 100;
+        [LabelText("Equation"), ShowIf("@maxValueType == StatValueType.Equation")] public string maxValueEquation;
+
+        public StatTemplate Setup(string title, string desc, int init, int min, int max, string valueEq="", string initEq="", string minEq="", string maxEq="")
+        {
+            this.title = title;
+            this.description = desc;
+            initialValue = init;
+            minValue = min;
+            maxValue = max;
+            currentValueType = StatValueType.Value;
+            initialValueType = StatValueType.Value;
+            minValueType = StatValueType.Value;
+            maxValueType = StatValueType.Value;
+            if(valueEq != "") 
+            {
+                currentValueEquation = valueEq;
+                currentValueType = StatValueType.Equation;
+            }
+            else if(initEq != "") 
+            {
+                initialValueEquation = initEq;
+                initialValueType = StatValueType.Equation;
+            }
+            if(minEq != "") 
+            {
+                minValueEquation = minEq;
+                minValueType = StatValueType.Equation;
+            }
+            if(maxEq != "") 
+            {
+                maxValueEquation = maxEq;
+                maxValueType = StatValueType.Equation;
+            }
+            return this;
+        }
+    }
+}

+ 11 - 0
Runtime/StatTemplate.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: eb5427bb5e8aade40ae4f70f41508552
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 42 - 0
Runtime/StatsComponent.cs

@@ -0,0 +1,42 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+
+namespace KairoEngine.Stats
+{
+    [HideMonoScript, AddComponentMenu("KairoEngine/Stats Controller")]
+    public class StatsComponent : MonoBehaviour
+    {
+        
+        [InlineProperty, HideLabel, PropertySpace(2, 2), OnInspectorInit("Initialize")] public StatsController statsController = new StatsController();
+
+        //[InlineButton("CreateNewStatusEffect", "Create")]
+        [LabelText("Add new StatusEffect"), ShowIf("@showEdit")] public StatusEffectTemplate template;
+
+        [ShowIf("@target == null")] public Transform target;
+
+        private bool showEdit = false;
+        
+        void Awake()
+        {
+            statsController.Initialize();
+        }
+
+        void OnDisable()
+        {
+            statsController.statusEffects.Stop();
+        }
+
+        private void Initialize()
+        {
+            statsController.statusEffects.target = target;
+            statsController.statusEffects.Start();
+        }
+
+        private void CreateNewStatusEffect()
+        {
+            statsController.statusEffects.Add(template, null);
+        }
+    }
+}

+ 11 - 0
Runtime/StatsComponent.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 9fa14d8470b987347ac3221898e4dadc
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 130 - 0
Runtime/StatsController.cs

@@ -0,0 +1,130 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+using Sirenix.Serialization;
+
+namespace KairoEngine.Stats
+{
+    [System.Serializable]
+    public class StatsController : ISerializationCallbackReceiver
+    {
+        [OnValueChanged("Setup"), HideIf("@statGroup != null")]
+        public StatGroup statGroup;
+
+        [OnInspectorInit("Initialize")]
+        [ListDrawerSettings(HideAddButton = true, HideRemoveButton = true, DraggableItems = false, Expanded = true, ShowPaging = false, ShowItemCount = false)]
+        public List<Stat> stats;
+
+        [InlineProperty, HideLabel, ShowIf("@statusEffects.statusEffects.Count > 0")]
+        public StatusEffectsController statusEffects;
+
+        public StatGroup StatGroup 
+        { 
+            get => statGroup; 
+            set 
+            {
+                statGroup = value;
+                if(statGroup != null) Setup();
+            } 
+        }
+
+        public void Setup()
+        {
+            if(StatGroup == null) return;
+            stats = new List<Stat>();
+            foreach (var statTemplate in StatGroup.statTemplates)
+            {
+                Stat stat = new Stat(statTemplate, this);
+                stats.Add(stat);
+            }
+            for (int a = 0; a < 4; a++)
+            {
+                for (int i = 0; i < stats.Count; i++) stats[i].Initialize();
+            }
+            UpdateStats();
+        }
+
+        public void Initialize()
+        {
+            if(statGroup == null) return;
+            if(stats != null)
+            {
+                if(stats.Count == 0)
+                { 
+                    Setup();
+                    return;
+                }
+            }
+            for (int i = 0; i < StatGroup.statTemplates.Count; i++)
+            {
+                bool found = false;
+                for (int a = 0; a < stats.Count; a++)
+                {
+                    if(stats[a].title == StatGroup.statTemplates[i].title) found = true;
+                }
+                if(found == false) 
+                {
+                    Stat stat = new Stat(StatGroup.statTemplates[i], this);
+                    stats.Add(stat);
+                }
+            }
+            List<Stat> newStats = new List<Stat>();
+            foreach(var stat in stats) newStats.Add(Stat.Deserialize(stat.Serialize(), this));
+            stats = newStats;
+            if(statusEffects != null) statusEffects.SetStatsController(this);
+            UpdateStats();
+        }
+
+        public void UpdateStats()
+        {
+            foreach (var stat in stats) stat.Update();
+            foreach (var stat in stats) stat.Update();
+        }
+
+        public int GetStatValue(string title)
+        {
+            int result = 0;
+            Stat stat = GetStat(title);
+            if(stat != null) result = stat.value;
+            return result;
+        }
+
+        public Stat GetStat(string title)
+        {
+            for (int i = 0; i < stats.Count; i++)
+            {
+                if(stats[i].title == title) return stats[i];
+            }
+            return null;
+        }
+
+        public StatTemplate GetStatTemplate(string title)
+        {
+            if(StatGroup == null) return null;
+            for (int i = 0; i < StatGroup.statTemplates.Count; i++)
+            {
+                if(StatGroup.statTemplates[i].title == title) return StatGroup.statTemplates[i];
+            }
+            return null;
+        }
+
+        public void OnBeforeSerialize()
+        {
+            
+        }
+
+        public void OnAfterDeserialize()
+        {
+            
+            for (int i = 0; i < stats.Count; i++)
+            {
+                stats[i].controller = this;
+                if(stats[i].modifiers != null) stats[i].modifiers.stat = stats[i];
+            }
+            if(statusEffects != null) statusEffects.controller = this;
+        }
+    }
+}
+

+ 11 - 0
Runtime/StatsController.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8b8bdc06c27313244843e3a646551c1a
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Runtime/StatusEffects.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 4a8e8b56abaf05e47b707961653f67ea
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 96 - 0
Runtime/StatusEffects/StatusEffect.cs

@@ -0,0 +1,96 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+using KairoEngine.Core.GameActions;
+using KairoEngine.Stats.GameActions;
+
+namespace KairoEngine.Stats
+{
+    [System.Serializable]
+    public class StatusEffect
+    {
+        [SerializeField, HideInInspector] private string name = "";
+        [FoldoutGroup("@name")] public StatusEffectTemplate template;
+        [FoldoutGroup("@name")] public Transform target;
+        [FoldoutGroup("@name")] public Transform source;
+        [FoldoutGroup("@name")] public int stack = 1;
+        [SerializeField, HideInInspector] private List<GameActionsController> actionsControllerList;
+        [SerializeField, HideInInspector] private GameActionContext context;
+        [SerializeField, HideInInspector] private List<float> time;
+
+        public StatusEffect(StatusEffectTemplate template, Transform target = null, 
+            Transform source = null, StatsController statsController = null)
+        {
+            this.stack = 1;
+            this.name = $"{template.title} ({stack})";
+            this.time = new List<float>();
+            this.time.Add(0);
+            this.target = target;
+            this.source = source;
+            this.template = template;
+            this.actionsControllerList = new List<GameActionsController>();
+            this.actionsControllerList.Add(template.onAddController.Duplicate());
+            this.actionsControllerList.Add(template.onStackController.Duplicate());
+            this.actionsControllerList.Add(template.onWhileController.Duplicate());
+            this.actionsControllerList.Add(template.onDestackController.Duplicate());
+            this.actionsControllerList.Add(template.onRemoveController.Duplicate());
+            this.context = template.context.Duplicate();
+            foreach (var ctrl in actionsControllerList) ctrl.context = this.context;
+            // Set Game Action Variables
+            var targetVar = context.GetVariable("Target GameObject");
+            if(targetVar != null) ((GameActionContextGameObject)targetVar).value = this.target.gameObject;
+            var statsVar = context.GetVariable("Stats");
+            if(statsVar != null) ((GameActionContextStatsController)statsVar).value = statsController;
+            actionsControllerList[0].Start(); // Run the OnAdd actions
+        }
+
+        public void AddToStack()
+        {
+            if(template.maxStack <= stack) return;
+            this.stack += 1;
+            this.name = $"{template.title} ({stack})";
+            this.time.Add(0);
+            this.actionsControllerList[1].Restart();
+            this.actionsControllerList[1].Start(); // Run the OnStack actions
+        }
+
+        public void OnRemove()
+        {
+            this.actionsControllerList[4].Start(); // Run OnRemove actions
+        }
+
+        /// <summary>Update times for each unit in the stack. If a unit has reached the template duration, 
+        /// it is removed from the stack and the OnDestack actions are run. </summary>
+        /// <param name="elapsed">How much time has passed since the last update. Just pass Time.deltaTime.</param>
+        public void Update(float elapsed)
+        {
+            if(this.template.hasDuration == false) return;
+            for (int i = 0; i < this.time.Count; i++) this.time[i] += elapsed;
+            for (int i = 0; i < this.time.Count; i++)
+            {
+                if(this.time[i] > template.duration)
+                {
+                    this.time.RemoveAt(i);
+                    this.stack -= 1;
+                    this.name = $"{template.title} ({stack})";
+                    if(stack > 0)
+                    {
+                        this.actionsControllerList[3].Restart();
+                        this.actionsControllerList[3].Start(); // Tun OnDestack actions;
+                    } 
+                    else OnRemove();
+                    
+                    
+                }
+            }
+            if(this.actionsControllerList[2].isDone || !this.actionsControllerList[2].started)
+            {
+                this.actionsControllerList[2].Restart();
+                this.actionsControllerList[2].Start(); // Run OnWhile actions
+            }
+            
+        }
+    }
+}
+

+ 11 - 0
Runtime/StatusEffects/StatusEffect.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c8675f34b4791064898a0913e1e65d7e
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 79 - 0
Runtime/StatusEffects/StatusEffectTemplate.cs

@@ -0,0 +1,79 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+using KairoEngine.Core;
+using KairoEngine.Core.GameActions;
+using KairoEngine.Stats.GameActions;
+
+namespace KairoEngine.Stats
+{
+    [CreateAssetMenu(fileName = "StatusEffect", menuName = "KairoEngine/Status Effect", order = 8)]
+    [HideMonoScript]
+    public class StatusEffectTemplate : ScriptableObject
+    {
+        public string title = "";
+        public string description = "";
+        public bool hasDuration = true;
+        [ShowIf("@hasDuration")] public float duration = 1;
+        [Range(1, 10)] public int maxStack = 1;
+
+        // [ListDrawerSettings(HideAddButton = false, HideRemoveButton = false, DraggableItems = false, Expanded = true, ShowPaging = false, ShowItemCount = false)]
+        // [Space]
+        // public List<StatModifier> modifiers = new List<StatModifier>();
+
+        [TitleGroup("Actions")]
+        [HideLabel, InlineProperty, OnInspectorInit("SetupContext"), PropertySpace(2, 4)]
+        public GameActionContext context = new GameActionContext();
+
+        [TabGroup("Actions/Triggers", "On Add"), InlineProperty, HideLabel, PropertySpace(1, 2)]
+        [InfoBox("Execute actions when the StatusEffect is added to a StatsController", InfoMessageType.Info)]
+        public GameActionsController onAddController;
+
+        [TabGroup("Actions/Triggers", "On Stack"), InlineProperty, HideLabel, ShowIf("@maxStack > 1"), PropertySpace(1, 2)]
+        [InfoBox("Executes when the StatusEffect stack is increased", InfoMessageType.Info)]
+        public GameActionsController onStackController;
+
+        [TabGroup("Actions/Triggers", "While"), InlineProperty, HideLabel, PropertySpace(1, 2)]
+        [InfoBox("Executes every frame while the StatusEffect is active", InfoMessageType.Info)]
+        public GameActionsController onWhileController;
+
+        [TabGroup("Actions/Triggers", "On Destack"), InlineProperty, HideLabel, ShowIf("@maxStack > 1"), PropertySpace(1, 2)]
+        [InfoBox("Executes when the StatusEffect stack is decreased", InfoMessageType.Info)]
+        public GameActionsController onDestackController;
+
+        [TabGroup("Actions/Triggers", "On Remove"), InlineProperty, HideLabel, PropertySpace(1, 2)]
+        [InfoBox("Executes when the StatusEffect is removed from the StatsController", InfoMessageType.Info)]
+        public GameActionsController onRemoveController;
+
+        private void SetupContext()
+        {
+            if(context == null) return;
+            onAddController.context = context;
+            onStackController.context = context;
+            onWhileController.context = context;
+            onDestackController.context = context;
+            onRemoveController.context = context;
+            if(!context.HasVariable("Target GameObject"))
+            {
+                var variable = new GameActionContextGameObject();
+                variable.name = "Target GameObject";
+                variable.value = null;
+                variable.canEdit = false;
+                context.variables.Add(variable);
+            } 
+            if(!context.HasVariable("Stats"))
+            {
+                var variable = new GameActionContextStatsController();
+                variable.name = "Stats";
+                variable.value = null;
+                variable.canEdit = false;
+                context.variables.Add(variable);
+            } 
+        }
+
+
+    }
+}
+
+

+ 11 - 0
Runtime/StatusEffects/StatusEffectTemplate.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 9618c117cf6736349a4b5b4b102d3646
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 108 - 0
Runtime/StatusEffects/StatusEffectsController.cs

@@ -0,0 +1,108 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+using UniRx;
+
+namespace KairoEngine.Stats
+{
+    [System.Serializable]
+    public class StatusEffectsController
+    {
+        [System.NonSerialized] public StatsController controller;
+
+        public void SetStatsController(StatsController controller) => this.controller = controller;
+        
+        [ListDrawerSettings(HideAddButton = true, HideRemoveButton = true, DraggableItems = false, Expanded = true, ShowPaging = false, ShowItemCount = false)]
+        public List<StatusEffect> statusEffects = new List<StatusEffect>();
+
+        /// <summary>The target Transform to pass on to the GameActionsContext for doing things like spawning particles.</summary>
+        [HideInInspector] public Transform target;
+
+        public StatusEffectsController(StatsController controller,  List<StatusEffect> statusEffects = null)
+        {
+            this.controller = controller;
+            if(statusEffects == null) statusEffects = new List<StatusEffect>();
+            this.statusEffects = statusEffects;
+        }
+
+        #region DatabaseOperations
+
+        public void Add(StatusEffectTemplate statusEffectTemplate, Transform source)
+        {
+            if(statusEffectTemplate == null)
+            {
+                Debug.LogError("statusEffect cannot be null");
+                return;
+            }
+            StatusEffect statusEffect = GetStatusEffect(statusEffectTemplate.title);
+            if(statusEffect == null) 
+            {
+                if(this.controller == null) Debug.LogError("Missing statsController");
+                statusEffect = new StatusEffect(statusEffectTemplate, target, source, this.controller);
+                statusEffects.Add(statusEffect);
+            }
+            else
+            {
+                if(statusEffect.template.maxStack > statusEffect.stack)
+                {
+                    statusEffect.AddToStack();
+                }
+                else { } // The statusEffect is already added and cannot be stacked 
+             }
+        }
+
+        public StatusEffect GetStatusEffect(string title)
+        {
+            for (int i = 0; i < statusEffects.Count; i++)
+            {
+                if(statusEffects[i].template.title == title) return statusEffects[i];
+            }
+            return null;
+        }
+
+        public void Remove(StatusEffect statusEffect)
+        {
+            statusEffect.OnRemove();
+            statusEffects.Remove(statusEffect);
+        }
+
+        #endregion
+
+        #region Flow
+
+        private CompositeDisposable cancel;
+
+        /// <summary>Let the StatusController flow in time</summary>
+        public void Start()
+        {
+            var observable = Observable.EveryUpdate();
+            cancel = new CompositeDisposable();
+            observable.Subscribe(xs => UpdateStatusEffects()).AddTo(cancel);
+        }
+
+        /// <summary>Stop this controller.</summary>
+        public void Stop()
+        {
+            if(cancel != null) cancel.Dispose();
+        }
+
+        private void UpdateStatusEffects()
+        {
+            for (int i = 0; i < statusEffects.Count; i++) 
+            {
+                statusEffects[i].Update(Time.deltaTime);
+            }
+            for (int i = 0; i < statusEffects.Count; i++) 
+            {
+                if(statusEffects[i].stack == 0)
+                {
+                    statusEffects.RemoveAt(i);
+                    return;
+                }
+            }
+        }
+
+        #endregion
+    }
+}

+ 11 - 0
Runtime/StatusEffects/StatusEffectsController.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 482de6b689985994486eee16437acad3
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Tests.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: b4e46cd1a1a138e4288fe9ef6661489e
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Tests/Editor.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f7d81c1be27596b45b4c7009e6e75846
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 22 - 0
Tests/Editor/KairoEngine.Stats.EditorTests.asmdef

@@ -0,0 +1,22 @@
+{
+    "name": "KairoEngine.Stats.EditorTests",
+    "references": [
+        "GUID:7e5ae6a38d1532248b4c890eca668b06",
+        "GUID:bc1ac81aedfa56c4083dea5332ed3810",
+        "GUID:0acc523941302664db1f4e527237feb3",
+        "GUID:27619889b8ba8c24980f49ee34dbb44a"
+    ],
+    "includePlatforms": [
+        "Editor"
+    ],
+    "excludePlatforms": [],
+    "allowUnsafeCode": false,
+    "overrideReferences": true,
+    "precompiledReferences": [
+        "nunit.framework.dll"
+    ],
+    "autoReferenced": true,
+    "defineConstraints": [],
+    "versionDefines": [],
+    "noEngineReferences": false
+}

+ 7 - 0
Tests/Editor/KairoEngine.Stats.EditorTests.asmdef.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 370da0b36d3bd3246a0e5f3a27990329
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 130 - 0
Tests/Editor/StatsControllerTests.cs

@@ -0,0 +1,130 @@
+using System.Collections;
+using System.Collections.Generic;
+using NUnit.Framework;
+using UnityEngine;
+using UnityEngine.TestTools;
+using KairoEngine.Stats;
+
+namespace KairoEngine.Stats.EditorTests
+{
+    public class StatsControllerTests
+    {
+        private StatsController controller;
+
+        [SetUp]
+        public void Setup()
+        {
+            controller = new StatsController();
+            StatGroup statGroup = ScriptableObject.CreateInstance("StatGroup") as StatGroup;
+            statGroup.statTemplates.Add(CreateStatTemplate().Setup("Endurance", "Stamina and HP", 0, 0, 10, "", "maxValue / 2", "", ""));
+            statGroup.statTemplates.Add(CreateStatTemplate().Setup("Hitpoints", "damage points", 0, 0, 0, "", "maxValue", "", "s('Endurance') * 10"));
+            controller.StatGroup = statGroup;
+        }
+
+        private StatTemplate CreateStatTemplate() => ScriptableObject.CreateInstance("StatTemplate") as StatTemplate;
+
+        [TearDown]
+        public void TearDown()
+        {
+            controller = null;
+        }
+
+        [Test]
+        public void StatsController_New_Controller_Test()
+        {
+            Assert.NotNull(controller);
+        }
+
+        [Test]
+        public void StatsController_GetStatValue_Test()
+        {
+            int n = controller.GetStatValue("Hitpoints");
+            Assert.AreEqual(50, n);
+        }
+
+        [Test]
+        public void StatsController_GetStat_NotNull_Test()
+        {
+            Assert.NotNull(controller.GetStat("Hitpoints"));
+        }
+
+        [Test]
+        public void StatsController_GetStat_WrongName_Test()
+        {
+            Assert.IsNull(controller.GetStat("HitpointZ"));
+        }
+
+        [Test]
+        public void StatConstroller_GetStat_With_Modifier_Test()
+        {
+            Stat stat = controller.GetStat("Endurance");
+            stat.modifiers.Add(new StatModifier("+", 1, "", "", "Endurance", "Vitality Ring"));
+            int n = stat.value;
+            Assert.AreEqual(6, n);
+            Assert.AreEqual(60, controller.GetStat("Hitpoints").maxValue);
+        }
+
+        [Test]
+        public void StatConstroller_GetStat_With_Multiple_Modifier_Test()
+        {
+            Stat stat = controller.GetStat("Endurance");
+            stat.modifiers.Add(new StatModifier("+", 1, "", "", "Endurance", "Vitality Ring"));
+            stat.modifiers.Add(new StatModifier("-", 1, "", "", "Endurance", "Hangover"));
+            stat.modifiers.Add(new StatModifier("+", 1, "", "", "Endurance", "Well rested"));
+            stat.modifiers.Add(new StatModifier("+", 1, "", "", "Endurance", "Well fed"));
+            int n = stat.value;
+            Assert.AreEqual(7, n);
+            Assert.AreEqual(70, controller.GetStat("Hitpoints").maxValue);
+        }
+
+        [Test]
+        public void StatConstroller_GetStat_With_MaxValue_Modifier_Test()
+        {
+            controller.GetStat("Endurance").modifiers.Add(new StatModifier("+", 1, "", "", "Endurance", "Well rested"));
+            controller.GetStat("Endurance").modifiers.Add(new StatModifier("+", 1, "", "", "Endurance", "Vitality Ring"));
+            Stat stat = controller.GetStat("Hitpoints");
+            stat.modifiers.Add(new StatModifier("+", 20, "", "max", "Hitpoints", "Healthy"));
+            Assert.AreEqual(90, stat.maxValue);
+        }
+
+        [Test]
+        public void StatConstroller_GetStat_With_MinValue_Modifier_Test()
+        {
+            controller.GetStat("Endurance").modifiers.Add(new StatModifier("+", 1, "", "", "Endurance", "Well rested"));
+            controller.GetStat("Endurance").modifiers.Add(new StatModifier("+", 1, "", "", "Endurance", "Vitality Ring"));
+            Stat stat = controller.GetStat("Hitpoints");
+            stat.modifiers.Add(new StatModifier("-", 20, "", "min", "Hitpoints", "Death Defiance"));
+            Assert.AreEqual(-20, stat.minValue);
+        }
+
+        [Test]
+        public void StatConstroller_GetStat_With_Percentage_Modifier_Test()
+        {
+            Stat stat = controller.GetStat("Hitpoints");
+            stat.modifiers.Add(new StatModifier("+", 100, "%", "max", "Hitpoints", "Healthy"));
+            Assert.AreEqual(100, stat.maxValue);
+        }
+
+        [Test]
+        public void StatConstroller_Add_StatModifier()
+        {
+            Stat stat = controller.GetStat("Endurance");
+            StatModifier modifier = new StatModifier("+", 1, "", "", "Endurance", "Vitality Ring");
+            Assert.AreEqual(false, stat.modifiers.HasModifier(modifier));
+            stat.modifiers.Add(modifier);
+            Assert.AreEqual(true, stat.modifiers.HasModifier(modifier));
+        }
+
+        [Test]
+        public void StatConstroller_Remove_StatModifier()
+        {
+            Stat stat = controller.GetStat("Endurance");
+            StatModifier modifier = new StatModifier("+", 1, "", "", "Endurance", "Vitality Ring");    
+            stat.modifiers.Add(modifier);
+            Assert.AreEqual(true, stat.modifiers.HasModifier(modifier));
+            stat.modifiers.Remove(modifier);
+            Assert.AreEqual(false, stat.modifiers.HasModifier(modifier));
+        }
+
+    }
+}

+ 11 - 0
Tests/Editor/StatsControllerTests.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e8fa61b1369327941ba768ce9c6fc9ae
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 17 - 0
package.json

@@ -0,0 +1,17 @@
+{
+    "name": "at.kairoscope.kairoengine.stats",
+    "displayName": "KairoEngine Stats",
+    "version": "0.1.1",
+    "unity": "2020.3",
+    "description": "Kairoengine stats library.",
+    "dependencies": {
+      "at.kairoscope.thirdparty.unirx":"1.0.0",
+      "at.kairoscope.thirdparty.sirenix":"1.0.0",
+      "at.kairoscope.kairoengine.core":"0.1.6"
+    },
+    "repository": {
+      "type": "git",
+      "url": "https://git.kairoscope.net/kairoengine/stats.git"
+    },
+    "author": "Kairoscope"
+  }