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
@@ -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
+
@@ -0,0 +1,102 @@
1
+ namespace WallstopStudios.UnityHelpers.Tags
2
+ {
3
+ using System;
4
+ using System.Collections.Generic;
5
+ using ProtoBuf;
6
+ using UnityEngine;
7
+
8
+ /// <summary>
9
+ /// Authoring data for a periodic modifier bundle that executes on a cadence while an effect is active.
10
+ /// </summary>
11
+ /// <remarks>
12
+ /// <para>
13
+ /// The owning <see cref="EffectHandler"/> evaluates each periodic definition after <see cref="initialDelay"/>, applies the configured
14
+ /// <see cref="modifications"/>, and repeats every <see cref="interval"/> seconds until <see cref="maxTicks"/> is reached or the effect ends.
15
+ /// </para>
16
+ /// <para>
17
+ /// Definitions are processed in list order and maintain independent runtime state per <see cref="EffectHandle"/>, enabling designers to mix
18
+ /// damage-over-time, heal-over-time, or custom triggers alongside bespoke <see cref="EffectBehavior"/> implementations.
19
+ /// </para>
20
+ /// </remarks>
21
+ /// <example>
22
+ /// <code language="csharp">
23
+ /// using System.Collections.Generic;
24
+ /// using UnityEngine;
25
+ /// using WallstopStudios.UnityHelpers.Tags;
26
+ ///
27
+ /// public sealed class BurnEffectAuthoring : MonoBehaviour
28
+ /// {
29
+ /// [SerializeField]
30
+ /// private AttributeEffect burnEffect;
31
+ ///
32
+ /// [SerializeField]
33
+ /// private EffectHandler effectHandler;
34
+ ///
35
+ /// public void ApplyBurn(GameObject target)
36
+ /// {
37
+ /// PeriodicEffectDefinition burnTick = new PeriodicEffectDefinition
38
+ /// {
39
+ /// name = "Burn Damage",
40
+ /// initialDelay = 0.5f,
41
+ /// interval = 1.0f,
42
+ /// maxTicks = 5,
43
+ /// modifications = new List<AttributeModification>
44
+ /// {
45
+ /// new AttributeModification("Health", ModificationAction.Addition, -5f),
46
+ /// },
47
+ /// };
48
+ ///
49
+ /// burnEffect.periodicEffects.Add(burnTick);
50
+ ///
51
+ /// if (effectHandler == null)
52
+ /// {
53
+ /// effectHandler = target.GetComponent<EffectHandler>();
54
+ /// }
55
+ ///
56
+ /// EffectHandle? handle = effectHandler.ApplyEffect(burnEffect);
57
+ /// }
58
+ /// }
59
+ /// </code>
60
+ /// <para>
61
+ /// In the example above the handler waits for <c>initialDelay</c>, applies the <see cref="modifications"/> every <c>interval</c> seconds,
62
+ /// and stops after <see cref="maxTicks"/> executions or as soon as the effect is removed.
63
+ /// </para>
64
+ /// </example>
65
+ [Serializable]
66
+ [ProtoContract]
67
+ public sealed class PeriodicEffectDefinition
68
+ {
69
+ /// <summary>
70
+ /// Optional label shown in tooling to help distinguish multiple periodic definitions.
71
+ /// </summary>
72
+ [ProtoMember(1)]
73
+ public string name;
74
+
75
+ /// <summary>
76
+ /// Time (seconds) before the first tick fires after the effect is applied.
77
+ /// </summary>
78
+ [Min(0f)]
79
+ [ProtoMember(2)]
80
+ public float initialDelay;
81
+
82
+ /// <summary>
83
+ /// Interval (seconds) between ticks once the first tick has executed.
84
+ /// </summary>
85
+ [Min(0.01f)]
86
+ [ProtoMember(3)]
87
+ public float interval = 1f;
88
+
89
+ /// <summary>
90
+ /// Maximum number of ticks to execute. Zero or negative means unlimited ticks.
91
+ /// </summary>
92
+ [Min(0)]
93
+ [ProtoMember(4)]
94
+ public int maxTicks;
95
+
96
+ /// <summary>
97
+ /// Attribute modifications applied each time the tick fires.
98
+ /// </summary>
99
+ [ProtoMember(5)]
100
+ public List<AttributeModification> modifications = new();
101
+ }
102
+ }
@@ -0,0 +1,4 @@
1
+ fileFormatVersion: 2
2
+ guid: 8d763ed3f32f4fbf9bcaca8f24ef1a31
3
+ timeCreated: 1759598400
4
+
@@ -0,0 +1,40 @@
1
+ namespace WallstopStudios.UnityHelpers.Tags
2
+ {
3
+ using UnityEngine;
4
+
5
+ /// <summary>
6
+ /// Runtime tracking for a periodic effect definition.
7
+ /// </summary>
8
+ internal sealed class PeriodicEffectRuntimeState
9
+ {
10
+ internal bool IsComplete => definition.maxTicks > 0 && ExecutedTicks >= definition.maxTicks;
11
+
12
+ internal float NextTickTime { get; private set; }
13
+
14
+ internal int ExecutedTicks { get; private set; }
15
+
16
+ internal readonly PeriodicEffectDefinition definition;
17
+ internal readonly float interval;
18
+
19
+ internal PeriodicEffectRuntimeState(PeriodicEffectDefinition definition, float startTime)
20
+ {
21
+ this.definition = definition;
22
+ ExecutedTicks = 0;
23
+ float clampedInterval = Mathf.Max(0.01f, definition.interval);
24
+ interval = clampedInterval;
25
+ NextTickTime = startTime + Mathf.Max(0f, definition.initialDelay);
26
+ }
27
+
28
+ internal bool TryConsumeTick(float currentTime)
29
+ {
30
+ if (currentTime < NextTickTime || IsComplete)
31
+ {
32
+ return false;
33
+ }
34
+
35
+ ++ExecutedTicks;
36
+ NextTickTime = currentTime + interval;
37
+ return true;
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,4 @@
1
+ fileFormatVersion: 2
2
+ guid: 5af38ac9186645fabd9858dfead3c001
3
+ timeCreated: 1759598400
4
+
@@ -250,6 +250,304 @@ namespace WallstopStudios.UnityHelpers.Tags
250
250
  return false;
251
251
  }
252
252
 
253
+ /// <summary>
254
+ /// Checks whether all of the specified tags are currently active.
255
+ /// Optimized for different collection types with specialized implementations.
256
+ /// </summary>
257
+ /// <param name="effectTags">The collection of tags to check.</param>
258
+ /// <returns><c>true</c> if all tags are active; otherwise, <c>false</c>. Returns <c>false</c> when <paramref name="effectTags"/> is <c>null</c>.</returns>
259
+ public bool HasAllTags(IEnumerable<string> effectTags)
260
+ {
261
+ if (effectTags == null)
262
+ {
263
+ return false;
264
+ }
265
+
266
+ switch (effectTags)
267
+ {
268
+ case IReadOnlyList<string> list:
269
+ {
270
+ return HasAllTags(list);
271
+ }
272
+ case HashSet<string> hashSet:
273
+ {
274
+ foreach (string effectTag in hashSet)
275
+ {
276
+ if (string.IsNullOrEmpty(effectTag))
277
+ {
278
+ continue;
279
+ }
280
+ if (!_tagCount.ContainsKey(effectTag))
281
+ {
282
+ return false;
283
+ }
284
+ }
285
+
286
+ return true;
287
+ }
288
+ case SortedSet<string> sortedSet:
289
+ {
290
+ foreach (string effectTag in sortedSet)
291
+ {
292
+ if (string.IsNullOrEmpty(effectTag))
293
+ {
294
+ continue;
295
+ }
296
+ if (!_tagCount.ContainsKey(effectTag))
297
+ {
298
+ return false;
299
+ }
300
+ }
301
+
302
+ return true;
303
+ }
304
+ case Queue<string> queue:
305
+ {
306
+ foreach (string effectTag in queue)
307
+ {
308
+ if (string.IsNullOrEmpty(effectTag))
309
+ {
310
+ continue;
311
+ }
312
+ if (!_tagCount.ContainsKey(effectTag))
313
+ {
314
+ return false;
315
+ }
316
+ }
317
+
318
+ return true;
319
+ }
320
+ case Stack<string> stack:
321
+ {
322
+ foreach (string effectTag in stack)
323
+ {
324
+ if (string.IsNullOrEmpty(effectTag))
325
+ {
326
+ continue;
327
+ }
328
+ if (!_tagCount.ContainsKey(effectTag))
329
+ {
330
+ return false;
331
+ }
332
+ }
333
+
334
+ return true;
335
+ }
336
+ case LinkedList<string> linkedList:
337
+ {
338
+ foreach (string effectTag in linkedList)
339
+ {
340
+ if (string.IsNullOrEmpty(effectTag))
341
+ {
342
+ continue;
343
+ }
344
+ if (!_tagCount.ContainsKey(effectTag))
345
+ {
346
+ return false;
347
+ }
348
+ }
349
+
350
+ return true;
351
+ }
352
+ }
353
+
354
+ foreach (string effectTag in effectTags)
355
+ {
356
+ if (string.IsNullOrEmpty(effectTag))
357
+ {
358
+ continue;
359
+ }
360
+ if (!_tagCount.ContainsKey(effectTag))
361
+ {
362
+ return false;
363
+ }
364
+ }
365
+
366
+ return true;
367
+ }
368
+
369
+ /// <summary>
370
+ /// Checks whether all of the specified tags are active.
371
+ /// Optimized for IReadOnlyList with index-based iteration.
372
+ /// </summary>
373
+ /// <param name="effectTags">The list of tags to check.</param>
374
+ /// <returns><c>true</c> if all of the tags are active, or if the list is empty; otherwise, <c>false</c>.</returns>
375
+ public bool HasAllTags(IReadOnlyList<string> effectTags)
376
+ {
377
+ if (effectTags == null)
378
+ {
379
+ return false;
380
+ }
381
+
382
+ for (int i = 0; i < effectTags.Count; ++i)
383
+ {
384
+ string effectTag = effectTags[i];
385
+ if (string.IsNullOrEmpty(effectTag))
386
+ {
387
+ continue;
388
+ }
389
+
390
+ if (!_tagCount.ContainsKey(effectTag))
391
+ {
392
+ return false;
393
+ }
394
+ }
395
+
396
+ return true;
397
+ }
398
+
399
+ /// <summary>
400
+ /// Determines whether none of the specified tags are active.
401
+ /// </summary>
402
+ /// <param name="effectTags">The collection of tags to inspect.</param>
403
+ /// <returns>
404
+ /// <c>true</c> when the collection is <c>null</c>, empty, or every tag is currently inactive; otherwise, <c>false</c>.
405
+ /// </returns>
406
+ /// <example>
407
+ /// <code>
408
+ /// if (tagHandler.HasNoneOfTags(new[] { "Stunned", "Frozen" }))
409
+ /// {
410
+ /// EnablePlayerInput();
411
+ /// }
412
+ /// </code>
413
+ /// </example>
414
+ public bool HasNoneOfTags(IEnumerable<string> effectTags)
415
+ {
416
+ if (effectTags == null)
417
+ {
418
+ return true;
419
+ }
420
+
421
+ return !HasAnyTag(effectTags);
422
+ }
423
+
424
+ /// <summary>
425
+ /// Determines whether none of the specified tags are active.
426
+ /// </summary>
427
+ /// <param name="effectTags">The list of tags to inspect.</param>
428
+ /// <returns>
429
+ /// <c>true</c> when the list is <c>null</c>, empty, or every tag is currently inactive; otherwise, <c>false</c>.
430
+ /// </returns>
431
+ public bool HasNoneOfTags(IReadOnlyList<string> effectTags)
432
+ {
433
+ if (effectTags == null)
434
+ {
435
+ return true;
436
+ }
437
+
438
+ return !HasAnyTag(effectTags);
439
+ }
440
+
441
+ /// <summary>
442
+ /// Attempts to retrieve the active instance count for the specified tag.
443
+ /// </summary>
444
+ /// <param name="effectTag">The tag whose count should be retrieved.</param>
445
+ /// <param name="count">
446
+ /// When this method returns, contains the active count for the tag (cast to <see cref="int"/>) if found; otherwise, zero.
447
+ /// </param>
448
+ /// <returns><c>true</c> if the tag is currently tracked; otherwise, <c>false</c>.</returns>
449
+ /// <example>
450
+ /// <code>
451
+ /// if (tagHandler.TryGetTagCount("Poisoned", out int stacks) && stacks >= 3)
452
+ /// {
453
+ /// TriggerCriticalWarning();
454
+ /// }
455
+ /// </code>
456
+ /// </example>
457
+ public bool TryGetTagCount(string effectTag, out int count)
458
+ {
459
+ if (string.IsNullOrEmpty(effectTag))
460
+ {
461
+ count = default;
462
+ return false;
463
+ }
464
+
465
+ if (_tagCount.TryGetValue(effectTag, out uint uintCount))
466
+ {
467
+ count = unchecked((int)uintCount);
468
+ return true;
469
+ }
470
+
471
+ count = default;
472
+ return false;
473
+ }
474
+
475
+ /// <summary>
476
+ /// Retrieves the set of currently active tags into an optional buffer.
477
+ /// </summary>
478
+ /// <param name="buffer">
479
+ /// Optional list to populate. When <c>null</c>, a new list is created. The buffer is cleared before population.
480
+ /// </param>
481
+ /// <returns>The populated buffer containing all active tags.</returns>
482
+ /// <example>
483
+ /// <code>
484
+ /// List&lt;string&gt; activeTags = tagHandler.GetActiveTags(_reusableTagBuffer);
485
+ /// if (activeTags.Contains("Rooted"))
486
+ /// {
487
+ /// DisableMovement();
488
+ /// }
489
+ /// </code>
490
+ /// </example>
491
+ public List<string> GetActiveTags(List<string> buffer = null)
492
+ {
493
+ buffer ??= new List<string>();
494
+ buffer.Clear();
495
+ foreach (KeyValuePair<string, uint> entry in _tagCount)
496
+ {
497
+ if (entry.Value == 0)
498
+ {
499
+ continue;
500
+ }
501
+
502
+ buffer.Add(entry.Key);
503
+ }
504
+
505
+ return buffer;
506
+ }
507
+
508
+ /// <summary>
509
+ /// Collects all active effect handles that currently contribute the specified tag.
510
+ /// </summary>
511
+ /// <param name="effectTag">The tag to query.</param>
512
+ /// <param name="buffer">
513
+ /// Optional list to populate. When <c>null</c>, a new list is created. The buffer is cleared before population.
514
+ /// </param>
515
+ /// <returns>The populated buffer containing matching effect handles.</returns>
516
+ /// <example>
517
+ /// <code>
518
+ /// List&lt;EffectHandle&gt; handles = tagHandler.GetHandlesWithTag("Burning", _handleBuffer);
519
+ /// foreach (EffectHandle handle in handles)
520
+ /// {
521
+ /// effectHandler.RemoveEffect(handle);
522
+ /// }
523
+ /// </code>
524
+ /// </example>
525
+ public List<EffectHandle> GetHandlesWithTag(
526
+ string effectTag,
527
+ List<EffectHandle> buffer = null
528
+ )
529
+ {
530
+ buffer ??= new List<EffectHandle>();
531
+ buffer.Clear();
532
+ if (string.IsNullOrEmpty(effectTag))
533
+ {
534
+ return buffer;
535
+ }
536
+
537
+ foreach (EffectHandle handle in _effectHandles.Values)
538
+ {
539
+ if (
540
+ handle.effect.effectTags != null
541
+ && handle.effect.effectTags.Contains(effectTag)
542
+ )
543
+ {
544
+ buffer.Add(handle);
545
+ }
546
+ }
547
+
548
+ return buffer;
549
+ }
550
+
253
551
  /// <summary>
254
552
  /// Applies a tag, incrementing its count. If the tag is new, raises <see cref="OnTagAdded"/>.
255
553
  /// Otherwise, raises <see cref="OnTagCountChanged"/>.
@@ -261,22 +559,52 @@ namespace WallstopStudios.UnityHelpers.Tags
261
559
  }
262
560
 
263
561
  /// <summary>
264
- /// Removes one or all instances of a tag.
562
+ /// Removes all instances of the specified tag and returns the contributing effect handles.
265
563
  /// </summary>
266
564
  /// <param name="effectTag">The tag to remove.</param>
267
- /// <param name="allInstances">
268
- /// If true, completely removes the tag regardless of count.
269
- /// If false, decrements the count by 1, removing it only when count reaches 0.
565
+ /// <param name="buffer">
566
+ /// Optional list that receives the handles whose effects applied <paramref name="effectTag"/>.
567
+ /// When <c>null</c>, a new list is created. The buffer is cleared before population.
270
568
  /// </param>
271
- public void RemoveTag(string effectTag, bool allInstances)
569
+ /// <returns>
570
+ /// The populated buffer of handles whose tags were removed. The buffer is empty when the tag was not active.
571
+ /// </returns>
572
+ /// <example>
573
+ /// <code>
574
+ /// List&lt;EffectHandle&gt; dispelled = tagHandler.RemoveTag("Stunned", _handles);
575
+ /// foreach (EffectHandle handle in dispelled)
576
+ /// {
577
+ /// NotifyDispel(handle);
578
+ /// }
579
+ /// </code>
580
+ /// </example>
581
+ public List<EffectHandle> RemoveTag(string effectTag, List<EffectHandle> buffer = null)
272
582
  {
273
- if (allInstances)
583
+ buffer ??= new List<EffectHandle>();
584
+ buffer.Clear();
585
+ if (string.IsNullOrEmpty(effectTag))
274
586
  {
275
- _tagCount.Remove(effectTag);
276
- return;
587
+ return buffer;
588
+ }
589
+
590
+ foreach (EffectHandle handle in _effectHandles.Values)
591
+ {
592
+ if (
593
+ handle.effect.effectTags != null
594
+ && handle.effect.effectTags.Contains(effectTag)
595
+ )
596
+ {
597
+ buffer.Add(handle);
598
+ }
277
599
  }
278
600
 
279
- InternalRemoveTag(effectTag);
601
+ foreach (EffectHandle handle in buffer)
602
+ {
603
+ ForceRemoveTags(handle);
604
+ }
605
+
606
+ InternalRemoveTag(effectTag, allInstances: true);
607
+ return buffer;
280
608
  }
281
609
 
282
610
  /// <summary>
@@ -292,7 +620,7 @@ namespace WallstopStudios.UnityHelpers.Tags
292
620
  return;
293
621
  }
