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
@@ -9,11 +9,61 @@ namespace WallstopStudios.UnityHelpers.Tags
9
9
  using Core.Extension;
10
10
  using Core.Helper;
11
11
  using UnityEngine;
12
+ using WallstopStudios.UnityHelpers.Core.Attributes;
12
13
  using WallstopStudios.UnityHelpers.Utils;
13
14
  #if ODIN_INSPECTOR
14
15
  using Sirenix.OdinInspector;
15
16
  #endif
16
17
 
18
+ /// <summary>
19
+ /// Determines which handles are considered the "same stack" when evaluating stacking policies.
20
+ /// </summary>
21
+ public enum EffectStackGroup
22
+ {
23
+ [Obsolete("Please use a valid EffectStackGroup instead.")]
24
+ None = 0,
25
+
26
+ /// <summary>
27
+ /// Uses the effect asset reference. Each ScriptableObject instance is its own group.
28
+ /// </summary>
29
+ Reference = 1,
30
+
31
+ /// <summary>
32
+ /// Uses a custom string key supplied via <see cref="AttributeEffect.stackGroupKey"/>.
33
+ /// Effects with matching keys share a stack regardless of asset reference.
34
+ /// </summary>
35
+ CustomKey = 2,
36
+ }
37
+
38
+ /// <summary>
39
+ /// Describes how additional applications of an effect interact with existing stacks.
40
+ /// </summary>
41
+ public enum EffectStackingMode
42
+ {
43
+ [Obsolete("Please use a valid EffectStackingMode instead.")]
44
+ None = 0,
45
+
46
+ /// <summary>
47
+ /// Always create a new stack (subject to optional stack limit).
48
+ /// </summary>
49
+ Stack = 1,
50
+
51
+ /// <summary>
52
+ /// Reuse the first existing stack and refresh duration if possible.
53
+ /// </summary>
54
+ Refresh = 2,
55
+
56
+ /// <summary>
57
+ /// Remove existing stacks sharing the same group before creating a new one.
58
+ /// </summary>
59
+ Replace = 3,
60
+
61
+ /// <summary>
62
+ /// Ignore new applications when a stack is already active.
63
+ /// </summary>
64
+ Ignore = 4,
65
+ }
66
+
17
67
  /// <summary>
18
68
  /// Reusable, data‑driven bundle of stat modifications, tags, and cosmetic feedback.
19
69
  /// Serves as the authoring unit for buffs, debuffs, and status effects.
@@ -76,22 +126,29 @@ namespace WallstopStudios.UnityHelpers.Tags
76
126
  /// </summary>
77
127
  public List<AttributeModification> modifications = new();
78
128
 
129
+ /// <summary>
130
+ /// Periodic modifier sets executed on a cadence while the effect remains active.
131
+ /// </summary>
132
+ public List<PeriodicEffectDefinition> periodicEffects = new();
133
+
79
134
  /// <summary>
80
135
  /// Specifies how long this effect should persist (Instant, Duration, or Infinite).
81
136
  /// </summary>
82
137
  public ModifierDurationType durationType = ModifierDurationType.Duration;
83
138
 
84
- #if ODIN_INSPECTOR
85
- [ShowIf("@durationType == ModifierDurationType.Duration")]
86
- #endif
87
139
  /// <summary>
88
140
  /// The duration in seconds for this effect. Only used when <see cref="durationType"/> is <see cref="ModifierDurationType.Duration"/>.
89
141
  /// </summary>
90
- public float duration;
91
-
92
142
  #if ODIN_INSPECTOR
93
143
  [ShowIf("@durationType == ModifierDurationType.Duration")]
144
+ #else
145
+ [WShowIf(
146
+ nameof(durationType),
147
+ expectedValues: new object[] { ModifierDurationType.Duration }
148
+ )]
94
149
  #endif
150
+ public float duration;
151
+
95
152
  /// <summary>
96
153
  /// If true, reapplying this effect while it's already active will reset the duration timer.
97
154
  /// Only used when <see cref="durationType"/> is <see cref="ModifierDurationType.Duration"/>.
