com.wallstop-studios.unity-helpers 2.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/Docs/DATA_STRUCTURES.md +7 -7
  2. package/Docs/EFFECTS_SYSTEM.md +836 -8
  3. package/Docs/EFFECTS_SYSTEM_TUTORIAL.md +77 -18
  4. package/Docs/HULLS.md +2 -2
  5. package/Docs/RANDOM_PERFORMANCE.md +1 -1
  6. package/Docs/REFLECTION_HELPERS.md +1 -1
  7. package/Docs/RELATIONAL_COMPONENTS.md +51 -6
  8. package/Docs/SERIALIZATION.md +1 -1
  9. package/Docs/SINGLETONS.md +2 -2
  10. package/Docs/SPATIAL_TREES_2D_GUIDE.md +3 -3
  11. package/Docs/SPATIAL_TREES_3D_GUIDE.md +3 -3
  12. package/Docs/SPATIAL_TREE_SEMANTICS.md +7 -7
  13. package/Editor/CustomDrawers/WShowIfPropertyDrawer.cs +131 -41
  14. package/Editor/Utils/ScriptableObjectSingletonCreator.cs +175 -18
  15. package/README.md +17 -3
  16. package/Runtime/Core/Helper/UnityMainThreadDispatcher.cs +4 -2
  17. package/Runtime/Tags/Attribute.cs +144 -24
  18. package/Runtime/Tags/AttributeEffect.cs +278 -11
  19. package/Runtime/Tags/AttributeModification.cs +59 -29
  20. package/Runtime/Tags/AttributeUtilities.cs +465 -0
  21. package/Runtime/Tags/AttributesComponent.cs +20 -0
  22. package/Runtime/Tags/EffectBehavior.cs +171 -0
  23. package/Runtime/Tags/EffectBehavior.cs.meta +4 -0
  24. package/Runtime/Tags/EffectHandle.cs +5 -0
  25. package/Runtime/Tags/EffectHandler.cs +564 -39
  26. package/Runtime/Tags/EffectStackKey.cs +79 -0
  27. package/Runtime/Tags/EffectStackKey.cs.meta +4 -0
  28. package/Runtime/Tags/PeriodicEffectDefinition.cs +102 -0
  29. package/Runtime/Tags/PeriodicEffectDefinition.cs.meta +4 -0
  30. package/Runtime/Tags/PeriodicEffectRuntimeState.cs +40 -0
  31. package/Runtime/Tags/PeriodicEffectRuntimeState.cs.meta +4 -0
  32. package/Runtime/Tags/TagHandler.cs +375 -21
  33. package/Samples~/DI - Zenject/README.md +0 -2
  34. package/Tests/Editor/Attributes/WShowIfPropertyDrawerTests.cs +285 -0
  35. package/Tests/Editor/Attributes/WShowIfPropertyDrawerTests.cs.meta +11 -0
  36. package/Tests/Editor/Core/Attributes/RelationalComponentAssignerTests.cs +2 -2
  37. package/Tests/Editor/Utils/ScriptableObjectSingletonTests.cs +41 -0
  38. package/Tests/Runtime/Serialization/JsonSerializationTest.cs +4 -3
  39. package/Tests/Runtime/Tags/AttributeEffectTests.cs +135 -0
  40. package/Tests/Runtime/Tags/AttributeEffectTests.cs.meta +3 -0
  41. package/Tests/Runtime/Tags/AttributeModificationTests.cs +137 -0
  42. package/Tests/Runtime/Tags/AttributeTests.cs +192 -0
  43. package/Tests/Runtime/Tags/AttributeTests.cs.meta +3 -0
  44. package/Tests/Runtime/Tags/AttributeUtilitiesTests.cs +245 -0
  45. package/Tests/Runtime/Tags/CosmeticAndCollisionTests.cs +1 -1
  46. package/Tests/Runtime/Tags/EffectBehaviorTests.cs +184 -0
  47. package/Tests/Runtime/Tags/EffectBehaviorTests.cs.meta +3 -0
  48. package/Tests/Runtime/Tags/EffectHandlerTests.cs +809 -0
  49. package/Tests/Runtime/Tags/Helpers/RecordingEffectBehavior.cs +89 -0
  50. package/Tests/Runtime/Tags/Helpers/RecordingEffectBehavior.cs.meta +4 -0
  51. package/Tests/Runtime/Tags/PeriodicEffectDefinitionSerializationTests.cs +92 -0
  52. package/Tests/Runtime/Tags/PeriodicEffectDefinitionSerializationTests.cs.meta +3 -0
  53. package/Tests/Runtime/Tags/TagHandlerTests.cs +130 -6
  54. package/package.json +1 -1
  55. package/scripts/lint-doc-links.ps1 +156 -11
  56. package/Tests/Runtime/Tags/AttributeDataTests.cs +0 -312
  57. package/node_modules.meta +0 -8
  58. /package/Tests/Runtime/Tags/{AttributeDataTests.cs.meta → AttributeModificationTests.cs.meta} +0 -0
