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
@@ -61,8 +61,9 @@ public class PlayerStats : AttributesComponent
61
61
  {
62
62
  // Define attributes that effects can modify
63
63
  public Attribute Speed = 5f;
64
- public Attribute Health = 100f;
65
- public Attribute Damage = 10f;
64
+ public Attribute MaxHealth = 100f;
65
+ public Attribute AttackDamage = 10f;
66
+ public Attribute Defense = 5f;
66
67
 
67
68
  void Start()
68
69
  {
@@ -80,6 +81,15 @@ public class PlayerStats : AttributesComponent
80
81
  - Calculates final value automatically (Add → Multiply → Override)
81
82
  - Raises events when value changes
82
83
 
84
+ **⚠️ Important: Use Attributes for "max" or "rate" values, NOT "current" depleting values!**
85
+
86
+ - ✅ **MaxHealth** - modified by buffs (good)
87
+ - ❌ **CurrentHealth** - modified by damage/healing from many systems (bad - causes state conflicts)
88
+ - ✅ **AttackDamage** - modified by strength buffs (good)
89
+ - ✅ **Speed** - modified by haste/slow effects (good)
90
+
91
+ If a value is frequently modified by systems outside the effects system (like health being reduced by damage), use a regular field instead. See the main documentation for details.
92
+
83
93
  ---
84
94
 
85
95
  ## Step 2: Add Stats to Your Player (30 seconds)
@@ -88,8 +98,9 @@ public class PlayerStats : AttributesComponent
88
98
  2. Add Component → `PlayerStats`
89
99
  3. Set values in Inspector:
90
100
  - Speed: `5`
91
- - Health: `100`
92
- - Damage: `10`
101
+ - MaxHealth: `100`
102
+ - AttackDamage: `10`
103
+ - Defense: `5`
93
104
 
94
105
  That's it! Your player now has modifiable attributes.
95
106
 
@@ -282,8 +293,8 @@ One effect can modify multiple attributes:
282
293
  **Create "Berserker Rage" effect:**
283
294
 
284
295
  - Modification 1: Speed × 1.3
285
- - Modification 2: Damage × 2.0
286
- - Modification 3: Health × 0.8 (trade-off!)
296
+ - Modification 2: AttackDamage × 2.0
297
+ - Modification 3: Defense × 0.5 (trade-off - more damage but less defense!)
287
298
  - Duration: 10 seconds
288
299
  - Tags: `"Berserker"`, `"Buff"`
289
300
 
@@ -312,23 +323,63 @@ if (handle.HasValue)
312
323
 
313
324
  ```csharp
314
325
  // Create "Poison" effect:
315
- // - Modification: Health + (-2) per second
326
+ // - periodicEffects: interval = 1s, maxTicks = 10, modifications = []
327
+ // - behaviors: PoisonDamageBehavior (below)
316
328
  // - Duration: 10 seconds
317
- // - Tags: "Poison", "DoT", "Debuff"
329
+ // - Tags: "Poisoned", "DoT", "Debuff"
330
+
331
+ void ApplyPoison(GameObject target)
332
+ {
333
+ target.ApplyEffect(poisonEffect);
334
+ }
335
+
336
+ [CreateAssetMenu(menuName = "Combat/Effects/Poison Damage")]
337
+ public sealed class PoisonDamageBehavior : EffectBehavior
338
+ {
339
+ [SerializeField]
340
+ private float damagePerTick = 2f;
341
+
342
+ public override void OnPeriodicTick(
343
+ EffectBehaviorContext context,
344
+ PeriodicEffectTickContext tickContext
345
+ )
346
+ {
347
+ if (!context.Target.TryGetComponent(out PlayerHealth health))
348
+ {
349
+ return;
350
+ }
318
351
 
319
- // Apply to enemy
320
- enemy.ApplyEffect(poisonEffect);
352
+ health.ApplyDamage(damagePerTick);
353
+ }
354
+ }
321
355
 
322
- // In PlayerStats, handle negative health:
323
- void Update()
356
+ public sealed class PlayerHealth : MonoBehaviour
324
357
  {
325
- if (Health.Value <= 0)
358
+ [SerializeField]
359
+ private float currentHealth = 100f;
360
+
361
+ public float CurrentHealth => currentHealth;
362
+
363
+ public void ApplyDamage(float amount)
326
364
  {
327
- Die();
365
+ currentHealth -= amount;
366
+
367
+ if (currentHealth <= 0f)
368
+ {
369
+ currentHealth = 0f;
370
+ Die();
371
+ }
372
+ }
373
+
374
+ private void Die()
375
+ {
376
+ // Handle player death
328
377
  }
329
378
  }
330
379
  ```
331
380
 
381
+ This keeps `CurrentHealth` as a regular gameplay field while the effect system triggers damage through behaviours.
382
+
332
383
  ### Cooldown Reduction
333
384
 
334
385
  ```csharp
@@ -376,6 +427,13 @@ void TryApplyBuff(AttributeEffect effect)
376
427
 
377
428
  ## Troubleshooting
378
429
 
430
+ ### "Should I use CurrentHealth as an Attribute?"
431
+
432
+ - **No!** Use `MaxHealth` as an Attribute (modified by buffs), but keep `CurrentHealth` as a regular field (modified by damage/healing)
433
+ - **Why:** CurrentHealth is modified by many systems (combat, regeneration, etc.). Using it as an Attribute causes state conflicts when effects and other systems both try to modify it
434
+ - **Pattern:** Attribute for max/cap, regular field for current/depleting value
435
+ - **See:** "Understanding Attributes: What to Model and What to Avoid" in main documentation
436
+
379
437
  ### "Attribute 'Speed' not found"
380
438
 
381
439
  - **Cause:** Attribute name in effect doesn't match field name in AttributesComponent
@@ -409,11 +467,12 @@ You now have a complete buff/debuff system! Here are some ideas to expand:
409
467
 
410
468
  ### Create More Effects
411
469
 
412
- - **Shield:** Health × 2.0, visual shield sprite
470
+ - **Shield:** MaxHealth × 1.5, visual shield sprite
413
471
  - **Slow:** Speed × 0.5, "Slowed" tag
414
- - **Critical:** Damage × 1.5, "Critical" tag
415
- - **Invisibility:** Just tags ("Invisible"), no stat changes
416
- - **Burn:** Health + (-5) per second, fire particles
472
+ - **Critical Strike:** AttackDamage × 2.0, "CriticalHit" tag, brief flash effect
473
+ - **Invisibility:** Just tags ("Invisible"), no stat changes, transparency effect
474
+ - **Armor Buff:** Defense + 10, metallic sheen cosmetic
475
+ - **Strength Potion:** AttackDamage × 1.5, red particle aura
417
476
 
418
477
  ### Build Systems Around Tags
419
478
 
package/Docs/HULLS.md CHANGED
@@ -18,7 +18,7 @@ This guide explains convex and concave hulls, when to use each, and how they dif
18
18
 
19
19
  Illustration:
20
20
 
21
- ![Convex Hull](Docs/Images/convex_hull.svg)
21
+ ![Convex Hull](Images/convex_hull.svg)
22
22
 
23
23
  ## Concave Hull
24
24
 
@@ -31,7 +31,7 @@ Illustration:
31
31
 
32
32
  Illustration:
33
33
 
34
- ![Concave Hull](Docs/Images/concave_hull.svg)
34
+ ![Concave Hull](Images/concave_hull.svg)
35
35
 
36
36
  ## Choosing Between Them
37
37
 
@@ -71,7 +71,7 @@ Threading
71
71
 
72
72
  Visual
73
73
 
74
- ![Random Generators](Docs/Images/random_generators.svg)
74
+ ![Random Generators](Images/random_generators.svg)
75
75
 
76
76
  This document contains performance benchmarks for the various random number generators included in Unity Helpers.
77
77
 
@@ -7,7 +7,7 @@
7
7
 
8
8
  Visual
9
9
 
10
- ![Reflection Scan](Docs/Images/reflection_scan.svg)
10
+ ![Reflection Scan](Images/reflection_scan.svg)
11
11
 
12
12
  ReflectionHelpers is a set of utilities for high‑performance reflection in Unity projects. It generates and caches delegates to access fields and properties, call methods and constructors, and quickly create common collections — with safe fallbacks when dynamic IL isn’t available.
13
13
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Visual
4
4
 
5
- ![Relational Wiring](Docs/Images/relational_wiring.svg)
5
+ ![Relational Wiring](Images/relational_wiring.svg)
6
6
 
7
7
  Auto-wire components in your hierarchy without `GetComponent` boilerplate. These attributes make common relationships explicit, robust, and easy to maintain.
8
8
 
@@ -10,7 +10,14 @@ Auto-wire components in your hierarchy without `GetComponent` boilerplate. These
10
10
  - `ParentComponent` — up the transform hierarchy
11
11
  - `ChildComponent` — down the transform hierarchy (breadth-first)
12
12
 
13
- Each works with single fields, arrays, `List<T>`, and `HashSet<T>`, supports optional assignment, filters (tag/name), depth limits, max results, and interface/base-type resolution.
13
+ **Collection Type Support:** Each attribute works with:
14
+
15
+ - Single fields (e.g., `Transform`)
16
+ - Arrays (e.g., `Collider2D[]`)
17
+ - **Lists** (e.g., `List<Rigidbody2D>`)
18
+ - **HashSets** (e.g., `HashSet<Renderer>`)
19
+
20
+ All attributes support optional assignment, filters (tag/name), depth limits, max results, and interface/base-type resolution.
14
21
 
15
22
  Having issues? Jump to Troubleshooting: see [Troubleshooting](#troubleshooting).
16
23
 
@@ -197,7 +204,8 @@ Examples:
197
204
  [SiblingComponent] private Animator animator; // required by default
198
205
  [SiblingComponent(Optional = true)] private Rigidbody2D rb; // optional
199
206
  [SiblingComponent(TagFilter = "Visual", NameFilter = "Sprite")] private Component[] visuals;
200
- [SiblingComponent(MaxCount = 2)] private List<Collider2D> firstTwo;
207
+ [SiblingComponent(MaxCount = 2)] private List<Collider2D> firstTwo; // List<T> supported
208
+ [SiblingComponent] private HashSet<Renderer> allRenderers; // HashSet<T> supported
201
209
  ```
202
210
 
203
211
  ### ParentComponent
@@ -232,7 +240,10 @@ Examples:
232
240
  // First matching descendant with a tag
233
241
  [ChildComponent(OnlyDescendants = true, TagFilter = "Weapon")] private Collider2D weaponCollider;
234
242
 
235
- // Gather into a hash set (unique results) and limit count
243
+ // Gather into a List (preserves insertion order)
244
+ [ChildComponent(OnlyDescendants = true)] private List<MeshRenderer> childRenderers;
245
+
246
+ // Gather into a HashSet (unique results, no duplicates) and limit count
236
247
  [ChildComponent(OnlyDescendants = true, MaxCount = 10)] private HashSet<Rigidbody2D> firstTenRigidbodies;
237
248
  ```
238
249
 
@@ -261,6 +272,39 @@ Examples:
261
272
  - `AllowInterfaces` (default: true)
262
273
  - If `true`, can assign by interface or base type; set `false` to restrict to concrete types
263
274
 
275
+ ### Choosing the Right Collection Type
276
+
277
+ **Use Arrays (`T[]`)** when:
278
+
279
+ - Collection size is fixed or rarely changes
280
+ - Need the smallest memory footprint
281
+ - Interoperating with APIs that require arrays
282
+
283
+ **Use Lists (`List<T>`)** when:
284
+
285
+ - Need insertion order preserved
286
+ - Plan to add/remove elements after assignment
287
+ - Want indexed access with `[]` operator
288
+ - Need compatibility with most LINQ operations
289
+
290
+ **Use HashSets (`HashSet<T>`)** when:
291
+
292
+ - Need guaranteed uniqueness (no duplicates)
293
+ - Performing frequent membership tests (`Contains()`)
294
+ - Order doesn't matter
295
+ - Want O(1) lookup performance
296
+
297
+ ```csharp
298
+ // Arrays: Fixed size, minimal overhead
299
+ [ChildComponent] private Collider2D[] colliders;
300
+
301
+ // Lists: Dynamic, ordered, index-based access
302
+ [ChildComponent] private List<Renderer> renderers;
303
+
304
+ // HashSets: Unique, fast lookups, unordered
305
+ [ChildComponent] private HashSet<AudioSource> audioSources;
306
+ ```
307
+
264
308
  ## Recipes
265
309
 
266
310
  - UI hierarchy references
@@ -564,7 +608,8 @@ Common pitfalls and how to avoid them
564
608
 
565
609
  **DI Integration Samples:**
566
610
 
567
- - [VContainer Integration](Samples~/DI%20-%20VContainer/README.md) - Complete VContainer setup guide
568
- - [Zenject Integration](Samples~/DI%20-%20Zenject/README.md) - Complete Zenject setup guide
611
+ - [VContainer Integration](../Samples~/DI%20-%20VContainer/README.md) - Complete VContainer setup guide
612
+ - [Zenject Integration](../Samples~/DI%20-%20Zenject/README.md) - Complete Zenject setup guide
613
+ - [Reflex Integration](../Samples~/DI%20-%20Reflex/README.md) - Complete Reflex setup guide
569
614
 
570
615
  **Need help?** [Open an issue](https://github.com/wallstop/unity-helpers/issues) | [Troubleshooting](#troubleshooting)
@@ -8,7 +8,7 @@
8
8
 
9
9
  Visuals
10
10
 
11
- ![Serialization Flow](Docs/Images/serialization_flow.svg)
11
+ ![Serialization Flow](Images/serialization_flow.svg)
12
12
 
13
13
  This package provides fast, compact serialization for save systems, configuration, and networking with a unified API.
14
14
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Visual
4
4
 
5
- ![Singletons Lifecycle](Docs/Images/singletons_lifecycle.svg)
5
+ ![Singletons Lifecycle](Images/singletons_lifecycle.svg)
6
6
 
7
7
  This package includes two lightweight, production‑ready singleton helpers that make global access patterns safe, consistent, and testable:
8
8
 
@@ -403,7 +403,7 @@ Use alternatives when one or more of these apply:
403
403
 
404
404
  Use this chart to pick an approach based on constraints:
405
405
 
406
- ![Data Distribution Strategy](Docs/Images/data_distribution_decision.svg)
406
+ ![Data Distribution Strategy](Images/data_distribution_decision.svg)
407
407
 
408
408
  <a id="troubleshooting"></a>
409
409
 
@@ -188,7 +188,7 @@ See [Buffering Pattern](../README.md#buffering-pattern) for the complete guide a
188
188
  - Pros: Simple structure; predictable performance; incremental updates straightforward.
189
189
  - Cons: Data hotspots deepen local trees; nearest neighbors slower than KDTree.
190
190
 
191
- Diagram: ![QuadTree2D](Docs/Images/quadtree_2d.svg)
191
+ Diagram: ![QuadTree2D](Images/quadtree_2d.svg)
192
192
 
193
193
  ### KDTree2D
194
194
 
@@ -197,7 +197,7 @@ Diagram: ![QuadTree2D](Docs/Images/quadtree_2d.svg)
197
197
  - Pros: Strong NN performance; balanced variant gives consistent query time.
198
198
  - Cons: Costly to maintain under heavy churn; unbalanced variant can degrade.
199
199
 
200
- Diagram: ![KDTree2D](Docs/Images/kdtree_2d.svg)
200
+ Diagram: ![KDTree2D](Images/kdtree_2d.svg)
201
201
 
202
202
  ### RTree2D
203
203
 
@@ -206,7 +206,7 @@ Diagram: ![KDTree2D](Docs/Images/kdtree_2d.svg)
206
206
  - Pros: Great for large bounds queries; matches bounds semantics.
207
207
  - Cons: Overlapping MBRs can increase node visits; not optimal for point NN.
208
208
 
209
- Diagram: ![RTree2D](Docs/Images/rtree_2d.svg)
209
+ Diagram: ![RTree2D](Images/rtree_2d.svg)
210
210
 
211
211
  ## Choosing a Structure
212
212
 
@@ -178,7 +178,7 @@ See [Buffering Pattern](../README.md#buffering-pattern) for the complete guide a
178
178
  - Pros: Good spatial locality; intuitive partitioning; balanced performance.
179
179
  - Cons: Nearest neighbors slower than KDTree on pure point data.
180
180
 
181
- ![Octree3D](Docs/Images/octree_3d.svg)
181
+ ![Octree3D](Images/octree_3d.svg)
182
182
 
183
183
  ### KDTree3D
184
184
 
@@ -187,7 +187,7 @@ See [Buffering Pattern](../README.md#buffering-pattern) for the complete guide a
187
187
  - Pros: Strong NN performance; balanced variant gives consistent query time.
188
188
  - Cons: Costly to maintain under heavy churn; unbalanced variant can degrade.
189
189
 
190
- ![KDTree3D](Docs/Images/kdtree_3d.svg)
190
+ ![KDTree3D](Images/kdtree_3d.svg)
191
191
 
192
192
  ### RTree3D
193
193
 
@@ -196,7 +196,7 @@ See [Buffering Pattern](../README.md#buffering-pattern) for the complete guide a
196
196
  - Pros: Great for large bounds queries; matches volumetric semantics.
197
197
  - Cons: Overlapping boxes can increase node visits; not optimal for point NN.
198
198
 
199
- ![RTree3D](Docs/Images/rtree_3d.svg)
199
+ ![RTree3D](Images/rtree_3d.svg)
200
200
 
201
201
  ## Choosing a Structure
202
202
 
@@ -16,19 +16,19 @@ This page explains how the 2D and 3D spatial structures compare in terms of corr
16
16
 
17
17
  Illustrations:
18
18
 
19
- ![QuadTree2D](Docs/Images/quadtree_2d.svg)
19
+ ![QuadTree2D](Images/quadtree_2d.svg)
20
20
 
21
- ![KDTree2D](Docs/Images/kdtree_2d.svg)
21
+ ![KDTree2D](Images/kdtree_2d.svg)
22
22
 
23
- ![RTree2D](Docs/Images/rtree_2d.svg)
23
+ ![RTree2D](Images/rtree_2d.svg)
24
24
 
25
25
  3D Variants
26
26
 
27
- ![Octree3D](Docs/Images/octree_3d.svg)
27
+ ![Octree3D](Images/octree_3d.svg)
28
28
 
29
- ![KDTree3D](Docs/Images/kdtree_3d.svg)
29
+ ![KDTree3D](Images/kdtree_3d.svg)
30
30
 
31
- ![RTree3D](Docs/Images/rtree_3d.svg)
31
+ ![RTree3D](Images/rtree_3d.svg)
32
32
 
33
33
  Diagram notes
34
34
 
@@ -91,7 +91,7 @@ Key reasons and scenarios:
91
91
 
92
92
  ## Boundary Semantics
93
93
 
94
- ![Query Boundaries](Docs/Images/query_boundaries.svg)
94
+ ![Query Boundaries](Images/query_boundaries.svg)
95
95
 
96
96
  Tips
97
97
 
@@ -42,63 +42,153 @@ namespace WallstopStudios.UnityHelpers.Editor.CustomDrawers
42
42
  SerializedProperty conditionProperty = property.serializedObject.FindProperty(
43
43
  showIf.conditionField
44
44
  );
45
- if (conditionProperty is not { propertyType: SerializedPropertyType.Boolean })
45
+ if (conditionProperty != null)
46
46
  {
47
- if (conditionProperty != null)
47
+ if (TryEvaluateCondition(conditionProperty, showIf, out bool serializedResult))
48
48
  {
49
- return true;
49
+ return serializedResult;
50
50
  }
51
+ return true;
52
+ }
51
53
 
52
- // This might not be a unity object, so fall back to reflection
53
- object enclosingObject = property.GetEnclosingObject(out _);
54
- if (enclosingObject == null)
55
- {
56
- return true;
57
- }
54
+ object enclosingObject = property.GetEnclosingObject(out _);
55
+ if (enclosingObject == null)
56
+ {
57
+ return true;
58
+ }
58
59
 
59
- Type type = enclosingObject.GetType();
60
- Dictionary<string, Func<object, object>> cachedFields = CachedFields.GetOrAdd(type);
61
- if (
62
- !cachedFields.TryGetValue(
63
- showIf.conditionField,
64
- out Func<object, object> accessor
65
- )
66
- )
60
+ Type type = enclosingObject.GetType();
61
+ Dictionary<string, Func<object, object>> cachedFields = CachedFields.GetOrAdd(type);
62
+ if (!cachedFields.TryGetValue(showIf.conditionField, out Func<object, object> accessor))
63
+ {
64
+ FieldInfo field = type.GetField(
65
+ showIf.conditionField,
66
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
67
+ );
68
+ if (field == null)
67
69
  {
68
- FieldInfo field = type.GetField(
69
- showIf.conditionField,
70
- BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
70
+ Debug.LogError(
71
+ $"Failed to find conditional field {showIf.conditionField} on {type.Name}!"
71
72
  );
72
- if (field == null)
73
- {
74
- Debug.LogError(
75
- $"Failed to find conditional field {showIf.conditionField} on {type.Name}!"
76
- );
77
- accessor = _ => null;
78
- }
79
- else
80
- {
81
- accessor = ReflectionHelpers.GetFieldGetter(field);
82
- }
83
- cachedFields[showIf.conditionField] = accessor;
73
+ accessor = _ => null;
84
74
  }
85
- object fieldValue = accessor(enclosingObject);
86
- if (fieldValue is bool maybeCondition)
75
+ else
87
76
  {
88
- return showIf.inverse ? !maybeCondition : maybeCondition;
77
+ accessor = ReflectionHelpers.GetFieldGetter(field);
89
78
  }
79
+ cachedFields[showIf.conditionField] = accessor;
80
+ }
81
+ object fieldValue = accessor(enclosingObject);
82
+ return !TryEvaluateCondition(fieldValue, showIf, out bool reflectedResult)
83
+ ? true
84
+ : reflectedResult;
85
+ }
90
86
 
91
- int index = Array.IndexOf(showIf.expectedValues, fieldValue);
92
- if (showIf.inverse)
87
+ private static bool TryEvaluateCondition(
88
+ SerializedProperty conditionProperty,
89
+ WShowIfAttribute showIf,
90
+ out bool shouldShow
91
+ )
92
+ {
93
+ if (conditionProperty == null)
94
+ {
95
+ shouldShow = true;
96
+ return false;
97
+ }
98
+
99
+ if (conditionProperty.propertyType == SerializedPropertyType.Boolean)
100
+ {
101
+ bool condition = conditionProperty.boolValue;
102
+ shouldShow = showIf.inverse ? !condition : condition;
103
+ return true;
104
+ }
105
+
106
+ object conditionValue = conditionProperty.GetTargetObjectWithField(out _);
107
+ return TryEvaluateCondition(conditionValue, showIf, out shouldShow);
108
+ }
109
+
110
+ private static bool TryEvaluateCondition(
111
+ object conditionValue,
112
+ WShowIfAttribute showIf,
113
+ out bool shouldShow
114
+ )
115
+ {
116
+ if (conditionValue is bool boolean)
117
+ {
118
+ shouldShow = showIf.inverse ? !boolean : boolean;
119
+ return true;
120
+ }
121
+
122
+ object[] expectedValues = showIf.expectedValues;
123
+ if (expectedValues == null || expectedValues.Length == 0)
124
+ {
125
+ shouldShow = true;
126
+ return false;
127
+ }
128
+
129
+ bool match = false;
130
+ for (int i = 0; i < expectedValues.Length; ++i)
131
+ {
132
+ if (ValuesEqual(conditionValue, expectedValues[i]))
93
133
  {
94
- return index < 0;
134
+ match = true;
135
+ break;
95
136
  }
137
+ }
138
+
139
+ shouldShow = showIf.inverse ? !match : match;
140
+ return true;
141
+ }
142
+
143
+ private static bool ValuesEqual(object actual, object expected)
144
+ {
145
+ if (ReferenceEquals(actual, expected))
146
+ {
147
+ return true;
148
+ }
149
+
150
+ if (actual == null || expected == null)
151
+ {
152
+ return false;
153
+ }
154
+
155
+ if (actual.Equals(expected))
156
+ {
157
+ return true;
158
+ }
159
+
160
+ Type actualType = actual.GetType();
161
+ Type expectedType = expected.GetType();
162
+
163
+ try
164
+ {
165
+ if (actualType.IsEnum || expectedType.IsEnum)
166
+ {
167
+ long actualValue = Convert.ToInt64(actual);
168
+ long expectedValue = Convert.ToInt64(expected);
169
+ return actualValue == expectedValue;
170
+ }
171
+ }
172
+ catch
173
+ {
174
+ return false;
175
+ }
96
176
 
97
- return 0 <= index;
177
+ if (actual is IConvertible && expected is IConvertible)
178
+ {
179
+ try
180
+ {
181
+ double actualValue = Convert.ToDouble(actual);
182
+ double expectedValue = Convert.ToDouble(expected);
183
+ return Math.Abs(actualValue - expectedValue) < double.Epsilon;
184
+ }
185
+ catch
186
+ {
187
+ return false;
188
+ }
98
189
  }
99
190
 
100
- bool condition = conditionProperty.boolValue;
101
- return showIf.inverse ? !condition : condition;
191
+ return false;
102
192
  }
103
193
  }
104
194
  #endif