@@ -100,6 +157,14 @@ namespace WallstopStudios.UnityHelpers.Tags
100
157
  /// A poison effect with resetDurationOnReapplication=true will restart its 5-second timer
101
158
  /// each time the poison is reapplied, preventing stacking but extending the effect.
102
159
  /// </example>
160
+ #if ODIN_INSPECTOR
161
+ [ShowIf("@durationType == ModifierDurationType.Duration")]
162
+ #else
163
+ [WShowIf(
164
+ nameof(durationType),
165
+ expectedValues: new object[] { ModifierDurationType.Duration }
166
+ )]
167
+ #endif
103
168
  public bool resetDurationOnReapplication;
104
169
 
105
170
  /// <summary>
@@ -112,6 +177,41 @@ namespace WallstopStudios.UnityHelpers.Tags
112
177
  /// </example>
113
178
  public List<string> effectTags = new();
114
179
 
180
+ /// <summary>
181
+ /// A list of cosmetic effect data that defines visual and audio feedback for this effect.
182
+ /// These are applied when the effect becomes active and removed when it expires.
183
+ /// </summary>
184
+ [JsonIgnore]
185
+ public List<CosmeticEffectData> cosmeticEffects = new();
186
+
187
+ /// <summary>
188
+ /// Custom behaviours instantiated per active handle.
189
+ /// </summary>
190
+ [JsonIgnore]
191
+ public List<EffectBehavior> behaviors = new();
192
+
193
+ /// <summary>
194
+ /// Determines how this effect groups stacks for stacking decisions.
195
+ /// </summary>
196
+ public EffectStackGroup stackGroup = EffectStackGroup.Reference;
197
+
198
+ /// <summary>
199
+ /// Optional stack key used when <see cref="stackGroup"/> is set to <see cref="EffectStackGroup.CustomKey"/>.
200
+ /// </summary>
201
+ public string stackGroupKey;
202
+
203
+ /// <summary>
204
+ /// Determines how successive applications interact with existing stacks for the same group.
205
+ /// </summary>
206
+ public EffectStackingMode stackingMode = EffectStackingMode.Refresh;
207
+
208
+ /// <summary>
209
+ /// Optional cap on simultaneous stacks when <see cref="stackingMode"/> is <see cref="EffectStackingMode.Stack"/>.
210
+ /// A value of 0 means unlimited stacks.
211
+ /// </summary>
212
+ [Min(0)]
213
+ public int maximumStacks;
214
+
115
215
  /// <summary>
116
216
  /// Determines whether this effect applies the specified tag.
117
217
  /// </summary>
@@ -272,17 +372,6 @@ namespace WallstopStudios.UnityHelpers.Tags
272
372
  return buffer;
273
373
  }
274
374
 
275
- /// <summary>
276
- /// A list of cosmetic effect data that defines visual and audio feedback for this effect.
277
- /// These are applied when the effect becomes active and removed when it expires.
278
- /// </summary>
279
- [JsonIgnore]
280
- public List<CosmeticEffectData> cosmeticEffects = new();
281
-
282
- private List<string> CosmeticEffectsForJson =>
283
- cosmeticEffects?.Select(cosmeticEffectData => cosmeticEffectData.name).ToList()
284
- ?? new List<string>(0);
285
-
286
375
  /// <summary>
287
376
  /// Converts this effect to a JSON string representation including all modifications, tags, and cosmetic effects.
288
377
  /// </summary>
@@ -300,6 +389,10 @@ namespace WallstopStudios.UnityHelpers.Tags
300
389
  }.ToJson();
301
390
  }
302
391
 
392
+ private List<string> CosmeticEffectsForJson =>
393
+ cosmeticEffects?.Select(cosmeticEffectData => cosmeticEffectData.name).ToList()
394
+ ?? new List<string>(0);
395
+
303
396
  private string BuildDescription()
304
397
  {
305
398
  if (modifications == null)
@@ -382,6 +475,16 @@ namespace WallstopStudios.UnityHelpers.Tags
382
475
  return descriptionBuilder.ToString();
383
476
  }
384
477
 
