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.
- package/Docs/DATA_STRUCTURES.md +7 -7
- package/Docs/EFFECTS_SYSTEM.md +836 -8
- package/Docs/EFFECTS_SYSTEM_TUTORIAL.md +77 -18
- package/Docs/HULLS.md +2 -2
- package/Docs/RANDOM_PERFORMANCE.md +1 -1
- package/Docs/REFLECTION_HELPERS.md +1 -1
- package/Docs/RELATIONAL_COMPONENTS.md +51 -6
- package/Docs/SERIALIZATION.md +1 -1
- package/Docs/SINGLETONS.md +2 -2
- package/Docs/SPATIAL_TREES_2D_GUIDE.md +3 -3
- package/Docs/SPATIAL_TREES_3D_GUIDE.md +3 -3
- package/Docs/SPATIAL_TREE_SEMANTICS.md +7 -7
- package/Editor/CustomDrawers/WShowIfPropertyDrawer.cs +131 -41
- package/Editor/Utils/ScriptableObjectSingletonCreator.cs +175 -18
- package/README.md +17 -3
- package/Runtime/Core/Helper/UnityMainThreadDispatcher.cs +4 -2
- package/Runtime/Tags/Attribute.cs +144 -24
- package/Runtime/Tags/AttributeEffect.cs +278 -11
- package/Runtime/Tags/AttributeModification.cs +59 -29
- package/Runtime/Tags/AttributeUtilities.cs +465 -0
- package/Runtime/Tags/AttributesComponent.cs +20 -0
- package/Runtime/Tags/EffectBehavior.cs +171 -0
- package/Runtime/Tags/EffectBehavior.cs.meta +4 -0
- package/Runtime/Tags/EffectHandle.cs +5 -0
- package/Runtime/Tags/EffectHandler.cs +564 -39
- package/Runtime/Tags/EffectStackKey.cs +79 -0
- package/Runtime/Tags/EffectStackKey.cs.meta +4 -0
- package/Runtime/Tags/PeriodicEffectDefinition.cs +102 -0
- package/Runtime/Tags/PeriodicEffectDefinition.cs.meta +4 -0
- package/Runtime/Tags/PeriodicEffectRuntimeState.cs +40 -0
- package/Runtime/Tags/PeriodicEffectRuntimeState.cs.meta +4 -0
- package/Runtime/Tags/TagHandler.cs +375 -21
- package/Samples~/DI - Zenject/README.md +0 -2
- package/Tests/Editor/Attributes/WShowIfPropertyDrawerTests.cs +285 -0
- package/Tests/Editor/Attributes/WShowIfPropertyDrawerTests.cs.meta +11 -0
- package/Tests/Editor/Core/Attributes/RelationalComponentAssignerTests.cs +2 -2
- package/Tests/Editor/Utils/ScriptableObjectSingletonTests.cs +41 -0
- package/Tests/Runtime/Serialization/JsonSerializationTest.cs +4 -3
- package/Tests/Runtime/Tags/AttributeEffectTests.cs +135 -0
- package/Tests/Runtime/Tags/AttributeEffectTests.cs.meta +3 -0
- package/Tests/Runtime/Tags/AttributeModificationTests.cs +137 -0
- package/Tests/Runtime/Tags/AttributeTests.cs +192 -0
- package/Tests/Runtime/Tags/AttributeTests.cs.meta +3 -0
- package/Tests/Runtime/Tags/AttributeUtilitiesTests.cs +245 -0
- package/Tests/Runtime/Tags/CosmeticAndCollisionTests.cs +1 -1
- package/Tests/Runtime/Tags/EffectBehaviorTests.cs +184 -0
- package/Tests/Runtime/Tags/EffectBehaviorTests.cs.meta +3 -0
- package/Tests/Runtime/Tags/EffectHandlerTests.cs +809 -0
- package/Tests/Runtime/Tags/Helpers/RecordingEffectBehavior.cs +89 -0
- package/Tests/Runtime/Tags/Helpers/RecordingEffectBehavior.cs.meta +4 -0
- package/Tests/Runtime/Tags/PeriodicEffectDefinitionSerializationTests.cs +92 -0
- package/Tests/Runtime/Tags/PeriodicEffectDefinitionSerializationTests.cs.meta +3 -0
- package/Tests/Runtime/Tags/TagHandlerTests.cs +130 -6
- package/package.json +1 -1
- package/scripts/lint-doc-links.ps1 +156 -11
- package/Tests/Runtime/Tags/AttributeDataTests.cs +0 -312
- package/node_modules.meta +0 -8
- /package/Tests/Runtime/Tags/{AttributeDataTests.cs.meta → AttributeModificationTests.cs.meta} +0 -0
|
@@ -18,6 +18,7 @@ namespace WallstopStudios.UnityHelpers.Editor.Utils
|
|
|
18
18
|
|
|
19
19
|
// Prevents re-entrant execution during domain reloads/asset refreshes
|
|
20
20
|
private static bool _isEnsuring;
|
|
21
|
+
private static int _assetEditingScopeDepth;
|
|
21
22
|
|
|
22
23
|
// Controls whether informational logs are emitted. Warnings still always log.
|
|
23
24
|
internal static bool VerboseLogging { get; set; }
|
|
@@ -41,6 +42,7 @@ namespace WallstopStudios.UnityHelpers.Editor.Utils
|
|
|
41
42
|
|
|
42
43
|
_isEnsuring = true;
|
|
43
44
|
AssetDatabase.StartAssetEditing();
|
|
45
|
+
_assetEditingScopeDepth++;
|
|
44
46
|
bool anyChanges = false;
|
|
45
47
|
try
|
|
46
48
|
{
|
|
@@ -94,10 +96,24 @@ namespace WallstopStudios.UnityHelpers.Editor.Utils
|
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
string resolvedResourcesRoot = EnsureAndResolveFolderPath(ResourcesRoot);
|
|
99
|
+
if (string.IsNullOrWhiteSpace(resolvedResourcesRoot))
|
|
100
|
+
{
|
|
101
|
+
Debug.LogError(
|
|
102
|
+
"ScriptableObjectSingletonCreator: Unable to resolve required Resources root folder. Aborting singleton auto-creation."
|
|
103
|
+
);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
97
106
|
|
|
98
107
|
string resourcesSubFolder = GetResourcesSubFolder(derivedType);
|
|
99
108
|
string targetFolderRequested = CombinePaths(ResourcesRoot, resourcesSubFolder);
|
|
100
109
|
string targetFolder = EnsureAndResolveFolderPath(targetFolderRequested);
|
|
110
|
+
if (string.IsNullOrWhiteSpace(targetFolder))
|
|
111
|
+
{
|
|
112
|
+
Debug.LogError(
|
|
113
|
+
$"ScriptableObjectSingletonCreator: Unable to ensure folder '{targetFolderRequested}' for singleton {derivedType.FullName}. Skipping asset creation."
|
|
114
|
+
);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
101
117
|
|
|
102
118
|
string targetAssetPath = CombinePaths(
|
|
103
119
|
targetFolder,
|
|
@@ -156,6 +172,10 @@ namespace WallstopStudios.UnityHelpers.Editor.Utils
|
|
|
156
172
|
finally
|
|
157
173
|
{
|
|
158
174
|
AssetDatabase.StopAssetEditing();
|
|
175
|
+
if (_assetEditingScopeDepth > 0)
|
|
176
|
+
{
|
|
177
|
+
_assetEditingScopeDepth--;
|
|
178
|
+
}
|
|
159
179
|
_isEnsuring = false;
|
|
160
180
|
|
|
161
181
|
if (anyChanges)
|
|
@@ -448,31 +468,119 @@ namespace WallstopStudios.UnityHelpers.Editor.Utils
|
|
|
448
468
|
{
|
|
449
469
|
string desiredName = parts[i];
|
|
450
470
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
string
|
|
454
|
-
if (subFolders != null)
|
|
471
|
+
string matchedExisting = FindMatchingSubfolder(current, desiredName);
|
|
472
|
+
|
|
473
|
+
if (string.IsNullOrEmpty(matchedExisting))
|
|
455
474
|
{
|
|
456
|
-
|
|
475
|
+
string intendedPath = current + "/" + desiredName;
|
|
476
|
+
string createdGuid = AssetDatabase.CreateFolder(current, desiredName);
|
|
477
|
+
string createdPath = string.Empty;
|
|
478
|
+
if (!string.IsNullOrEmpty(createdGuid))
|
|
479
|
+
{
|
|
480
|
+
createdPath = NormalizePath(AssetDatabase.GUIDToAssetPath(createdGuid));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
string actualPath = FindMatchingSubfolder(current, desiredName);
|
|
484
|
+
if (string.IsNullOrEmpty(actualPath))
|
|
485
|
+
{
|
|
486
|
+
actualPath = createdPath;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
bool intendedValid = AssetDatabase.IsValidFolder(intendedPath);
|
|
490
|
+
bool actualValid =
|
|
491
|
+
!string.IsNullOrEmpty(actualPath)
|
|
492
|
+
&& AssetDatabase.IsValidFolder(actualPath);
|
|
493
|
+
|
|
494
|
+
if (!intendedValid && !actualValid)
|
|
495
|
+
{
|
|
496
|
+
bool directoryExists =
|
|
497
|
+
Directory.Exists(intendedPath)
|
|
498
|
+
|| (!string.IsNullOrEmpty(actualPath) && Directory.Exists(actualPath));
|
|
499
|
+
if (directoryExists || !string.IsNullOrEmpty(createdGuid))
|
|
500
|
+
{
|
|
501
|
+
ForceAssetDatabaseSync();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
intendedValid = AssetDatabase.IsValidFolder(intendedPath);
|
|
505
|
+
if (!intendedValid)
|
|
506
|
+
{
|
|
507
|
+
actualPath = FindMatchingSubfolder(current, desiredName);
|
|
508
|
+
if (
|
|
509
|
+
string.IsNullOrEmpty(actualPath)
|
|
510
|
+
&& !string.IsNullOrEmpty(createdGuid)
|
|
511
|
+
)
|
|
512
|
+
{
|
|
513
|
+
actualPath = NormalizePath(
|
|
514
|
+
AssetDatabase.GUIDToAssetPath(createdGuid)
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
actualValid =
|
|
519
|
+
!string.IsNullOrEmpty(actualPath)
|
|
520
|
+
&& AssetDatabase.IsValidFolder(actualPath);
|
|
521
|
+
}
|
|
522
|
+
else
|
|
523
|
+
{
|
|
524
|
+
actualPath = intendedPath;
|
|
525
|
+
actualValid = true;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (intendedValid)
|
|
457
530
|
{
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
531
|
+
current = ResolveExistingFolderPath(intendedPath);
|
|
532
|
+
LogVerbose(
|
|
533
|
+
$"ScriptableObjectSingletonCreator: Created folder '{current}'."
|
|
534
|
+
);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (
|
|
539
|
+
actualValid
|
|
540
|
+
&& string.Equals(
|
|
541
|
+
actualPath,
|
|
542
|
+
intendedPath,
|
|
543
|
+
StringComparison.OrdinalIgnoreCase
|
|
463
544
|
)
|
|
545
|
+
)
|
|
546
|
+
{
|
|
547
|
+
string renameError = AssetDatabase.MoveAsset(actualPath, intendedPath);
|
|
548
|
+
if (string.IsNullOrEmpty(renameError))
|
|
549
|
+
{
|
|
550
|
+
LogVerbose(
|
|
551
|
+
$"ScriptableObjectSingletonCreator: Renamed folder '{actualPath}' to '{intendedPath}' to correct casing."
|
|
552
|
+
);
|
|
553
|
+
current = ResolveExistingFolderPath(intendedPath);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
LogVerbose(
|
|
558
|
+
$"ScriptableObjectSingletonCreator: Reusing newly created folder '{actualPath}' when casing correction to '{intendedPath}' failed: {renameError}."
|
|
559
|
+
);
|
|
560
|
+
current = ResolveExistingFolderPath(actualPath);
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (actualValid && AssetDatabase.IsValidFolder(actualPath))
|
|
565
|
+
{
|
|
566
|
+
bool deleted = AssetDatabase.DeleteAsset(actualPath);
|
|
567
|
+
if (!deleted)
|
|
464
568
|
{
|
|
465
|
-
|
|
466
|
-
|
|
569
|
+
Debug.LogWarning(
|
|
570
|
+
$"ScriptableObjectSingletonCreator: Unexpected folder '{actualPath}' was created while attempting to create '{intendedPath}', but it could not be removed."
|
|
571
|
+
);
|
|
467
572
|
}
|
|
573
|
+
|
|
574
|
+
Debug.LogError(
|
|
575
|
+
$"ScriptableObjectSingletonCreator: Expected to create folder '{intendedPath}', but Unity created '{actualPath}'. Aborting to avoid duplicate folders."
|
|
576
|
+
);
|
|
577
|
+
return string.Empty;
|
|
468
578
|
}
|
|
469
|
-
}
|
|
470
579
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
LogVerbose($"ScriptableObjectSingletonCreator: Created folder '{current}'.");
|
|
580
|
+
Debug.LogError(
|
|
581
|
+
$"ScriptableObjectSingletonCreator: Failed to create folder '{intendedPath}'."
|
|
582
|
+
);
|
|
583
|
+
return string.Empty;
|
|
476
584
|
}
|
|
477
585
|
else
|
|
478
586
|
{
|
|
@@ -561,6 +669,33 @@ namespace WallstopStudios.UnityHelpers.Editor.Utils
|
|
|
561
669
|
return current;
|
|
562
670
|
}
|
|
563
671
|
|
|
672
|
+
private static string FindMatchingSubfolder(string parent, string desiredName)
|
|
673
|
+
{
|
|
674
|
+
if (string.IsNullOrWhiteSpace(parent) || string.IsNullOrWhiteSpace(desiredName))
|
|
675
|
+
{
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
string[] subFolders = AssetDatabase.GetSubFolders(parent);
|
|
680
|
+
if (subFolders == null || subFolders.Length == 0)
|
|
681
|
+
{
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
for (int i = 0; i < subFolders.Length; i++)
|
|
686
|
+
{
|
|
687
|
+
string sub = subFolders[i];
|
|
688
|
+
int lastSlash = sub.LastIndexOf('/', sub.Length - 1);
|
|
689
|
+
string terminal = lastSlash >= 0 ? sub.Substring(lastSlash + 1) : sub;
|
|
690
|
+
if (string.Equals(terminal, desiredName, StringComparison.OrdinalIgnoreCase))
|
|
691
|
+
{
|
|
692
|
+
return sub;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
|
|
564
699
|
private static string ResolveExistingFolderPath(string intended)
|
|
565
700
|
{
|
|
566
701
|
if (string.IsNullOrWhiteSpace(intended))
|
|
@@ -621,6 +756,28 @@ namespace WallstopStudios.UnityHelpers.Editor.Utils
|
|
|
621
756
|
return current;
|
|
622
757
|
}
|
|
623
758
|
|
|
759
|
+
private static void ForceAssetDatabaseSync()
|
|
760
|
+
{
|
|
761
|
+
if (_assetEditingScopeDepth > 0)
|
|
762
|
+
{
|
|
763
|
+
AssetDatabase.StopAssetEditing();
|
|
764
|
+
try
|
|
765
|
+
{
|
|
766
|
+
AssetDatabase.SaveAssets();
|
|
767
|
+
AssetDatabase.Refresh();
|
|
768
|
+
}
|
|
769
|
+
finally
|
|
770
|
+
{
|
|
771
|
+
AssetDatabase.StartAssetEditing();
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
else
|
|
775
|
+
{
|
|
776
|
+
AssetDatabase.SaveAssets();
|
|
777
|
+
AssetDatabase.Refresh();
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
624
781
|
private static void LogVerbose(string message)
|
|
625
782
|
{
|
|
626
783
|
if (VerboseLogging)
|
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ void Awake() {
|
|
|
77
77
|
void Awake() => this.AssignRelationalComponents();
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
**Bonus:** Works with VContainer/Zenject for automatic DI + relational wiring!
|
|
80
|
+
**Bonus:** Works with VContainer/Zenject/Reflex for automatic DI + relational wiring!
|
|
81
81
|
|
|
82
82
|
[📖 Learn More](Docs/RELATIONAL_COMPONENTS.md) | [🎯 DI – VContainer](Samples~/DI%20-%20VContainer/README.md) | [🎯 DI – Zenject](Samples~/DI%20-%20Zenject/README.md) | [🎯 DI – Reflex](Samples~/DI%20-%20Reflex/README.md)
|
|
83
83
|
|
|
@@ -1018,7 +1018,7 @@ void ProcessLargeDataset(int size)
|
|
|
1018
1018
|
|
|
1019
1019
|
- When the define is present, optional assemblies under `Runtime/Integrations/*` compile automatically and expose helpers like `RelationalComponentsInstaller` (Zenject/Reflex) and `RegisterRelationalComponents()` (VContainer).
|
|
1020
1020
|
- If you use UPM, no manual defines are required — the package IDs above trigger symbols via `versionDefines` in the asmdefs.
|
|
1021
|
-
- For test scenarios without LifetimeScope (VContainer)
|
|
1021
|
+
- For test scenarios without LifetimeScope (VContainer), SceneContext (Zenject), or SceneScope (Reflex), see [DI Integrations: Testing and Edge Cases](Docs/RELATIONAL_COMPONENTS.md#di-integrations-testing-and-edge-cases) for step‑by‑step patterns.
|
|
1022
1022
|
|
|
1023
1023
|
**Quick start:**
|
|
1024
1024
|
|
|
@@ -1046,6 +1046,12 @@ using Zenject;
|
|
|
1046
1046
|
using WallstopStudios.UnityHelpers.Integrations.Zenject;
|
|
1047
1047
|
|
|
1048
1048
|
var enemy = Container.InstantiateComponentWithRelations(enemyPrefab, parent);
|
|
1049
|
+
|
|
1050
|
+
// Reflex — prefab instantiation with DI + relations
|
|
1051
|
+
using Reflex.Core;
|
|
1052
|
+
using WallstopStudios.UnityHelpers.Integrations.Reflex;
|
|
1053
|
+
|
|
1054
|
+
var enemy = container.InstantiateComponentWithRelations(enemyPrefab, parent);
|
|
1049
1055
|
```
|
|
1050
1056
|
|
|
1051
1057
|
See the full guide with scenarios, troubleshooting, and testing patterns: [Relational Components Guide](Docs/RELATIONAL_COMPONENTS.md)
|
|
@@ -1064,10 +1070,17 @@ See the full guide with scenarios, troubleshooting, and testing patterns: [Relat
|
|
|
1064
1070
|
- `container.InjectGameObjectWithRelations(root, includeInactiveChildren)` — inject hierarchy + assign
|
|
1065
1071
|
- `container.InstantiateGameObjectWithRelations(prefab, parent)` — instantiate GO + inject + assign
|
|
1066
1072
|
|
|
1073
|
+
- Reflex:
|
|
1074
|
+
- `container.InjectWithRelations(component)` — inject + assign a single instance
|
|
1075
|
+
- `container.InstantiateComponentWithRelations(prefab, parent)` — instantiate + inject + assign
|
|
1076
|
+
- `container.InjectGameObjectWithRelations(root, includeInactiveChildren)` — inject hierarchy + assign
|
|
1077
|
+
- `container.InstantiateGameObjectWithRelations(prefab, parent)` — instantiate GO + inject + assign
|
|
1078
|
+
|
|
1067
1079
|
### Additive Scene Loads
|
|
1068
1080
|
|
|
1069
1081
|
- VContainer: `RegisterRelationalComponents(..., enableAdditiveSceneListener: true)` registers a listener that hydrates components in newly loaded scenes.
|
|
1070
|
-
- Zenject: `RelationalComponentsInstaller` exposes a toggle
|
|
1082
|
+
- Zenject: `RelationalComponentsInstaller` exposes a toggle "Listen For Additive Scenes" to register the same behavior.
|
|
1083
|
+
- Reflex: `RelationalComponentsInstaller` exposes a toggle "Listen For Additive Scenes" to register the same behavior.
|
|
1071
1084
|
- Only the newly loaded scene is processed; other loaded scenes are not re‑scanned.
|
|
1072
1085
|
|
|
1073
1086
|
### Performance Options
|
|
@@ -1076,6 +1089,7 @@ See the full guide with scenarios, troubleshooting, and testing patterns: [Relat
|
|
|
1076
1089
|
- Single-pass scan (default) reduces `FindObjectsOfType` calls by scanning once and checking type ancestry.
|
|
1077
1090
|
- VContainer: `new RelationalSceneAssignmentOptions(includeInactive: true, useSinglePassScan: true)`
|
|
1078
1091
|
- Zenject: `new RelationalSceneAssignmentOptions(includeInactive: true, useSinglePassScan: true)`
|
|
1092
|
+
- Reflex: `new RelationalSceneAssignmentOptions(includeInactive: true, useSinglePassScan: true)`
|
|
1079
1093
|
- Per-object paths (instantiate/inject helpers, pools) avoid global scans entirely for objects created via DI.
|
|
1080
1094
|
|
|
1081
1095
|
---
|
|
@@ -2,6 +2,7 @@ namespace WallstopStudios.UnityHelpers.Core.Helper
|
|
|
2
2
|
{
|
|
3
3
|
using System;
|
|
4
4
|
using System.Collections.Concurrent;
|
|
5
|
+
using System.Threading.Tasks;
|
|
5
6
|
using UnityEngine;
|
|
6
7
|
using Utils;
|
|
7
8
|
#if UNITY_EDITOR
|
|
@@ -104,7 +105,8 @@ namespace WallstopStudios.UnityHelpers.Core.Helper
|
|
|
104
105
|
/// </summary>
|
|
105
106
|
public System.Threading.Tasks.Task RunAsync(Action action)
|
|
106
107
|
{
|
|
107
|
-
|
|
108
|
+
TaskCompletionSource<bool> tcs =
|
|
109
|
+
new System.Threading.Tasks.TaskCompletionSource<bool>();
|
|
108
110
|
RunOnMainThread(() =>
|
|
109
111
|
{
|
|
110
112
|
try
|
|
@@ -125,7 +127,7 @@ namespace WallstopStudios.UnityHelpers.Core.Helper
|
|
|
125
127
|
/// </summary>
|
|
126
128
|
public System.Threading.Tasks.Task<T> Post<T>(Func<T> func)
|
|
127
129
|
{
|
|
128
|
-
|
|
130
|
+
TaskCompletionSource<T> tcs = new System.Threading.Tasks.TaskCompletionSource<T>();
|
|
129
131
|
RunOnMainThread(() =>
|
|
130
132
|
{
|
|
131
133
|
try
|
|
@@ -4,6 +4,7 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
4
4
|
using System.Collections.Generic;
|
|
5
5
|
using System.ComponentModel;
|
|
6
6
|
using System.Globalization;
|
|
7
|
+
using System.Runtime.CompilerServices;
|
|
7
8
|
using System.Runtime.Serialization;
|
|
8
9
|
using System.Text.Json.Serialization;
|
|
9
10
|
using Core.Extension;
|
|
@@ -40,7 +41,6 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
40
41
|
/// </para>
|
|
41
42
|
/// </remarks>
|
|
42
43
|
[Serializable]
|
|
43
|
-
[ProtoContract]
|
|
44
44
|
public sealed class Attribute
|
|
45
45
|
: IEquatable<Attribute>,
|
|
46
46
|
IEquatable<float>,
|
|
@@ -91,6 +91,38 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
91
91
|
|
|
92
92
|
private bool _currentValueCalculated;
|
|
93
93
|
|
|
94
|
+
private readonly Dictionary<EffectHandle, List<AttributeModification>> _modifications =
|
|
95
|
+
new();
|
|
96
|
+
|
|
97
|
+
/// <summary>
|
|
98
|
+
/// Initializes a new instance of the <see cref="Attribute"/> class with a base value of 0.
|
|
99
|
+
/// </summary>
|
|
100
|
+
public Attribute()
|
|
101
|
+
: this(0) { }
|
|
102
|
+
|
|
103
|
+
/// <summary>
|
|
104
|
+
/// Initializes a new instance of the <see cref="Attribute"/> class with the specified base value.
|
|
105
|
+
/// </summary>
|
|
106
|
+
/// <param name="value">The base value for this attribute.</param>
|
|
107
|
+
public Attribute(float value)
|
|
108
|
+
{
|
|
109
|
+
_baseValue = value;
|
|
110
|
+
_currentValueCalculated = false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// <summary>
|
|
114
|
+
/// Initializes a new instance of the <see cref="Attribute"/> class for JSON deserialization.
|
|
115
|
+
/// </summary>
|
|
116
|
+
/// <param name="baseValue">The base value for this attribute.</param>
|
|
117
|
+
/// <param name="currentValue">The cached current value.</param>
|
|
118
|
+
[JsonConstructor]
|
|
119
|
+
public Attribute(float baseValue, float currentValue)
|
|
120
|
+
{
|
|
121
|
+
_baseValue = baseValue;
|
|
122
|
+
_currentValue = currentValue;
|
|
123
|
+
_currentValueCalculated = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
94
126
|
/// <summary>
|
|
95
127
|
/// Recalculates the current value by applying all active modifications to the base value.
|
|
96
128
|
/// Modifications are sorted and applied in order: Addition, Multiplication, then Override.
|
|
@@ -99,8 +131,9 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
99
131
|
{
|
|
100
132
|
float calculatedValue = _baseValue;
|
|
101
133
|
using PooledResource<List<AttributeModification>> modificationBuffer =
|
|
102
|
-
Buffers<AttributeModification>.List.Get(
|
|
103
|
-
|
|
134
|
+
Buffers<AttributeModification>.List.Get(
|
|
135
|
+
out List<AttributeModification> modifications
|
|
136
|
+
);
|
|
104
137
|
foreach (
|
|
105
138
|
KeyValuePair<EffectHandle, List<AttributeModification>> entry in _modifications
|
|
106
139
|
)
|
|
@@ -108,8 +141,7 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
108
141
|
modifications.AddRange(entry.Value);
|
|
109
142
|
}
|
|
110
143
|
|
|
111
|
-
modifications.Sort(
|
|
112
|
-
|
|
144
|
+
modifications.Sort();
|
|
113
145
|
foreach (AttributeModification attributeModification in modifications)
|
|
114
146
|
{
|
|
115
147
|
ApplyAttributeModification(attributeModification, ref calculatedValue);
|
|
@@ -119,9 +151,6 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
119
151
|
_currentValueCalculated = true;
|
|
120
152
|
}
|
|
121
153
|
|
|
122
|
-
private readonly Dictionary<EffectHandle, List<AttributeModification>> _modifications =
|
|
123
|
-
new();
|
|
124
|
-
|
|
125
154
|
/// <summary>
|
|
126
155
|
/// Implicitly converts an Attribute to its current float value.
|
|
127
156
|
/// </summary>
|
|
@@ -137,32 +166,112 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
137
166
|
public static implicit operator Attribute(float value) => new(value);
|
|
138
167
|
|
|
139
168
|
/// <summary>
|
|
140
|
-
///
|
|
169
|
+
/// Applies a temporary additive modification to the attribute.
|
|
141
170
|
/// </summary>
|
|
142
|
-
|
|
143
|
-
|
|
171
|
+
/// <param name="value">The amount to add to the attribute's calculated value.</param>
|
|
172
|
+
/// <returns>
|
|
173
|
+
/// An effect handle that can later be supplied to <see cref="RemoveAttributeModification(EffectHandle)"/>
|
|
174
|
+
/// to revoke this addition.
|
|
175
|
+
/// </returns>
|
|
176
|
+
/// <exception cref="ArgumentException">
|
|
177
|
+
/// Thrown when <paramref name="value"/> is not a finite number.
|
|
178
|
+
/// </exception>
|
|
179
|
+
public EffectHandle Add(float value)
|
|
180
|
+
{
|
|
181
|
+
ValidateInput(value);
|
|
182
|
+
|
|
183
|
+
EffectHandle handle = EffectHandle.CreateInstanceInternal();
|
|
184
|
+
AttributeModification modification = new()
|
|
185
|
+
{
|
|
186
|
+
action = ModificationAction.Addition,
|
|
187
|
+
value = value,
|
|
188
|
+
};
|
|
189
|
+
ApplyAttributeModification(modification, handle);
|
|
190
|
+
return handle;
|
|
191
|
+
}
|
|
144
192
|
|
|
145
193
|
/// <summary>
|
|
146
|
-
///
|
|
194
|
+
/// Applies a temporary subtractive modification to the attribute.
|
|
147
195
|
/// </summary>
|
|
148
|
-
/// <param name="value">The
|
|
149
|
-
|
|
196
|
+
/// <param name="value">The amount to subtract from the attribute's calculated value.</param>
|
|
197
|
+
/// <returns>
|
|
198
|
+
/// An effect handle that can later be supplied to <see cref="RemoveAttributeModification(EffectHandle)"/>
|
|
199
|
+
/// to revoke this subtraction.
|
|
200
|
+
/// </returns>
|
|
201
|
+
/// <exception cref="ArgumentException">
|
|
202
|
+
/// Thrown when <paramref name="value"/> is not a finite number.
|
|
203
|
+
/// </exception>
|
|
204
|
+
public EffectHandle Subtract(float value)
|
|
150
205
|
{
|
|
151
|
-
|
|
152
|
-
|
|
206
|
+
ValidateInput(value);
|
|
207
|
+
|
|
208
|
+
EffectHandle handle = EffectHandle.CreateInstanceInternal();
|
|
209
|
+
AttributeModification modification = new()
|
|
210
|
+
{
|
|
211
|
+
action = ModificationAction.Addition,
|
|
212
|
+
// Subtraction is represented as a negative additive modifier to preserve modifier ordering.
|
|
213
|
+
value = -value,
|
|
214
|
+
};
|
|
215
|
+
ApplyAttributeModification(modification, handle);
|
|
216
|
+
return handle;
|
|
153
217
|
}
|
|
154
218
|
|
|
155
219
|
/// <summary>
|
|
156
|
-
///
|
|
220
|
+
/// Applies a temporary division-based modification to the attribute.
|
|
157
221
|
/// </summary>
|
|
158
|
-
/// <param name="
|
|
159
|
-
///
|
|
160
|
-
|
|
161
|
-
|
|
222
|
+
/// <param name="value">
|
|
223
|
+
/// The divisor that will be applied to the attribute's calculated value.
|
|
224
|
+
/// </param>
|
|
225
|
+
/// <returns>
|
|
226
|
+
/// An effect handle that can later be supplied to <see cref="RemoveAttributeModification(EffectHandle)"/>
|
|
227
|
+
/// to revoke this division.
|
|
228
|
+
/// </returns>
|
|
229
|
+
/// <exception cref="ArgumentException">
|
|
230
|
+
/// Thrown when <paramref name="value"/> is zero or not a finite number.
|
|
231
|
+
/// </exception>
|
|
232
|
+
public EffectHandle Divide(float value)
|
|
162
233
|
{
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
234
|
+
ValidateInput(value);
|
|
235
|
+
|
|
236
|
+
if (value == 0f)
|
|
237
|
+
{
|
|
238
|
+
throw new ArgumentException("Cannot divide by zero.", nameof(value));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
EffectHandle handle = EffectHandle.CreateInstanceInternal();
|
|
242
|
+
AttributeModification modification = new()
|
|
243
|
+
{
|
|
244
|
+
action = ModificationAction.Multiplication,
|
|
245
|
+
// Apply division by multiplying by the reciprocal to maintain multiplication ordering guarantees.
|
|
246
|
+
value = 1f / value,
|
|
247
|
+
};
|
|
248
|
+
ApplyAttributeModification(modification, handle);
|
|
249
|
+
return handle;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// <summary>
|
|
253
|
+
/// Applies a temporary multiplicative modification to the attribute.
|
|
254
|
+
/// </summary>
|
|
255
|
+
/// <param name="value">The multiplier to apply to the attribute's calculated value.</param>
|
|
256
|
+
/// <returns>
|
|
257
|
+
/// An effect handle that can later be supplied to <see cref="RemoveAttributeModification(EffectHandle)"/>
|
|
258
|
+
/// to revoke this multiplication.
|
|
259
|
+
/// </returns>
|
|
260
|
+
/// <exception cref="ArgumentException">
|
|
261
|
+
/// Thrown when <paramref name="value"/> is not a finite number.
|
|
262
|
+
/// </exception>
|
|
263
|
+
public EffectHandle Multiply(float value)
|
|
264
|
+
{
|
|
265
|
+
ValidateInput(value);
|
|
266
|
+
|
|
267
|
+
EffectHandle handle = EffectHandle.CreateInstanceInternal();
|
|
268
|
+
AttributeModification modification = new()
|
|
269
|
+
{
|
|
270
|
+
action = ModificationAction.Multiplication,
|
|
271
|
+
value = value,
|
|
272
|
+
};
|
|
273
|
+
ApplyAttributeModification(modification, handle);
|
|
274
|
+
return handle;
|
|
166
275
|
}
|
|
167
276
|
|
|
168
277
|
/// <summary>
|
|
@@ -173,6 +282,17 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
173
282
|
_currentValueCalculated = false;
|
|
174
283
|
}
|
|
175
284
|
|
|
285
|
+
private static void ValidateInput(float value, [CallerMemberName] string caller = null)
|
|
286
|
+
{
|
|
287
|
+
if (!float.IsFinite(value))
|
|
288
|
+
{
|
|
289
|
+
throw new ArgumentException(
|
|
290
|
+
$"Cannot {caller?.ToLowerInvariant()} by infinity or NaN.",
|
|
291
|
+
nameof(value)
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
176
296
|
/// <summary>
|
|
177
297
|
/// Applies an attribute modification to this attribute.
|
|
178
298
|
/// If a handle is provided, the modification is temporary and can be removed.
|