com.wallstop-studios.unity-helpers 2.0.4 → 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 (52) 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/Tags/Attribute.cs +144 -24
  17. package/Runtime/Tags/AttributeEffect.cs +119 -16
  18. package/Runtime/Tags/AttributeModification.cs +59 -29
  19. package/Runtime/Tags/AttributesComponent.cs +20 -0
  20. package/Runtime/Tags/EffectBehavior.cs +171 -0
  21. package/Runtime/Tags/EffectBehavior.cs.meta +4 -0
  22. package/Runtime/Tags/EffectHandle.cs +5 -0
  23. package/Runtime/Tags/EffectHandler.cs +385 -39
  24. package/Runtime/Tags/EffectStackKey.cs +79 -0
  25. package/Runtime/Tags/EffectStackKey.cs.meta +4 -0
  26. package/Runtime/Tags/PeriodicEffectDefinition.cs +102 -0
  27. package/Runtime/Tags/PeriodicEffectDefinition.cs.meta +4 -0
  28. package/Runtime/Tags/PeriodicEffectRuntimeState.cs +40 -0
  29. package/Runtime/Tags/PeriodicEffectRuntimeState.cs.meta +4 -0
  30. package/Samples~/DI - Zenject/README.md +0 -2
  31. package/Tests/Editor/Attributes/WShowIfPropertyDrawerTests.cs +285 -0
  32. package/Tests/Editor/Attributes/WShowIfPropertyDrawerTests.cs.meta +11 -0
  33. package/Tests/Editor/Core/Attributes/RelationalComponentAssignerTests.cs +2 -2
  34. package/Tests/Editor/Utils/ScriptableObjectSingletonTests.cs +41 -0
  35. package/Tests/Runtime/Serialization/JsonSerializationTest.cs +4 -3
  36. package/Tests/Runtime/Tags/AttributeEffectTests.cs +135 -0
  37. package/Tests/Runtime/Tags/AttributeEffectTests.cs.meta +3 -0
  38. package/Tests/Runtime/Tags/AttributeModificationTests.cs +137 -0
  39. package/Tests/Runtime/Tags/AttributeTests.cs +192 -0
  40. package/Tests/Runtime/Tags/AttributeTests.cs.meta +3 -0
  41. package/Tests/Runtime/Tags/EffectBehaviorTests.cs +184 -0
  42. package/Tests/Runtime/Tags/EffectBehaviorTests.cs.meta +3 -0
  43. package/Tests/Runtime/Tags/EffectHandlerTests.cs +618 -0
  44. package/Tests/Runtime/Tags/Helpers/RecordingEffectBehavior.cs +89 -0
  45. package/Tests/Runtime/Tags/Helpers/RecordingEffectBehavior.cs.meta +4 -0
  46. package/Tests/Runtime/Tags/PeriodicEffectDefinitionSerializationTests.cs +92 -0
  47. package/Tests/Runtime/Tags/PeriodicEffectDefinitionSerializationTests.cs.meta +3 -0
  48. package/package.json +1 -1
  49. package/scripts/lint-doc-links.ps1 +156 -11
  50. package/Tests/Runtime/Tags/AttributeDataTests.cs +0 -312
  51. package/node_modules.meta +0 -8
  52. /package/Tests/Runtime/Tags/{AttributeDataTests.cs.meta → AttributeModificationTests.cs.meta} +0 -0
