com.wallstop-studios.unity-helpers 2.0.4 → 2.1.1
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/ILIST_SORTING_PERFORMANCE.md +92 -0
- package/Docs/ILIST_SORTING_PERFORMANCE.md.meta +7 -0
- package/Docs/INDEX.md +10 -1
- package/Docs/Images/random_generators.svg +7 -7
- package/Docs/RANDOM_PERFORMANCE.md +18 -15
- 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_2D_PERFORMANCE.md +64 -64
- package/Docs/SPATIAL_TREE_3D_PERFORMANCE.md +64 -64
- package/Docs/SPATIAL_TREE_SEMANTICS.md +7 -7
- package/Editor/Core/Helper/AnimationEventHelpers.cs +1 -1
- package/Editor/CustomDrawers/WShowIfPropertyDrawer.cs +131 -41
- package/Editor/Utils/ScriptableObjectSingletonCreator.cs +175 -18
- package/README.md +42 -18
- package/Runtime/Core/Extension/IListExtensions.cs +720 -12
- package/Runtime/Core/Helper/UnityMainThreadDispatcher.cs +2 -3
- package/Runtime/Core/Random/AbstractRandom.cs +52 -5
- package/Runtime/Core/Random/DotNetRandom.cs +3 -3
- package/Runtime/Core/Random/FlurryBurstRandom.cs +285 -0
- package/Runtime/Core/Random/FlurryBurstRandom.cs.meta +3 -0
- package/Runtime/Core/Random/IllusionFlow.cs +3 -3
- package/Runtime/Core/Random/LinearCongruentialGenerator.cs +3 -3
- package/Runtime/Core/Random/PcgRandom.cs +6 -6
- package/Runtime/Core/Random/PhotonSpinRandom.cs +387 -0
- package/Runtime/Core/Random/PhotonSpinRandom.cs.meta +3 -0
- package/Runtime/Core/Random/RomuDuo.cs +3 -3
- package/Runtime/Core/Random/SplitMix64.cs +3 -3
- package/Runtime/Core/Random/SquirrelRandom.cs +6 -4
- package/Runtime/Core/Random/StormDropRandom.cs +271 -0
- package/Runtime/Core/Random/StormDropRandom.cs.meta +3 -0
- package/Runtime/Core/Random/UnityRandom.cs +3 -3
- package/Runtime/Core/Random/WyRandom.cs +6 -4
- package/Runtime/Core/Random/XorShiftRandom.cs +3 -3
- package/Runtime/Core/Random/XoroShiroRandom.cs +3 -3
- package/Runtime/Tags/Attribute.cs +144 -24
- package/Runtime/Tags/AttributeEffect.cs +119 -16
- package/Runtime/Tags/AttributeMetadataCache.cs +312 -3
- package/Runtime/Tags/AttributeModification.cs +59 -29
- 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 +385 -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/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/Tags/AttributeMetadataCacheTests.cs +192 -0
- package/Tests/Editor/Tags/AttributeMetadataCacheTests.cs.meta +11 -0
- package/{node_modules.meta → Tests/Editor/Tags.meta} +1 -1
- package/Tests/Editor/Utils/ScriptableObjectSingletonTests.cs +41 -0
- package/Tests/Runtime/Extensions/IListExtensionTests.cs +187 -1
- package/Tests/Runtime/Helper/ObjectsTests.cs +3 -3
- package/Tests/Runtime/Integrations/Reflex/RelationalComponentsReflexTests.cs +2 -2
- package/Tests/Runtime/Performance/IListSortingPerformanceTests.cs +346 -0
- package/Tests/Runtime/Performance/IListSortingPerformanceTests.cs.meta +11 -0
- package/Tests/Runtime/Performance/RandomPerformanceTests.cs +3 -0
- package/Tests/Runtime/Random/FlurryBurstRandomTests.cs +12 -0
- package/Tests/Runtime/Random/FlurryBurstRandomTests.cs.meta +3 -0
- package/Tests/Runtime/Random/PhotonSpinRandomTests.cs +12 -0
- package/Tests/Runtime/Random/PhotonSpinRandomTests.cs.meta +3 -0
- package/Tests/Runtime/Random/RandomProtoSerializationTests.cs +14 -0
- package/Tests/Runtime/Random/RandomTestBase.cs +39 -4
- package/Tests/Runtime/Random/StormDropRandomTests.cs +12 -0
- package/Tests/Runtime/Random/StormDropRandomTests.cs.meta +3 -0
- package/Tests/Runtime/Serialization/JsonSerializationTest.cs +4 -3
- package/Tests/Runtime/Serialization/ProtoInterfaceResolutionEdgeTests.cs +2 -2
- package/Tests/Runtime/Serialization/ProtoRootRegistrationTests.cs +1 -1
- package/Tests/Runtime/Serialization/ProtoSerializeBehaviorTests.cs +1 -1
- 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/EffectBehaviorTests.cs +184 -0
- package/Tests/Runtime/Tags/EffectBehaviorTests.cs.meta +3 -0
- package/Tests/Runtime/Tags/EffectHandlerTests.cs +618 -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/package.json +1 -1
- package/scripts/lint-doc-links.ps1 +156 -11
- package/Tests/Runtime/Tags/AttributeDataTests.cs +0 -312
- /package/Tests/Runtime/Tags/{AttributeDataTests.cs.meta → AttributeModificationTests.cs.meta} +0 -0
|
@@ -9,11 +9,61 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
9
9
|
using Core.Extension;
|
|
10
10
|
using Core.Helper;
|
|
11
11
|
using UnityEngine;
|
|
12
|
+
using WallstopStudios.UnityHelpers.Core.Attributes;
|
|
12
13
|
using WallstopStudios.UnityHelpers.Utils;
|
|
13
14
|
#if ODIN_INSPECTOR
|
|
14
15
|
using Sirenix.OdinInspector;
|
|
15
16
|
#endif
|
|
16
17
|
|
|
18
|
+
/// <summary>
|
|
19
|
+
/// Determines which handles are considered the "same stack" when evaluating stacking policies.
|
|
20
|
+
/// </summary>
|
|
21
|
+
public enum EffectStackGroup
|
|
22
|
+
{
|
|
23
|
+
[Obsolete("Please use a valid EffectStackGroup instead.")]
|
|
24
|
+
None = 0,
|
|
25
|
+
|
|
26
|
+
/// <summary>
|
|
27
|
+
/// Uses the effect asset reference. Each ScriptableObject instance is its own group.
|
|
28
|
+
/// </summary>
|
|
29
|
+
Reference = 1,
|
|
30
|
+
|
|
31
|
+
/// <summary>
|
|
32
|
+
/// Uses a custom string key supplied via <see cref="AttributeEffect.stackGroupKey"/>.
|
|
33
|
+
/// Effects with matching keys share a stack regardless of asset reference.
|
|
34
|
+
/// </summary>
|
|
35
|
+
CustomKey = 2,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// <summary>
|
|
39
|
+
/// Describes how additional applications of an effect interact with existing stacks.
|
|
40
|
+
/// </summary>
|
|
41
|
+
public enum EffectStackingMode
|
|
42
|
+
{
|
|
43
|
+
[Obsolete("Please use a valid EffectStackingMode instead.")]
|
|
44
|
+
None = 0,
|
|
45
|
+
|
|
46
|
+
/// <summary>
|
|
47
|
+
/// Always create a new stack (subject to optional stack limit).
|
|
48
|
+
/// </summary>
|
|
49
|
+
Stack = 1,
|
|
50
|
+
|
|
51
|
+
/// <summary>
|
|
52
|
+
/// Reuse the first existing stack and refresh duration if possible.
|
|
53
|
+
/// </summary>
|
|
54
|
+
Refresh = 2,
|
|
55
|
+
|
|
56
|
+
/// <summary>
|
|
57
|
+
/// Remove existing stacks sharing the same group before creating a new one.
|
|
58
|
+
/// </summary>
|
|
59
|
+
Replace = 3,
|
|
60
|
+
|
|
61
|
+
/// <summary>
|
|
62
|
+
/// Ignore new applications when a stack is already active.
|
|
63
|
+
/// </summary>
|
|
64
|
+
Ignore = 4,
|
|
65
|
+
}
|
|
66
|
+
|
|
17
67
|
/// <summary>
|
|
18
68
|
/// Reusable, data‑driven bundle of stat modifications, tags, and cosmetic feedback.
|
|
19
69
|
/// Serves as the authoring unit for buffs, debuffs, and status effects.
|
|
@@ -76,22 +126,29 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
76
126
|
/// </summary>
|
|
77
127
|
public List<AttributeModification> modifications = new();
|
|
78
128
|
|
|
129
|
+
/// <summary>
|
|
130
|
+
/// Periodic modifier sets executed on a cadence while the effect remains active.
|
|
131
|
+
/// </summary>
|
|
132
|
+
public List<PeriodicEffectDefinition> periodicEffects = new();
|
|
133
|
+
|
|
79
134
|
/// <summary>
|
|
80
135
|
/// Specifies how long this effect should persist (Instant, Duration, or Infinite).
|
|
81
136
|
/// </summary>
|
|
82
137
|
public ModifierDurationType durationType = ModifierDurationType.Duration;
|
|
83
138
|
|
|
84
|
-
#if ODIN_INSPECTOR
|
|
85
|
-
[ShowIf("@durationType == ModifierDurationType.Duration")]
|
|
86
|
-
#endif
|
|
87
139
|
/// <summary>
|
|
88
140
|
/// The duration in seconds for this effect. Only used when <see cref="durationType"/> is <see cref="ModifierDurationType.Duration"/>.
|
|
89
141
|
/// </summary>
|
|
90
|
-
public float duration;
|
|
91
|
-
|
|
92
142
|
#if ODIN_INSPECTOR
|
|
93
143
|
[ShowIf("@durationType == ModifierDurationType.Duration")]
|
|
144
|
+
#else
|
|
145
|
+
[WShowIf(
|
|
146
|
+
nameof(durationType),
|
|
147
|
+
expectedValues: new object[] { ModifierDurationType.Duration }
|
|
148
|
+
)]
|
|
94
149
|
#endif
|
|
150
|
+
public float duration;
|
|
151
|
+
|
|
95
152
|
/// <summary>
|
|
96
153
|
/// If true, reapplying this effect while it's already active will reset the duration timer.
|
|
97
154
|
/// Only used when <see cref="durationType"/> is <see cref="ModifierDurationType.Duration"/>.
|
|
@@ -100,6 +157,14 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
100
157
|
/// A poison effect with resetDurationOnReapplication=true will restart its 5-second timer
|
|
101
158
|
/// each time the poison is reapplied, preventing stacking but extending the effect.
|
|
102
159
|
/// </example>
|
|
160
|
+
#if ODIN_INSPECTOR
|
|
161
|
+
[ShowIf("@durationType == ModifierDurationType.Duration")]
|
|
162
|
+
#else
|
|
163
|
+
[WShowIf(
|
|
164
|
+
nameof(durationType),
|
|
165
|
+
expectedValues: new object[] { ModifierDurationType.Duration }
|
|
166
|
+
)]
|
|
167
|
+
#endif
|
|
103
168
|
public bool resetDurationOnReapplication;
|
|
104
169
|
|
|
105
170
|
/// <summary>
|
|
@@ -112,6 +177,41 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
112
177
|
/// </example>
|
|
113
178
|
public List<string> effectTags = new();
|
|
114
179
|
|
|
180
|
+
/// <summary>
|
|
181
|
+
/// A list of cosmetic effect data that defines visual and audio feedback for this effect.
|
|
182
|
+
/// These are applied when the effect becomes active and removed when it expires.
|
|
183
|
+
/// </summary>
|
|
184
|
+
[JsonIgnore]
|
|
185
|
+
public List<CosmeticEffectData> cosmeticEffects = new();
|
|
186
|
+
|
|
187
|
+
/// <summary>
|
|
188
|
+
/// Custom behaviours instantiated per active handle.
|
|
189
|
+
/// </summary>
|
|
190
|
+
[JsonIgnore]
|
|
191
|
+
public List<EffectBehavior> behaviors = new();
|
|
192
|
+
|
|
193
|
+
/// <summary>
|
|
194
|
+
/// Determines how this effect groups stacks for stacking decisions.
|
|
195
|
+
/// </summary>
|
|
196
|
+
public EffectStackGroup stackGroup = EffectStackGroup.Reference;
|
|
197
|
+
|
|
198
|
+
/// <summary>
|
|
199
|
+
/// Optional stack key used when <see cref="stackGroup"/> is set to <see cref="EffectStackGroup.CustomKey"/>.
|
|
200
|
+
/// </summary>
|
|
201
|
+
public string stackGroupKey;
|
|
202
|
+
|
|
203
|
+
/// <summary>
|
|
204
|
+
/// Determines how successive applications interact with existing stacks for the same group.
|
|
205
|
+
/// </summary>
|
|
206
|
+
public EffectStackingMode stackingMode = EffectStackingMode.Refresh;
|
|
207
|
+
|
|
208
|
+
/// <summary>
|
|
209
|
+
/// Optional cap on simultaneous stacks when <see cref="stackingMode"/> is <see cref="EffectStackingMode.Stack"/>.
|
|
210
|
+
/// A value of 0 means unlimited stacks.
|
|
211
|
+
/// </summary>
|
|
212
|
+
[Min(0)]
|
|
213
|
+
public int maximumStacks;
|
|
214
|
+
|
|
115
215
|
/// <summary>
|
|
116
216
|
/// Determines whether this effect applies the specified tag.
|
|
117
217
|
/// </summary>
|
|
@@ -272,17 +372,6 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
272
372
|
return buffer;
|
|
273
373
|
}
|
|
274
374
|
|
|
275
|
-
/// <summary>
|
|
276
|
-
/// A list of cosmetic effect data that defines visual and audio feedback for this effect.
|
|
277
|
-
/// These are applied when the effect becomes active and removed when it expires.
|
|
278
|
-
/// </summary>
|
|
279
|
-
[JsonIgnore]
|
|
280
|
-
public List<CosmeticEffectData> cosmeticEffects = new();
|
|
281
|
-
|
|
282
|
-
private List<string> CosmeticEffectsForJson =>
|
|
283
|
-
cosmeticEffects?.Select(cosmeticEffectData => cosmeticEffectData.name).ToList()
|
|
284
|
-
?? new List<string>(0);
|
|
285
|
-
|
|
286
375
|
/// <summary>
|
|
287
376
|
/// Converts this effect to a JSON string representation including all modifications, tags, and cosmetic effects.
|
|
288
377
|
/// </summary>
|
|
@@ -300,6 +389,10 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
300
389
|
}.ToJson();
|
|
301
390
|
}
|
|
302
391
|
|
|
392
|
+
private List<string> CosmeticEffectsForJson =>
|
|
393
|
+
cosmeticEffects?.Select(cosmeticEffectData => cosmeticEffectData.name).ToList()
|
|
394
|
+
?? new List<string>(0);
|
|
395
|
+
|
|
303
396
|
private string BuildDescription()
|
|
304
397
|
{
|
|
305
398
|
if (modifications == null)
|
|
@@ -382,6 +475,16 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
382
475
|
return descriptionBuilder.ToString();
|
|
383
476
|
}
|
|
384
477
|
|
|
478
|
+
internal EffectStackKey GetStackKey()
|
|
479
|
+
{
|
|
480
|
+
if (stackGroup == EffectStackGroup.CustomKey && !string.IsNullOrEmpty(stackGroupKey))
|
|
481
|
+
{
|
|
482
|
+
return EffectStackKey.CreateCustom(stackGroupKey);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return EffectStackKey.CreateReference(this);
|
|
486
|
+
}
|
|
487
|
+
|
|
385
488
|
/// <summary>
|
|
386
489
|
/// Determines whether this effect is equal to another effect by comparing all fields.
|
|
387
490
|
/// This is needed because deserialization creates new instances, so reference equality is insufficient.
|
|
@@ -484,9 +484,15 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
484
484
|
RelationalTypeMetadata[] relationalTypeMetadata
|
|
485
485
|
)
|
|
486
486
|
{
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
487
|
+
string[] normalizedAttributeNames = SortAttributeNames(allAttributeNames);
|
|
488
|
+
TypeFieldMetadata[] normalizedTypeMetadata = SortTypeMetadata(typeMetadata);
|
|
489
|
+
RelationalTypeMetadata[] normalizedRelationalMetadata = SortRelationalTypeMetadata(
|
|
490
|
+
relationalTypeMetadata
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
_allAttributeNames = normalizedAttributeNames;
|
|
494
|
+
_typeMetadata = normalizedTypeMetadata;
|
|
495
|
+
_relationalTypeMetadata = normalizedRelationalMetadata;
|
|
490
496
|
_computedAllAttributeNames = null;
|
|
491
497
|
_computedAllAttributeNamesIncludesTests = false;
|
|
492
498
|
_typeFieldsLookup = null;
|
|
@@ -495,6 +501,309 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
495
501
|
_elementTypeLookup = null;
|
|
496
502
|
UnityEditor.EditorUtility.SetDirty(this);
|
|
497
503
|
}
|
|
504
|
+
|
|
505
|
+
private static string[] SortAttributeNames(string[] attributeNames)
|
|
506
|
+
{
|
|
507
|
+
if (attributeNames == null || attributeNames.Length == 0)
|
|
508
|
+
{
|
|
509
|
+
return Array.Empty<string>();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
string[] result = new string[attributeNames.Length];
|
|
513
|
+
Array.Copy(attributeNames, result, attributeNames.Length);
|
|
514
|
+
Array.Sort(result, StringComparer.Ordinal);
|
|
515
|
+
return result;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private static TypeFieldMetadata[] SortTypeMetadata(TypeFieldMetadata[] typeMetadata)
|
|
519
|
+
{
|
|
520
|
+
if (typeMetadata == null || typeMetadata.Length == 0)
|
|
521
|
+
{
|
|
522
|
+
return Array.Empty<TypeFieldMetadata>();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
int nonNullCount = 0;
|
|
526
|
+
for (int i = 0; i < typeMetadata.Length; i++)
|
|
527
|
+
{
|
|
528
|
+
if (typeMetadata[i] != null)
|
|
529
|
+
{
|
|
530
|
+
nonNullCount++;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (nonNullCount == 0)
|
|
535
|
+
{
|
|
536
|
+
return Array.Empty<TypeFieldMetadata>();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
TypeFieldMetadata[] result = new TypeFieldMetadata[nonNullCount];
|
|
540
|
+
int resultIndex = 0;
|
|
541
|
+
for (int i = 0; i < typeMetadata.Length; i++)
|
|
542
|
+
{
|
|
543
|
+
TypeFieldMetadata metadata = typeMetadata[i];
|
|
544
|
+
if (metadata == null)
|
|
545
|
+
{
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
string typeName = metadata.typeName ?? string.Empty;
|
|
550
|
+
string[] fieldNames = metadata.fieldNames ?? Array.Empty<string>();
|
|
551
|
+
string[] sortedFieldNames =
|
|
552
|
+
fieldNames.Length == 0 ? Array.Empty<string>() : CopyAndSort(fieldNames);
|
|
553
|
+
|
|
554
|
+
result[resultIndex] = new TypeFieldMetadata(typeName, sortedFieldNames);
|
|
555
|
+
resultIndex++;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
Array.Sort(result, CompareTypeFieldMetadata);
|
|
559
|
+
return result;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private static RelationalTypeMetadata[] SortRelationalTypeMetadata(
|
|
563
|
+
RelationalTypeMetadata[] relationalTypeMetadata
|
|
564
|
+
)
|
|
565
|
+
{
|
|
566
|
+
if (relationalTypeMetadata == null || relationalTypeMetadata.Length == 0)
|
|
567
|
+
{
|
|
568
|
+
return Array.Empty<RelationalTypeMetadata>();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
int nonNullCount = 0;
|
|
572
|
+
for (int i = 0; i < relationalTypeMetadata.Length; i++)
|
|
573
|
+
{
|
|
574
|
+
if (relationalTypeMetadata[i] != null)
|
|
575
|
+
{
|
|
576
|
+
nonNullCount++;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (nonNullCount == 0)
|
|
581
|
+
{
|
|
582
|
+
return Array.Empty<RelationalTypeMetadata>();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
RelationalTypeMetadata[] result = new RelationalTypeMetadata[nonNullCount];
|
|
586
|
+
int resultIndex = 0;
|
|
587
|
+
for (int i = 0; i < relationalTypeMetadata.Length; i++)
|
|
588
|
+
{
|
|
589
|
+
RelationalTypeMetadata metadata = relationalTypeMetadata[i];
|
|
590
|
+
if (metadata == null)
|
|
591
|
+
{
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
string typeName = metadata.typeName ?? string.Empty;
|
|
596
|
+
RelationalFieldMetadata[] sortedFields = SortRelationalFields(metadata.fields);
|
|
597
|
+
result[resultIndex] = new RelationalTypeMetadata(typeName, sortedFields);
|
|
598
|
+
resultIndex++;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
Array.Sort(result, CompareRelationalTypeMetadata);
|
|
602
|
+
return result;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
private static RelationalFieldMetadata[] SortRelationalFields(
|
|
606
|
+
RelationalFieldMetadata[] relationalFields
|
|
607
|
+
)
|
|
608
|
+
{
|
|
609
|
+
if (relationalFields == null || relationalFields.Length == 0)
|
|
610
|
+
{
|
|
611
|
+
return Array.Empty<RelationalFieldMetadata>();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
int nonNullCount = 0;
|
|
615
|
+
for (int i = 0; i < relationalFields.Length; i++)
|
|
616
|
+
{
|
|
617
|
+
if (relationalFields[i] != null)
|
|
618
|
+
{
|
|
619
|
+
nonNullCount++;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (nonNullCount == 0)
|
|
624
|
+
{
|
|
625
|
+
return Array.Empty<RelationalFieldMetadata>();
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
RelationalFieldMetadata[] result = new RelationalFieldMetadata[nonNullCount];
|
|
629
|
+
int resultIndex = 0;
|
|
630
|
+
for (int i = 0; i < relationalFields.Length; i++)
|
|
631
|
+
{
|
|
632
|
+
RelationalFieldMetadata field = relationalFields[i];
|
|
633
|
+
if (field == null)
|
|
634
|
+
{
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
result[resultIndex] = new RelationalFieldMetadata(
|
|
639
|
+
field.fieldName,
|
|
640
|
+
field.attributeKind,
|
|
641
|
+
field.fieldKind,
|
|
642
|
+
field.elementTypeName,
|
|
643
|
+
field.isInterface
|
|
644
|
+
);
|
|
645
|
+
resultIndex++;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
Array.Sort(result, CompareRelationalFieldMetadata);
|
|
649
|
+
return result;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private static string[] CopyAndSort(string[] values)
|
|
653
|
+
{
|
|
654
|
+
string[] result = new string[values.Length];
|
|
655
|
+
Array.Copy(values, result, values.Length);
|
|
656
|
+
Array.Sort(result, StringComparer.Ordinal);
|
|
657
|
+
return result;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private static int CompareTypeFieldMetadata(TypeFieldMetadata left, TypeFieldMetadata right)
|
|
661
|
+
{
|
|
662
|
+
if (ReferenceEquals(left, right))
|
|
663
|
+
{
|
|
664
|
+
return 0;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (left == null)
|
|
668
|
+
{
|
|
669
|
+
return -1;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (right == null)
|
|
673
|
+
{
|
|
674
|
+
return 1;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
int typeNameComparison = string.CompareOrdinal(left.typeName, right.typeName);
|
|
678
|
+
if (typeNameComparison != 0)
|
|
679
|
+
{
|
|
680
|
+
return typeNameComparison;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
string[] leftFields = left.fieldNames ?? Array.Empty<string>();
|
|
684
|
+
string[] rightFields = right.fieldNames ?? Array.Empty<string>();
|
|
685
|
+
|
|
686
|
+
int lengthComparison = leftFields.Length.CompareTo(rightFields.Length);
|
|
687
|
+
if (lengthComparison != 0)
|
|
688
|
+
{
|
|
689
|
+
return lengthComparison;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
for (int i = 0; i < leftFields.Length; i++)
|
|
693
|
+
{
|
|
694
|
+
int fieldComparison = string.CompareOrdinal(leftFields[i], rightFields[i]);
|
|
695
|
+
if (fieldComparison != 0)
|
|
696
|
+
{
|
|
697
|
+
return fieldComparison;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return 0;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
private static int CompareRelationalTypeMetadata(
|
|
705
|
+
RelationalTypeMetadata left,
|
|
706
|
+
RelationalTypeMetadata right
|
|
707
|
+
)
|
|
708
|
+
{
|
|
709
|
+
if (ReferenceEquals(left, right))
|
|
710
|
+
{
|
|
711
|
+
return 0;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (left == null)
|
|
715
|
+
{
|
|
716
|
+
return -1;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (right == null)
|
|
720
|
+
{
|
|
721
|
+
return 1;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
int typeNameComparison = string.CompareOrdinal(left.typeName, right.typeName);
|
|
725
|
+
if (typeNameComparison != 0)
|
|
726
|
+
{
|
|
727
|
+
return typeNameComparison;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
RelationalFieldMetadata[] leftFields =
|
|
731
|
+
left.fields ?? Array.Empty<RelationalFieldMetadata>();
|
|
732
|
+
RelationalFieldMetadata[] rightFields =
|
|
733
|
+
right.fields ?? Array.Empty<RelationalFieldMetadata>();
|
|
734
|
+
|
|
735
|
+
int lengthComparison = leftFields.Length.CompareTo(rightFields.Length);
|
|
736
|
+
if (lengthComparison != 0)
|
|
737
|
+
{
|
|
738
|
+
return lengthComparison;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
for (int i = 0; i < leftFields.Length; i++)
|
|
742
|
+
{
|
|
743
|
+
int fieldComparison = CompareRelationalFieldMetadata(leftFields[i], rightFields[i]);
|
|
744
|
+
if (fieldComparison != 0)
|
|
745
|
+
{
|
|
746
|
+
return fieldComparison;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return 0;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private static int CompareRelationalFieldMetadata(
|
|
754
|
+
RelationalFieldMetadata left,
|
|
755
|
+
RelationalFieldMetadata right
|
|
756
|
+
)
|
|
757
|
+
{
|
|
758
|
+
if (ReferenceEquals(left, right))
|
|
759
|
+
{
|
|
760
|
+
return 0;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (left == null)
|
|
764
|
+
{
|
|
765
|
+
return -1;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (right == null)
|
|
769
|
+
{
|
|
770
|
+
return 1;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
int fieldNameComparison = string.CompareOrdinal(left.fieldName, right.fieldName);
|
|
774
|
+
if (fieldNameComparison != 0)
|
|
775
|
+
{
|
|
776
|
+
return fieldNameComparison;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
int attributeComparison = left.attributeKind.CompareTo(right.attributeKind);
|
|
780
|
+
if (attributeComparison != 0)
|
|
781
|
+
{
|
|
782
|
+
return attributeComparison;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
int fieldKindComparison = left.fieldKind.CompareTo(right.fieldKind);
|
|
786
|
+
if (fieldKindComparison != 0)
|
|
787
|
+
{
|
|
788
|
+
return fieldKindComparison;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
int elementTypeComparison = string.CompareOrdinal(
|
|
792
|
+
left.elementTypeName,
|
|
793
|
+
right.elementTypeName
|
|
794
|
+
);
|
|
795
|
+
if (elementTypeComparison != 0)
|
|
796
|
+
{
|
|
797
|
+
return elementTypeComparison;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (left.isInterface == right.isInterface)
|
|
801
|
+
{
|
|
802
|
+
return 0;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return left.isInterface ? -1 : 1;
|
|
806
|
+
}
|
|
498
807
|
#endif
|
|
499
808
|
}
|
|
500
809
|
}
|
|
@@ -1,62 +1,68 @@
|
|
|
1
1
|
namespace WallstopStudios.UnityHelpers.Tags
|
|
2
2
|
{
|
|
3
3
|
using System;
|
|
4
|
+
using System.Text.Json.Serialization;
|
|
4
5
|
using Core.Extension;
|
|
5
6
|
using Core.Helper;
|
|
7
|
+
using ProtoBuf;
|
|
6
8
|
|
|
7
9
|
/// <summary>
|
|
8
|
-
/// Declarative change to an <see cref="Attribute"
|
|
9
|
-
///
|
|
10
|
+
/// Declarative change applied to an <see cref="Attribute"/>.
|
|
11
|
+
/// Each instance represents a single operation (add, multiply, or override) referenced by an <see cref="AttributeEffect"/>.
|
|
10
12
|
/// </summary>
|
|
11
13
|
/// <remarks>
|
|
14
|
+
/// <para>Key properties:</para>
|
|
15
|
+
/// <list type="bullet">
|
|
16
|
+
/// <item><description>Non-destructive: temporary handles can add/remove modifications without mutating base values.</description></item>
|
|
17
|
+
/// <item><description>Deterministic ordering: <see cref="Attribute"/> always processes Addition, then Multiplication, then Override.</description></item>
|
|
18
|
+
/// <item><description>Flexible authoring: supports both instant (permanent) and duration-based effects.</description></item>
|
|
19
|
+
/// </list>
|
|
20
|
+
/// <para>Stack processing order:</para>
|
|
21
|
+
/// <list type="number">
|
|
22
|
+
/// <item><description>Addition (value += x)</description></item>
|
|
23
|
+
/// <item><description>Multiplication (value *= x)</description></item>
|
|
24
|
+
/// <item><description>Override (value = x)</description></item>
|
|
25
|
+
/// </list>
|
|
12
26
|
/// <para>
|
|
13
|
-
///
|
|
14
|
-
///
|
|
15
|
-
/// - Clear stacking rules via action ordering
|
|
16
|
-
/// - Works with both permanent (Instant) and temporary (Duration/Infinite) effects
|
|
27
|
+
/// The <see cref="attribute"/> field must match an <see cref="Attribute"/> field on the target <see cref="AttributesComponent"/>.
|
|
28
|
+
/// The Attribute Metadata Cache generator can provide editor dropdowns to avoid typos. Unknown names are ignored at runtime.
|
|
17
29
|
/// </para>
|
|
18
|
-
/// <para>
|
|
19
|
-
/// Stacking and order: Modifications are applied in this order across a target attribute:
|
|
20
|
-
/// 1) Addition (value += x) → 2) Multiplication (value *= x) → 3) Override (value = x).
|
|
21
|
-
/// This means Overrides always win last; use with care.
|
|
22
|
-
/// </para>
|
|
23
|
-
/// <para>
|
|
24
|
-
/// Addressing: The <see cref="attribute"/> field names an <see cref="AttributesComponent"/> field of type
|
|
25
|
-
/// <see cref="Attribute"/>. Misspelled or missing names are ignored at runtime to keep effects robust.
|
|
26
|
-
/// Use the Attribute Metadata Cache generator to populate editor dropdowns and avoid typos.
|
|
27
|
-
/// </para>
|
|
28
|
-
/// <para>
|
|
29
|
-
/// Examples:
|
|
30
|
+
/// <para>Sample definitions:</para>
|
|
30
31
|
/// <code>
|
|
31
|
-
/// // +50 flat
|
|
32
|
+
/// // +50 flat health
|
|
32
33
|
/// new AttributeModification { attribute = "Health", action = ModificationAction.Addition, value = 50f };
|
|
33
34
|
///
|
|
34
|
-
/// // +50%
|
|
35
|
+
/// // +50% speed
|
|
35
36
|
/// new AttributeModification { attribute = "Speed", action = ModificationAction.Multiplication, value = 1.5f };
|
|
36
37
|
///
|
|
37
|
-
/// //
|
|
38
|
+
/// // Hard-set defense to 0
|
|
38
39
|
/// new AttributeModification { attribute = "Defense", action = ModificationAction.Override, value = 0f };
|
|
39
40
|
/// </code>
|
|
40
|
-
///
|
|
41
|
-
/// <
|
|
42
|
-
///
|
|
43
|
-
///
|
|
44
|
-
///
|
|
45
|
-
///
|
|
46
|
-
/// </para>
|
|
41
|
+
/// <para>Authoring tips:</para>
|
|
42
|
+
/// <list type="bullet">
|
|
43
|
+
/// <item><description>Use Addition for flat buffs/debuffs; Multiplication for percentage-style adjustments.</description></item>
|
|
44
|
+
/// <item><description>Reserve Override for hard clamps (it always executes last).</description></item>
|
|
45
|
+
/// <item><description>Negative Addition subtracts; Multiplication values below 1.0 reduce the attribute.</description></item>
|
|
46
|
+
/// </list>
|
|
47
47
|
/// </remarks>
|
|
48
48
|
[Serializable]
|
|
49
|
-
|
|
49
|
+
[ProtoContract]
|
|
50
|
+
public struct AttributeModification
|
|
51
|
+
: IEquatable<AttributeModification>,
|
|
52
|
+
IComparable<AttributeModification>,
|
|
53
|
+
IComparable
|
|
50
54
|
{
|
|
51
55
|
/// <summary>
|
|
52
56
|
/// The name of the attribute to modify. This should match a field name in an <see cref="AttributesComponent"/> subclass.
|
|
53
57
|
/// </summary>
|
|
54
58
|
[StringInList(typeof(AttributeUtilities), nameof(AttributeUtilities.GetAllAttributeNames))]
|
|
59
|
+
[ProtoMember(1)]
|
|
55
60
|
public string attribute;
|
|
56
61
|
|
|
57
62
|
/// <summary>
|
|
58
63
|
/// The type of modification action to perform (Addition, Multiplication, or Override).
|
|
59
64
|
/// </summary>
|
|
65
|
+
[ProtoMember(2)]
|
|
60
66
|
public ModificationAction action;
|
|
61
67
|
|
|
62
68
|
/// <summary>
|
|
@@ -65,8 +71,17 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
65
71
|
/// <para>- Multiplication: The multiplier to apply (e.g., 1.5 for +50%, 0.5 for -50%)</para>
|
|
66
72
|
/// <para>- Override: The new absolute value to set</para>
|
|
67
73
|
/// </summary>
|
|
74
|
+
[ProtoMember(3)]
|
|
68
75
|
public float value;
|
|
69
76
|
|
|
77
|
+
[JsonConstructor]
|
|
78
|
+
public AttributeModification(string attribute, ModificationAction action, float value)
|
|
79
|
+
{
|
|
80
|
+
this.attribute = attribute;
|
|
81
|
+
this.action = action;
|
|
82
|
+
this.value = value;
|
|
83
|
+
}
|
|
84
|
+
|
|
70
85
|
/// <summary>
|
|
71
86
|
/// Converts this modification to a JSON string representation.
|
|
72
87
|
/// </summary>
|
|
@@ -76,6 +91,21 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
76
91
|
return this.ToJson();
|
|
77
92
|
}
|
|
78
93
|
|
|
94
|
+
public int CompareTo(object obj)
|
|
95
|
+
{
|
|
96
|
+
if (obj is AttributeModification other)
|
|
97
|
+
{
|
|
98
|
+
return CompareTo(other);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return -1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public int CompareTo(AttributeModification other)
|
|
105
|
+
{
|
|
106
|
+
return ((int)action).CompareTo((int)other.action);
|
|
107
|
+
}
|
|
108
|
+
|
|
79
109
|
/// <summary>
|
|
80
110
|
/// Determines whether two attribute modifications are not equal.
|
|
81
111
|
/// </summary>
|
|
@@ -121,6 +121,26 @@ namespace WallstopStudios.UnityHelpers.Tags
|
|
|
121
121
|
IEnumerable<AttributeModification> attributeModifications
|
|
122
122
|
)
|
|
123
123
|
{
|
|
124
|
+
if (attributeModifications is IReadOnlyList<AttributeModification> readonlyList)
|
|
125
|
+
{
|
|
126
|
+
for (int i = 0; i < readonlyList.Count; ++i)
|
|
127
|
+
{
|
|
128
|
+
AttributeModification modification = readonlyList[i];
|
|
129
|
+
if (!TryGetAttribute(modification.attribute, out Attribute attribute))
|
|
130
|
+
{
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
float oldValue = attribute;
|
|
135
|
+
attribute.ApplyAttributeModification(modification);
|
|
136
|
+
float currentValue = attribute;
|
|
137
|
+
|
|
138
|
+
OnAttributeModified?.Invoke(modification.attribute, oldValue, currentValue);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
124
144
|
foreach (AttributeModification modification in attributeModifications)
|
|
125
145
|
{
|
|
126
146
|
if (!TryGetAttribute(modification.attribute, out Attribute attribute))
|