478
+ internal EffectStackKey GetStackKey()
479
+ {
480
+ if (stackGroup == EffectStackGroup.CustomKey && !string.IsNullOrEmpty(stackGroupKey))
481
+ {
482
+ return EffectStackKey.CreateCustom(stackGroupKey);
483
+ }
484
+
485
+ return EffectStackKey.CreateReference(this);
486
+ }
487
+
385
488
  /// <summary>
386
489
  /// Determines whether this effect is equal to another effect by comparing all fields.
387
490
  /// This is needed because deserialization creates new instances, so reference equality is insufficient.
@@ -1,62 +1,68 @@
1
1
  namespace WallstopStudios.UnityHelpers.Tags
2
2
  {
3
3
  using System;
4
+ using System.Text.Json.Serialization;
4
5
  using Core.Extension;
5
6
  using Core.Helper;
7
+ using ProtoBuf;
6
8
 
7
9
  /// <summary>
8
- /// Declarative change to an <see cref="Attribute"/> value (add, multiply, or override).
9
- /// Forms the stat‑modification payload inside an <see cref="AttributeEffect"/>.
10
+ /// Declarative change applied to an <see cref="Attribute"/>.
11
+ /// Each instance represents a single operation (add, multiply, or override) referenced by an <see cref="AttributeEffect"/>.
10
12
  /// </summary>
11
13
  /// <remarks>
14
+ /// <para>Key properties:</para>
15
+ /// <list type="bullet">
16
+ /// <item><description>Non-destructive: temporary handles can add/remove modifications without mutating base values.</description></item>
17
+ /// <item><description>Deterministic ordering: <see cref="Attribute"/> always processes Addition, then Multiplication, then Override.</description></item>
18
+ /// <item><description>Flexible authoring: supports both instant (permanent) and duration-based effects.</description></item>
19
+ /// </list>
20
+ /// <para>Stack processing order:</para>
21
+ /// <list type="number">
22
+ /// <item><description>Addition (value += x)</description></item>
23
+ /// <item><description>Multiplication (value *= x)</description></item>
24
+ /// <item><description>Override (value = x)</description></item>
25
+ /// </list>
12
26
  /// <para>
13
- /// Problems solved:
14
- /// - Non‑destructive stat changes that can be added/removed per effect instance
15
- /// - Clear stacking rules via action ordering
16
- /// - Works with both permanent (Instant) and temporary (Duration/Infinite) effects
27
+ /// The <see cref="attribute"/> field must match an <see cref="Attribute"/> field on the target <see cref="AttributesComponent"/>.
28
+ /// The Attribute Metadata Cache generator can provide editor dropdowns to avoid typos. Unknown names are ignored at runtime.
17
29
  /// </para>
18
- /// <para>
19
- /// Stacking and order: Modifications are applied in this order across a target attribute:
20
- /// 1) Addition (value += x) → 2) Multiplication (value *= x) → 3) Override (value = x).
21
- /// This means Overrides always win last; use with care.
22
- /// </para>
23
- /// <para>
24
- /// Addressing: The <see cref="attribute"/> field names an <see cref="AttributesComponent"/> field of type
25
- /// <see cref="Attribute"/>. Misspelled or missing names are ignored at runtime to keep effects robust.
26
- /// Use the Attribute Metadata Cache generator to populate editor dropdowns and avoid typos.
27
- /// </para>
28
- /// <para>
29
- /// Examples:
30
+ /// <para>Sample definitions:</para>
30
31
  /// <code>
31
- /// // +50 flat Health
32
+ /// // +50 flat health
32
33
  /// new AttributeModification { attribute = "Health", action = ModificationAction.Addition, value = 50f };
33
34
  ///
34
- /// // +50% Speed (i.e., multiply by 1.5)
35
+ /// // +50% speed
35
36
  /// new AttributeModification { attribute = "Speed", action = ModificationAction.Multiplication, value = 1.5f };
36
37
  ///
37
- /// // Set Defense to 0 (hard override)
38
+ /// // Hard-set defense to 0
38
39
  /// new AttributeModification { attribute = "Defense", action = ModificationAction.Override, value = 0f };
39
40
  /// </code>
40
- /// </para>
41
- /// <para>
42
- /// Tips:
43
- /// - Prefer Addition for small buffs/debuffs; prefer Multiplication for % changes.
44
- /// - Avoid frequent Overrides unless you intend to fully clamp a value.
45
- /// - Use negative Addition values to subtract; use Multiplication < 1.0 for % reductions.
46
- /// </para>
41
+ /// <para>Authoring tips:</para>
42
+ /// <list type="bullet">
43
+ /// <item><description>Use Addition for flat buffs/debuffs; Multiplication for percentage-style adjustments.</description></item>
44
+ /// <item><description>Reserve Override for hard clamps (it always executes last).</description></item>
45
+ /// <item><description>Negative Addition subtracts; Multiplication values below 1.0 reduce the attribute.</description></item>
46
+ /// </list>
47
47
  /// </remarks>
48
48
  [Serializable]
49
- public struct AttributeModification : IEquatable<AttributeModification>
49
+ [ProtoContract]
50
+ public struct AttributeModification
51
+ : IEquatable<AttributeModification>,
52
+ IComparable<AttributeModification>,
53
+ IComparable
50
54
  {
51
55
  /// <summary>
52
56
  /// The name of the attribute to modify. This should match a field name in an <see cref="AttributesComponent"/> subclass.
53
57
  /// </summary>
54
58
  [StringInList(typeof(AttributeUtilities), nameof(AttributeUtilities.GetAllAttributeNames))]
59
+ [ProtoMember(1)]
55
60
  public string attribute;
56
61
 
57
62
  /// <summary>
58
63
  /// The type of modification action to perform (Addition, Multiplication, or Override).
59
64
  /// </summary>
65
+ [ProtoMember(2)]
60
66
  public ModificationAction action;
61
67
 
62
68
  /// <summary>
@@ -65,8 +71,17 @@ namespace WallstopStudios.UnityHelpers.Tags
65
71
  /// <para>- Multiplication: The multiplier to apply (e.g., 1.5 for +50%, 0.5 for -50%)</para>
66
72
  /// <para>- Override: The new absolute value to set</para>
67
73
  /// </summary>
74
+ [ProtoMember(3)]
68
75
  public float value;
69
76
 
77
+ [JsonConstructor]
78
+ public AttributeModification(string attribute, ModificationAction action, float value)
79
+ {
80
+ this.attribute = attribute;
81
+ this.action = action;
82
+ this.value = value;
83
+ }
84
+
70
85
  /// <summary>
71
86
  /// Converts this modification to a JSON string representation.
72
87
  /// </summary>
@@ -76,6 +91,21 @@ namespace WallstopStudios.UnityHelpers.Tags
76
91
  return this.ToJson();
77
92
  }
78
93
 
94
+ public int CompareTo(object obj)
95
+ {
96
+ if (obj is AttributeModification other)
97
+ {
98
+ return CompareTo(other);
99
+ }
100
+
101
+ return -1;
102
+ }
103
+
104
+ public int CompareTo(AttributeModification other)
105
+ {
106
+ return ((int)action).CompareTo((int)other.action);
107
+ }
108
+
79
109
  /// <summary>
80
110
  /// Determines whether two attribute modifications are not equal.
81
111
  /// </summary>
@@ -121,6 +121,26 @@ namespace WallstopStudios.UnityHelpers.Tags
121
121
  IEnumerable<AttributeModification> attributeModifications
122
122
  )
