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
@@ -67,6 +67,9 @@ namespace WallstopStudios.UnityHelpers.Tags
67
67
  List<CosmeticEffectData>
68
68
  > _instancedCosmeticEffects = new();
69
69
 
70
+ private readonly Dictionary<EffectStackKey, List<EffectHandle>> _handlesByStackKey = new();
71
+ private readonly Dictionary<long, EffectStackKey> _stackKeyByHandleId = new();
72
+
70
73
  // Stores expiration time of duration effects (We store by Id because it's much cheaper to iterate Guids than it is EffectHandles
71
74
  private readonly Dictionary<long, float> _effectExpirations = new();
72
75
  private readonly Dictionary<long, EffectHandle> _effectHandlesById = new();
@@ -74,6 +77,9 @@ namespace WallstopStudios.UnityHelpers.Tags
74
77
  // Used only to save allocations in Update()
75
78
  private readonly List<long> _expiredEffectIds = new();
76
79
  private readonly List<EffectHandle> _appliedEffects = new();
80
+ private readonly Dictionary<long, List<PeriodicEffectRuntimeState>> _periodicEffectStates =
81
+ new();
82
+ private readonly Dictionary<long, List<EffectBehavior>> _behaviorsByHandleId = new();
77
83
 
78
84
  private bool _initialized;
79
85
 
@@ -118,51 +124,109 @@ namespace WallstopStudios.UnityHelpers.Tags
118
124
  /// </remarks>
119
125
  public EffectHandle? ApplyEffect(AttributeEffect effect)
120
126
  {
121
- EffectHandle? maybeHandle = null;
127
+ if (effect == null)
128
+ {
129
+ return null;
130
+ }
122
131
 
123
- if (effect.durationType != ModifierDurationType.Instant)
132
+ if (effect.durationType == ModifierDurationType.Instant)
124
133
  {
125
- if (effect.durationType == ModifierDurationType.Duration)
134
+ if (RequiresHandle(effect))
126
135
  {
127
- foreach (EffectHandle appliedEffect in _appliedEffects)
136
+ this.LogWarn(
137
+ $"Effect {effect:json} defines periodic or behaviour data but is Instant. These features require a Duration or Infinite effect."
138
+ );
139
+ }
140
+
141
+ InternalApplyEffect(effect);
142
+ return null;
143
+ }
144
+
145
+ EffectStackKey stackKey = effect.GetStackKey();
146
+ List<EffectHandle> existingHandles = TryGetStackHandles(stackKey);
147
+
148
+ switch (effect.stackingMode)
149
+ {
150
+ case EffectStackingMode.Ignore:
151
+ {
152
+ if (existingHandles is { Count: > 0 })
128
153
  {
129
- if (appliedEffect.effect == null)
154
+ return existingHandles[0];
155
+ }
156
+
157
+ break;
158
+ }
159
+ case EffectStackingMode.Refresh:
160
+ {
161
+ if (existingHandles is { Count: > 0 })
162
+ {
163
+ EffectHandle handle = existingHandles[0];
164
+ InternalApplyEffect(handle);
165
+ return handle;
166
+ }
167
+
168
+ break;
169
+ }
170
+ case EffectStackingMode.Replace:
171
+ {
172
+ if (existingHandles is { Count: > 0 })
173
+ {
174
+ using PooledResource<List<EffectHandle>> handleBufferResource =
175
+ Buffers<EffectHandle>.List.Get(out List<EffectHandle> handleBuffer);
176
+ handleBuffer.AddRange(existingHandles);
177
+ for (int i = 0; i < handleBuffer.Count; ++i)
130
178
  {
131
- continue;
179
+ RemoveEffect(handleBuffer[i]);
132
180
  }
181
+ }
133
182
 
134
- string serializableName = appliedEffect.effect.name;
135
- if (string.Equals(effect.name, serializableName, StringComparison.Ordinal))
183
+ break;
184
+ }
185
+ case EffectStackingMode.Stack:
186
+ {
187
+ if (existingHandles is { Count: > 0 } && effect.maximumStacks > 0)
188
+ {
189
+ while (existingHandles.Count >= effect.maximumStacks)
136
190
  {
137
- maybeHandle = appliedEffect;
138
- break;
191
+ EffectHandle oldestHandle = existingHandles[0];
192
+ RemoveEffect(oldestHandle);
139
193
  }
140
194
  }
141
- }
142
195
 
143
- maybeHandle ??= EffectHandle.CreateInstance(effect);
144
- }
145
-
146
- if (maybeHandle.HasValue)
147
- {
148
- EffectHandle handle = maybeHandle.Value;
149
- InternalApplyEffect(handle);
150
- if (
151
- effect.durationType == ModifierDurationType.Duration
152
- && (effect.resetDurationOnReapplication || !_appliedEffects.Contains(handle))
153
- )
154
- {
155
- long handleId = handle.id;
156
- _effectExpirations[handleId] = Time.time + effect.duration;
157
- _effectHandlesById[handleId] = handle;
196
+ break;
158
197
  }
159
198
  }
160
- else
199
+
200
+ EffectHandle newHandle = EffectHandle.CreateInstance(effect);
201
+ RegisterStackHandle(stackKey, newHandle);
202
+ InternalApplyEffect(newHandle);
203
+ return newHandle;
204
+ }
205
+
206
+ private static bool RequiresHandle(AttributeEffect effect)
207
+ {
208
+ return (effect.periodicEffects is { Count: > 0 })
209
+ || (effect.behaviors is { Count: > 0 });
210
+ }
211
+
212
+ private List<EffectHandle> TryGetStackHandles(EffectStackKey stackKey)
213
+ {
214
+ _ = _handlesByStackKey.TryGetValue(stackKey, out List<EffectHandle> handles);
215
+ return handles;
216
+ }
217
+
218
+ private void RegisterStackHandle(EffectStackKey stackKey, EffectHandle handle)
219
+ {
220
+ long handleId = handle.id;
221
+ _stackKeyByHandleId[handleId] = stackKey;
222
+
223
+ if (!_handlesByStackKey.TryGetValue(stackKey, out List<EffectHandle> handles))
161
224
  {
162
- InternalApplyEffect(effect);
225
+ handles = new List<EffectHandle>();
226
+ _handlesByStackKey.Add(stackKey, handles);
163
227
  }
164
228
 
165
- return maybeHandle;
229
+ handles.Add(handle);
166
230
  }
