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
@@ -2,6 +2,7 @@ namespace WallstopStudios.UnityHelpers.Tests.Tags
2
2
  {
3
3
  using System.Collections;
4
4
  using System.Collections.Generic;
5
+ using System.Text.RegularExpressions;
5
6
  using NUnit.Framework;
6
7
  using UnityEngine;
7
8
  using UnityEngine.TestTools;
@@ -16,6 +17,7 @@ namespace WallstopStudios.UnityHelpers.Tests.Tags
16
17
  {
17
18
  ResetEffectHandleId();
18
19
  RecordingCosmeticComponent.ResetCounters();
20
+ RecordingEffectBehavior.Reset();
19
21
  }
20
22
 
21
23
  [UnityTest]
@@ -276,6 +278,7 @@ namespace WallstopStudios.UnityHelpers.Tests.Tags
276
278
  e =>
277
279
  {
278
280
  e.durationType = ModifierDurationType.Infinite;
281
+ e.stackingMode = EffectStackingMode.Stack;
279
282
  }
280
283
  );
281
284
 
@@ -291,6 +294,158 @@ namespace WallstopStudios.UnityHelpers.Tests.Tags
291
294
  Assert.AreEqual(0, handler.GetEffectStackCount(effect));
292
295
  }
293
296
 
297
+ [UnityTest]
298
+ public IEnumerator StackingModeIgnoreReturnsExistingHandle()
299
+ {
300
+ (
301
+ GameObject entity,
302
+ EffectHandler handler,
303
+ TestAttributesComponent attributes,
304
+ TagHandler tags
305
+ ) = CreateEntity();
306
+ yield return null;
307
+
308
+ AttributeEffect effect = CreateEffect(
309
+ "Ignore",
310
+ e =>
311
+ {
312
+ e.durationType = ModifierDurationType.Infinite;
313
+ e.stackingMode = EffectStackingMode.Ignore;
314
+ e.modifications.Add(
315
+ new AttributeModification
316
+ {
317
+ attribute = nameof(TestAttributesComponent.health),
318
+ action = ModificationAction.Addition,
319
+ value = 5f,
320
+ }
321
+ );
322
+ }
323
+ );
324
+
325
+ EffectHandle first = handler.ApplyEffect(effect).Value;
326
+ float afterFirst = attributes.health.CurrentValue;
327
+
328
+ EffectHandle? second = handler.ApplyEffect(effect);
329
+ Assert.IsTrue(second.HasValue);
330
+ Assert.AreEqual(first, second.Value);
331
+ Assert.AreEqual(afterFirst, attributes.health.CurrentValue);
332
+
333
+ handler.RemoveEffect(first);
334
+ }
335
+
336
+ [UnityTest]
337
+ public IEnumerator RefreshModeWithoutResetDurationPreservesTimer()
338
+ {
339
+ (
340
+ GameObject entity,
341
+ EffectHandler handler,
342
+ TestAttributesComponent attributes,
343
+ TagHandler tags
344
+ ) = CreateEntity();
345
+ yield return null;
346
+
347
+ AttributeEffect effect = CreateEffect(
348
+ "RefreshNoReset",
349
+ e =>
350
+ {
351
+ e.duration = 0.3f;
352
+ e.stackingMode = EffectStackingMode.Refresh;
353
+ e.resetDurationOnReapplication = false;
354
+ }
355
+ );
356
+
357
+ EffectHandle handle = handler.ApplyEffect(effect).Value;
358
+ yield return new WaitForSeconds(0.05f);
359
+ Assert.IsTrue(handler.TryGetRemainingDuration(handle, out float beforeReapply));
360
+
361
+ EffectHandle? reapplied = handler.ApplyEffect(effect);
362
+ Assert.IsTrue(reapplied.HasValue);
363
+ Assert.AreEqual(handle, reapplied.Value);
364
+
365
+ Assert.IsTrue(handler.TryGetRemainingDuration(handle, out float afterReapply));
366
+ Assert.LessOrEqual(afterReapply, beforeReapply + 0.01f);
367
+
368
+ handler.RemoveEffect(handle);
369
+ }
370
+
371
+ [UnityTest]
372
+ public IEnumerator CustomStackGroupStackAcrossAssets()
373
+ {
374
+ (
375
+ GameObject entity,
376
+ EffectHandler handler,
377
+ TestAttributesComponent attributes,
378
+ TagHandler tags
379
+ ) = CreateEntity();
380
+ yield return null;
381
+
382
+ AttributeEffect effectA = CreateEffect(
383
+ "GroupA",
384
+ e =>
385
+ {
386
+ e.durationType = ModifierDurationType.Infinite;
387
+ e.stackGroup = EffectStackGroup.CustomKey;
388
+ e.stackGroupKey = "shared";
389
+ e.stackingMode = EffectStackingMode.Stack;
390
+ }
391
+ );
392
+
393
+ AttributeEffect effectB = CreateEffect(
394
+ "GroupB",
395
+ e =>
396
+ {
397
+ e.durationType = ModifierDurationType.Infinite;
398
+ e.stackGroup = EffectStackGroup.CustomKey;
399
+ e.stackGroupKey = "shared";
400
+ e.stackingMode = EffectStackingMode.Stack;
401
+ }
402
+ );
403
+
404
+ EffectHandle a1 = handler.ApplyEffect(effectA).Value;
405
+ EffectHandle b1 = handler.ApplyEffect(effectB).Value;
406
+ EffectHandle a2 = handler.ApplyEffect(effectA).Value;
407
+
408
+ List<EffectHandle> active = handler.GetActiveEffects();
409
+ Assert.AreEqual(3, active.Count);
410
+ Assert.AreEqual(2, handler.GetEffectStackCount(effectA));
411
+ Assert.AreEqual(1, handler.GetEffectStackCount(effectB));
412
+
413
+ handler.RemoveAllEffects();
414
+ }
415
+
416
+ [UnityTest]
417
+ public IEnumerator StackedTagsPersistUntilFinalStackRemoved()
418
+ {
419
+ (
420
+ GameObject entity,
421
+ EffectHandler handler,
422
+ TestAttributesComponent attributes,
423
+ TagHandler tags
424
+ ) = CreateEntity();
425
+ yield return null;
426
+
427
+ AttributeEffect effect = CreateEffect(
428
+ "Tagged",
429
+ e =>
430
+ {
431
+ e.durationType = ModifierDurationType.Infinite;
432
+ e.stackingMode = EffectStackingMode.Stack;
433
+ e.effectTags.Add("Shielded");
434
+ }
435
+ );
436
+
437
+ EffectHandle first = handler.ApplyEffect(effect).Value;
438
+ EffectHandle second = handler.ApplyEffect(effect).Value;
439
+
440
+ Assert.IsTrue(tags.HasTag("Shielded"));
441
+
442
+ handler.RemoveEffect(first);
443
+ Assert.IsTrue(tags.HasTag("Shielded"));
444
+
445
+ handler.RemoveEffect(second);
446
+ Assert.IsFalse(tags.HasTag("Shielded"));
447
+ }
448
+
294
449
  [UnityTest]