294
622
 
295
- ForceApplyEffect(handle.effect);
623
+ ApplyEffectTags(handle.effect);
296
624
  }
297
625
 
298
626
  /// <summary>
@@ -302,10 +630,7 @@ namespace WallstopStudios.UnityHelpers.Tags
302
630
  /// <param name="effect">The effect containing tags to apply.</param>
303
631
  public void ForceApplyEffect(AttributeEffect effect)
304
632
  {
305
- foreach (string effectTag in effect.effectTags)
306
- {
307
- InternalApplyTag(effectTag);
308
- }
633
+ ApplyEffectTags(effect);
309
634
  }
310
635
 
311
636
  /// <summary>
@@ -321,11 +646,7 @@ namespace WallstopStudios.UnityHelpers.Tags
321
646
  return false;
322
647
  }
323
648
 
324
- foreach (string effectTag in appliedHandle.effect.effectTags)
325
- {
326
- InternalRemoveTag(effectTag);
327
- }
328
-
649
+ RemoveEffectTags(appliedHandle.effect);
329
650
  return true;
330
651
  }
331
652
 
@@ -346,7 +667,7 @@ namespace WallstopStudios.UnityHelpers.Tags
346
667
  }
347
668
  }
348
669
 
349
- private void InternalRemoveTag(string effectTag)
670
+ private void InternalRemoveTag(string effectTag, bool allInstances)
350
671
  {
351
672
  if (!_tagCount.TryGetValue(effectTag, out uint count))
352
673
  {
@@ -355,7 +676,14 @@ namespace WallstopStudios.UnityHelpers.Tags
355
676
 
356
677
  if (count != 0)
357
678
  {
358
- --count;
679
+ if (!allInstances)
680
+ {
681
+ --count;
682
+ }
683
+ else
684
+ {
685
+ count = 0;
686
+ }
359
687
  }
360
688
 
361
689
  if (count == 0)
@@ -369,5 +697,31 @@ namespace WallstopStudios.UnityHelpers.Tags
369
697
  OnTagCountChanged?.Invoke(effectTag, count);
370
698
  }
371
699
  }
700
+
701
+ private void ApplyEffectTags(AttributeEffect effect)
702
+ {
703
+ if (effect.effectTags == null)
704
+ {
705
+ return;
706
+ }
707
+
708
+ foreach (string effectTag in effect.effectTags)
709
+ {
710
+ InternalApplyTag(effectTag);
711
+ }
712
+ }
713
+
714
+ private void RemoveEffectTags(AttributeEffect effect)
715
+ {
716
+ if (effect.effectTags == null)
717
+ {
718
+ return;
719
+ }
720
+
721
+ foreach (string effectTag in effect.effectTags)
722
+ {
723
+ InternalRemoveTag(effectTag, allInstances: false);
724
+ }
725
+ }
372
726
  }
373
727
  }