167
231
 
168
232
  /// <summary>
@@ -173,17 +237,57 @@ namespace WallstopStudios.UnityHelpers.Tags
173
237
  {
174
238
  InternalRemoveEffect(handle);
175
239
  _ = _appliedEffects.Remove(handle);
240
+ DeregisterHandle(handle);
176
241
  }
177
242
 
178
243
  public void RemoveAllEffects()
179
244
  {
180
- foreach (EffectHandle handle in _appliedEffects.ToArray())
245
+ using PooledResource<List<EffectHandle>> handleBufferResource =
246
+ Buffers<EffectHandle>.List.Get(out List<EffectHandle> handleBuffer);
247
+ handleBuffer.AddRange(_appliedEffects);
248
+ foreach (EffectHandle handle in handleBuffer)
181
249
  {
182
- InternalRemoveEffect(handle);
250
+ RemoveEffect(handle);
183
251
  }
184
252
  _appliedEffects.Clear();
185
253
  }
186
254
 
255
+ private void DeregisterHandle(EffectHandle handle)
256
+ {
257
+ long handleId = handle.id;
258
+ if (_stackKeyByHandleId.TryGetValue(handleId, out EffectStackKey stackKey))
259
+ {
260
+ if (_handlesByStackKey.TryGetValue(stackKey, out List<EffectHandle> handles))
261
+ {
262
+ handles.Remove(handle);
263
+ if (handles.Count == 0)
264
+ {
265
+ _handlesByStackKey.Remove(stackKey);
266
+ }
267
+ }
268
+
269
+ _stackKeyByHandleId.Remove(handleId);
270
+ }
271
+
272
+ _periodicEffectStates.Remove(handleId);
273
+
274
+ if (_behaviorsByHandleId.Remove(handleId, out List<EffectBehavior> behaviorInstances))
275
+ {
276
+ EffectBehaviorContext context = new(this, handle, 0f);
277
+ for (int i = 0; i < behaviorInstances.Count; ++i)
278
+ {
279
+ EffectBehavior behavior = behaviorInstances[i];
280
+ if (behavior == null)
281
+ {
282
+ continue;
283
+ }
284
+
285
+ behavior.OnRemove(context);
286
+ Destroy(behavior);
287
+ }
288
+ }
289
+ }
290
+
187
291
  /// <summary>