@@ -71,8 +71,6 @@ public class Enemy : MonoBehaviour
71
71
  2. Add the `RelationalComponentsInstaller` component to the same GameObject
72
72
  3. Enable **"Assign Scene On Initialize"** to automatically wire all scene components after the container builds (recommended)
73
73
 
74
- ![SceneContext Setup](../../Docs/Images/zenject_setup.png)
75
-
76
74
  > 💡 **Beginner tip:** Enable both checkboxes in the inspector:
77
75
  >
78
76
  > - ✅ **Assign Scene On Initialize** → Auto-wires all scene objects (saves you from calling it manually)
@@ -0,0 +1,285 @@
1
+ namespace WallstopStudios.UnityHelpers.Tests.Editor.Attributes
2
+ {
3
+ using System.Reflection;
4
+ using NUnit.Framework;
5
+ using UnityEditor;
6
+ using UnityEngine;
7
+ using WallstopStudios.UnityHelpers.Core.Attributes;
8
+ using WallstopStudios.UnityHelpers.Editor.CustomDrawers;
9
+ using WallstopStudios.UnityHelpers.Tags;
10
+ using WallstopStudios.UnityHelpers.Tests.Editor.Utils;
11
+
12
+ [TestFixture]
13
+ public sealed class WShowIfPropertyDrawerTests : CommonTestBase
14
+ {
15
+ private static readonly FieldInfo AttributeField = typeof(PropertyDrawer).GetField(
16
+ "m_Attribute",
17
+ BindingFlags.Instance | BindingFlags.NonPublic
18
+ );
19
+
20
+ [Test]
21
+ public void BoolConditionHidesFieldWhenFalse()
22
+ {
23
+ TestContainer container = CreateScriptableObject<TestContainer>();
24
+ SerializedObject serializedObject = new(container);
25
+ serializedObject.Update();
26
+
27
+ SerializedProperty dependentProperty = serializedObject.FindProperty(
28
+ nameof(TestContainer.boolDependent)
29
+ );
30
+ Assert.NotNull(dependentProperty);
31
+
32
+ WShowIfPropertyDrawer drawer = CreateDrawer(
33
+ new WShowIfAttribute(nameof(TestContainer.boolCondition))
34
+ );
35
+
36
+ container.boolCondition = false;
37
+ serializedObject.Update();
38
+ float hiddenHeight = drawer.GetPropertyHeight(
39
+ dependentProperty,
40
+ new GUIContent("boolDependent")
41
+ );
42
+ Assert.That(hiddenHeight, Is.Zero);
43
+
44
+ container.boolCondition = true;
45
+ serializedObject.Update();
46
+ float shownHeight = drawer.GetPropertyHeight(
47
+ dependentProperty,
48
+ new GUIContent("boolDependent")
49
+ );
50
+ Assert.That(shownHeight, Is.GreaterThan(0f));
51
+ }
52
+
53
+ [Test]
54
+ public void EnumConditionMatchesExpectedValue()
55
+ {
56
+ TestContainer container = CreateScriptableObject<TestContainer>();
57
+ SerializedObject serializedObject = new(container);
58
+ serializedObject.Update();
59
+
60
+ SerializedProperty dependentProperty = serializedObject.FindProperty(
61
+ nameof(TestContainer.durationDependent)
62
+ );
63
+ Assert.NotNull(dependentProperty);
64
+
65
+ WShowIfPropertyDrawer drawer = CreateDrawer(
66
+ new WShowIfAttribute(
67
+ nameof(TestContainer.durationType),
68
+ expectedValues: new object[] { ModifierDurationType.Duration }
69
+ )
70
+ );
71
+
72
+ container.durationType = ModifierDurationType.Instant;
73
+ serializedObject.Update();
74
+ float hiddenHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
75
+ Assert.That(hiddenHeight, Is.Zero);
76
+
77
+ container.durationType = ModifierDurationType.Duration;
78
+ serializedObject.Update();
79
+ float shownHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
80
+ Assert.That(shownHeight, Is.GreaterThan(0f));
81
+ }
82
+
83
+ [Test]
84
+ public void EnumConditionHonorsInverseFlag()
85
+ {
86
+ TestContainer container = CreateScriptableObject<TestContainer>();
87
+ SerializedObject serializedObject = new(container);
88
+ serializedObject.Update();
89
+
90
+ SerializedProperty dependentProperty = serializedObject.FindProperty(
91
+ nameof(TestContainer.inverseDependent)
92
+ );
93
+ Assert.NotNull(dependentProperty);
94
+
95
+ WShowIfPropertyDrawer drawer = CreateDrawer(
96
+ new WShowIfAttribute(
97
+ nameof(TestContainer.durationType),
98
+ inverse: true,
99
+ expectedValues: new object[] { ModifierDurationType.Instant }
100
+ )
101
+ );
102
+
103
+ container.durationType = ModifierDurationType.Instant;
104
+ serializedObject.Update();
105
+ float hiddenHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
106
+ Assert.That(hiddenHeight, Is.Zero);
107
+
108
+ container.durationType = ModifierDurationType.Infinite;
109
+ serializedObject.Update();
110
+ float shownHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
111
+ Assert.That(shownHeight, Is.GreaterThan(0f));
112
+ }
113
+
114
+ [Test]
115
+ public void FloatConditionMatchesExpectedValue()
116
+ {
117
+ TestContainer container = CreateScriptableObject<TestContainer>();
118
+ SerializedObject serializedObject = new(container);
119
+ serializedObject.Update();
120
+
121
+ SerializedProperty dependentProperty = serializedObject.FindProperty(
122
+ nameof(TestContainer.floatDependent)
123
+ );
124
+ Assert.NotNull(dependentProperty);
125
+
126
+ WShowIfPropertyDrawer drawer = CreateDrawer(
127
+ new WShowIfAttribute(
128
+ nameof(TestContainer.floatCondition),
129
+ expectedValues: new object[] { 3.5f }
130
+ )
131
+ );
132
+
133
+ container.floatCondition = 0f;
134
+ serializedObject.Update();
135
+ float hiddenHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
136
+ Assert.That(hiddenHeight, Is.Zero);
137
+
138
+ container.floatCondition = 3.5f;
139
+ serializedObject.Update();
140
+ float shownHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
141
+ Assert.That(shownHeight, Is.GreaterThan(0f));
142
+ }
143
+
144
+ [Test]
145
+ public void DoubleConditionMatchesEquivalentIntExpectedValue()
146
+ {
147
+ TestContainer container = CreateScriptableObject<TestContainer>();
148
+ SerializedObject serializedObject = new(container);
149
+ serializedObject.Update();
150
+
151
+ SerializedProperty dependentProperty = serializedObject.FindProperty(
152
+ nameof(TestContainer.doubleDependent)
153
+ );
154
+ Assert.NotNull(dependentProperty);
155
+
156
+ WShowIfPropertyDrawer drawer = CreateDrawer(
157
+ new WShowIfAttribute(
158
+ nameof(TestContainer.doubleCondition),
159
+ expectedValues: new object[] { 7 }
160
+ )
161
+ );
162
+
163
+ container.doubleCondition = 2.5d;
164
+ serializedObject.Update();
165
+ float hiddenHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
166
+ Assert.That(hiddenHeight, Is.Zero);
167
+
168
+ container.doubleCondition = 7d;
169
+ serializedObject.Update();
170
+ float shownHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
171
+ Assert.That(shownHeight, Is.GreaterThan(0f));
172
+ }
173
+
174
+ [Test]
175
+ public void IntConditionMatchesExpectedValue()
176
+ {
177
+ TestContainer container = CreateScriptableObject<TestContainer>();
178
+ SerializedObject serializedObject = new(container);
179
+ serializedObject.Update();
180
+
181
+ SerializedProperty dependentProperty = serializedObject.FindProperty(
182
+ nameof(TestContainer.intDependent)
183
+ );
184
+ Assert.NotNull(dependentProperty);
185
+
186
+ WShowIfPropertyDrawer drawer = CreateDrawer(
187
+ new WShowIfAttribute(
188
+ nameof(TestContainer.intCondition),
189
+ expectedValues: new object[] { 42 }
190
+ )
191
+ );
192
+
193
+ container.intCondition = 7;
194
+ serializedObject.Update();
195
+ float hiddenHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
196
+ Assert.That(hiddenHeight, Is.Zero);
197
+
198
+ container.intCondition = 42;
199
+ serializedObject.Update();
200
+ float shownHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
201
+ Assert.That(shownHeight, Is.GreaterThan(0f));
202
+ }
203
+
204
+ [Test]
205
+ public void StringConditionMatchesExpectedValues()
206
+ {
207
+ TestContainer container = CreateScriptableObject<TestContainer>();
208
+ SerializedObject serializedObject = new(container);
209
+ serializedObject.Update();
210
+
211
+ SerializedProperty dependentProperty = serializedObject.FindProperty(
212
+ nameof(TestContainer.stringDependent)
213
+ );
214
+ Assert.NotNull(dependentProperty);
215
+
216
+ WShowIfPropertyDrawer drawer = CreateDrawer(
217
+ new WShowIfAttribute(
218
+ nameof(TestContainer.stringCondition),
219
+ expectedValues: new object[] { "alpha", "beta" }
220
+ )
221
+ );
222
+
223
+ container.stringCondition = "gamma";
224
+ serializedObject.Update();
225
+ float hiddenHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
226
+ Assert.That(hiddenHeight, Is.Zero);
227
+
228
+ container.stringCondition = "alpha";
229
+ serializedObject.Update();
230
+ float shownHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
231
+ Assert.That(shownHeight, Is.GreaterThan(0f));
232
+ }
233
+
234
+ private static WShowIfPropertyDrawer CreateDrawer(WShowIfAttribute attribute)
235
+ {
236
+ WShowIfPropertyDrawer drawer = new();
237
+ Assert.NotNull(AttributeField);
238
+ AttributeField.SetValue(drawer, attribute);
239
+ return drawer;
240
+ }
241
+
242
+ private sealed class TestContainer : ScriptableObject
243
+ {
244
+ public bool boolCondition;
245
+
246
+ [WShowIf(nameof(boolCondition))]
247
+ public int boolDependent;
248
+
249
+ public ModifierDurationType durationType = ModifierDurationType.Instant;
250
+
251
+ [WShowIf(
252
+ nameof(durationType),
253
+ expectedValues: new object[] { ModifierDurationType.Duration }
254
+ )]
255
+ public int durationDependent;
256
+
257
+ [WShowIf(
258
+ nameof(durationType),
259
+ inverse: true,
260
+ expectedValues: new object[] { ModifierDurationType.Instant }
261
+ )]
262
+ public int inverseDependent;
263
+
264
+ public float floatCondition;
265
+
266
+ [WShowIf(nameof(floatCondition), expectedValues: new object[] { 3.5f })]
267
+ public int floatDependent;
268
+
269
+ public double doubleCondition;
270
+
271
+ [WShowIf(nameof(doubleCondition), expectedValues: new object[] { 7 })]
272
+ public int doubleDependent;
273
+
274
+ public int intCondition;
275
+
276
+ [WShowIf(nameof(intCondition), expectedValues: new object[] { 42 })]
277
+ public int intDependent;
278
+
279
+ public string stringCondition;
280
+
281
+ [WShowIf(nameof(stringCondition), expectedValues: new object[] { "alpha", "beta" })]
282
+ public int stringDependent;
283
+ }
284
+ }
285
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: 1e5fb5f2a5e7464493dca6801e4eb1a6
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -24,7 +24,7 @@ namespace WallstopStudios.UnityHelpers.Tests.Core.Attributes
24
24
  }
