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.
- 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/Tags/Attribute.cs +144 -24
- package/Runtime/Tags/AttributeEffect.cs +119 -16
- 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/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/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/node_modules.meta +0 -8
- /package/Tests/Runtime/Tags/{AttributeDataTests.cs.meta → AttributeModificationTests.cs.meta} +0 -0
package/Docs/EFFECTS_SYSTEM.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
- **⭐ Build buff/debuff systems without writing custom code for every effect.**
|
|
6
6
|
- Data‑driven ScriptableObjects: designers create 100s of effects, programmers build system once.
|
|
7
7
|
- **Time saved: Weeks of boilerplate eliminated + designers empowered to iterate freely.**
|
|
8
|
+
- **✨ Attributes are NOT required!** Use the system purely for tag-based state management and timed cosmetic effects.
|
|
8
9
|
|
|
9
10
|
### ⭐ The Designer Empowerment Killer Feature
|
|
10
11
|
|
|
@@ -88,7 +89,7 @@ Visuals
|
|
|
88
89
|
|
|
89
90
|
- `Attribute` — A dynamic numeric value with a base and a calculated current value. Current value applies all active modifications.
|
|
90
91
|
- `AttributeModification` — Declarative change to an `Attribute`. Actions: Addition, Multiplication, Override. Applied in that order.
|
|
91
|
-
- `AttributeEffect` — ScriptableObject asset bundling modifications, tags, cosmetic data, and
|
|
92
|
+
- `AttributeEffect` — ScriptableObject asset bundling modifications, tags, cosmetic data, duration policy, periodic tick schedules, and optional runtime behaviours.
|
|
92
93
|
- `EffectHandle` — Opaque identifier for a specific application instance (for Duration/Infinite effects). Used to remove one stack.
|
|
93
94
|
- `AttributesComponent` — Base MonoBehaviour exposing modifiable `Attribute` fields (e.g., Health, Speed) on your character.
|
|
94
95
|
- `EffectHandler` — Component that applies/removes effects, tracks durations, forwards modifications to `AttributesComponent`, applies tags and cosmetics.
|
|
@@ -115,8 +116,9 @@ Instant effects modify base values permanently and return `null` instead of a ha
|
|
|
115
116
|
```csharp
|
|
116
117
|
public class CharacterStats : AttributesComponent
|
|
117
118
|
{
|
|
118
|
-
public Attribute
|
|
119
|
+
public Attribute MaxHealth = 100f;
|
|
119
120
|
public Attribute Speed = 5f;
|
|
121
|
+
public Attribute AttackDamage = 10f;
|
|
120
122
|
public Attribute Defense = 10f;
|
|
121
123
|
}
|
|
122
124
|
```
|
|
@@ -151,6 +153,635 @@ if (player.HasTag("Stunned"))
|
|
|
151
153
|
}
|
|
152
154
|
```
|
|
153
155
|
|
|
156
|
+
## Understanding Attributes: What to Model and What to Avoid
|
|
157
|
+
|
|
158
|
+
**Important: Attributes are NOT required!** The Effects System is extremely powerful even when used solely for tag-based state management and cosmetic effects.
|
|
159
|
+
|
|
160
|
+
### What Makes a Good Attribute?
|
|
161
|
+
|
|
162
|
+
Attributes work best for values that are:
|
|
163
|
+
|
|
164
|
+
- **Primarily modified by the effects system** (buffs, debuffs, equipment)
|
|
165
|
+
- **Derived from a base value** (MaxHealth, Speed, AttackDamage, Defense)
|
|
166
|
+
- **Calculated values** where you need to see the result of all modifications
|
|
167
|
+
|
|
168
|
+
### What Makes a Poor Attribute?
|
|
169
|
+
|
|
170
|
+
**❌ DON'T use Attributes for "current" values like CurrentHealth, CurrentMana, or CurrentAmmo!**
|
|
171
|
+
|
|
172
|
+
**Why?** These values are frequently modified by multiple systems:
|
|
173
|
+
|
|
174
|
+
- Combat system subtracts health on damage
|
|
175
|
+
- Healing system adds health
|
|
176
|
+
- Regeneration ticks add health over time
|
|
177
|
+
- Death system resets health to zero
|
|
178
|
+
- Save/load system restores health
|
|
179
|
+
|
|
180
|
+
**The Problem:**
|
|
181
|
+
|
|
182
|
+
```csharp
|
|
183
|
+
// ❌ BAD: CurrentHealth as an Attribute
|
|
184
|
+
public class PlayerStats : AttributesComponent
|
|
185
|
+
{
|
|
186
|
+
public Attribute CurrentHealth = 100f; // DON'T DO THIS!
|
|
187
|
+
public Attribute MaxHealth = 100f; // This is fine
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Multiple systems modify CurrentHealth:
|
|
191
|
+
void TakeDamage(float damage)
|
|
192
|
+
{
|
|
193
|
+
// Direct mutation bypasses the effects system
|
|
194
|
+
playerStats.CurrentHealth.BaseValue -= damage;
|
|
195
|
+
|
|
196
|
+
// Problem 1: If an effect was modifying CurrentHealth,
|
|
197
|
+
// it still applies! Now calculations are wrong.
|
|
198
|
+
// Problem 2: If you remove an effect, it may restore
|
|
199
|
+
// the ORIGINAL base value, undoing damage taken.
|
|
200
|
+
// Problem 3: Save/load becomes complicated - do you save
|
|
201
|
+
// base or current? What about active modifiers?
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**The Solution - Separate Current and Max:**
|
|
206
|
+
|
|
207
|
+
```csharp
|
|
208
|
+
// ✅ GOOD: CurrentHealth is a regular field, MaxHealth is an Attribute
|
|
209
|
+
public class PlayerStats : AttributesComponent
|
|
210
|
+
{
|
|
211
|
+
// Regular field - modified by combat/healing systems directly
|
|
212
|
+
private float currentHealth = 100f;
|
|
213
|
+
|
|
214
|
+
// Attribute - modified by buffs/effects
|
|
215
|
+
public Attribute MaxHealth = 100f;
|
|
216
|
+
|
|
217
|
+
public float CurrentHealth
|
|
218
|
+
{
|
|
219
|
+
get => currentHealth;
|
|
220
|
+
set => currentHealth = Mathf.Clamp(value, 0, MaxHealth.Value);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
void Start()
|
|
224
|
+
{
|
|
225
|
+
// Initialize current health to max
|
|
226
|
+
currentHealth = MaxHealth.Value;
|
|
227
|
+
|
|
228
|
+
// When max health changes, clamp current health
|
|
229
|
+
MaxHealth.OnValueChanged += (oldMax, newMax) =>
|
|
230
|
+
{
|
|
231
|
+
// If max decreased, ensure current doesn't exceed new max
|
|
232
|
+
if (currentHealth > newMax)
|
|
233
|
+
{
|
|
234
|
+
currentHealth = newMax;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Combat system can now safely modify current health
|
|
241
|
+
void TakeDamage(float damage)
|
|
242
|
+
{
|
|
243
|
+
playerStats.CurrentHealth -= damage; // Simple and correct
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Effects system modifies max health
|
|
247
|
+
void ApplyHealthBuff()
|
|
248
|
+
{
|
|
249
|
+
// MaxHealth × 1.5 (buffs max, current stays same)
|
|
250
|
+
player.ApplyEffect(healthBuffEffect);
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Attribute Best Practices
|
|
255
|
+
|
|
256
|
+
**✅ DO use Attributes for:**
|
|
257
|
+
|
|
258
|
+
- **MaxHealth, MaxMana, MaxStamina** - caps that buffs modify
|
|
259
|
+
- **Speed, MovementSpeed** - continuous values modified by effects
|
|
260
|
+
- **AttackDamage, Defense, CritChance** - combat stats
|
|
261
|
+
- **CooldownReduction, CastSpeed** - multiplicative modifiers
|
|
262
|
+
- **CarryCapacity, JumpHeight** - gameplay parameters
|
|
263
|
+
|
|
264
|
+
**❌ DON'T use Attributes for:**
|
|
265
|
+
|
|
266
|
+
- **CurrentHealth, CurrentMana** - depleting resources with complex mutation
|
|
267
|
+
- **Position, Rotation** - physics/transform state
|
|
268
|
+
- **Inventory count, Currency** - discrete counts from multiple sources
|
|
269
|
+
- **Quest progress, Level** - progression state
|
|
270
|
+
- **Input state, UI state** - transient application state
|
|
271
|
+
|
|
272
|
+
### Why This Matters
|
|
273
|
+
|
|
274
|
+
When you use Attributes for frequently-mutated "current" values:
|
|
275
|
+
|
|
276
|
+
1. **State conflicts** - Effects system and other systems fight over the value
|
|
277
|
+
2. **Save/load bugs** - Unclear whether to save base value or current value with modifiers
|
|
278
|
+
3. **Unexpected restorations** - Removing an effect may restore old base value, losing damage/healing
|
|
279
|
+
4. **Performance overhead** - Recalculating modifications on every damage tick
|
|
280
|
+
5. **Complexity** - Need to carefully coordinate between effects and direct mutations
|
|
281
|
+
|
|
282
|
+
**The Golden Rule:** If a value is modified by systems outside the effects system regularly (combat, regeneration, consumption), it should NOT be an Attribute. Use a regular field instead, and let Attributes handle the maximums/limits.
|
|
283
|
+
|
|
284
|
+
## Using Tags WITHOUT Attributes
|
|
285
|
+
|
|
286
|
+
Even without any Attributes, the Effects System is extremely powerful for tag-based state management and cosmetic effects.
|
|
287
|
+
|
|
288
|
+
### When to Use Tags Without Attributes
|
|
289
|
+
|
|
290
|
+
You should consider tag-only effects when:
|
|
291
|
+
|
|
292
|
+
- Managing categorical states ("Stunned", "Invisible", "InDialogue")
|
|
293
|
+
- Implementing temporary permissions ("CanDash", "CanDoubleJump")
|
|
294
|
+
- Coordinating system interactions ("InCombat", "InCutscene")
|
|
295
|
+
- Creating purely visual effects (particles, overlays) with timed lifetimes
|
|
296
|
+
- Building capability systems without numeric modifiers
|
|
297
|
+
|
|
298
|
+
### Example: Pure Tag Effects
|
|
299
|
+
|
|
300
|
+
```csharp
|
|
301
|
+
// No AttributesComponent needed!
|
|
302
|
+
public class StealthCharacter : MonoBehaviour
|
|
303
|
+
{
|
|
304
|
+
[SerializeField] private AttributeEffect invisibilityEffect;
|
|
305
|
+
[SerializeField] private AttributeEffect stunnedEffect;
|
|
306
|
+
|
|
307
|
+
void Start()
|
|
308
|
+
{
|
|
309
|
+
// Apply invisibility for 5 seconds
|
|
310
|
+
// InvisibilityEffect.asset:
|
|
311
|
+
// - durationType: Duration (5 seconds)
|
|
312
|
+
// - effectTags: ["Invisible", "Stealthy"]
|
|
313
|
+
// - modifications: (EMPTY - no attributes needed!)
|
|
314
|
+
// - cosmeticEffects: shimmer particles
|
|
315
|
+
this.ApplyEffect(invisibilityEffect);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
void Update()
|
|
319
|
+
{
|
|
320
|
+
// Check tags to gate behavior
|
|
321
|
+
if (this.HasTag("Stunned"))
|
|
322
|
+
{
|
|
323
|
+
// Prevent all actions
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// AI can't detect invisible characters
|
|
328
|
+
if (!this.HasTag("Invisible"))
|
|
329
|
+
{
|
|
330
|
+
BroadcastPosition();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Example: Tag Lifetimes for Cosmetics
|
|
337
|
+
|
|
338
|
+
Tags with durations provide automatic cleanup for visual effects:
|
|
339
|
+
|
|
340
|
+
```csharp
|
|
341
|
+
// Create a "ShowDamageIndicator" effect:
|
|
342
|
+
// DamageIndicator.asset:
|
|
343
|
+
// - durationType: Duration (1.5 seconds)
|
|
344
|
+
// - effectTags: ["DamageIndicator"]
|
|
345
|
+
// - modifications: (EMPTY)
|
|
346
|
+
// - cosmeticEffects: DamageNumbersPrefab
|
|
347
|
+
|
|
348
|
+
public class CombatFeedback : MonoBehaviour
|
|
349
|
+
{
|
|
350
|
+
[SerializeField] private AttributeEffect damageIndicator;
|
|
351
|
+
|
|
352
|
+
public void ShowDamage(float amount)
|
|
353
|
+
{
|
|
354
|
+
// Apply effect - cosmetic spawns automatically
|
|
355
|
+
this.ApplyEffect(damageIndicator);
|
|
356
|
+
|
|
357
|
+
// After 1.5 seconds, cosmetic is automatically cleaned up
|
|
358
|
+
// No manual cleanup code needed!
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Benefits of Tag-Only Usage
|
|
364
|
+
|
|
365
|
+
✅ **Simpler setup** - No AttributesComponent required
|
|
366
|
+
✅ **Automatic cleanup** - Duration-based tags clean up themselves
|
|
367
|
+
✅ **Reference counting** - Multiple sources work naturally
|
|
368
|
+
✅ **Cosmetic integration** - Visual effects lifecycle managed automatically
|
|
369
|
+
✅ **System decoupling** - Any system can query tags without dependencies
|
|
370
|
+
|
|
371
|
+
### Tag-Only Patterns
|
|
372
|
+
|
|
373
|
+
**1. Temporary Permissions:**
|
|
374
|
+
|
|
375
|
+
```csharp
|
|
376
|
+
// PowerUpEffect.asset:
|
|
377
|
+
// - durationType: Duration (10 seconds)
|
|
378
|
+
// - effectTags: ["CanDash", "CanDoubleJump", "PoweredUp"]
|
|
379
|
+
// - modifications: (EMPTY)
|
|
380
|
+
|
|
381
|
+
public void GrantPowerUp()
|
|
382
|
+
{
|
|
383
|
+
player.ApplyEffect(powerUpEffect);
|
|
384
|
+
// Player now has special abilities for 10 seconds
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**2. State Management:**
|
|
389
|
+
|
|
390
|
+
```csharp
|
|
391
|
+
// DialogueStateEffect.asset:
|
|
392
|
+
// - durationType: Infinite
|
|
393
|
+
// - effectTags: ["InDialogue", "InputDisabled"]
|
|
394
|
+
|
|
395
|
+
EffectHandle? dialogueHandle = player.ApplyEffect(dialogueState);
|
|
396
|
+
// ... dialogue system runs ...
|
|
397
|
+
player.RemoveEffect(dialogueHandle.Value);
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**3. Visual-Only Effects:**
|
|
401
|
+
|
|
402
|
+
```csharp
|
|
403
|
+
// LevelUpEffect.asset:
|
|
404
|
+
// - durationType: Duration (2 seconds)
|
|
405
|
+
// - effectTags: ["LevelingUp"]
|
|
406
|
+
// - cosmeticEffects: GlowParticles, LevelUpSound
|
|
407
|
+
|
|
408
|
+
player.ApplyEffect(levelUpEffect);
|
|
409
|
+
// Particles and sound play, then clean up automatically
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Cosmetic Effects - Complete Guide
|
|
413
|
+
|
|
414
|
+
Cosmetic effects handle the visual and audio presentation of effects. They provide a clean separation between gameplay logic (tags, attributes) and presentation (particles, sounds, UI).
|
|
415
|
+
|
|
416
|
+
### Architecture Overview
|
|
417
|
+
|
|
418
|
+
**Component Hierarchy:**
|
|
419
|
+
|
|
420
|
+
```text
|
|
421
|
+
CosmeticEffectData (Container GameObject/Prefab)
|
|
422
|
+
└─ CosmeticEffectComponent (Base class - abstract)
|
|
423
|
+
└─ Your custom implementations:
|
|
424
|
+
- ParticleCosmeticEffect
|
|
425
|
+
- AudioCosmeticEffect
|
|
426
|
+
- UICosmeticEffect
|
|
427
|
+
- AnimationCosmeticEffect
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Creating a Cosmetic Effect
|
|
431
|
+
|
|
432
|
+
### Step 1: Create a prefab with CosmeticEffectData\*\*
|
|
433
|
+
|
|
434
|
+
1. Create new GameObject in scene
|
|
435
|
+
2. Add Component → `CosmeticEffectData`
|
|
436
|
+
3. Add your custom cosmetic components (particle systems, audio sources, etc.)
|
|
437
|
+
4. Save as prefab
|
|
438
|
+
5. Reference this prefab in your `AttributeEffect.cosmeticEffects` list
|
|
439
|
+
|
|
440
|
+
### Step 2: Implement CosmeticEffectComponent subclasses\*\*
|
|
441
|
+
|
|
442
|
+
```csharp
|
|
443
|
+
using UnityEngine;
|
|
444
|
+
using WallstopStudios.UnityHelpers.Tags;
|
|
445
|
+
|
|
446
|
+
public class ParticleCosmeticEffect : CosmeticEffectComponent
|
|
447
|
+
{
|
|
448
|
+
[SerializeField] private ParticleSystem particles;
|
|
449
|
+
|
|
450
|
+
// RequiresInstance = true creates a new instance per application
|
|
451
|
+
// RequiresInstance = false shares one instance across all applications
|
|
452
|
+
public override bool RequiresInstance => true;
|
|
453
|
+
|
|
454
|
+
// CleansUpSelf = true means you handle destruction yourself
|
|
455
|
+
// CleansUpSelf = false means EffectHandler destroys the GameObject
|
|
456
|
+
public override bool CleansUpSelf => false;
|
|
457
|
+
|
|
458
|
+
public override void OnApplyEffect(GameObject target)
|
|
459
|
+
{
|
|
460
|
+
base.OnApplyEffect(target);
|
|
461
|
+
|
|
462
|
+
// Attach cosmetic to target
|
|
463
|
+
transform.SetParent(target.transform);
|
|
464
|
+
transform.localPosition = Vector3.zero;
|
|
465
|
+
|
|
466
|
+
// Start visual effect
|
|
467
|
+
particles.Play();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
public override void OnRemoveEffect(GameObject target)
|
|
471
|
+
{
|
|
472
|
+
base.OnRemoveEffect(target);
|
|
473
|
+
|
|
474
|
+
// Stop particles
|
|
475
|
+
particles.Stop();
|
|
476
|
+
|
|
477
|
+
// If CleansUpSelf = false, GameObject is destroyed automatically
|
|
478
|
+
// If CleansUpSelf = true, you must handle destruction
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### RequiresInstance: Shared vs Instanced
|
|
484
|
+
|
|
485
|
+
**RequiresInstance = false (Shared):**
|
|
486
|
+
|
|
487
|
+
- One cosmetic instance is reused for all applications
|
|
488
|
+
- Best for: UI overlays, status icons, shared audio managers
|
|
489
|
+
- Lower memory footprint
|
|
490
|
+
- All targets share the same cosmetic GameObject
|
|
491
|
+
|
|
492
|
+
```csharp
|
|
493
|
+
public class StatusIconCosmetic : CosmeticEffectComponent
|
|
494
|
+
{
|
|
495
|
+
public override bool RequiresInstance => false; // SHARED
|
|
496
|
+
|
|
497
|
+
[SerializeField] private Image iconImage;
|
|
498
|
+
private int activeCount = 0;
|
|
499
|
+
|
|
500
|
+
public override void OnApplyEffect(GameObject target)
|
|
501
|
+
{
|
|
502
|
+
base.OnApplyEffect(target);
|
|
503
|
+
activeCount++;
|
|
504
|
+
|
|
505
|
+
// Show icon if this is first application
|
|
506
|
+
if (activeCount == 1)
|
|
507
|
+
{
|
|
508
|
+
iconImage.enabled = true;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
public override void OnRemoveEffect(GameObject target)
|
|
513
|
+
{
|
|
514
|
+
base.OnRemoveEffect(target);
|
|
515
|
+
activeCount--;
|
|
516
|
+
|
|
517
|
+
// Hide icon when no more applications
|
|
518
|
+
if (activeCount == 0)
|
|
519
|
+
{
|
|
520
|
+
iconImage.enabled = false;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
**RequiresInstance = true (Instanced):**
|
|
527
|
+
|
|
528
|
+
- New cosmetic instance created for each application
|
|
529
|
+
- Best for: Particles, per-effect animations, independent visuals
|
|
530
|
+
- Each application has isolated state
|
|
531
|
+
- Higher memory cost, but full independence
|
|
532
|
+
|
|
533
|
+
```csharp
|
|
534
|
+
public class FireParticleCosmetic : CosmeticEffectComponent
|
|
535
|
+
{
|
|
536
|
+
public override bool RequiresInstance => true; // INSTANCED
|
|
537
|
+
|
|
538
|
+
[SerializeField] private ParticleSystem fireParticles;
|
|
539
|
+
|
|
540
|
+
public override void OnApplyEffect(GameObject target)
|
|
541
|
+
{
|
|
542
|
+
base.OnApplyEffect(target);
|
|
543
|
+
|
|
544
|
+
// Each instance is independent
|
|
545
|
+
transform.SetParent(target.transform);
|
|
546
|
+
transform.localPosition = Vector3.zero;
|
|
547
|
+
fireParticles.Play();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### CleansUpSelf: Automatic vs Manual Cleanup
|
|
553
|
+
|
|
554
|
+
**CleansUpSelf = false (Automatic - Default):**
|
|
555
|
+
|
|
556
|
+
- EffectHandler destroys the GameObject when effect is removed
|
|
557
|
+
- Simplest option for most cases
|
|
558
|
+
- Immediate cleanup
|
|
559
|
+
|
|
560
|
+
```csharp
|
|
561
|
+
public class SimpleParticleEffect : CosmeticEffectComponent
|
|
562
|
+
{
|
|
563
|
+
public override bool CleansUpSelf => false; // AUTOMATIC
|
|
564
|
+
|
|
565
|
+
public override void OnRemoveEffect(GameObject target)
|
|
566
|
+
{
|
|
567
|
+
base.OnRemoveEffect(target);
|
|
568
|
+
// GameObject destroyed automatically by EffectHandler
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
**CleansUpSelf = true (Manual Cleanup):**
|
|
574
|
+
|
|
575
|
+
- You are responsible for destroying the GameObject
|
|
576
|
+
- Use when you need delayed cleanup (fade out animations, particle finish)
|
|
577
|
+
- More control over cleanup timing
|
|
578
|
+
|
|
579
|
+
```csharp
|
|
580
|
+
public class FadeOutEffect : CosmeticEffectComponent
|
|
581
|
+
{
|
|
582
|
+
public override bool CleansUpSelf => true; // MANUAL
|
|
583
|
+
|
|
584
|
+
[SerializeField] private float fadeOutDuration = 1f;
|
|
585
|
+
private bool isRemoving = false;
|
|
586
|
+
|
|
587
|
+
public override void OnRemoveEffect(GameObject target)
|
|
588
|
+
{
|
|
589
|
+
base.OnRemoveEffect(target);
|
|
590
|
+
|
|
591
|
+
if (!isRemoving)
|
|
592
|
+
{
|
|
593
|
+
isRemoving = true;
|
|
594
|
+
StartCoroutine(FadeOutAndDestroy());
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
private IEnumerator FadeOutAndDestroy()
|
|
599
|
+
{
|
|
600
|
+
// Fade out over time
|
|
601
|
+
float elapsed = 0f;
|
|
602
|
+
SpriteRenderer sprite = GetComponent<SpriteRenderer>();
|
|
603
|
+
Color originalColor = sprite.color;
|
|
604
|
+
|
|
605
|
+
while (elapsed < fadeOutDuration)
|
|
606
|
+
{
|
|
607
|
+
elapsed += Time.deltaTime;
|
|
608
|
+
float alpha = 1f - (elapsed / fadeOutDuration);
|
|
609
|
+
sprite.color = new Color(originalColor.r, originalColor.g, originalColor.b, alpha);
|
|
610
|
+
yield return null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Now safe to destroy
|
|
614
|
+
Destroy(gameObject);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### Complete Cosmetic Examples
|
|
620
|
+
|
|
621
|
+
#### Example 1: Buff Visual with Particles and Sound\*\*
|
|
622
|
+
|
|
623
|
+
```csharp
|
|
624
|
+
public class BuffCosmetic : CosmeticEffectComponent
|
|
625
|
+
{
|
|
626
|
+
[SerializeField] private ParticleSystem buffParticles;
|
|
627
|
+
[SerializeField] private AudioSource audioSource;
|
|
628
|
+
[SerializeField] private AudioClip applySound;
|
|
629
|
+
[SerializeField] private AudioClip removeSound;
|
|
630
|
+
|
|
631
|
+
public override bool RequiresInstance => true;
|
|
632
|
+
public override bool CleansUpSelf => false;
|
|
633
|
+
|
|
634
|
+
public override void OnApplyEffect(GameObject target)
|
|
635
|
+
{
|
|
636
|
+
base.OnApplyEffect(target);
|
|
637
|
+
|
|
638
|
+
// Position cosmetic on target
|
|
639
|
+
transform.SetParent(target.transform);
|
|
640
|
+
transform.localPosition = Vector3.zero;
|
|
641
|
+
|
|
642
|
+
// Play effects
|
|
643
|
+
buffParticles.Play();
|
|
644
|
+
audioSource.PlayOneShot(applySound);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
public override void OnRemoveEffect(GameObject target)
|
|
648
|
+
{
|
|
649
|
+
base.OnRemoveEffect(target);
|
|
650
|
+
|
|
651
|
+
audioSource.PlayOneShot(removeSound);
|
|
652
|
+
buffParticles.Stop();
|
|
653
|
+
// Automatic cleanup after this
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
#### Example 2: Status UI Overlay (Shared)\*\*
|
|
659
|
+
|
|
660
|
+
```csharp
|
|
661
|
+
public class StatusOverlayCosmetic : CosmeticEffectComponent
|
|
662
|
+
{
|
|
663
|
+
[SerializeField] private SpriteRenderer overlaySprite;
|
|
664
|
+
[SerializeField] private Color overlayColor = Color.red;
|
|
665
|
+
|
|
666
|
+
public override bool RequiresInstance => false; // SHARED
|
|
667
|
+
public override bool CleansUpSelf => false;
|
|
668
|
+
|
|
669
|
+
private SpriteRenderer targetSprite;
|
|
670
|
+
|
|
671
|
+
public override void OnApplyEffect(GameObject target)
|
|
672
|
+
{
|
|
673
|
+
base.OnApplyEffect(target);
|
|
674
|
+
|
|
675
|
+
targetSprite = target.GetComponent<SpriteRenderer>();
|
|
676
|
+
if (targetSprite != null)
|
|
677
|
+
{
|
|
678
|
+
// Tint the sprite
|
|
679
|
+
targetSprite.color = overlayColor;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
public override void OnRemoveEffect(GameObject target)
|
|
684
|
+
{
|
|
685
|
+
base.OnRemoveEffect(target);
|
|
686
|
+
|
|
687
|
+
if (targetSprite != null)
|
|
688
|
+
{
|
|
689
|
+
// Restore original color
|
|
690
|
+
targetSprite.color = Color.white;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
#### Example 3: Animation Trigger\*\*
|
|
697
|
+
|
|
698
|
+
```csharp
|
|
699
|
+
public class AnimationCosmetic : CosmeticEffectComponent
|
|
700
|
+
{
|
|
701
|
+
[SerializeField] private string applyTrigger = "BuffApplied";
|
|
702
|
+
[SerializeField] private string removeTrigger = "BuffRemoved";
|
|
703
|
+
|
|
704
|
+
public override bool RequiresInstance => false;
|
|
705
|
+
|
|
706
|
+
public override void OnApplyEffect(GameObject target)
|
|
707
|
+
{
|
|
708
|
+
base.OnApplyEffect(target);
|
|
709
|
+
|
|
710
|
+
Animator animator = target.GetComponent<Animator>();
|
|
711
|
+
if (animator != null)
|
|
712
|
+
{
|
|
713
|
+
animator.SetTrigger(applyTrigger);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
public override void OnRemoveEffect(GameObject target)
|
|
718
|
+
{
|
|
719
|
+
base.OnRemoveEffect(target);
|
|
720
|
+
|
|
721
|
+
Animator animator = target.GetComponent<Animator>();
|
|
722
|
+
if (animator != null)
|
|
723
|
+
{
|
|
724
|
+
animator.SetTrigger(removeTrigger);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
### Combining Multiple Cosmetics
|
|
731
|
+
|
|
732
|
+
A single effect can have multiple cosmetic components with different behaviors:
|
|
733
|
+
|
|
734
|
+
```csharp
|
|
735
|
+
// PoisonEffect prefab:
|
|
736
|
+
// - CosmeticEffectData
|
|
737
|
+
// - PoisonParticles (RequiresInstance = true) // One per stack
|
|
738
|
+
// - PoisonStatusIcon (RequiresInstance = false) // Shared UI element
|
|
739
|
+
// - PoisonAudioLoop (RequiresInstance = true) // One audio loop per stack
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
### Cosmetic Lifecycle
|
|
743
|
+
|
|
744
|
+
**Application Flow:**
|
|
745
|
+
|
|
746
|
+
1. `AttributeEffect` applied to GameObject
|
|
747
|
+
2. `EffectHandler` checks `cosmeticEffects` list
|
|
748
|
+
3. For each `CosmeticEffectData`:
|
|
749
|
+
- If `RequiresInstancing = true`: Instantiate and parent to target
|
|
750
|
+
- If `RequiresInstancing = false`: Reuse existing instance
|
|
751
|
+
4. Call `OnApplyEffect(target)` on all components
|
|
752
|
+
5. Cosmetics remain active while effect is active
|
|
753
|
+
|
|
754
|
+
**Removal Flow:**
|
|
755
|
+
|
|
756
|
+
1. Effect expires or is manually removed
|
|
757
|
+
2. `EffectHandler` calls `OnRemoveEffect(target)` on all components
|
|
758
|
+
3. For each component:
|
|
759
|
+
- If `CleansUpSelf = false`: EffectHandler destroys GameObject immediately
|
|
760
|
+
- If `CleansUpSelf = true`: Component handles its own destruction
|
|
761
|
+
|
|
762
|
+
### Best Practices
|
|
763
|
+
|
|
764
|
+
**Performance:**
|
|
765
|
+
|
|
766
|
+
- ✅ Prefer `RequiresInstance = false` when possible (lower overhead)
|
|
767
|
+
- ✅ Use object pooling for frequently spawned instanced cosmetics
|
|
768
|
+
- ✅ Keep `OnApplyEffect` and `OnRemoveEffect` lightweight
|
|
769
|
+
- ❌ Avoid expensive operations in these callbacks
|
|
770
|
+
|
|
771
|
+
**Architecture:**
|
|
772
|
+
|
|
773
|
+
- ✅ One responsibility per cosmetic component (particles, audio, UI separate)
|
|
774
|
+
- ✅ Store references in `OnApplyEffect`, use them in `OnRemoveEffect`
|
|
775
|
+
- ✅ Always call `base.OnApplyEffect()` and `base.OnRemoveEffect()`
|
|
776
|
+
- ❌ Don't access gameplay logic from cosmetics (maintain separation)
|
|
777
|
+
|
|
778
|
+
**Cleanup:**
|
|
779
|
+
|
|
780
|
+
- ✅ Use `CleansUpSelf = false` unless you need delayed cleanup
|
|
781
|
+
- ✅ If using `CleansUpSelf = true`, ensure you always destroy the GameObject
|
|
782
|
+
- ✅ Handle null targets gracefully (target may be destroyed early)
|
|
783
|
+
- ❌ Don't leak GameObjects by forgetting to clean up
|
|
784
|
+
|
|
154
785
|
## Recipes
|
|
155
786
|
|
|
156
787
|
### 1) Buff with % Speed for 5s (refreshable)
|
|
@@ -158,12 +789,38 @@ if (player.HasTag("Stunned"))
|
|
|
158
789
|
- Effect: Multiplication `Speed *= 1.5f`, `Duration=5`, `resetDurationOnReapplication=true`, tag `Haste`.
|
|
159
790
|
- Apply to extend: reapply before expiry to reset the timer.
|
|
160
791
|
|
|
161
|
-
### 2) Poison:
|
|
792
|
+
### 2) Poison: "Poisoned" tag for 10s with periodic damage
|
|
162
793
|
|
|
163
|
-
-
|
|
164
|
-
-
|
|
794
|
+
- periodicEffects: add a definition with `interval = 1s`, `maxTicks = 10`, and an empty `modifications` array (ticks drive behaviours)
|
|
795
|
+
- behaviors: attach a `PoisonDamageBehavior` that applies damage during `OnPeriodicTick` (sample below)
|
|
796
|
+
- durationType: Duration `10s` (or Infinite if the periodic schedule should drive expiry)
|
|
165
797
|
- effectTags: `[ "Poisoned" ]`
|
|
166
798
|
- cosmetics: particles + UI icon
|
|
799
|
+
- Optional: add an immediate modification for on-apply burst damage
|
|
800
|
+
|
|
801
|
+
```csharp
|
|
802
|
+
[CreateAssetMenu(menuName = "Combat/Effects/Poison Damage")]
|
|
803
|
+
public sealed class PoisonDamageBehavior : EffectBehavior
|
|
804
|
+
{
|
|
805
|
+
[SerializeField]
|
|
806
|
+
private float damagePerTick = 5f;
|
|
807
|
+
|
|
808
|
+
public override void OnPeriodicTick(
|
|
809
|
+
EffectBehaviorContext context,
|
|
810
|
+
PeriodicEffectTickContext tickContext
|
|
811
|
+
)
|
|
812
|
+
{
|
|
813
|
+
if (!context.Target.TryGetComponent(out PlayerHealth health))
|
|
814
|
+
{
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
health.ApplyDamage(damagePerTick);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
Pair this with a health component that owns mutable current-health state instead of modelling `CurrentHealth` as an Attribute.
|
|
167
824
|
|
|
168
825
|
### 3) Equipment Aura: +10 Defense while equipped
|
|
169
826
|
|
|
@@ -178,14 +835,33 @@ if (player.HasTag("Stunned"))
|
|
|
178
835
|
|
|
179
836
|
### 5) Stacking Multiple Instances
|
|
180
837
|
|
|
181
|
-
-
|
|
182
|
-
-
|
|
838
|
+
- Set `stackingMode` on the effect asset to control reapplication:
|
|
839
|
+
- `Stack` keeps separate handles (respecting `maximumStacks`, trimming the oldest when the cap is reached).
|
|
840
|
+
- `Refresh` reuses the first handle; set `resetDurationOnReapplication = true` if the timer should reset on reapply.
|
|
841
|
+
- `Replace` removes existing handles in the same group before adding a new one.
|
|
842
|
+
- `Ignore` rejects duplicate applications.
|
|
843
|
+
- Use `stackGroup = CustomKey` with a shared `stackGroupKey` when different assets should share a stack identity.
|
|
844
|
+
- Inspect active stacks with `EffectHandler.GetEffectStackCount(effect)` or tag counts for debugging and UI.
|
|
183
845
|
|
|
184
846
|
### 6) Shared vs Instanced Cosmetics
|
|
185
847
|
|
|
186
848
|
- In `CosmeticEffectData`, set a component’s `RequiresInstance = true` for per‑application instances (e.g., particles).
|
|
187
849
|
- Keep `RequiresInstance = false` for shared presenters (e.g., status icon overlay).
|
|
188
850
|
|
|
851
|
+
### Periodic Tick Payloads
|
|
852
|
+
|
|
853
|
+
- Populate the `periodicEffects` list on an `AttributeEffect` to schedule damage/heal-over-time, resource regen, or scripted pulses without external coroutines.
|
|
854
|
+
- Each definition supports `initialDelay`, `interval`, and `maxTicks` (0 = infinite) plus its own `AttributeModification` bundle applied on every tick.
|
|
855
|
+
- Periodic payloads run only for Duration/Infinite effects; they automatically stop after `maxTicks` or when the effect handle is removed.
|
|
856
|
+
- Combine multiple definitions for mixed cadences (e.g., fast minor regen + slower burst heals).
|
|
857
|
+
|
|
858
|
+
### Effect Behaviours
|
|
859
|
+
|
|
860
|
+
- Attach `EffectBehavior` ScriptableObjects to the `behaviors` list for per-handle runtime logic.
|
|
861
|
+
- The system clones behaviours on apply and calls `OnApply`, `OnTick` (each frame), `OnPeriodicTick` (after periodic payloads fire), and `OnRemove`.
|
|
862
|
+
- Behaviours are ideal for integrating bespoke systems (e.g., camera shakes, AI hooks, quest tracking) while keeping designer-authored effects data-driven.
|
|
863
|
+
- Keep behaviours stateless or store per-handle state on the cloned instance; clean up in `OnRemove`.
|
|
864
|
+
|
|
189
865
|
## Best Practices
|
|
190
866
|
|
|
191
867
|
- Use Addition for flat changes; Multiplication for percentage changes; Override sparingly (wins last).
|
|
@@ -382,9 +1058,153 @@ void LogEffect(EffectType effectType)
|
|
|
382
1058
|
|
|
383
1059
|
This eliminates the need to manually maintain a `DisplayNames` dictionary as shown in the earlier example—the package already provides optimized caching infrastructure.
|
|
384
1060
|
|
|
1061
|
+
## API Reference
|
|
1062
|
+
|
|
1063
|
+
### AttributeEffect Query Methods
|
|
1064
|
+
|
|
1065
|
+
**Checking for Tags:**
|
|
1066
|
+
|
|
1067
|
+
```csharp
|
|
1068
|
+
// Check if effect has a specific tag
|
|
1069
|
+
bool hasTag = effect.HasTag("Haste");
|
|
1070
|
+
|
|
1071
|
+
// Check if effect has any of the specified tags
|
|
1072
|
+
bool hasAny = effect.HasAnyTag(new[] { "Haste", "Speed", "Boost" });
|
|
1073
|
+
bool hasAnyFromList = effect.HasAnyTag(myTagList); // IReadOnlyList<string> overload
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
**Checking for Attribute Modifications:**
|
|
1077
|
+
|
|
1078
|
+
```csharp
|
|
1079
|
+
// Check if effect modifies a specific attribute
|
|
1080
|
+
bool modifiesSpeed = effect.ModifiesAttribute("Speed");
|
|
1081
|
+
|
|
1082
|
+
// Get all modifications for a specific attribute
|
|
1083
|
+
using var lease = Buffers<AttributeModification>.List.Get(out List<AttributeModification> mods);
|
|
1084
|
+
effect.GetModifications("Speed", mods);
|
|
1085
|
+
foreach (AttributeModification mod in mods)
|
|
1086
|
+
{
|
|
1087
|
+
Debug.Log($"Action: {mod.action}, Value: {mod.value}");
|
|
1088
|
+
}
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
### TagHandler Query Methods
|
|
1092
|
+
|
|
1093
|
+
**Basic Tag Queries:**
|
|
1094
|
+
|
|
1095
|
+
```csharp
|
|
1096
|
+
// Check if a single tag is active
|
|
1097
|
+
if (player.HasTag("Stunned"))
|
|
1098
|
+
{
|
|
1099
|
+
DisableInput();
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Check if any of the tags are active
|
|
1103
|
+
if (player.HasAnyTag(new[] { "Stunned", "Frozen", "Sleeping" }))
|
|
1104
|
+
{
|
|
1105
|
+
PreventMovement();
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Check if all tags are active
|
|
1109
|
+
if (player.HasAllTags(new[] { "Wet", "Grounded" }))
|
|
1110
|
+
{
|
|
1111
|
+
ApplyElectricShock();
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Check if none of the tags are active
|
|
1115
|
+
if (player.HasNoneOfTags(new[] { "Invulnerable", "Untargetable" }))
|
|
1116
|
+
{
|
|
1117
|
+
AllowDamage();
|
|
1118
|
+
}
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
**Tag Count Queries:**
|
|
1122
|
+
|
|
1123
|
+
```csharp
|
|
1124
|
+
// Get the active count for a tag
|
|
1125
|
+
if (player.TryGetTagCount("Poisoned", out int stacks) && stacks >= 3)
|
|
1126
|
+
{
|
|
1127
|
+
TriggerCriticalPoisonWarning();
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Get all active tags
|
|
1131
|
+
List<string> activeTags = player.GetActiveTags();
|
|
1132
|
+
foreach (string tag in activeTags)
|
|
1133
|
+
{
|
|
1134
|
+
Debug.Log($"Active tag: {tag}");
|
|
1135
|
+
}
|
|
1136
|
+
```
|
|
1137
|
+
|
|
1138
|
+
**Collection Type Support:**
|
|
1139
|
+
|
|
1140
|
+
All tag query methods support multiple collection types with optimized implementations:
|
|
1141
|
+
|
|
1142
|
+
- `IReadOnlyList<string>` (optimized with index-based iteration)
|
|
1143
|
+
- `List<string>`
|
|
1144
|
+
- `HashSet<string>`
|
|
1145
|
+
- `SortedSet<string>`
|
|
1146
|
+
- `Queue<string>`
|
|
1147
|
+
- `Stack<string>`
|
|
1148
|
+
- `LinkedList<string>`
|
|
1149
|
+
- Any `IEnumerable<string>`
|
|
1150
|
+
|
|
1151
|
+
```csharp
|
|
1152
|
+
// Example with different collection types
|
|
1153
|
+
HashSet<string> immunityTags = new() { "Invulnerable", "Immune" };
|
|
1154
|
+
if (player.HasAnyTag(immunityTags))
|
|
1155
|
+
{
|
|
1156
|
+
PreventDamage();
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
List<string> crowdControlTags = new() { "Stunned", "Rooted", "Silenced" };
|
|
1160
|
+
if (player.HasNoneOfTags(crowdControlTags))
|
|
1161
|
+
{
|
|
1162
|
+
EnableAllAbilities();
|
|
1163
|
+
}
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
### EffectHandler Query Methods
|
|
1167
|
+
|
|
1168
|
+
**Effect State Queries:**
|
|
1169
|
+
|
|
1170
|
+
```csharp
|
|
1171
|
+
// Check if a specific effect is currently active
|
|
1172
|
+
if (effectHandler.IsEffectActive(hasteEffect))
|
|
1173
|
+
{
|
|
1174
|
+
ShowHasteIndicator();
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// Get the stack count for an effect
|
|
1178
|
+
int hasteStacks = effectHandler.GetEffectStackCount(hasteEffect);
|
|
1179
|
+
Debug.Log($"Haste stacks: {hasteStacks}");
|
|
1180
|
+
|
|
1181
|
+
// Get remaining duration for a specific effect instance
|
|
1182
|
+
if (effectHandler.TryGetRemainingDuration(effectHandle, out float remaining))
|
|
1183
|
+
{
|
|
1184
|
+
UpdateDurationUI(remaining);
|
|
1185
|
+
}
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
**Effect Manipulation:**
|
|
1189
|
+
|
|
1190
|
+
```csharp
|
|
1191
|
+
// Refresh an effect's duration
|
|
1192
|
+
if (effectHandler.RefreshEffect(effectHandle))
|
|
1193
|
+
{
|
|
1194
|
+
Debug.Log("Effect duration refreshed");
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// Refresh effect ignoring reapplication policy
|
|
1198
|
+
effectHandler.RefreshEffect(effectHandle, ignoreReapplicationPolicy: true);
|
|
1199
|
+
```
|
|
1200
|
+
|
|
385
1201
|
## FAQ
|
|
386
1202
|
|
|
387
|
-
Q:
|
|
1203
|
+
Q: Should I use an Attribute for CurrentHealth?
|
|
1204
|
+
|
|
1205
|
+
- **No!** Use Attributes for values primarily modified by the effects system (MaxHealth, Speed, AttackDamage). CurrentHealth is modified by multiple systems (combat, healing, regeneration) and should be a regular field. See "Understanding Attributes: What to Model and What to Avoid" section above for details. Mixing direct mutations with effect modifications causes state conflicts and save/load bugs.
|
|
1206
|
+
|
|
1207
|
+
Q: Why didn't I get an `EffectHandle`?
|
|
388
1208
|
|
|
389
1209
|
- Instant effects modify the base value permanently and do not return a handle (`null`). Duration/Infinite do.
|
|
390
1210
|
|
|
@@ -404,6 +1224,14 @@ Q: When should I use tags vs checking stats?
|
|
|
404
1224
|
|
|
405
1225
|
- Use tags to represent categorical states (e.g., Stunned/Poisoned/Invulnerable) independent from numeric values. Check stats for numeric thresholds or calculations.
|
|
406
1226
|
|
|
1227
|
+
Q: How do I check if an effect modifies a specific attribute?
|
|
1228
|
+
|
|
1229
|
+
- Use `effect.ModifiesAttribute("AttributeName")` to check if an effect contains modifications for a specific attribute, or `effect.GetModifications("AttributeName", buffer)` to retrieve all modifications for that attribute.
|
|
1230
|
+
|
|
1231
|
+
Q: How do I query tag counts or check multiple tags at once?
|
|
1232
|
+
|
|
1233
|
+
- Use `TryGetTagCount(tag, out int count)` to get the active count for a tag, `HasAllTags(tags)` to check if all tags are active, or `HasNoneOfTags(tags)` to check if none are active.
|
|
1234
|
+
|
|
407
1235
|
## Troubleshooting
|
|
408
1236
|
|
|
409
1237
|
- Attribute name doesn’t apply
|