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
@@ -9,10 +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;
13
+ using WallstopStudios.UnityHelpers.Utils;
12
14
  #if ODIN_INSPECTOR
13
15
  using Sirenix.OdinInspector;
14
16
  #endif
15
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
+
16
67
  /// <summary>
17
68
  /// Reusable, data‑driven bundle of stat modifications, tags, and cosmetic feedback.
18
69
  /// Serves as the authoring unit for buffs, debuffs, and status effects.
@@ -52,6 +103,7 @@ namespace WallstopStudios.UnityHelpers.Tags
52
103
  /// </para>
53
104
  /// </remarks>
54
105
  [Serializable]
106
+ [CreateAssetMenu(menuName = "Wallstop Studios/Unity Helpers/Attribute Effect")]
55
107
  public sealed class AttributeEffect :
56
108
  #if ODIN_INSPECTOR
57
109
  SerializedScriptableObject
@@ -72,24 +124,31 @@ namespace WallstopStudios.UnityHelpers.Tags
72
124
  /// The list of attribute modifications to apply when this effect is activated.
73
125
  /// Each modification specifies an attribute name, action type, and value.
74
126
  /// </summary>
75
- public readonly List<AttributeModification> modifications = new();
127
+ public List<AttributeModification> modifications = new();
128
+
129
+ /// <summary>
130
+ /// Periodic modifier sets executed on a cadence while the effect remains active.
131
+ /// </summary>
132
+ public List<PeriodicEffectDefinition> periodicEffects = new();
76
133
 
77
134
  /// <summary>
78
135
  /// Specifies how long this effect should persist (Instant, Duration, or Infinite).
79
136
  /// </summary>
80
137
  public ModifierDurationType durationType = ModifierDurationType.Duration;
81
138
 
82
- #if ODIN_INSPECTOR
83
- [ShowIf("@durationType == ModifierDurationType.Duration")]
84
- #endif
85
139
  /// <summary>
86
140
  /// The duration in seconds for this effect. Only used when <see cref="durationType"/> is <see cref="ModifierDurationType.Duration"/>.
87
141
  /// </summary>
88
- public float duration;
89
-
90
142
  #if ODIN_INSPECTOR
91
143
  [ShowIf("@durationType == ModifierDurationType.Duration")]
144
+ #else
145
+ [WShowIf(
146
+ nameof(durationType),
147
+ expectedValues: new object[] { ModifierDurationType.Duration }
148
+ )]
92
149
  #endif
150
+ public float duration;
151
+
93
152
  /// <summary>
94
153
  /// If true, reapplying this effect while it's already active will reset the duration timer.
95
154
  /// Only used when <see cref="durationType"/> is <see cref="ModifierDurationType.Duration"/>.
@@ -98,6 +157,14 @@ namespace WallstopStudios.UnityHelpers.Tags
98
157
  /// A poison effect with resetDurationOnReapplication=true will restart its 5-second timer
99
158
  /// each time the poison is reapplied, preventing stacking but extending the effect.
100
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
101
168
  public bool resetDurationOnReapplication;
102
169
 
103
170
  /// <summary>
@@ -115,11 +182,195 @@ namespace WallstopStudios.UnityHelpers.Tags
115
182
  /// These are applied when the effect becomes active and removed when it expires.
116
183
  /// </summary>
117
184
  [JsonIgnore]
118
- public readonly List<CosmeticEffectData> cosmeticEffects = new();
185
+ public List<CosmeticEffectData> cosmeticEffects = new();
119
186
 