25
25
 
26
26
  [Test]
27
- public void HasRelationalAssignments_RespectsMetadata()
27
+ public void HasRelationalAssignmentsRespectsMetadata()
28
28
  {
29
29
  AttributeMetadataCache cache = CreateScriptableObject<AttributeMetadataCache>();
30
30
 
@@ -62,7 +62,7 @@ namespace WallstopStudios.UnityHelpers.Tests.Core.Attributes
62
62
  }
63
63
 
64
64
  [Test]
65
- public void Assign_IEnumerable_AssignsOnlyRelationalTypes_AndSkipsNulls()
65
+ public void AssignIEnumerableAssignsOnlyRelationalTypesAndSkipsNulls()
66
66
  {
67
67
  AttributeMetadataCache cache = CreateScriptableObject<AttributeMetadataCache>();
68
68
 
@@ -34,6 +34,8 @@ namespace WallstopStudios.UnityHelpers.Tests.Editor.Utils
34
34
  yield return null;
35
35
  DeleteAssetIfExists("Assets/Resources/CustomPath/CustomPathSingleton.asset");
36
36
  yield return null;
37
+ DeleteFolderIfEmpty("Assets/Resources/CustomPath");
38
+ yield return null;
37
39
 
38
40
  // For nested test types, Unity cannot create valid .asset files (no script file).
39
41
  // Instead, create in-memory instances so the singleton loader can discover them via FindObjectsOfTypeAll.
@@ -81,6 +83,43 @@ namespace WallstopStudios.UnityHelpers.Tests.Editor.Utils
81
83
  }