295
450
  public IEnumerator GetActiveEffectsPopulatesBuffer()
296
451
  {
@@ -307,6 +462,7 @@ namespace WallstopStudios.UnityHelpers.Tests.Tags
307
462
  e =>
308
463
  {
309
464
  e.durationType = ModifierDurationType.Infinite;
465
+ e.stackingMode = EffectStackingMode.Stack;
310
466
  }
311
467
  );
312
468
 
@@ -325,6 +481,168 @@ namespace WallstopStudios.UnityHelpers.Tests.Tags
325
481
  handler.RemoveEffect(second);
326
482
  }
327
483
 
484
+ [UnityTest]
485
+ public IEnumerator PeriodicEffectHonorsInitialDelayAndMaxTicks()
486
+ {
487
+ (
488
+ GameObject entity,
489
+ EffectHandler handler,
490
+ TestAttributesComponent attributes,
491
+ TagHandler tags
492
+ ) = CreateEntity();
493
+ yield return null;
494
+
495
+ AttributeEffect effect = CreateEffect(
496
+ "PeriodicLimited",
497
+ e =>
498
+ {
499
+ e.durationType = ModifierDurationType.Infinite;
500
+ PeriodicEffectDefinition definition = new()
501
+ {
502
+ initialDelay = 0.05f,
503
+ interval = 0.05f,
504
+ maxTicks = 2,
505
+ };
506
+ definition.modifications.Add(
507
+ new AttributeModification
508
+ {
509
+ attribute = nameof(TestAttributesComponent.health),
510
+ action = ModificationAction.Addition,
511
+ value = -10f,
512
+ }
513
+ );
514
+ e.periodicEffects.Add(definition);
515
+ }
516
+ );
517
+
518
+ EffectHandle handle = handler.ApplyEffect(effect).Value;
519
+ yield return null;
520
+ Assert.AreEqual(100f, attributes.health.CurrentValue, 0.01f);
521
+
522
+ yield return new WaitForSeconds(0.03f);
523
+ Assert.AreEqual(100f, attributes.health.CurrentValue, 0.01f);
524
+
525
+ yield return new WaitForSeconds(0.06f);
526
+ Assert.AreEqual(90f, attributes.health.CurrentValue, 0.01f);
527
+
528
+ yield return new WaitForSeconds(0.06f);
529
+ Assert.AreEqual(80f, attributes.health.CurrentValue, 0.01f);
530
+
531
+ yield return new WaitForSeconds(0.06f);
532
+ Assert.AreEqual(80f, attributes.health.CurrentValue, 0.01f);
533
+
534
+ handler.RemoveEffect(handle);
535
+ }
536
+
537
+ [UnityTest]
538
+ public IEnumerator PeriodicEffectUnlimitedTicksStopOnRemoval()
539
+ {
540
+ (
541
+ GameObject entity,
542
+ EffectHandler handler,
543
+ TestAttributesComponent attributes,
544
+ TagHandler tags
545
+ ) = CreateEntity();
546
+ yield return null;
547
+
548
+ AttributeEffect effect = CreateEffect(
549
+ "PeriodicUnlimited",
550
+ e =>
551
+ {
552
+ e.durationType = ModifierDurationType.Infinite;
553
+ PeriodicEffectDefinition definition = new() { interval = 0.05f, maxTicks = 0 };
554
+ definition.modifications.Add(
555
+ new AttributeModification
556
+ {
557
+ attribute = nameof(TestAttributesComponent.health),
558
+ action = ModificationAction.Addition,
559
+ value = -5f,
560
+ }
561
+ );
562
+ e.periodicEffects.Add(definition);
563
+ }
564
+ );
565
+
566
+ EffectHandle handle = handler.ApplyEffect(effect).Value;
567
+ yield return new WaitForSeconds(0.16f);
568
+ float afterTicks = attributes.health.CurrentValue;
569
+ Assert.Less(afterTicks, 100f);
570
+
571
+ handler.RemoveEffect(handle);
572
+ float afterRemoval = attributes.health.CurrentValue;
573
+ yield return new WaitForSeconds(0.1f);
574
+ Assert.AreEqual(afterRemoval, attributes.health.CurrentValue, 0.01f);
575
+ }
576
+
577
+ [UnityTest]
578
+ public IEnumerator MultiplePeriodicDefinitionsAffectAttributesIndependently()
579
+ {
580
+ (
581
+ GameObject entity,
582
+ EffectHandler handler,
583
+ TestAttributesComponent attributes,
584
+ TagHandler tags
585
+ ) = CreateEntity();
586
+ yield return null;
587
+
588
+ AttributeEffect effect = CreateEffect(
589
+ "PeriodicMulti",
590
+ e =>
591
+ {
592
+ e.durationType = ModifierDurationType.Infinite;
593
+
594
+ PeriodicEffectDefinition damage = new()
595
+ {
596
+ initialDelay = 0.05f,
597
+ interval = 0.05f,
598
+ maxTicks = 2,
599
+ };
600
+ damage.modifications.Add(
601
+ new AttributeModification
602
+ {
603
+ attribute = nameof(TestAttributesComponent.health),
604
+ action = ModificationAction.Addition,
605
+ value = -5f,
606
+ }
607
+ );
608
+
609
+ PeriodicEffectDefinition armorGain = new()
610
+ {
611
+ initialDelay = 0.02f,
612
+ interval = 0.1f,
613
+ maxTicks = 3,
614
+ };
615
+ armorGain.modifications.Add(
616
+ new AttributeModification
617
+ {
618
+ attribute = nameof(TestAttributesComponent.armor),
619
+ action = ModificationAction.Addition,
620
+ value = 1f,
621
+ }
622
+ );
623
+
624
+ e.periodicEffects.Add(damage);
625
+ e.periodicEffects.Add(armorGain);
626
+ }
627
+ );
628
+
629
+ EffectHandle handle = handler.ApplyEffect(effect).Value;
630
+
631
+ yield return new WaitForSeconds(0.06f);
632
+ Assert.AreEqual(95f, attributes.health.CurrentValue, 0.01f);
633
+ Assert.AreEqual(51f, attributes.armor.CurrentValue, 0.01f);
634
+
635
+ yield return new WaitForSeconds(0.1f);
636
+ Assert.AreEqual(90f, attributes.health.CurrentValue, 0.01f);
637
+ Assert.AreEqual(52f, attributes.armor.CurrentValue, 0.01f);
638
+
639
+ yield return new WaitForSeconds(0.2f);
640
+ Assert.AreEqual(90f, attributes.health.CurrentValue, 0.01f);
641
+ Assert.AreEqual(53f, attributes.armor.CurrentValue, 0.01f);
642
+
643
+ handler.RemoveEffect(handle);
644
+ }
645
+
328
646
  [UnityTest]