120
- private List<string> CosmeticEffectsForJson =>
121
- cosmeticEffects?.Select(cosmeticEffectData => cosmeticEffectData.name).ToList()
122
- ?? new List<string>(0);
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
+
215
+ /// <summary>
216
+ /// Determines whether this effect applies the specified tag.
217
+ /// </summary>
218
+ /// <param name="effectTag">The tag to check.</param>
219
+ /// <returns><c>true</c> if the tag is present; otherwise, <c>false</c>.</returns>
220
+ public bool HasTag(string effectTag)
221
+ {
222
+ if (effectTags == null || string.IsNullOrEmpty(effectTag))
223
+ {
224
+ return false;
225
+ }
226
+
227
+ for (int i = 0; i < effectTags.Count; ++i)
228
+ {
229
+ if (string.Equals(effectTags[i], effectTag, StringComparison.Ordinal))
230
+ {
231
+ return true;
232
+ }
233
+ }
234
+
235
+ return false;
236
+ }
237
+
238
+ /// <summary>
239
+ /// Determines whether this effect applies any of the specified tags.
240
+ /// </summary>
241
+ /// <param name="effectTagsToCheck">The tags to inspect.</param>
242
+ /// <returns><c>true</c> if at least one tag is applied; otherwise, <c>false</c>.</returns>
243
+ public bool HasAnyTag(IEnumerable<string> effectTagsToCheck)
244
+ {
245
+ if (effectTags == null || effectTagsToCheck == null)
246
+ {
247
+ return false;
248
+ }
249
+
250
+ switch (effectTagsToCheck)
251
+ {
252
+ case IReadOnlyList<string> list:
253
+ {
254
+ return HasAnyTag(list);
255
+ }
256
+ case HashSet<string> hashSet:
257
+ {
258
+ foreach (string candidate in hashSet)
259
+ {
260
+ if (string.IsNullOrEmpty(candidate))
261
+ {
262
+ continue;
263
+ }
264
+
265
+ if (HasTag(candidate))
266
+ {
267
+ return true;
268
+ }
269
+ }
270
+
271
+ return false;
272
+ }
273
+ }
274
+
275
+ foreach (string candidate in effectTagsToCheck)
276
+ {
277
+ if (string.IsNullOrEmpty(candidate))
278
+ {
279
+ continue;
280
+ }
281
+
282
+ if (HasTag(candidate))
283
+ {
284
+ return true;
285
+ }
286
+ }
287
+
288
+ return false;
289
+ }
290
+
291
+ /// <summary>
292
+ /// Determines whether this effect applies any of the specified tags.
293
+ /// Optimized for indexed collections.
294
+ /// </summary>
295
+ /// <param name="effectTagsToCheck">The tags to inspect.</param>
296
+ /// <returns><c>true</c> if at least one tag is applied; otherwise, <c>false</c>.</returns>
297
+ public bool HasAnyTag(IReadOnlyList<string> effectTagsToCheck)
298
+ {
299
+ if (effectTags == null || effectTagsToCheck == null)
300
+ {
301
+ return false;
302
+ }
303
+
304
+ for (int i = 0; i < effectTagsToCheck.Count; ++i)
305
+ {
306
+ string candidate = effectTagsToCheck[i];
307
+ if (string.IsNullOrEmpty(candidate))
308
+ {
309
+ continue;
310
+ }
311
+
312
+ if (HasTag(candidate))
313
+ {
314
+ return true;
315
+ }
316
+ }
317
+
318
+ return false;
319
+ }
320
+
321
+ /// <summary>
322
+ /// Determines whether this effect contains modifications for the specified attribute.
323
+ /// </summary>
324
+ /// <param name="attributeName">The attribute name to inspect.</param>
325
+ /// <returns><c>true</c> if the effect modifies <paramref name="attributeName"/>; otherwise, <c>false</c>.</returns>
326
+ public bool ModifiesAttribute(string attributeName)
327
+ {
328
+ if (modifications == null || string.IsNullOrEmpty(attributeName))
329
+ {
330
+ return false;
331
+ }
332
+
333
+ for (int i = 0; i < modifications.Count; ++i)
334
+ {
335
+ AttributeModification modification = modifications[i];
336
+ if (string.Equals(modification.attribute, attributeName, StringComparison.Ordinal))
337
+ {
338
+ return true;
339
+ }
340
+ }
341
+
342
+ return false;
343
+ }
344
+
345
+ /// <summary>
346
+ /// Copies all modifications that affect the specified attribute into the provided buffer.
347
+ /// </summary>
348
+ /// <param name="attributeName">The attribute to filter by.</param>
349
+ /// <param name="buffer">The destination buffer. Existing entries are preserved.</param>
350
+ /// <returns>The number of modifications added to <paramref name="buffer"/>.</returns>
351
+ public List<AttributeModification> GetModifications(
352
+ string attributeName,
353
+ List<AttributeModification> buffer = null
354
+ )
355
+ {
356
+ buffer ??= new List<AttributeModification>();
357
+ buffer.Clear();
358
+ if (modifications == null || string.IsNullOrEmpty(attributeName))
359
+ {
360
+ return buffer;
361
+ }
362
+
363
+ for (int i = 0; i < modifications.Count; ++i)
364
+ {
365
+ AttributeModification modification = modifications[i];
366
+ if (string.Equals(modification.attribute, attributeName, StringComparison.Ordinal))
367
+ {
368
+ buffer.Add(modification);
369
+ }
370
+ }
371
+
372
+ return buffer;
373
+ }
123
374
 
124
375
  /// <summary>
125
376
  /// Converts this effect to a JSON string representation including all modifications, tags, and cosmetic effects.
@@ -138,6 +389,10 @@ namespace WallstopStudios.UnityHelpers.Tags
138
389
  }.ToJson();
139
390
  }
140
391
 
392
+ private List<string> CosmeticEffectsForJson =>
393
+ cosmeticEffects?.Select(cosmeticEffectData => cosmeticEffectData.name).ToList()
394
+ ?? new List<string>(0);
395
+
141
396
  private string BuildDescription()
142
397
  {
143
398
  if (modifications == null)
@@ -145,7 +400,9 @@ namespace WallstopStudios.UnityHelpers.Tags
145
400
  return nameof(AttributeEffect);
146
401
  }
147
402
 
148
- StringBuilder descriptionBuilder = new();
403
+ using PooledResource<StringBuilder> stringBuilderBuffer = Buffers.StringBuilder.Get(
404
+ out StringBuilder descriptionBuilder
405
+ );
149
406
  for (int i = 0; i < modifications.Count; ++i)
150
407
  {
151
408
  AttributeModification modification = modifications[i];
@@ -218,6 +475,16 @@ namespace WallstopStudios.UnityHelpers.Tags
218
475
  return descriptionBuilder.ToString();
219
476
  }
220
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
+
221
488
  /// <summary>
222
489
  /// Determines whether this effect is equal to another effect by comparing all fields.
223
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>