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
@@ -0,0 +1,102 @@
1
+ namespace WallstopStudios.UnityHelpers.Tags
2
+ {
3
+ using System;
4
+ using System.Collections.Generic;
5
+ using ProtoBuf;
6
+ using UnityEngine;
7
+
8
+ /// <summary>
9
+ /// Authoring data for a periodic modifier bundle that executes on a cadence while an effect is active.
10
+ /// </summary>
11
+ /// <remarks>
12
+ /// <para>
13
+ /// The owning <see cref="EffectHandler"/> evaluates each periodic definition after <see cref="initialDelay"/>, applies the configured
14
+ /// <see cref="modifications"/>, and repeats every <see cref="interval"/> seconds until <see cref="maxTicks"/> is reached or the effect ends.
15
+ /// </para>
16
+ /// <para>
17
+ /// Definitions are processed in list order and maintain independent runtime state per <see cref="EffectHandle"/>, enabling designers to mix
18
+ /// damage-over-time, heal-over-time, or custom triggers alongside bespoke <see cref="EffectBehavior"/> implementations.
19
+ /// </para>
20
+ /// </remarks>
21
+ /// <example>
22
+ /// <code language="csharp">
23
+ /// using System.Collections.Generic;
24
+ /// using UnityEngine;
25
+ /// using WallstopStudios.UnityHelpers.Tags;
26
+ ///
27
+ /// public sealed class BurnEffectAuthoring : MonoBehaviour
28
+ /// {
29
+ /// [SerializeField]
30
+ /// private AttributeEffect burnEffect;
31
+ ///
32
+ /// [SerializeField]
33
+ /// private EffectHandler effectHandler;
34
+ ///
35
+ /// public void ApplyBurn(GameObject target)
36
+ /// {
37
+ /// PeriodicEffectDefinition burnTick = new PeriodicEffectDefinition
38
+ /// {
39
+ /// name = "Burn Damage",
40
+ /// initialDelay = 0.5f,
41
+ /// interval = 1.0f,
42
+ /// maxTicks = 5,
43
+ /// modifications = new List<AttributeModification>
44
+ /// {
45
+ /// new AttributeModification("Health", ModificationAction.Addition, -5f),
46
+ /// },
47
+ /// };
48
+ ///
49
+ /// burnEffect.periodicEffects.Add(burnTick);
50
+ ///
51
+ /// if (effectHandler == null)
52
+ /// {
53
+ /// effectHandler = target.GetComponent<EffectHandler>();
54
+ /// }
55
+ ///
56
+ /// EffectHandle? handle = effectHandler.ApplyEffect(burnEffect);
57
+ /// }
58
+ /// }
59
+ /// </code>
60
+ /// <para>
61
+ /// In the example above the handler waits for <c>initialDelay</c>, applies the <see cref="modifications"/> every <c>interval</c> seconds,
62
+ /// and stops after <see cref="maxTicks"/> executions or as soon as the effect is removed.
63
+ /// </para>
64
+ /// </example>
65
+ [Serializable]
66
+ [ProtoContract]
67
+ public sealed class PeriodicEffectDefinition
68
+ {
69
+ /// <summary>
70
+ /// Optional label shown in tooling to help distinguish multiple periodic definitions.
71
+ /// </summary>
72
+ [ProtoMember(1)]
73
+ public string name;
74
+
75
+ /// <summary>
76
+ /// Time (seconds) before the first tick fires after the effect is applied.
77
+ /// </summary>
78
+ [Min(0f)]
79
+ [ProtoMember(2)]
80
+ public float initialDelay;
81
+
82
+ /// <summary>
83
+ /// Interval (seconds) between ticks once the first tick has executed.
84
+ /// </summary>
85
+ [Min(0.01f)]
86
+ [ProtoMember(3)]
87
+ public float interval = 1f;
88
+
89
+ /// <summary>
90
+ /// Maximum number of ticks to execute. Zero or negative means unlimited ticks.
91
+ /// </summary>
92
+ [Min(0)]
93
+ [ProtoMember(4)]
94
+ public int maxTicks;
95
+
96
+ /// <summary>
97
+ /// Attribute modifications applied each time the tick fires.
98
+ /// </summary>
99
+ [ProtoMember(5)]
100
+ public List<AttributeModification> modifications = new();
101
+ }
102
+ }
@@ -0,0 +1,4 @@
1
+ fileFormatVersion: 2
2
+ guid: 8d763ed3f32f4fbf9bcaca8f24ef1a31
3
+ timeCreated: 1759598400
4
+
@@ -0,0 +1,40 @@
1
+ namespace WallstopStudios.UnityHelpers.Tags
2
+ {
3
+ using UnityEngine;
4
+
5
+ /// <summary>
6
+ /// Runtime tracking for a periodic effect definition.
7
+ /// </summary>
8
+ internal sealed class PeriodicEffectRuntimeState
9
+ {
10
+ internal bool IsComplete => definition.maxTicks > 0 && ExecutedTicks >= definition.maxTicks;
11
+
12
+ internal float NextTickTime { get; private set; }
13
+
14
+ internal int ExecutedTicks { get; private set; }
15
+
16
+ internal readonly PeriodicEffectDefinition definition;
17
+ internal readonly float interval;
18
+
19
+ internal PeriodicEffectRuntimeState(PeriodicEffectDefinition definition, float startTime)
20
+ {
21
+ this.definition = definition;
22
+ ExecutedTicks = 0;
23
+ float clampedInterval = Mathf.Max(0.01f, definition.interval);
24
+ interval = clampedInterval;
25
+ NextTickTime = startTime + Mathf.Max(0f, definition.initialDelay);
26
+ }
27
+
28
+ internal bool TryConsumeTick(float currentTime)
29
+ {
30
+ if (currentTime < NextTickTime || IsComplete)
31
+ {
32
+ return false;
33
+ }
34
+
35
+ ++ExecutedTicks;
36
+ NextTickTime = currentTime + interval;
37
+ return true;
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,4 @@
1
+ fileFormatVersion: 2
2
+ guid: 5af38ac9186645fabd9858dfead3c001
3
+ timeCreated: 1759598400
4
+
@@ -71,8 +71,6 @@ public class Enemy : MonoBehaviour
71
71
  2. Add the `RelationalComponentsInstaller` component to the same GameObject
72
72
  3. Enable **"Assign Scene On Initialize"** to automatically wire all scene components after the container builds (recommended)
73
73
 
74
- ![SceneContext Setup](../../Docs/Images/zenject_setup.png)
75
-
76
74
  > 💡 **Beginner tip:** Enable both checkboxes in the inspector:
77
75
  >
78
76
  > - ✅ **Assign Scene On Initialize** → Auto-wires all scene objects (saves you from calling it manually)
@@ -0,0 +1,285 @@
1
+ namespace WallstopStudios.UnityHelpers.Tests.Editor.Attributes
2
+ {
3
+ using System.Reflection;
4
+ using NUnit.Framework;
5
+ using UnityEditor;
6
+ using UnityEngine;
7
+ using WallstopStudios.UnityHelpers.Core.Attributes;
8
+ using WallstopStudios.UnityHelpers.Editor.CustomDrawers;
9
+ using WallstopStudios.UnityHelpers.Tags;
10
+ using WallstopStudios.UnityHelpers.Tests.Editor.Utils;
11
+
12
+ [TestFixture]
13
+ public sealed class WShowIfPropertyDrawerTests : CommonTestBase
14
+ {
15
+ private static readonly FieldInfo AttributeField = typeof(PropertyDrawer).GetField(
16
+ "m_Attribute",
17
+ BindingFlags.Instance | BindingFlags.NonPublic
18
+ );
19
+
20
+ [Test]
21
+ public void BoolConditionHidesFieldWhenFalse()
22
+ {
23
+ TestContainer container = CreateScriptableObject<TestContainer>();
24
+ SerializedObject serializedObject = new(container);
25
+ serializedObject.Update();
26
+
27
+ SerializedProperty dependentProperty = serializedObject.FindProperty(
28
+ nameof(TestContainer.boolDependent)
29
+ );
30
+ Assert.NotNull(dependentProperty);
31
+
32
+ WShowIfPropertyDrawer drawer = CreateDrawer(
33
+ new WShowIfAttribute(nameof(TestContainer.boolCondition))
34
+ );
35
+
36
+ container.boolCondition = false;
37
+ serializedObject.Update();
38
+ float hiddenHeight = drawer.GetPropertyHeight(
39
+ dependentProperty,
40
+ new GUIContent("boolDependent")
41
+ );
42
+ Assert.That(hiddenHeight, Is.Zero);
43
+
44
+ container.boolCondition = true;
45
+ serializedObject.Update();
46
+ float shownHeight = drawer.GetPropertyHeight(
47
+ dependentProperty,
48
+ new GUIContent("boolDependent")
49
+ );
50
+ Assert.That(shownHeight, Is.GreaterThan(0f));
51
+ }
52
+
53
+ [Test]
54
+ public void EnumConditionMatchesExpectedValue()
55
+ {
56
+ TestContainer container = CreateScriptableObject<TestContainer>();
57
+ SerializedObject serializedObject = new(container);
58
+ serializedObject.Update();
59
+
60
+ SerializedProperty dependentProperty = serializedObject.FindProperty(
61
+ nameof(TestContainer.durationDependent)
62
+ );
63
+ Assert.NotNull(dependentProperty);
64
+
65
+ WShowIfPropertyDrawer drawer = CreateDrawer(
66
+ new WShowIfAttribute(
67
+ nameof(TestContainer.durationType),
68
+ expectedValues: new object[] { ModifierDurationType.Duration }
69
+ )
70
+ );
71
+
72
+ container.durationType = ModifierDurationType.Instant;
73
+ serializedObject.Update();
74
+ float hiddenHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
75
+ Assert.That(hiddenHeight, Is.Zero);
76
+
77
+ container.durationType = ModifierDurationType.Duration;
78
+ serializedObject.Update();
79
+ float shownHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
80
+ Assert.That(shownHeight, Is.GreaterThan(0f));
81
+ }
82
+
83
+ [Test]
84
+ public void EnumConditionHonorsInverseFlag()
85
+ {
86
+ TestContainer container = CreateScriptableObject<TestContainer>();
87
+ SerializedObject serializedObject = new(container);
88
+ serializedObject.Update();
89
+
90
+ SerializedProperty dependentProperty = serializedObject.FindProperty(
91
+ nameof(TestContainer.inverseDependent)
92
+ );
93
+ Assert.NotNull(dependentProperty);
94
+
95
+ WShowIfPropertyDrawer drawer = CreateDrawer(
96
+ new WShowIfAttribute(
97
+ nameof(TestContainer.durationType),
98
+ inverse: true,
99
+ expectedValues: new object[] { ModifierDurationType.Instant }
100
+ )
101
+ );
102
+
103
+ container.durationType = ModifierDurationType.Instant;
104
+ serializedObject.Update();
105
+ float hiddenHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
106
+ Assert.That(hiddenHeight, Is.Zero);
107
+
108
+ container.durationType = ModifierDurationType.Infinite;
109
+ serializedObject.Update();
110
+ float shownHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
111
+ Assert.That(shownHeight, Is.GreaterThan(0f));
112
+ }
113
+
114
+ [Test]
115
+ public void FloatConditionMatchesExpectedValue()
116
+ {
117
+ TestContainer container = CreateScriptableObject<TestContainer>();
118
+ SerializedObject serializedObject = new(container);
119
+ serializedObject.Update();
120
+
121
+ SerializedProperty dependentProperty = serializedObject.FindProperty(
122
+ nameof(TestContainer.floatDependent)
123
+ );
124
+ Assert.NotNull(dependentProperty);
125
+
126
+ WShowIfPropertyDrawer drawer = CreateDrawer(
127
+ new WShowIfAttribute(
128
+ nameof(TestContainer.floatCondition),
129
+ expectedValues: new object[] { 3.5f }
130
+ )
131
+ );
132
+
133
+ container.floatCondition = 0f;
134
+ serializedObject.Update();
135
+ float hiddenHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
136
+ Assert.That(hiddenHeight, Is.Zero);
137
+
138
+ container.floatCondition = 3.5f;
139
+ serializedObject.Update();
140
+ float shownHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
141
+ Assert.That(shownHeight, Is.GreaterThan(0f));
142
+ }
143
+
144
+ [Test]
145
+ public void DoubleConditionMatchesEquivalentIntExpectedValue()
146
+ {
147
+ TestContainer container = CreateScriptableObject<TestContainer>();
148
+ SerializedObject serializedObject = new(container);
149
+ serializedObject.Update();
150
+
151
+ SerializedProperty dependentProperty = serializedObject.FindProperty(
152
+ nameof(TestContainer.doubleDependent)
153
+ );
154
+ Assert.NotNull(dependentProperty);
155
+
156
+ WShowIfPropertyDrawer drawer = CreateDrawer(
157
+ new WShowIfAttribute(
158
+ nameof(TestContainer.doubleCondition),
159
+ expectedValues: new object[] { 7 }
160
+ )
161
+ );
162
+
163
+ container.doubleCondition = 2.5d;
164
+ serializedObject.Update();
165
+ float hiddenHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
166
+ Assert.That(hiddenHeight, Is.Zero);
167
+
168
+ container.doubleCondition = 7d;
169
+ serializedObject.Update();
170
+ float shownHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
171
+ Assert.That(shownHeight, Is.GreaterThan(0f));
172
+ }
173
+
174
+ [Test]
175
+ public void IntConditionMatchesExpectedValue()
176
+ {
177
+ TestContainer container = CreateScriptableObject<TestContainer>();
178
+ SerializedObject serializedObject = new(container);
179
+ serializedObject.Update();
180
+
181
+ SerializedProperty dependentProperty = serializedObject.FindProperty(
182
+ nameof(TestContainer.intDependent)
183
+ );
184
+ Assert.NotNull(dependentProperty);
185
+
186
+ WShowIfPropertyDrawer drawer = CreateDrawer(
187
+ new WShowIfAttribute(
188
+ nameof(TestContainer.intCondition),
189
+ expectedValues: new object[] { 42 }
190
+ )
191
+ );
192
+
193
+ container.intCondition = 7;
194
+ serializedObject.Update();
195
+ float hiddenHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
196
+ Assert.That(hiddenHeight, Is.Zero);
197
+
198
+ container.intCondition = 42;
199
+ serializedObject.Update();
200
+ float shownHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
201
+ Assert.That(shownHeight, Is.GreaterThan(0f));
202
+ }
203
+
204
+ [Test]
205
+ public void StringConditionMatchesExpectedValues()
206
+ {
207
+ TestContainer container = CreateScriptableObject<TestContainer>();
208
+ SerializedObject serializedObject = new(container);
209
+ serializedObject.Update();
210
+
211
+ SerializedProperty dependentProperty = serializedObject.FindProperty(
212
+ nameof(TestContainer.stringDependent)
213
+ );
214
+ Assert.NotNull(dependentProperty);
215
+
216
+ WShowIfPropertyDrawer drawer = CreateDrawer(
217
+ new WShowIfAttribute(
218
+ nameof(TestContainer.stringCondition),
219
+ expectedValues: new object[] { "alpha", "beta" }
220
+ )
221
+ );
222
+
223
+ container.stringCondition = "gamma";
224
+ serializedObject.Update();
225
+ float hiddenHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
226
+ Assert.That(hiddenHeight, Is.Zero);
227
+
228
+ container.stringCondition = "alpha";
229
+ serializedObject.Update();
230
+ float shownHeight = drawer.GetPropertyHeight(dependentProperty, GUIContent.none);
231
+ Assert.That(shownHeight, Is.GreaterThan(0f));
232
+ }
233
+
234
+ private static WShowIfPropertyDrawer CreateDrawer(WShowIfAttribute attribute)
235
+ {
236
+ WShowIfPropertyDrawer drawer = new();
237
+ Assert.NotNull(AttributeField);
238
+ AttributeField.SetValue(drawer, attribute);
239
+ return drawer;
240
+ }
241
+
242
+ private sealed class TestContainer : ScriptableObject
243
+ {
244
+ public bool boolCondition;
245
+
246
+ [WShowIf(nameof(boolCondition))]
247
+ public int boolDependent;
248
+
249
+ public ModifierDurationType durationType = ModifierDurationType.Instant;
250
+
251
+ [WShowIf(
252
+ nameof(durationType),
253
+ expectedValues: new object[] { ModifierDurationType.Duration }
254
+ )]
255
+ public int durationDependent;
256
+
257
+ [WShowIf(
258
+ nameof(durationType),
259
+ inverse: true,
260
+ expectedValues: new object[] { ModifierDurationType.Instant }
261
+ )]
262
+ public int inverseDependent;
263
+
264
+ public float floatCondition;
265
+
266
+ [WShowIf(nameof(floatCondition), expectedValues: new object[] { 3.5f })]
267
+ public int floatDependent;
268
+
269
+ public double doubleCondition;
270
+
271
+ [WShowIf(nameof(doubleCondition), expectedValues: new object[] { 7 })]
272
+ public int doubleDependent;
273
+
274
+ public int intCondition;
275
+
276
+ [WShowIf(nameof(intCondition), expectedValues: new object[] { 42 })]
277
+ public int intDependent;
278
+
279
+ public string stringCondition;
280
+
281
+ [WShowIf(nameof(stringCondition), expectedValues: new object[] { "alpha", "beta" })]
282
+ public int stringDependent;
283
+ }
284
+ }
285
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: 1e5fb5f2a5e7464493dca6801e4eb1a6
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -24,7 +24,7 @@ namespace WallstopStudios.UnityHelpers.Tests.Core.Attributes
24
24
  }
25
25
 
26
26
  [Test]
27
- public void HasRelationalAssignments_RespectsMetadata()
27
+ public void HasRelationalAssignmentsRespectsMetadata()
28
28
  {
29
29
  AttributeMetadataCache cache = CreateScriptableObject<AttributeMetadataCache>();
30
30
 
@@ -62,7 +62,7 @@ namespace WallstopStudios.UnityHelpers.Tests.Core.Attributes
62
62
  }
63
63
 
64
64
  [Test]
65
- public void Assign_IEnumerable_AssignsOnlyRelationalTypes_AndSkipsNulls()
65
+ public void AssignIEnumerableAssignsOnlyRelationalTypesAndSkipsNulls()
66
66
  {
67
67
  AttributeMetadataCache cache = CreateScriptableObject<AttributeMetadataCache>();
68
68
 
@@ -34,6 +34,8 @@ namespace WallstopStudios.UnityHelpers.Tests.Editor.Utils
34
34
  yield return null;
35
35
  DeleteAssetIfExists("Assets/Resources/CustomPath/CustomPathSingleton.asset");
36
36
  yield return null;
37
+ DeleteFolderIfEmpty("Assets/Resources/CustomPath");
38
+ yield return null;
37
39
 
38
40
  // For nested test types, Unity cannot create valid .asset files (no script file).
39
41
  // Instead, create in-memory instances so the singleton loader can discover them via FindObjectsOfTypeAll.
@@ -81,6 +83,43 @@ namespace WallstopStudios.UnityHelpers.Tests.Editor.Utils
81
83
  }
82
84
  }
83
85
 
86
+ private static void DeleteFolderIfEmpty(string folderPath)
87
+ {
88
+ if (string.IsNullOrWhiteSpace(folderPath))
89
+ {
90
+ return;
91
+ }
92
+
93
+ if (!AssetDatabase.IsValidFolder(folderPath))
94
+ {
95
+ return;
96
+ }
97
+
98
+ string[] subFolders = AssetDatabase.GetSubFolders(folderPath);
99
+ if (subFolders != null && subFolders.Length > 0)
100
+ {
101
+ return;
102
+ }
103
+
104
+ string[] assetGuids = AssetDatabase.FindAssets(string.Empty, new[] { folderPath });
105
+ for (int i = 0; i < assetGuids.Length; i++)
106
+ {
107
+ string guid = assetGuids[i];
108
+ string assetPath = AssetDatabase.GUIDToAssetPath(guid);
109
+ if (
110
+ !string.IsNullOrEmpty(assetPath)
111
+ && !string.Equals(assetPath, folderPath, System.StringComparison.Ordinal)
112
+ )
113
+ {
114
+ return;
115
+ }
116
+ }
117
+
118
+ AssetDatabase.DeleteAsset(folderPath);
119
+ AssetDatabase.SaveAssets();
120
+ AssetDatabase.Refresh();
121
+ }
122
+
84
123
  private static TType CreateInMemoryInstance<TType>()
85
124
  where TType : ScriptableObject
86
125
  {
@@ -144,6 +183,8 @@ namespace WallstopStudios.UnityHelpers.Tests.Editor.Utils
144
183
  AssetDatabase.SaveAssets();
145
184
  AssetDatabase.Refresh();
146
185
  yield return null;
186
+ DeleteFolderIfEmpty("Assets/Resources/CustomPath");
187
+ yield return null;
147
188
  // Prefer public API surface over reflection to clean up the cached instance
148
189
  if (TestSingleton.HasInstance)
149
190
  {
@@ -4,6 +4,7 @@ namespace WallstopStudios.UnityHelpers.Tests.Serialization
4
4
  using System.Collections.Generic;
5
5
  using System.Linq;
6
6
  using System.Runtime.Serialization;
7
+ using System.Text.Json;
7
8
  using System.Text.Json.Serialization;
8
9
  using NUnit.Framework;
9
10
  using UnityEngine;
@@ -42,9 +43,9 @@ namespace WallstopStudios.UnityHelpers.Tests.Serialization
42
43
  using System.Text.Json.JsonDocument doc = System.Text.Json.JsonDocument.Parse(json);
43
44
  System.Text.Json.JsonElement root = doc.RootElement;
44
45
  Assert.AreEqual(System.Text.Json.JsonValueKind.Object, root.ValueKind);
45
- Assert.True(root.TryGetProperty("name", out var name));
46
- Assert.True(root.TryGetProperty("type", out var type));
47
- Assert.True(root.TryGetProperty("instanceId", out var id));
46
+ Assert.True(root.TryGetProperty("name", out JsonElement name));
47
+ Assert.True(root.TryGetProperty("type", out JsonElement type));
48
+ Assert.True(root.TryGetProperty("instanceId", out JsonElement id));
48
49
  Assert.AreEqual("Test GameObject", name.GetString());
49
50
  StringAssert.Contains("UnityEngine.GameObject", type.GetString());
50
51
  Assert.AreEqual(expectedId, id.GetInt32());