82
84
  }
83
85
 
86
+ private static void DeleteFolderIfEmpty(string folderPath)
87
+ {
88
+ if (string.IsNullOrWhiteSpace(folderPath))
89
+ {
90
+ return;
91
+ }
92
+
93
+ if (!AssetDatabase.IsValidFolder(folderPath))
94
+ {
95
+ return;
96
+ }
97
+
98
+ string[] subFolders = AssetDatabase.GetSubFolders(folderPath);
99
+ if (subFolders != null && subFolders.Length > 0)
100
+ {
101
+ return;
102
+ }
103
+
104
+ string[] assetGuids = AssetDatabase.FindAssets(string.Empty, new[] { folderPath });
105
+ for (int i = 0; i < assetGuids.Length; i++)
106
+ {
107
+ string guid = assetGuids[i];
108
+ string assetPath = AssetDatabase.GUIDToAssetPath(guid);
109
+ if (
110
+ !string.IsNullOrEmpty(assetPath)
111
+ && !string.Equals(assetPath, folderPath, System.StringComparison.Ordinal)
112
+ )
113
+ {
114
+ return;
115
+ }
116
+ }
117
+
118
+ AssetDatabase.DeleteAsset(folderPath);
119
+ AssetDatabase.SaveAssets();
120
+ AssetDatabase.Refresh();
121
+ }
122
+
84
123
  private static TType CreateInMemoryInstance<TType>()