188
292
  /// Determines whether the specified effect is currently active on this handler.
189
293
  /// </summary>
@@ -396,17 +500,24 @@ namespace WallstopStudios.UnityHelpers.Tags
396
500
  _appliedEffects.Add(handle);
397
501
  }
398
502
 
503
+ long handleId = handle.id;
504
+ _effectHandlesById[handleId] = handle;
505
+
399
506
  AttributeEffect effect = handle.effect;
400
507
  if (effect.durationType == ModifierDurationType.Duration)
401
508
  {
402
- if (effect.resetDurationOnReapplication || !exists)
509
+ if (!exists || effect.resetDurationOnReapplication)
403
510
  {
404
- long handleId = handle.id;
405
511
  _effectExpirations[handleId] = Time.time + effect.duration;
406
- _effectHandlesById[handleId] = handle;
407
512
  }
408
513
  }
409
514
 
515
+ if (!exists)
516
+ {
517
+ RegisterPeriodicRuntime(handle);
518
+ RegisterBehaviors(handle);
519
+ }
520
+
410
521
  if (!_initialized && _tagHandler == null)
411
522
  {
412
523
  this.AssignRelationalComponents();
@@ -435,6 +546,13 @@ namespace WallstopStudios.UnityHelpers.Tags
435
546
 
436
547
  private void InternalApplyEffect(AttributeEffect effect)
437
548
  {
549
+ if (effect.durationType == ModifierDurationType.Instant && RequiresHandle(effect))
550
+ {
551
+ this.LogWarn(
552
+ $"Effect {effect:json} defines periodic or behaviour data but is Instant. These features require a Duration or Infinite effect."
553
+ );
554
+ }
555
+
438
556
  if (!_initialized && _tagHandler == null)
439
557
  {
440
558
  this.AssignRelationalComponents();
@@ -459,6 +577,116 @@ namespace WallstopStudios.UnityHelpers.Tags
459
577
  }
460
578
  }
461
579
 
580
+ private void RegisterPeriodicRuntime(EffectHandle handle)
581
+ {
582
+ AttributeEffect effect = handle.effect;
583
+ if (effect.periodicEffects is not { Count: > 0 })
584
+ {
585
+ return;
586
+ }
587
+
588
+ List<PeriodicEffectRuntimeState> runtimeStates = null;
589
+ float startTime = Time.time;
590
+
591
+ foreach (PeriodicEffectDefinition definition in effect.periodicEffects)
592
+ {
593
+ if (definition == null)
594
+ {
595
+ continue;
596
+ }
597
+
598
+ (runtimeStates ??= new List<PeriodicEffectRuntimeState>()).Add(
599
+ new PeriodicEffectRuntimeState(definition, startTime)
600
+ );
601
+ }
602
+
603
+ if (runtimeStates is { Count: > 0 })
604
+ {
605
+ _periodicEffectStates[handle.id] = runtimeStates;
606
+ }
607
+ }
608
+
609
+ private void RegisterBehaviors(EffectHandle handle)
610
+ {
611
+ AttributeEffect effect = handle.effect;
612
+ if (effect.behaviors is not { Count: > 0 })
613
+ {
614
+ return;
615
+ }
616
+
617
+ List<EffectBehavior> instances = null;
618
+ foreach (EffectBehavior behavior in effect.behaviors)
619
+ {
620
+ if (behavior == null)
621
+ {
622
+ continue;
623
+ }
624
+
625
+ EffectBehavior clone = Instantiate(behavior);
626
+ (instances ??= new List<EffectBehavior>()).Add(clone);
627
+ }
628
+
629
+ if (instances is not { Count: > 0 })
630
+ {
631
+ return;
632
+ }
633
+
634
+ EffectBehaviorContext context = new(this, handle, 0f);
635
+ for (int i = 0; i < instances.Count; ++i)
636
+ {
637
+ EffectBehavior instance = instances[i];
638
+ if (instance == null)
639
+ {
640
+ continue;
641
+ }
642
+
643
+ instance.OnApply(context);
644
+ }
645
+
646
+ _behaviorsByHandleId[handle.id] = instances;
647
+ }
648
+
649
+ private void ApplyPeriodicTick(
650
+ EffectHandle handle,
651
+ PeriodicEffectRuntimeState runtimeState,
652
+ float currentTime,
653
+ float deltaTime
654
+ )
655
+ {
656
+ PeriodicEffectDefinition definition = runtimeState.definition;
657
+ if (_attributes is { Count: > 0 } && definition.modifications is { Count: > 0 })
658
+ {
659
+ foreach (AttributesComponent attributesComponent in _attributes)
660
+ {
661
+ attributesComponent.ApplyAttributeModifications(definition.modifications, null);
662
+ }
663
+ }
664
+
665
+ if (
666
+ _behaviorsByHandleId.TryGetValue(handle.id, out List<EffectBehavior> behaviors)
667
+ && behaviors.Count > 0
668
+ )
669
+ {
670
+ EffectBehaviorContext context = new(this, handle, deltaTime);
671
+ PeriodicEffectTickContext tickContext = new(
672
+ definition,
673
+ runtimeState.ExecutedTicks,
674
+ currentTime
675
+ );
676
+
677
+ for (int i = 0; i < behaviors.Count; ++i)
678
+ {
679
+ EffectBehavior behavior = behaviors[i];
680
+ if (behavior == null)
681
+ {
682
+ continue;
683
+ }
684
+
685
+ behavior.OnPeriodicTick(context, tickContext);
686
+ }
687
+ }
688
+ }
689
+
462
690
  private void InternalApplyCosmeticEffects(EffectHandle handle)
463
691
  {
464
692
  if (_instancedCosmeticEffects.ContainsKey(handle))
@@ -511,8 +739,7 @@ namespace WallstopStudios.UnityHelpers.Tags
511
739
  {
512
740
  foreach (CosmeticEffectData cosmeticEffectData in attributeEffect.cosmeticEffects)
513
741
  {
514
- CosmeticEffectData cosmeticEffect = cosmeticEffectData;
515
- if (cosmeticEffect == null)
742
+ if (cosmeticEffectData == null)
516
743
  {
517
744
  this.LogError(
518
745
  $"CosmeticEffectData is null for effect {attributeEffect:json}, cannot determine instancing scheme."
@@ -532,7 +759,7 @@ namespace WallstopStudios.UnityHelpers.Tags
532
759
  Buffers<CosmeticEffectComponent>.List.Get();
533
760
  List<CosmeticEffectComponent> cosmeticEffectsBuffer =
534
761
  cosmeticEffectsResource.resource;
535
- cosmeticEffect.GetComponents(cosmeticEffectsBuffer);
762
+ cosmeticEffectData.GetComponents(cosmeticEffectsBuffer);
536
763
  foreach (CosmeticEffectComponent cosmeticComponent in cosmeticEffectsBuffer)
537
764
  {
538
765
  cosmeticComponent.OnApplyEffect(gameObject);
@@ -621,6 +848,13 @@ namespace WallstopStudios.UnityHelpers.Tags
621
848
  }
622
849
 
623
850
  private void Update()
851
+ {
852
+ ProcessEffectExpirations();
853
+ ProcessBehaviorTicks();
854
+ ProcessPeriodicEffects();
855
+ }
856
+
857
+ private void ProcessEffectExpirations()
624
858
  {
625
859
  if (_effectExpirations.Count <= 0)
626
860
  {
@@ -631,19 +865,131 @@ namespace WallstopStudios.UnityHelpers.Tags
631
865
  float currentTime = Time.time;
632
866
  foreach (KeyValuePair<long, float> entry in _effectExpirations)
633
867
  {
634
- if (entry.Value < currentTime)
868
+ if (entry.Value <= currentTime)
635
869
  {
636
870
  _expiredEffectIds.Add(entry.Key);
637
871
  }
638
872
  }
639
873
 
640
- foreach (long expiredHandleId in _expiredEffectIds)
874
+ for (int i = 0; i < _expiredEffectIds.Count; ++i)
641
875
  {
876
+ long expiredHandleId = _expiredEffectIds[i];
642
877
  if (_effectHandlesById.TryGetValue(expiredHandleId, out EffectHandle expiredHandle))
643
878
  {
644
879
  RemoveEffect(expiredHandle);
645
880
  }
646
881
  }
882
+
883
+ _expiredEffectIds.Clear();
884
+ }
885
+
886
+ private void ProcessBehaviorTicks()
887
+ {
888
+ if (_behaviorsByHandleId.Count <= 0)
889
+ {
890
+ return;
891
+ }
892
+
893
+ using PooledResource<List<long>> behaviorHandleIdsResource = Buffers<long>.List.Get(
894
+ out List<long> behaviorHandleIdsBuffer
895
+ );
896
+ behaviorHandleIdsBuffer.AddRange(_behaviorsByHandleId.Keys);
897
+ float deltaTime = Time.deltaTime;
898
+
899
+ for (int i = 0; i < behaviorHandleIdsBuffer.Count; ++i)
900
+ {
901
+ long handleId = behaviorHandleIdsBuffer[i];
902
+ if (!_effectHandlesById.TryGetValue(handleId, out EffectHandle handle))
903
+ {
904
+ continue;
905
+ }
906
+
907
+ if (!_behaviorsByHandleId.TryGetValue(handleId, out List<EffectBehavior> behaviors))
908
+ {
909
+ continue;
910
+ }
911
+
912
+ EffectBehaviorContext context = new(this, handle, deltaTime);
913
+ for (int j = 0; j < behaviors.Count; ++j)
914
+ {
915
+ EffectBehavior behavior = behaviors[j];
916
+ if (behavior == null)
917
+ {
918
+ continue;
919
+ }
920
+
921
+ behavior.OnTick(context);
922
+ }
923
+ }
924
+ }
925
+
926
+ private void ProcessPeriodicEffects()
927
+ {
928
+ if (_periodicEffectStates.Count <= 0)
929
+ {
930
+ return;
931
+ }
932
+
933
+ float currentTime = Time.time;
934
+ float deltaTime = Time.deltaTime;
935
+ using PooledResource<List<long>> periodicRemovalResource = Buffers<long>.List.Get(
936
+ out List<long> periodicRemovalBuffer
937
+ );
938
+ using PooledResource<List<long>> periodHandleIdsResource = Buffers<long>.List.Get(
939
+ out List<long> periodicHandleIdsBuffer
940
+ );
941
+ periodicHandleIdsBuffer.AddRange(_periodicEffectStates.Keys);
942
+
943
+ for (int handleIndex = 0; handleIndex < periodicHandleIdsBuffer.Count; ++handleIndex)
944
+ {
945
+ long handleId = periodicHandleIdsBuffer[handleIndex];
946
+ if (!_effectHandlesById.TryGetValue(handleId, out EffectHandle handle))
947
+ {
948
+ periodicRemovalBuffer.Add(handleId);
949
+ continue;
950
+ }
951
+
952
+ if (
953
+ !_periodicEffectStates.TryGetValue(
954
+ handleId,
955
+ out List<PeriodicEffectRuntimeState> runtimes
956
+ )
957
+ )
958
+ {
959
+ continue;
960
+ }
961
+
962
+ bool hasActive = false;
963
+
964
+ for (int i = 0; i < runtimes.Count; ++i)
965
+ {
966
+ PeriodicEffectRuntimeState runtimeState = runtimes[i];
967
+ if (runtimeState == null)
968
+ {
969
+ continue;
970
+ }
971
+
972
+ while (runtimeState.TryConsumeTick(currentTime))
973
+ {
974
+ ApplyPeriodicTick(handle, runtimeState, currentTime, deltaTime);
975
+ }
976
+
977
+ if (!runtimeState.IsComplete)
978
+ {
979
+ hasActive = true;
980
+ }
981
+ }
982
+
983
+ if (!hasActive)
984
+ {
985
+ periodicRemovalBuffer.Add(handleId);
986
+ }
987
+ }
988
+
989
+ for (int i = 0; i < periodicRemovalBuffer.Count; ++i)
990
+ {
991
+ _periodicEffectStates.Remove(periodicRemovalBuffer[i]);
992
+ }
647
993
  }
648
994
  }
649
995
  }
@@ -0,0 +1,79 @@
1
+ namespace WallstopStudios.UnityHelpers.Tags
2
+ {
3
+ using System;
4
+ using WallstopStudios.UnityHelpers.Core.Helper;
5
+
6
+ /// <summary>
7
+ /// Key used to group effect handles for stacking decisions.
8
+ /// </summary>
9
+ internal readonly struct EffectStackKey : IEquatable<EffectStackKey>
10
+ {
11
+ private readonly EffectStackGroup _group;
12
+ private readonly AttributeEffect _effect;
13
+ private readonly string _customKey;
14
+
15
+ private EffectStackKey(EffectStackGroup group, AttributeEffect effect, string customKey)
16
+ {
17
+ _group = group;
18
+ _effect = effect;
19
+ _customKey = customKey;
20
+ }
21
+
22
+ public static EffectStackKey CreateReference(AttributeEffect effect)
23
+ {
24
+ return new EffectStackKey(EffectStackGroup.Reference, effect, null);
25
+ }
26
+
27
+ public static EffectStackKey CreateCustom(string customKey)
28
+ {
29
+ return new EffectStackKey(EffectStackGroup.CustomKey, null, customKey);
30
+ }
31
+
32
+ public bool Equals(EffectStackKey other)
33
+ {
34
+ if (_group != other._group)
35
+ {
36
+ return false;
37
+ }
38
+
39
+ return _group switch
40
+ {
41
+ EffectStackGroup.Reference => ReferenceEquals(_effect, other._effect),
42
+ EffectStackGroup.CustomKey => string.Equals(
43
+ _customKey,
44
+ other._customKey,
45
+ StringComparison.Ordinal
46
+ ),
47
+ _ => false,
48
+ };
49
+ }
50
+
51
+ public override bool Equals(object obj)
52
+ {
53
+ return obj is EffectStackKey other && Equals(other);
54
+ }
55
+
56
+ public override int GetHashCode()
57
+ {
58
+ return _group switch
59
+ {
60
+ EffectStackGroup.Reference => Objects.HashCode(_group, _effect),
61
+ EffectStackGroup.CustomKey => Objects.HashCode(
62
+ _group,
63
+ _customKey != null ? StringComparer.Ordinal.GetHashCode(_customKey) : 0
64
+ ),
65
+ _ => Objects.HashCode(_group),
66
+ };
67
+ }
68
+
69
+ public static bool operator ==(EffectStackKey left, EffectStackKey right)
70
+ {
71
+ return left.Equals(right);
72
+ }
73
+
74
+ public static bool operator !=(EffectStackKey left, EffectStackKey right)
75
+ {
76
+ return !(left == right);
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,4 @@
1
+ fileFormatVersion: 2
2
+ guid: 6e83f3c3d7ef4ed28d8f9777084ffcf2
3
+ timeCreated: 1759598400
4
+