329
647
  public IEnumerator TryGetRemainingDurationReportsTime()
330
648
  {
@@ -429,6 +747,306 @@ namespace WallstopStudios.UnityHelpers.Tests.Tags
429
747
  handler.RemoveEffect(handle);
430
748
  }
431
749
 
750
+ [UnityTest]
751
+ public IEnumerator PeriodicEffectAppliesTicksAndStops()
752
+ {
753
+ (
754
+ GameObject entity,
755
+ EffectHandler handler,
756
+ TestAttributesComponent attributes,
757
+ TagHandler tags
758
+ ) = CreateEntity();
759
+ yield return null;
760
+
761
+ AttributeEffect effect = CreateEffect(
762
+ "Periodic",
763
+ e =>
764
+ {
765
+ e.durationType = ModifierDurationType.Infinite;
766
+ PeriodicEffectDefinition periodic = new() { interval = 0.1f, maxTicks = 3 };
767
+ periodic.modifications.Add(
768
+ new AttributeModification
769
+ {
770
+ attribute = nameof(TestAttributesComponent.health),
771
+ action = ModificationAction.Addition,
772
+ value = -10f,
773
+ }
774
+ );
775
+ e.periodicEffects.Add(periodic);
776
+ }
777
+ );
778
+
779
+ EffectHandle handle = handler.ApplyEffect(effect).Value;
780
+ yield return new WaitForSeconds(0.35f);
781
+ Assert.AreEqual(70f, attributes.health.CurrentValue, 0.01f);
782
+
783
+ yield return new WaitForSeconds(0.2f);
784
+ handler.RemoveEffect(handle);
785
+ Assert.AreEqual(70f, attributes.health.CurrentValue, 0.01f);
786
+ }
787
+
788
+ [UnityTest]
789
+ public IEnumerator EffectBehaviorReceivesCallbacks()
790
+ {
791
+ (
792
+ GameObject entity,
793
+ EffectHandler handler,
794
+ TestAttributesComponent attributes,
795
+ TagHandler tags
796
+ ) = CreateEntity();
797
+ yield return null;
798
+
799
+ AttributeEffect effect = CreateEffect(
800
+ "Behavior",
801
+ e =>
802
+ {
803
+ e.duration = 0.25f;
804
+ e.periodicEffects.Add(
805
+ new PeriodicEffectDefinition { interval = 0.05f, maxTicks = 2 }
806
+ );
807
+ }
808
+ );
809
+
810
+ RecordingEffectBehavior behavior = Track(
811
+ ScriptableObject.CreateInstance<RecordingEffectBehavior>()
812
+ );
813
+ effect.behaviors.Add(behavior);
814
+
815
+ EffectHandle handle = handler.ApplyEffect(effect).Value;
816
+ Assert.AreEqual(1, RecordingEffectBehavior.ApplyCount);
817
+
818
+ yield return null;
819
+ Assert.Greater(RecordingEffectBehavior.TickCount, 0);
820
+
821
+ yield return new WaitForSeconds(0.12f);
822
+ Assert.GreaterOrEqual(RecordingEffectBehavior.PeriodicTickCount, 1);
823
+
824
+ handler.RemoveEffect(handle);
825
+ Assert.AreEqual(1, RecordingEffectBehavior.RemoveCount);
826
+ }
827
+
828
+ [UnityTest]
829
+ public IEnumerator EffectBehaviorClonesPerHandle()
830
+ {
831
+ (
832
+ GameObject entity,
833
+ EffectHandler handler,
834
+ TestAttributesComponent attributes,
835
+ TagHandler tags
836
+ ) = CreateEntity();
837
+ yield return null;
838
+
839
+ AttributeEffect effect = CreateEffect(
840
+ "BehaviorStacks",
841
+ e =>
842
+ {
843
+ e.durationType = ModifierDurationType.Infinite;
844
+ e.stackingMode = EffectStackingMode.Stack;
845
+ }
846
+ );
847
+
848
+ RecordingEffectBehavior behavior = Track(
849
+ ScriptableObject.CreateInstance<RecordingEffectBehavior>()
850
+ );
851
+ effect.behaviors.Add(behavior);
852
+
853
+ int startingInstances = RecordingEffectBehavior.InstanceCount;
854
+
855
+ EffectHandle first = handler.ApplyEffect(effect).Value;
856
+ Assert.AreEqual(startingInstances + 1, RecordingEffectBehavior.InstanceCount);
857
+
858
+ EffectHandle second = handler.ApplyEffect(effect).Value;
859
+ Assert.AreEqual(startingInstances + 2, RecordingEffectBehavior.InstanceCount);
860
+ Assert.AreEqual(2, RecordingEffectBehavior.ApplyCount);
861
+
862
+ handler.RemoveEffect(first);
863
+ handler.RemoveEffect(second);
864
+ Assert.AreEqual(2, RecordingEffectBehavior.RemoveCount);
865
+ }
866
+
867
+ [UnityTest]
868
+ public IEnumerator EffectBehaviorWithoutPeriodicSkipsPeriodicCallbacks()
869
+ {
870
+ (
871
+ GameObject entity,
872
+ EffectHandler handler,
873
+ TestAttributesComponent attributes,
874
+ TagHandler tags
875
+ ) = CreateEntity();
876
+ yield return null;
877
+
878
+ AttributeEffect effect = CreateEffect(
879
+ "BehaviorNoPeriodic",
880
+ e =>
881
+ {
882
+ e.durationType = ModifierDurationType.Infinite;
883
+ }
884
+ );
885
+
886
+ RecordingEffectBehavior behavior = Track(
887
+ ScriptableObject.CreateInstance<RecordingEffectBehavior>()
888
+ );
889
+ effect.behaviors.Add(behavior);
890
+
891
+ EffectHandle handle = handler.ApplyEffect(effect).Value;
892
+ yield return new WaitForSeconds(0.1f);
893
+
894
+ Assert.AreEqual(0, RecordingEffectBehavior.PeriodicTickCount);
895
+
896
+ handler.RemoveEffect(handle);
897
+ }
898
+
899
+ [UnityTest]
900
+ public IEnumerator StackingModeStackRespectsMaximumStacks()
901
+ {
902
+ (
903
+ GameObject entity,
904
+ EffectHandler handler,
905
+ TestAttributesComponent attributes,
906
+ TagHandler tags
907
+ ) = CreateEntity();
908
+ yield return null;
909
+
910
+ AttributeEffect effect = CreateEffect(
911
+ "Stacking",
912
+ e =>
913
+ {
914
+ e.durationType = ModifierDurationType.Infinite;
915
+ e.stackingMode = EffectStackingMode.Stack;
916
+ e.maximumStacks = 2;
917
+ }
918
+ );
919
+
920
+ EffectHandle first = handler.ApplyEffect(effect).Value;
921
+ EffectHandle second = handler.ApplyEffect(effect).Value;
922
+ EffectHandle third = handler.ApplyEffect(effect).Value;
923
+
924
+ List<EffectHandle> active = handler.GetActiveEffects();
925
+ Assert.AreEqual(2, active.Count);
926
+ CollectionAssert.DoesNotContain(active, first);
927
+ CollectionAssert.Contains(active, second);
928
+ CollectionAssert.Contains(active, third);
929
+
930
+ handler.RemoveAllEffects();
931
+ }
932
+
933
+ [UnityTest]
934
+ public IEnumerator InstantEffectWithPeriodicLogsWarning()
935
+ {
936
+ (
937
+ GameObject entity,
938
+ EffectHandler handler,
939
+ TestAttributesComponent attributes,
940
+ TagHandler tags
941
+ ) = CreateEntity();
942
+ yield return null;
943
+
944
+ AttributeEffect effect = CreateEffect(
945
+ "InstantPeriodic",
946
+ e =>
947
+ {
948
+ e.durationType = ModifierDurationType.Instant;
949
+ PeriodicEffectDefinition definition = new() { interval = 0.05f, maxTicks = 1 };
950
+ definition.modifications.Add(
951
+ new AttributeModification
952
+ {
953
+ attribute = nameof(TestAttributesComponent.health),
954
+ action = ModificationAction.Addition,
955
+ value = -10f,
956
+ }
957
+ );
958
+ e.periodicEffects.Add(definition);
959
+ }
960
+ );
961
+
962
+ LogAssert.Expect(
963
+ LogType.Warning,
964
+ new Regex("defines periodic or behaviour data but is Instant")
965
+ );
966
+
967
+ EffectHandle? handle = handler.ApplyEffect(effect);
968
+ Assert.IsFalse(handle.HasValue);
969
+
970
+ yield return new WaitForSeconds(0.1f);
971
+ Assert.AreEqual(100f, attributes.health.CurrentValue, 0.01f);
972
+ }
973
+
974
+ [UnityTest]
975
+ public IEnumerator StackingModeReplaceSwapsHandles()
976
+ {
977
+ (
978
+ GameObject entity,
979
+ EffectHandler handler,
980
+ TestAttributesComponent attributes,
981
+ TagHandler tags
982
+ ) = CreateEntity();
983
+ yield return null;
984
+
985
+ AttributeEffect effect = CreateEffect(
986
+ "Replace",
987
+ e =>
988
+ {
989
+ e.durationType = ModifierDurationType.Infinite;
990
+ e.stackingMode = EffectStackingMode.Replace;
991
+ }
992
+ );
993
+
994
+ EffectHandle first = handler.ApplyEffect(effect).Value;
995
+ EffectHandle second = handler.ApplyEffect(effect).Value;
996
+
997
+ List<EffectHandle> active = handler.GetActiveEffects();
998
+ Assert.AreEqual(1, active.Count);
999
+ Assert.AreEqual(second, active[0]);
1000
+ Assert.AreNotEqual(first, second);
1001
+
1002
+ handler.RemoveAllEffects();
1003
+ }
1004
+
1005
+ [UnityTest]
1006
+ public IEnumerator CustomStackGroupSharesAcrossEffects()
1007
+ {
1008
+ (
1009
+ GameObject entity,
1010
+ EffectHandler handler,
1011
+ TestAttributesComponent attributes,
1012
+ TagHandler tags
1013
+ ) = CreateEntity();
1014
+ yield return null;
1015
+
1016
+ AttributeEffect effectA = CreateEffect(
1017
+ "GroupA",
1018
+ e =>
1019
+ {
1020
+ e.durationType = ModifierDurationType.Infinite;
1021
+ e.stackGroup = EffectStackGroup.CustomKey;
1022
+ e.stackGroupKey = "shared";
1023
+ e.stackingMode = EffectStackingMode.Replace;
1024
+ }
1025
+ );
1026
+ AttributeEffect effectB = CreateEffect(
1027
+ "GroupB",
1028
+ e =>
1029
+ {
1030
+ e.durationType = ModifierDurationType.Infinite;
1031
+ e.stackGroup = EffectStackGroup.CustomKey;
1032
+ e.stackGroupKey = "shared";
1033
+ e.stackingMode = EffectStackingMode.Replace;
1034
+ }
1035
+ );
1036
+
1037
+ EffectHandle first = handler.ApplyEffect(effectA).Value;
1038
+ EffectHandle second = handler.ApplyEffect(effectB).Value;
1039
+
1040
+ List<EffectHandle> active = handler.GetActiveEffects();
1041
+ Assert.AreEqual(1, active.Count);
1042
+ Assert.AreEqual(second, active[0]);
1043
+ Assert.IsFalse(handler.IsEffectActive(effectA));
1044
+ Assert.IsTrue(handler.IsEffectActive(effectB));
1045
+ Assert.AreNotEqual(first, second);
1046
+
1047
+ handler.RemoveAllEffects();
1048
+ }
1049
+
432
1050
  private (
433
1051
  GameObject entity,
434
1052
  EffectHandler handler,