@@ -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
+ }
@@ -0,0 +1,192 @@
1
+ namespace WallstopStudios.UnityHelpers.Tests.Tags
2
+ {
3
+ using System;
4
+ using NUnit.Framework;
5
+ using UnityEngine;
6
+ using WallstopStudios.UnityHelpers.Tags;
7
+ using Attribute = WallstopStudios.UnityHelpers.Tags.Attribute;
8
+
9
+ [TestFixture]
10
+ public sealed class AttributeTests : AttributeTagsTestBase
11
+ {
12
+ [SetUp]
13
+ public void SetUp()
14
+ {
15
+ ResetEffectHandleId();
16
+ }
17
+
18
+ [Test]
19
+ public void CurrentValueReflectsBaseValueWhenUnmodified()
20
+ {
21
+ Attribute attribute = new(12f);
22
+ Assert.AreEqual(12f, attribute.CurrentValue);
23
+ Assert.AreEqual(12f, attribute.BaseValue);
24
+ }
25
+
26
+ [Test]
27
+ public void ApplyAttributeModificationWithoutHandleMutatesBase()
28
+ {
29
+ Attribute attribute = new(10f);
30
+ AttributeModification modification = new()
31
+ {
32
+ attribute = "health",
33
+ action = ModificationAction.Addition,
34
+ value = 5f,
35
+ };
36
+
37
+ attribute.ApplyAttributeModification(modification);
38
+ Assert.AreEqual(15f, attribute.BaseValue);
39
+ Assert.AreEqual(15f, attribute.CurrentValue);
40
+ }
41
+
42
+ [Test]
43
+ public void ApplyAndRemoveAttributeModificationWithHandleRecalculates()
44
+ {
45
+ Attribute attribute = new(100f);
46
+ AttributeModification addition = new()
47
+ {
48
+ attribute = "health",
49
+ action = ModificationAction.Addition,
50
+ value = 25f,
51
+ };
52
+
53
+ AttributeEffect effect = Track(ScriptableObject.CreateInstance<AttributeEffect>());
54
+ effect.name = "Buff";
55
+ EffectHandle handle = EffectHandle.CreateInstance(effect);
56
+
57
+ attribute.ApplyAttributeModification(addition, handle);
58
+ Assert.AreEqual(125f, attribute.CurrentValue);
59
+ Assert.AreEqual(100f, attribute.BaseValue);
60
+
61
+ bool removed = attribute.RemoveAttributeModification(handle);
62
+ Assert.IsTrue(removed);
63
+ Assert.AreEqual(100f, attribute.CurrentValue);
64
+ }
65
+
66
+ [Test]
67
+ public void ApplyAttributeModificationWithMultiplicationExecutesInOrder()
68
+ {
69
+ Attribute attribute = new(10f);
70
+ AttributeEffect effect = Track(ScriptableObject.CreateInstance<AttributeEffect>());
71
+ effect.name = "Stacking";
72
+ EffectHandle handle = EffectHandle.CreateInstance(effect);
73
+
74
+ attribute.ApplyAttributeModification(
75
+ new AttributeModification
76
+ {
77
+ attribute = "health",
78
+ action = ModificationAction.Addition,
79
+ value = 5f,
80
+ },
81
+ handle
82
+ );
83
+ attribute.ApplyAttributeModification(
84
+ new AttributeModification
85
+ {
86
+ attribute = "health",
87
+ action = ModificationAction.Multiplication,
88
+ value = 2f,
89
+ },
90
+ handle
91
+ );
92
+
93
+ Assert.AreEqual(30f, attribute.CurrentValue);
94
+ }
95
+
96
+ [Test]
97
+ public void AttributeEqualsSupportsFloatComparisons()
98
+ {
99
+ Attribute attribute = new(7.25f);
100
+ Assert.IsTrue(attribute.Equals(7.25f));
101
+ Assert.IsTrue(attribute.Equals((double)7.25f));
102
+ Assert.IsFalse(attribute.Equals(7.5f));
103
+ Assert.AreEqual("7.25", attribute.ToString());
104
+ }
105
+
106
+ [Test]
107
+ public void ClearCacheForcesRecalculation()
108
+ {
109
+ Attribute attribute = new(10f);
110
+ AttributeModification addition = new()
111
+ {
112
+ attribute = "health",
113
+ action = ModificationAction.Addition,
114
+ value = 5f,
115
+ };
116
+
117
+ attribute.ApplyAttributeModification(addition);
118
+ Assert.AreEqual(15f, attribute.CurrentValue);
119
+
120
+ attribute.ClearCache();
121
+ Assert.AreEqual(15f, attribute.CurrentValue);
122
+ }
123
+
124
+ [Test]
125
+ public void AddProducesHandleAndAppliesAddition()
126
+ {
127
+ Attribute attribute = new(10f);
128
+
129
+ EffectHandle handle = attribute.Add(5f);
130
+ Assert.AreEqual(15f, attribute.CurrentValue);
131
+ Assert.AreEqual(1L, handle.id);
132
+
133
+ bool removed = attribute.RemoveAttributeModification(handle);
134
+ Assert.IsTrue(removed);
135
+ Assert.AreEqual(10f, attribute.CurrentValue);
136
+ }
137
+
138
+ [Test]
139
+ public void SubtractStacksAsNegativeAddition()
140
+ {
141
+ Attribute attribute = new(20f);
142
+
143
+ EffectHandle addition = attribute.Add(5f);
144
+ EffectHandle subtraction = attribute.Subtract(8f);
145
+ EffectHandle multiplier = attribute.Multiply(2f);
146
+
147
+ Assert.AreEqual(34f, attribute.CurrentValue);
148
+
149
+ bool subtractionRemoved = attribute.RemoveAttributeModification(subtraction);
150
+ Assert.IsTrue(subtractionRemoved);
151
+ Assert.AreEqual(50f, attribute.CurrentValue);
152
+
153
+ attribute.RemoveAttributeModification(addition);
154
+ attribute.RemoveAttributeModification(multiplier);
155
+ }
156
+
157
+ [Test]
158
+ public void DivideAppliesReciprocalMultiplication()
159
+ {
160
+ Attribute attribute = new(12f);
161
+
162
+ EffectHandle addition = attribute.Add(6f);
163
+ EffectHandle division = attribute.Divide(3f);
164
+
165
+ Assert.AreEqual(6f, attribute.CurrentValue);
166
+
167
+ bool divisionRemoved = attribute.RemoveAttributeModification(division);
168
+ Assert.IsTrue(divisionRemoved);
169
+ Assert.AreEqual(18f, attribute.CurrentValue);
170
+
171
+ attribute.RemoveAttributeModification(addition);
172
+ }
173
+
174
+ [Test]
175
+ public void DivideThrowsWhenValueIsZero()
176
+ {
177
+ Attribute attribute = new(10f);
178
+ Assert.Throws<ArgumentException>(() => attribute.Divide(0f));
179
+ }
180
+
181
+ [Test]
182
+ public void ArithmeticHelpersThrowWhenValueIsNotFinite()
183
+ {
184
+ Attribute attribute = new(5f);
185
+
186
+ Assert.Throws<ArgumentException>(() => attribute.Add(float.NaN));
187
+ Assert.Throws<ArgumentException>(() => attribute.Subtract(float.PositiveInfinity));
188
+ Assert.Throws<ArgumentException>(() => attribute.Multiply(float.NegativeInfinity));
189
+ Assert.Throws<ArgumentException>(() => attribute.Divide(float.PositiveInfinity));
190
+ }
191
+ }
192
+ }
@@ -0,0 +1,3 @@
1
+ fileFormatVersion: 2
2
+ guid: 6cef49ba760748098e3c1fc18a7b1142
3
+ timeCreated: 1761249381
@@ -0,0 +1,184 @@
1
+ namespace WallstopStudios.UnityHelpers.Tests.Tags
2
+ {
3
+ using System.Collections;
4
+ using NUnit.Framework;
5
+ using UnityEngine;
6
+ using UnityEngine.TestTools;
7
+ using WallstopStudios.UnityHelpers.Tags;
8
+ using WallstopStudios.UnityHelpers.Tests.Tags.Helpers;
9
+
10
+ [TestFixture]
11
+ public sealed class EffectBehaviorTests : TagsTestBase
12
+ {
13
+ [SetUp]
14
+ public void SetUp()
15
+ {
16
+ ResetEffectHandleId();
17
+ RecordingEffectBehavior.Reset();
18
+ }
19
+
20
+ [UnityTest]
21
+ public IEnumerator LifecycleCallbacksProvideContextData()
22
+ {
23
+ (GameObject entity, EffectHandler handler, _, _) = CreateEntity();
24
+
25
+ PeriodicEffectDefinition periodicDefinition = new()
26
+ {
27
+ name = "Pulse",
28
+ initialDelay = 0f,
29
+ interval = 0.05f,
30
+ maxTicks = 1,
31
+ };
32
+
33
+ AttributeEffect effect = CreateEffect(
34
+ "Lifecycle",
35
+ e =>
36
+ {
37
+ e.periodicEffects.Add(periodicDefinition);
38
+ }
39
+ );
40
+
41
+ RecordingEffectBehavior behavior = Track(
42
+ ScriptableObject.CreateInstance<RecordingEffectBehavior>()
43
+ );
44
+ effect.behaviors.Add(behavior);
45
+
46
+ EffectHandle handle = handler.ApplyEffect(effect).Value;
47
+ Assert.AreEqual(1, RecordingEffectBehavior.ApplyCount);
48
+ Assert.AreEqual(
49
+ 1,
50
+ RecordingEffectBehavior.ApplyContexts.Count,
51
+ "OnApply should fire immediately."
52
+ );
53
+
54
+ yield return null;
55
+ yield return null;
56
+
57
+ Assert.IsNotEmpty(
58
+ RecordingEffectBehavior.TickContexts,
59
+ "OnTick should run after Update."
60
+ );
61
+
62
+ yield return new WaitForSeconds(0.08f);
63
+
64
+ Assert.AreEqual(
65
+ 1,
66
+ RecordingEffectBehavior.PeriodicInvocations.Count,
67
+ "Expected one periodic tick."
68
+ );
69
+
70
+ int removeCountBefore = RecordingEffectBehavior.RemoveCount;
71
+ handler.RemoveEffect(handle);
72
+ Assert.AreEqual(removeCountBefore + 1, RecordingEffectBehavior.RemoveCount);
73
+ Assert.AreEqual(
74
+ 1,
75
+ RecordingEffectBehavior.RemoveContexts.Count,
76
+ "OnRemove should fire once."
77
+ );
78
+
79
+ EffectBehaviorContext applyContext = RecordingEffectBehavior.ApplyContexts[0];
80
+ Assert.AreSame(handler, applyContext.handler);
81
+ Assert.AreSame(entity, applyContext.Target);
82
+ Assert.AreEqual(effect, applyContext.Effect);
83
+ Assert.AreEqual(0f, applyContext.deltaTime);
84
+
85
+ EffectBehaviorContext tickContext = RecordingEffectBehavior.TickContexts[0];
86
+ Assert.AreSame(handler, tickContext.handler);
87
+ Assert.AreSame(entity, tickContext.Target);
88
+ Assert.AreEqual(effect, tickContext.Effect);
89
+ Assert.Greater(tickContext.deltaTime, 0f);
90
+
91
+ RecordingEffectBehavior.PeriodicInvocation periodicInvocation =
92
+ RecordingEffectBehavior.PeriodicInvocations[0];
93
+ Assert.AreSame(handler, periodicInvocation.Context.handler);
94
+ Assert.AreSame(entity, periodicInvocation.Context.Target);
95
+ Assert.AreEqual(effect, periodicInvocation.Context.Effect);
96
+ Assert.Greater(periodicInvocation.Context.deltaTime, 0f);
97
+ Assert.AreSame(periodicDefinition, periodicInvocation.TickContext.definition);
98
+ Assert.AreEqual(1, periodicInvocation.TickContext.executedTicks);
99
+ Assert.GreaterOrEqual(periodicInvocation.TickContext.currentTime, 0f);
100
+
101
+ EffectBehaviorContext removeContext = RecordingEffectBehavior.RemoveContexts[0];
102
+ Assert.AreSame(handler, removeContext.handler);
103
+ Assert.AreSame(entity, removeContext.Target);
104
+ Assert.AreEqual(effect, removeContext.Effect);
105
+ Assert.AreEqual(0f, removeContext.deltaTime);
106
+ }
107
+
108
+ [UnityTest]
109
+ public IEnumerator PeriodicTickContextTracksExecutedTicksAndTime()
110
+ {
111
+ (GameObject entity, EffectHandler handler, _, _) = CreateEntity();
112
+
113
+ PeriodicEffectDefinition periodicDefinition = new()
114
+ {
115
+ name = "Stacking Pulse",
116
+ initialDelay = 0f,
117
+ interval = 0.05f,
118
+ maxTicks = 3,
119
+ };
120
+
121
+ AttributeEffect effect = CreateEffect(
122
+ "Periodic",
123
+ e =>
124
+ {
125
+ e.periodicEffects.Add(periodicDefinition);
126
+ }
127
+ );
128
+
129
+ RecordingEffectBehavior behavior = Track(
130
+ ScriptableObject.CreateInstance<RecordingEffectBehavior>()
131
+ );
132
+ effect.behaviors.Add(behavior);
133
+
134
+ EffectHandle handle = handler.ApplyEffect(effect).Value;
135
+
136
+ yield return new WaitForSeconds(0.18f);
137
+
138
+ Assert.AreEqual(
139
+ 3,
140
+ RecordingEffectBehavior.PeriodicInvocations.Count,
141
+ "Expected periodic callbacks for each executed tick."
142
+ );
143
+
144
+ for (int i = 0; i < RecordingEffectBehavior.PeriodicInvocations.Count; ++i)
145
+ {
146
+ RecordingEffectBehavior.PeriodicInvocation invocation =
147
+ RecordingEffectBehavior.PeriodicInvocations[i];
148
+ Assert.AreSame(periodicDefinition, invocation.TickContext.definition);
149
+ Assert.AreEqual(i + 1, invocation.TickContext.executedTicks);
150
+ Assert.Greater(invocation.Context.deltaTime, 0f);
151
+
152
+ if (i > 0)
153
+ {
154
+ float previousTime = RecordingEffectBehavior
155
+ .PeriodicInvocations[i - 1]
156
+ .TickContext
157
+ .currentTime;
158
+ Assert.GreaterOrEqual(invocation.TickContext.currentTime, previousTime);
159
+ }
160
+ }
161
+
162
+ handler.RemoveEffect(handle);
163
+ }
164
+
165
+ private (
166
+ GameObject entity,
167
+ EffectHandler handler,
168
+ TestAttributesComponent attributes,
169
+ TagHandler tags
170
+ ) CreateEntity()
171
+ {
172
+ GameObject entity = CreateTrackedGameObject(
173
+ "EffectBehaviorEntity",
174
+ typeof(TestAttributesComponent)
175
+ );
176
+ return (
177
+ entity,
178
+ entity.GetComponent<EffectHandler>(),
179
+ entity.GetComponent<TestAttributesComponent>(),
180
+ entity.GetComponent<TagHandler>()
181
+ );
182
+ }
183
+ }
184
+ }
@@ -0,0 +1,3 @@
1
+ fileFormatVersion: 2
2
+ guid: 978c882cc7a7493b934680f4aef33214
3
+ timeCreated: 1761254817