85
124
  where TType : ScriptableObject
86
125
  {
@@ -144,6 +183,8 @@ namespace WallstopStudios.UnityHelpers.Tests.Editor.Utils
144
183
  AssetDatabase.SaveAssets();
145
184
  AssetDatabase.Refresh();
146
185
  yield return null;
186
+ DeleteFolderIfEmpty("Assets/Resources/CustomPath");
187
+ yield return null;
147
188
  // Prefer public API surface over reflection to clean up the cached instance
148
189
  if (TestSingleton.HasInstance)
149
190
  {
@@ -4,6 +4,7 @@ namespace WallstopStudios.UnityHelpers.Tests.Serialization
4
4
  using System.Collections.Generic;
5
5
  using System.Linq;
6
6
  using System.Runtime.Serialization;
7
+ using System.Text.Json;
7
8
  using System.Text.Json.Serialization;
8
9
  using NUnit.Framework;
9
10
  using UnityEngine;
@@ -42,9 +43,9 @@ namespace WallstopStudios.UnityHelpers.Tests.Serialization
42
43
  using System.Text.Json.JsonDocument doc = System.Text.Json.JsonDocument.Parse(json);
43
44
  System.Text.Json.JsonElement root = doc.RootElement;
44
45
  Assert.AreEqual(System.Text.Json.JsonValueKind.Object, root.ValueKind);
45
- Assert.True(root.TryGetProperty("name", out var name));
46
- Assert.True(root.TryGetProperty("type", out var type));
47
- Assert.True(root.TryGetProperty("instanceId", out var id));
46
+ Assert.True(root.TryGetProperty("name", out JsonElement name));
47
+ Assert.True(root.TryGetProperty("type", out JsonElement type));
48
+ Assert.True(root.TryGetProperty("instanceId", out JsonElement id));
48
49
  Assert.AreEqual("Test GameObject", name.GetString());
49
50
  StringAssert.Contains("UnityEngine.GameObject", type.GetString());
50
51
  Assert.AreEqual(expectedId, id.GetInt32());
@@ -0,0 +1,135 @@
1
+ namespace WallstopStudios.UnityHelpers.Tests.Tags
2
+ {
3
+ using System.Text.Json;
4
+ using NUnit.Framework;
5
+ using UnityEngine;
6
+ using WallstopStudios.UnityHelpers.Tags;
7
+
8
+ [TestFixture]
9
+ public sealed class AttributeEffectTests : AttributeTagsTestBase
10
+ {
11
+ [Test]
12
+ public void HumanReadableDescriptionFormatsAllModificationTypes()
13
+ {
14
+ AttributeEffect effect = Track(ScriptableObject.CreateInstance<AttributeEffect>());
15
+ effect.name = "Composite";
16
+ effect.modifications.Add(
17
+ new AttributeModification
18
+ {
19
+ attribute = "health",
20
+ action = ModificationAction.Addition,
21
+ value = 5f,
22
+ }
23
+ );
24
+ effect.modifications.Add(
25
+ new AttributeModification
26
+ {
27
+ attribute = "attack_speed",
28
+ action = ModificationAction.Multiplication,
29
+ value = 1.5f,
30
+ }
31
+ );
32
+ effect.modifications.Add(
33
+ new AttributeModification
34
+ {
35
+ attribute = "armor",
36
+ action = ModificationAction.Override,
37
+ value = 10f,
38
+ }
39
+ );
40
+
41
+ string description = effect.HumanReadableDescription;
42
+ Assert.AreEqual("+5 Health, +50% Attack Speed, 10 Armor", description);
43
+ }
44
+
45
+ [Test]
46
+ public void HumanReadableDescriptionSkipsNeutralModifications()
47
+ {
48
+ AttributeEffect effect = Track(ScriptableObject.CreateInstance<AttributeEffect>());
49
+ effect.modifications.Add(
50
+ new AttributeModification
51
+ {
52
+ attribute = "health",
53
+ action = ModificationAction.Addition,
54
+ value = 0f,
55
+ }
56
+ );
57
+ effect.modifications.Add(
58
+ new AttributeModification
59
+ {
60
+ attribute = "speed",
61
+ action = ModificationAction.Multiplication,
62
+ value = 1f,
63
+ }
64
+ );
65
+
66
+ Assert.IsEmpty(effect.HumanReadableDescription);
67
+ }
68
+
69
+ [Test]
70
+ public void ToStringSerializesSummaryAndCollections()
71
+ {
72
+ AttributeEffect effect = Track(ScriptableObject.CreateInstance<AttributeEffect>());
73
+ effect.name = "JsonEffect";
74
+ effect.durationType = ModifierDurationType.Duration;
75
+ effect.duration = 3.25f;
76
+ effect.resetDurationOnReapplication = true;
77
+ effect.modifications.Add(
78
+ new AttributeModification
79
+ {
80
+ attribute = "health",
81
+ action = ModificationAction.Addition,
82
+ value = 10f,
83
+ }
84
+ );
85
+ effect.effectTags.Add("Buff");
86
+
87
+ GameObject cosmeticHolder = Track(new GameObject("Glow", typeof(CosmeticEffectData)));
88
+ CosmeticEffectData cosmeticData = cosmeticHolder.GetComponent<CosmeticEffectData>();
89
+ effect.cosmeticEffects.Add(cosmeticData);
90
+
91
+ using JsonDocument document = JsonDocument.Parse(effect.ToString());
92
+ JsonElement root = document.RootElement;
93
+ Assert.AreEqual(
94
+ effect.HumanReadableDescription,
95
+ root.GetProperty("Description").GetString()
96
+ );
97
+ Assert.AreEqual("Duration", root.GetProperty("durationType").GetString());
98
+ Assert.AreEqual(3.25f, root.GetProperty("duration").GetSingle());
99
+ Assert.AreEqual("Buff", root.GetProperty("tags")[0].GetString());
100
+ Assert.AreEqual("Glow", root.GetProperty("CosmeticEffects")[0].GetString());
101
+ Assert.AreEqual(1, root.GetProperty("modifications").GetArrayLength());
102
+ }
103
+
104
+ [Test]
105
+ public void EqualsRequiresMatchingState()
106
+ {
107
+ AttributeEffect left = Track(ScriptableObject.CreateInstance<AttributeEffect>());
108
+ AttributeEffect right = Track(ScriptableObject.CreateInstance<AttributeEffect>());
109
+ left.name = right.name = "Stack";
110
+ left.durationType = right.durationType = ModifierDurationType.Duration;
111
+ left.duration = right.duration = 2f;
112
+ left.resetDurationOnReapplication = right.resetDurationOnReapplication = false;
113
+
114
+ AttributeModification modification = new()
115
+ {
116
+ attribute = "health",
117
+ action = ModificationAction.Addition,
118
+ value = 5f,
119
+ };
120
+
121
+ left.modifications.Add(modification);
122
+ right.modifications.Add(modification);
123
+ Assert.IsTrue(left.Equals(right));
124
+
125
+ right.modifications[0] = new AttributeModification
126
+ {
127
+ attribute = "health",
128
+ action = ModificationAction.Addition,
129
+ value = 10f,
130
+ };
131
+
132
+ Assert.IsFalse(left.Equals(right));
133
+ }
134
+ }
135
+ }
@@ -0,0 +1,3 @@
1
+ fileFormatVersion: 2
2
+ guid: f9cf8158bca5461c9fba12c31ab373b6
3
+ timeCreated: 1761249361
@@ -0,0 +1,137 @@
1
+ namespace WallstopStudios.UnityHelpers.Tests.Tags
2
+ {
3
+ using System;
4
+ using System.Text.Json;
5
+ using NUnit.Framework;
6
+ using WallstopStudios.UnityHelpers.Core.Serialization;
7
+ using WallstopStudios.UnityHelpers.Tags;
8
+ using WallstopStudios.UnityHelpers.Tests.TestUtils;
9
+
10
+ public abstract class AttributeTagsTestBase : CommonTestBase
11
+ {
12
+ protected static void ResetEffectHandleId(long value = 0)
13
+ {
14
+ EffectHandle.Id = value;
15
+ }
16
+ }
17
+
18
+ [TestFixture]
19
+ public sealed class AttributeModificationTests
20
+ {
21
+ [Test]
22
+ public void EqualityOperatorsRespectFields()
23
+ {
24
+ AttributeModification baseline = new()
25
+ {
26
+ attribute = "health",
27
+ action = ModificationAction.Multiplication,
28
+ value = 1.5f,
29
+ };
30
+
31
+ AttributeModification clone = baseline;
32
+ Assert.IsTrue(baseline == clone);
33
+ Assert.IsFalse(baseline != clone);
34
+ Assert.AreEqual(baseline, clone);
35
+ Assert.AreEqual(baseline.GetHashCode(), clone.GetHashCode());
36
+
37
+ AttributeModification differentAttribute = baseline;
38
+ differentAttribute.attribute = "armor";
39
+ Assert.IsFalse(baseline == differentAttribute);
40
+ Assert.IsTrue(baseline != differentAttribute);
41
+
42
+ AttributeModification differentAction = baseline;
43
+ differentAction.action = ModificationAction.Addition;
44
+ Assert.IsFalse(baseline.Equals(differentAction));
45
+
46
+ AttributeModification differentValue = baseline;
47
+ differentValue.value = 2f;
48
+ Assert.IsFalse(baseline.Equals(differentValue));
49
+ }
50
+
51
+ [Test]
52
+ public void ToStringSerializesAllFields()
53
+ {
54
+ AttributeModification modification = new()
55
+ {
56
+ attribute = "health",
57
+ action = ModificationAction.Override,
58
+ value = 42.5f,
59
+ };
60
+
61
+ using JsonDocument document = JsonDocument.Parse(modification.ToString());
62
+ JsonElement root = document.RootElement;
63
+ Assert.AreEqual(
64
+ "health",
65
+ root.GetProperty(nameof(AttributeModification.attribute)).GetString()
66
+ );
67
+ Assert.AreEqual(
68
+ "Override",
69
+ root.GetProperty(nameof(AttributeModification.action)).GetString()
70
+ );
71
+ Assert.AreEqual(
72
+ 42.5f,
73
+ root.GetProperty(nameof(AttributeModification.value)).GetSingle()
74
+ );
75
+ }
76
+
77
+ [Test]
78
+ public void SystemTextJsonRoundtripPreservesFields()
79
+ {
80
+ AttributeModification modification = new(
81
+ "armor",
82
+ ModificationAction.Multiplication,
83
+ 1.25f
84
+ );
85
+ string json = Serializer.JsonStringify(modification);
86
+ AttributeModification clone = Serializer.JsonDeserialize<AttributeModification>(json);
87
+
88
+ Assert.AreEqual(modification, clone);
89
+ }
90
+
91
+ [Test]
92
+ public void ProtoBufRoundtripPreservesFields()
93
+ {
94
+ AttributeModification modification = new("speed", ModificationAction.Addition, -3f);
95
+ byte[] payload = Serializer.ProtoSerialize(modification);
96
+ AttributeModification clone = Serializer.ProtoDeserialize<AttributeModification>(
97
+ payload
98
+ );
99
+ Assert.AreEqual(modification, clone);
100
+ }
101
+
102
+ [Test]
103
+ public void CompareToOrdersBasedOnAction()
104
+ {
105
+ AttributeModification addition = new("health", ModificationAction.Addition, 10f);
106
+ AttributeModification multiplication = new(
107
+ "health",
108
+ ModificationAction.Multiplication,
109
+ 2f
110
+ );
111
+ AttributeModification overrideValue = new("health", ModificationAction.Override, 0f);
112
+
113
+ AttributeModification[] unsorted = { overrideValue, multiplication, addition };
114
+
115
+ Array.Sort(unsorted);
116
+
117
+ Assert.AreEqual(addition, unsorted[0], "Addition should be applied first when sorted.");
118
+ Assert.AreEqual(
119
+ multiplication,
120
+ unsorted[1],
121
+ "Multiplication should appear after additions when sorted."
122
+ );
123
+ Assert.AreEqual(
124
+ overrideValue,
125
+ unsorted[2],
126
+ "Override should be processed last when sorted."
127
+ );
128
+ }
129
+
130
+ [Test]
131
+ public void CompareToObjectReturnsMinusOneForNonAttributeModification()
132
+ {
133
+ AttributeModification addition = new("health", ModificationAction.Addition, 5f);
134
+ Assert.AreEqual(-1, addition.CompareTo("not a modification"));
135
+ }
136
+ }
137
+ }