123
123
  {
124
+ if (attributeModifications is IReadOnlyList<AttributeModification> readonlyList)
125
+ {
126
+ for (int i = 0; i < readonlyList.Count; ++i)
127
+ {
128
+ AttributeModification modification = readonlyList[i];
129
+ if (!TryGetAttribute(modification.attribute, out Attribute attribute))
130
+ {
131
+ continue;
132
+ }
133
+
134
+ float oldValue = attribute;
135
+ attribute.ApplyAttributeModification(modification);
136
+ float currentValue = attribute;
137
+
138
+ OnAttributeModified?.Invoke(modification.attribute, oldValue, currentValue);
139
+ }
140
+
141
+ return;
142
+ }
143
+
124
144
  foreach (AttributeModification modification in attributeModifications)
125
145
  {
126
146
  if (!TryGetAttribute(modification.attribute, out Attribute attribute))
@@ -0,0 +1,171 @@
1
+ namespace WallstopStudios.UnityHelpers.Tags
2
+ {
3
+ using UnityEngine;
4
+
5
+ /// <summary>
6
+ /// Base class for authoring custom effect behaviours that respond to the lifecycle of an active <see cref="AttributeEffect"/>.
7
+ /// </summary>
8
+ /// <remarks>
9
+ /// <para>
10
+ /// Each behaviour asset is cloned per <see cref="EffectHandle"/>, which means derived classes can safely store mutable state
11
+ /// between calls to <see cref="OnApply(EffectBehaviorContext)"/>, <see cref="OnTick(EffectBehaviorContext)"/>,
12
+ /// <see cref="OnPeriodicTick(EffectBehaviorContext, PeriodicEffectTickContext)"/>, and <see cref="OnRemove(EffectBehaviorContext)"/>.
13
+ /// </para>
14
+ /// <para>
15
+ /// Attach behaviour assets to <see cref="AttributeEffect.behaviors"/> to augment the data-driven attribute pipeline with bespoke
16
+ /// gameplay logic, visual or audio feedback, or integration hooks into other game systems.
17
+ /// </para>
18
+ /// <para>
19
+ /// All callbacks are synchronously invoked by <see cref="EffectHandler"/> on the main thread, ensuring safe interaction with Unity APIs.
20
+ /// </para>
21
+ /// </remarks>
22
+ /// <example>
23
+ /// <code language="csharp">
24
+ /// using UnityEngine;
25
+ /// using WallstopStudios.UnityHelpers.Tags;
26
+ ///
27
+ /// [CreateAssetMenu(menuName = "Game/Effects/Burning Behaviour")]
28
+ /// public sealed class BurningBehavior : EffectBehavior
29
+ /// {
30
+ /// [SerializeField]
31
+ /// private GameObject flamePrefab;
32
+ ///
33
+ /// private GameObject spawnedInstance;
34
+ ///
35
+ /// public override void OnApply(EffectBehaviorContext context)
36
+ /// {
37
+ /// if (flamePrefab == null)
38
+ /// {
39
+ /// return;
40
+ /// }
41
+ ///
42
+ /// Transform parent = context.Target.transform;
43
+ /// spawnedInstance = Object.Instantiate(flamePrefab, parent.position, parent.rotation, parent);
44
+ /// }
45
+ ///
46
+ /// public override void OnPeriodicTick(EffectBehaviorContext context, PeriodicEffectTickContext tickContext)
47
+ /// {
48
+ /// // Cancel the effect early once the periodic bundle has executed three times.
49
+ /// if (tickContext.executedTicks >= 3)
50
+ /// {
51
+ /// context.handler.RemoveEffect(context.handle);
52
+ /// }
53
+ /// }
54
+ ///
55
+ /// public override void OnRemove(EffectBehaviorContext context)
56
+ /// {
57
+ /// if (spawnedInstance != null)
58
+ /// {
59
+ /// Object.Destroy(spawnedInstance);
60
+ /// spawnedInstance = null;
61
+ /// }
62
+ /// }
63
+ /// }
64
+ ///
65
+ /// // Assign the behaviour asset to an AttributeEffect so it is cloned per application.
66
+ /// AttributeEffect burnEffect = ScriptableObject.CreateInstance&lt;AttributeEffect&gt;();
67
+ /// burnEffect.behaviors.Add(burningBehaviorAsset);
68
+ /// </code>
69
+ /// </example>
70
+ public abstract class EffectBehavior : ScriptableObject
71
+ {
72
+ /// <summary>
73
+ /// Invoked once when the effect handle becomes active.
74
+ /// </summary>
75
+ /// <param name="context">Runtime context for the effect instance.</param>
76
+ public virtual void OnApply(EffectBehaviorContext context) { }
77
+
78
+ /// <summary>
79
+ /// Invoked every frame while the effect remains active.
80
+ /// </summary>
81
+ /// <param name="context">Runtime context for the effect instance.</param>
82
+ public virtual void OnTick(EffectBehaviorContext context) { }
83
+
84
+ /// <summary>
85
+ /// Invoked after a periodic tick has been processed for the owning effect.
86
+ /// </summary>
87
+ /// <param name="context">Runtime context for the effect instance.</param>
88
+ /// <param name="tickContext">Information about the specific periodic tick.</param>
89
+ public virtual void OnPeriodicTick(
90
+ EffectBehaviorContext context,
91
+ PeriodicEffectTickContext tickContext
92
+ ) { }
93
+
94
+ /// <summary>
95
+ /// Invoked when the effect handle is removed or expires.
96
+ /// </summary>
97
+ /// <param name="context">Runtime context for the effect instance.</param>
98
+ public virtual void OnRemove(EffectBehaviorContext context) { }
99
+ }
100
+
101
+ /// <summary>
102
+ /// Immutable runtime data passed to behaviour callbacks.
103
+ /// </summary>
104
+ public readonly struct EffectBehaviorContext
105
+ {
106
+ /// <summary>
107
+ /// Gets the effect asset backing the handle.
108
+ /// </summary>
109
+ public AttributeEffect Effect => handle.effect;
110
+
111
+ /// <summary>
112
+ /// Gets the GameObject targeted by the effect handler.
113
+ /// </summary>
114
+ public GameObject Target => handler.gameObject;
115
+
116
+ /// <summary>
117
+ /// Gets the deltaTime used for the current invocation. For <see cref="EffectBehavior.OnApply"/>,
118
+ /// this value is zero.
119
+ /// </summary>
120
+ public readonly float deltaTime;
121
+
122
+ /// <summary>
123
+ /// Gets the handler managing the effect.
124
+ /// </summary>
125
+ public readonly EffectHandler handler;
126
+
127
+ /// <summary>
128
+ /// Gets the handle associated with this behaviour invocation.
129
+ /// </summary>
130
+ public readonly EffectHandle handle;
131
+
132
+ public EffectBehaviorContext(EffectHandler handler, EffectHandle handle, float deltaTime)
133
+ {
134
+ this.handler = handler;
135
+ this.handle = handle;
136
+ this.deltaTime = deltaTime;
137
+ }
138
+ }
139
+
140
+ /// <summary>
141
+ /// Details about a specific periodic tick that just executed.
142
+ /// </summary>
143
+ public readonly struct PeriodicEffectTickContext
144
+ {
145
+ /// <summary>
146
+ /// Gets the periodic definition that produced this tick.
147
+ /// </summary>
148
+ public readonly PeriodicEffectDefinition definition;
149
+
150
+ /// <summary>
151
+ /// Gets the number of ticks executed so far, including this one.
152
+ /// </summary>
153
+ public readonly int executedTicks;
154
+
155
+ /// <summary>
156
+ /// Gets the timestamp when the tick occurred.
157
+ /// </summary>
158
+ public readonly float currentTime;
159
+
160
+ public PeriodicEffectTickContext(
161
+ PeriodicEffectDefinition definition,
162
+ int executedTicks,
163
+ float currentTime
164
+ )
165
+ {
166
+ this.definition = definition;
167
+ this.executedTicks = executedTicks;
168
+ this.currentTime = currentTime;
169
+ }
170
+ }
171
+ }
@@ -0,0 +1,4 @@
1
+ fileFormatVersion: 2
2
+ guid: c5c02017f2cf40f59c135b9cf2d6fb3a
3
+ timeCreated: 1759598400
4
+
@@ -72,6 +72,11 @@ namespace WallstopStudios.UnityHelpers.Tags
72
72
  return new EffectHandle(Interlocked.Increment(ref Id), effect);
73
73
  }
74
74
 
75
+ internal static EffectHandle CreateInstanceInternal()
76
+ {
77
+ return new EffectHandle(Interlocked.Increment(ref Id), null);
78
+ }
79
+
75
80
  private EffectHandle(long id, AttributeEffect effect)
76
81
  {
77
82
  this.id = id;