com.wallstop-studios.unity-helpers 2.0.2 → 2.0.4

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.
@@ -184,6 +184,185 @@ namespace WallstopStudios.UnityHelpers.Tags
184
184
  _appliedEffects.Clear();
185
185
  }
186
186
 
187
+ /// <summary>
188
+ /// Determines whether the specified effect is currently active on this handler.
189
+ /// </summary>
190
+ /// <param name="effect">The effect to check.</param>
191
+ /// <returns><c>true</c> if at least one handle for the effect is active; otherwise, <c>false</c>.</returns>
192
+ public bool IsEffectActive(AttributeEffect effect)
193
+ {
194
+ if (effect == null)
195
+ {
196
+ return false;
197
+ }
198
+
199
+ for (int i = 0; i < _appliedEffects.Count; ++i)
200
+ {
201
+ EffectHandle handle = _appliedEffects[i];
202
+ if (handle.effect == effect)
203
+ {
204
+ return true;
205
+ }
206
+ }
207
+
208
+ return false;
209
+ }
210
+
211
+ /// <summary>
212
+ /// Gets the number of active handles for the specified effect.
213
+ /// </summary>
214
+ /// <param name="effect">The effect to count.</param>
215
+ /// <returns>The number of active handles associated with <paramref name="effect"/>.</returns>
216
+ public int GetEffectStackCount(AttributeEffect effect)
217
+ {
218
+ if (effect == null)
219
+ {
220
+ return 0;
221
+ }
222
+
223
+ int count = 0;
224
+ for (int i = 0; i < _appliedEffects.Count; ++i)
225
+ {
226
+ EffectHandle handle = _appliedEffects[i];
227
+ if (handle.effect == effect)
228
+ {
229
+ ++count;
230
+ }
231
+ }
232
+
233
+ return count;
234
+ }
235
+
236
+ /// <summary>
237
+ /// Copies all active effect handles into the provided buffer.
238
+ /// </summary>
239
+ /// <param name="buffer">
240
+ /// Optional list to populate. When <c>null</c>, a new list is created. The buffer is cleared before population.
241
+ /// </param>
242
+ /// <returns>The populated buffer containing all currently active effect handles.</returns>
243
+ public List<EffectHandle> GetActiveEffects(List<EffectHandle> buffer = null)
244
+ {
245
+ buffer ??= new List<EffectHandle>();
246
+ buffer.Clear();
247
+ buffer.AddRange(_appliedEffects);
248
+ return buffer;
249
+ }
250
+
251
+ /// <summary>
252
+ /// Attempts to retrieve the remaining duration for the specified effect handle.
253
+ /// </summary>
254
+ /// <param name="handle">The handle to inspect.</param>
255
+ /// <param name="remainingDuration">When this method returns, contains the remaining time in seconds, or zero if unavailable.</param>
256
+ /// <returns><c>true</c> if the handle has a tracked duration; otherwise, <c>false</c>.</returns>
257
+ public bool TryGetRemainingDuration(EffectHandle handle, out float remainingDuration)
258
+ {
259
+ long handleId = handle.id;
260
+ if (!_effectExpirations.TryGetValue(handleId, out float expiration))
261
+ {
262
+ remainingDuration = 0f;
263
+ return false;
264
+ }
265
+
266
+ float timeRemaining = expiration - Time.time;
267
+ if (timeRemaining < 0f)
268
+ {
269
+ timeRemaining = 0f;
270
+ }
271
+
272
+ remainingDuration = timeRemaining;
273
+ return true;
274
+ }
275
+
276
+ /// <summary>
277
+ /// Ensures an effect handle exists for the specified effect, optionally refreshing its duration if already active.
278
+ /// </summary>
279
+ /// <param name="effect">The effect to apply or refresh.</param>
280
+ /// <returns>An active handle for the effect, or <c>null</c> for instant effects.</returns>
281
+ public EffectHandle? EnsureHandle(AttributeEffect effect)
282
+ {
283
+ return EnsureHandle(effect, refreshDuration: true);
284
+ }
285
+
286
+ /// <summary>
287
+ /// Ensures an effect handle exists for the specified effect, optionally refreshing its duration if already active.
288
+ /// </summary>
289
+ /// <param name="effect">The effect to apply or refresh.</param>
290
+ /// <param name="refreshDuration">
291
+ /// When <c>true</c>, attempts to refresh the effect's duration when it is already active and supports reapplication.
292
+ /// </param>
293
+ /// <returns>An active handle for the effect, or <c>null</c> for instant effects.</returns>
294
+ public EffectHandle? EnsureHandle(AttributeEffect effect, bool refreshDuration)
295
+ {
296
+ if (effect == null)
297
+ {
298
+ return null;
299
+ }
300
+
301
+ for (int i = 0; i < _appliedEffects.Count; ++i)
302
+ {
303
+ EffectHandle handle = _appliedEffects[i];
304
+ if (handle.effect == effect)
305
+ {
306
+ if (refreshDuration)
307
+ {
308
+ _ = RefreshEffect(handle);
309
+ }
310
+
311
+ return handle;
312
+ }
313
+ }
314
+
315
+ return ApplyEffect(effect);
316
+ }
317
+
318
+ /// <summary>
319
+ /// Attempts to refresh the duration of the specified effect handle.
320
+ /// </summary>
321
+ /// <param name="handle">The handle to refresh.</param>
322
+ /// <returns><c>true</c> if the duration was refreshed; otherwise, <c>false</c>.</returns>
323
+ public bool RefreshEffect(EffectHandle handle)
324
+ {
325
+ return RefreshEffect(handle, ignoreReapplicationPolicy: false);
326
+ }
327
+
328
+ /// <summary>
329
+ /// Attempts to refresh the duration of the specified effect handle.
330
+ /// </summary>
331
+ /// <param name="handle">The handle to refresh.</param>
332
+ /// <param name="ignoreReapplicationPolicy">
333
+ /// When <c>true</c>, refreshes the duration even if <see cref="AttributeEffect.resetDurationOnReapplication"/> is <c>false</c>.
334
+ /// </param>
335
+ /// <returns><c>true</c> if the duration was refreshed; otherwise, <c>false</c>.</returns>
336
+ public bool RefreshEffect(EffectHandle handle, bool ignoreReapplicationPolicy)
337
+ {
338
+ AttributeEffect effect = handle.effect;
339
+ if (effect == null)
340
+ {
341
+ return false;
342
+ }
343
+
344
+ if (effect.durationType != ModifierDurationType.Duration)
345
+ {
346
+ return false;
347
+ }
348
+
349
+ if (!ignoreReapplicationPolicy && !effect.resetDurationOnReapplication)
350
+ {
351
+ return false;
352
+ }
353
+
354
+ long handleId = handle.id;
355
+ if (!_effectExpirations.ContainsKey(handleId))
356
+ {
357
+ return false;
358
+ }
359
+
360
+ float newExpiration = Time.time + effect.duration;
361
+ _effectExpirations[handleId] = newExpiration;
362
+ _effectHandlesById[handleId] = handle;
363
+ return true;
364
+ }
365
+
187
366
  private void InternalRemoveEffect(EffectHandle handle)
188
367
  {
189
368
  foreach (AttributesComponent attributesComponent in _attributes)
@@ -250,6 +250,304 @@ namespace WallstopStudios.UnityHelpers.Tags
250
250
  return false;
251
251
  }
252
252
 
253
+ /// <summary>
254
+ /// Checks whether all of the specified tags are currently active.
255
+ /// Optimized for different collection types with specialized implementations.
256
+ /// </summary>
257
+ /// <param name="effectTags">The collection of tags to check.</param>
258
+ /// <returns><c>true</c> if all tags are active; otherwise, <c>false</c>. Returns <c>false</c> when <paramref name="effectTags"/> is <c>null</c>.</returns>
259
+ public bool HasAllTags(IEnumerable<string> effectTags)
260
+ {
261
+ if (effectTags == null)
262
+ {
263
+ return false;
264
+ }
265
+
266
+ switch (effectTags)
267
+ {
268
+ case IReadOnlyList<string> list:
269
+ {
270
+ return HasAllTags(list);
271
+ }
272
+ case HashSet<string> hashSet:
273
+ {
274
+ foreach (string effectTag in hashSet)
275
+ {
276
+ if (string.IsNullOrEmpty(effectTag))
277
+ {
278
+ continue;
279
+ }
280
+ if (!_tagCount.ContainsKey(effectTag))
281
+ {
282
+ return false;
283
+ }
284
+ }
285
+
286
+ return true;
287
+ }
288
+ case SortedSet<string> sortedSet:
289
+ {
290
+ foreach (string effectTag in sortedSet)
291
+ {
292
+ if (string.IsNullOrEmpty(effectTag))
293
+ {
294
+ continue;
295
+ }
296
+ if (!_tagCount.ContainsKey(effectTag))
297
+ {
298
+ return false;
299
+ }
300
+ }
301
+
302
+ return true;
303
+ }
304
+ case Queue<string> queue:
305
+ {
306
+ foreach (string effectTag in queue)
307
+ {
308
+ if (string.IsNullOrEmpty(effectTag))
309
+ {
310
+ continue;
311
+ }
312
+ if (!_tagCount.ContainsKey(effectTag))
313
+ {
314
+ return false;
315
+ }
316
+ }
317
+
318
+ return true;
319
+ }
320
+ case Stack<string> stack:
321
+ {
322
+ foreach (string effectTag in stack)
323
+ {
324
+ if (string.IsNullOrEmpty(effectTag))
325
+ {
326
+ continue;
327
+ }
328
+ if (!_tagCount.ContainsKey(effectTag))
329
+ {
330
+ return false;
331
+ }
332
+ }
333
+
334
+ return true;
335
+ }
336
+ case LinkedList<string> linkedList:
337
+ {
338
+ foreach (string effectTag in linkedList)
339
+ {
340
+ if (string.IsNullOrEmpty(effectTag))
341
+ {
342
+ continue;
343
+ }
344
+ if (!_tagCount.ContainsKey(effectTag))
345
+ {
346
+ return false;
347
+ }
348
+ }
349
+
350
+ return true;
351
+ }
352
+ }
353
+
354
+ foreach (string effectTag in effectTags)
355
+ {
356
+ if (string.IsNullOrEmpty(effectTag))
357
+ {
358
+ continue;
359
+ }
360
+ if (!_tagCount.ContainsKey(effectTag))
361
+ {
362
+ return false;
363
+ }
364
+ }
365
+
366
+ return true;
367
+ }
368
+
369
+ /// <summary>
370
+ /// Checks whether all of the specified tags are active.
371
+ /// Optimized for IReadOnlyList with index-based iteration.
372
+ /// </summary>
373
+ /// <param name="effectTags">The list of tags to check.</param>
374
+ /// <returns><c>true</c> if all of the tags are active, or if the list is empty; otherwise, <c>false</c>.</returns>
375
+ public bool HasAllTags(IReadOnlyList<string> effectTags)
376
+ {
377
+ if (effectTags == null)
378
+ {
379
+ return false;
380
+ }
381
+
382
+ for (int i = 0; i < effectTags.Count; ++i)
383
+ {
384
+ string effectTag = effectTags[i];
385
+ if (string.IsNullOrEmpty(effectTag))
386
+ {
387
+ continue;
388
+ }
389
+
390
+ if (!_tagCount.ContainsKey(effectTag))
391
+ {
392
+ return false;
393
+ }
394
+ }
395
+
396
+ return true;
397
+ }
398
+
399
+ /// <summary>
400
+ /// Determines whether none of the specified tags are active.
401
+ /// </summary>
402
+ /// <param name="effectTags">The collection of tags to inspect.</param>
403
+ /// <returns>
404
+ /// <c>true</c> when the collection is <c>null</c>, empty, or every tag is currently inactive; otherwise, <c>false</c>.
405
+ /// </returns>
406
+ /// <example>
407
+ /// <code>
408
+ /// if (tagHandler.HasNoneOfTags(new[] { "Stunned", "Frozen" }))
409
+ /// {
410
+ /// EnablePlayerInput();
411
+ /// }
412
+ /// </code>
413
+ /// </example>
414
+ public bool HasNoneOfTags(IEnumerable<string> effectTags)
415
+ {
416
+ if (effectTags == null)
417
+ {
418
+ return true;
419
+ }
420
+
421
+ return !HasAnyTag(effectTags);
422
+ }
423
+
424
+ /// <summary>
425
+ /// Determines whether none of the specified tags are active.
426
+ /// </summary>
427
+ /// <param name="effectTags">The list of tags to inspect.</param>
428
+ /// <returns>
429
+ /// <c>true</c> when the list is <c>null</c>, empty, or every tag is currently inactive; otherwise, <c>false</c>.
430
+ /// </returns>
431
+ public bool HasNoneOfTags(IReadOnlyList<string> effectTags)
432
+ {
433
+ if (effectTags == null)
434
+ {
435
+ return true;
436
+ }
437
+
438
+ return !HasAnyTag(effectTags);
439
+ }
440
+
441
+ /// <summary>
442
+ /// Attempts to retrieve the active instance count for the specified tag.
443
+ /// </summary>
444
+ /// <param name="effectTag">The tag whose count should be retrieved.</param>
445
+ /// <param name="count">
446
+ /// When this method returns, contains the active count for the tag (cast to <see cref="int"/>) if found; otherwise, zero.
447
+ /// </param>
448
+ /// <returns><c>true</c> if the tag is currently tracked; otherwise, <c>false</c>.</returns>
449
+ /// <example>
450
+ /// <code>
451
+ /// if (tagHandler.TryGetTagCount("Poisoned", out int stacks) && stacks >= 3)
452
+ /// {
453
+ /// TriggerCriticalWarning();
454
+ /// }
455
+ /// </code>
456
+ /// </example>
457
+ public bool TryGetTagCount(string effectTag, out int count)
458
+ {
459
+ if (string.IsNullOrEmpty(effectTag))
460
+ {
461
+ count = default;
462
+ return false;
463
+ }
464
+
465
+ if (_tagCount.TryGetValue(effectTag, out uint uintCount))
466
+ {
467
+ count = unchecked((int)uintCount);
468
+ return true;
469
+ }
470
+
471
+ count = default;
472
+ return false;
473
+ }
474
+
475
+ /// <summary>
476
+ /// Retrieves the set of currently active tags into an optional buffer.
477
+ /// </summary>
478
+ /// <param name="buffer">
479
+ /// Optional list to populate. When <c>null</c>, a new list is created. The buffer is cleared before population.
480
+ /// </param>
481
+ /// <returns>The populated buffer containing all active tags.</returns>
482
+ /// <example>
483
+ /// <code>
484
+ /// List&lt;string&gt; activeTags = tagHandler.GetActiveTags(_reusableTagBuffer);
485
+ /// if (activeTags.Contains("Rooted"))
486
+ /// {
487
+ /// DisableMovement();
488
+ /// }
489
+ /// </code>
490
+ /// </example>
491
+ public List<string> GetActiveTags(List<string> buffer = null)
492
+ {
493
+ buffer ??= new List<string>();
494
+ buffer.Clear();
495
+ foreach (KeyValuePair<string, uint> entry in _tagCount)
496
+ {
497
+ if (entry.Value == 0)
498
+ {
499
+ continue;
500
+ }
501
+
502
+ buffer.Add(entry.Key);
503
+ }
504
+
505
+ return buffer;
506
+ }
507
+
508
+ /// <summary>
509
+ /// Collects all active effect handles that currently contribute the specified tag.
510
+ /// </summary>
511
+ /// <param name="effectTag">The tag to query.</param>
512
+ /// <param name="buffer">
513
+ /// Optional list to populate. When <c>null</c>, a new list is created. The buffer is cleared before population.
514
+ /// </param>
515
+ /// <returns>The populated buffer containing matching effect handles.</returns>
516
+ /// <example>
517
+ /// <code>
518
+ /// List&lt;EffectHandle&gt; handles = tagHandler.GetHandlesWithTag("Burning", _handleBuffer);
519
+ /// foreach (EffectHandle handle in handles)
520
+ /// {
521
+ /// effectHandler.RemoveEffect(handle);
522
+ /// }
523
+ /// </code>
524
+ /// </example>
525
+ public List<EffectHandle> GetHandlesWithTag(
526
+ string effectTag,
527
+ List<EffectHandle> buffer = null
528
+ )
529
+ {
530
+ buffer ??= new List<EffectHandle>();
531
+ buffer.Clear();
532
+ if (string.IsNullOrEmpty(effectTag))
533
+ {
534
+ return buffer;
535
+ }
536
+
537
+ foreach (EffectHandle handle in _effectHandles.Values)
538
+ {
539
+ if (
540
+ handle.effect.effectTags != null
541
+ && handle.effect.effectTags.Contains(effectTag)
542
+ )
543
+ {
544
+ buffer.Add(handle);
545
+ }
546
+ }
547
+
548
+ return buffer;
549
+ }
550
+
253
551
  /// <summary>
254
552
  /// Applies a tag, incrementing its count. If the tag is new, raises <see cref="OnTagAdded"/>.
255
553
  /// Otherwise, raises <see cref="OnTagCountChanged"/>.
@@ -261,22 +559,52 @@ namespace WallstopStudios.UnityHelpers.Tags
261
559
  }
262
560
 
263
561
  /// <summary>
264
- /// Removes one or all instances of a tag.
562
+ /// Removes all instances of the specified tag and returns the contributing effect handles.
265
563
  /// </summary>
266
564
  /// <param name="effectTag">The tag to remove.</param>
267
- /// <param name="allInstances">
268
- /// If true, completely removes the tag regardless of count.
269
- /// If false, decrements the count by 1, removing it only when count reaches 0.
565
+ /// <param name="buffer">
566
+ /// Optional list that receives the handles whose effects applied <paramref name="effectTag"/>.
567
+ /// When <c>null</c>, a new list is created. The buffer is cleared before population.
270
568
  /// </param>
271
- public void RemoveTag(string effectTag, bool allInstances)
569
+ /// <returns>
570
+ /// The populated buffer of handles whose tags were removed. The buffer is empty when the tag was not active.
571
+ /// </returns>
572
+ /// <example>
573
+ /// <code>
574
+ /// List&lt;EffectHandle&gt; dispelled = tagHandler.RemoveTag("Stunned", _handles);
575
+ /// foreach (EffectHandle handle in dispelled)
576
+ /// {
577
+ /// NotifyDispel(handle);
578
+ /// }
579
+ /// </code>
580
+ /// </example>
581
+ public List<EffectHandle> RemoveTag(string effectTag, List<EffectHandle> buffer = null)
272
582
  {
273
- if (allInstances)
583
+ buffer ??= new List<EffectHandle>();
584
+ buffer.Clear();
585
+ if (string.IsNullOrEmpty(effectTag))
274
586
  {
275
- _tagCount.Remove(effectTag);
276
- return;
587
+ return buffer;
588
+ }
589
+
590
+ foreach (EffectHandle handle in _effectHandles.Values)
591
+ {
592
+ if (
593
+ handle.effect.effectTags != null
594
+ && handle.effect.effectTags.Contains(effectTag)
595
+ )
596
+ {
597
+ buffer.Add(handle);
598
+ }
277
599
  }
278
600
 
279
- InternalRemoveTag(effectTag);
601
+ foreach (EffectHandle handle in buffer)
602
+ {
603
+ ForceRemoveTags(handle);
604
+ }
605
+
606
+ InternalRemoveTag(effectTag, allInstances: true);
607
+ return buffer;
280
608
  }
281
609
 
282
610
  /// <summary>
@@ -292,7 +620,7 @@ namespace WallstopStudios.UnityHelpers.Tags
292
620
  return;
293
621
  }
294
622
 
295
- ForceApplyEffect(handle.effect);
623
+ ApplyEffectTags(handle.effect);
296
624
  }
297
625
 
298
626
  /// <summary>
@@ -302,10 +630,7 @@ namespace WallstopStudios.UnityHelpers.Tags
302
630
  /// <param name="effect">The effect containing tags to apply.</param>
303
631
  public void ForceApplyEffect(AttributeEffect effect)
304
632
  {
305
- foreach (string effectTag in effect.effectTags)
306
- {
307
- InternalApplyTag(effectTag);
308
- }
633
+ ApplyEffectTags(effect);
309
634
  }
310
635
 
311
636
  /// <summary>
@@ -321,11 +646,7 @@ namespace WallstopStudios.UnityHelpers.Tags
321
646
  return false;
322
647
  }
323
648
 
324
- foreach (string effectTag in appliedHandle.effect.effectTags)
325
- {
326
- InternalRemoveTag(effectTag);
327
- }
328
-
649
+ RemoveEffectTags(appliedHandle.effect);
329
650
  return true;
330
651
  }
331
652
 
@@ -346,7 +667,7 @@ namespace WallstopStudios.UnityHelpers.Tags
346
667
  }
347
668
  }
348
669
 
349
- private void InternalRemoveTag(string effectTag)
670
+ private void InternalRemoveTag(string effectTag, bool allInstances)
350
671
  {
351
672
  if (!_tagCount.TryGetValue(effectTag, out uint count))
352
673
  {
@@ -355,7 +676,14 @@ namespace WallstopStudios.UnityHelpers.Tags
355
676
 
356
677
  if (count != 0)
357
678
  {
358
- --count;
679
+ if (!allInstances)
680
+ {
681
+ --count;
682
+ }
683
+ else
684
+ {
685
+ count = 0;
686
+ }
359
687
  }
360
688
 
361
689
  if (count == 0)
@@ -369,5 +697,31 @@ namespace WallstopStudios.UnityHelpers.Tags
369
697
  OnTagCountChanged?.Invoke(effectTag, count);
370
698
  }
371
699
  }
700
+
701
+ private void ApplyEffectTags(AttributeEffect effect)
702
+ {
703
+ if (effect.effectTags == null)
704
+ {
705
+ return;
706
+ }
707
+
708
+ foreach (string effectTag in effect.effectTags)
709
+ {
710
+ InternalApplyTag(effectTag);
711
+ }
712
+ }
713
+
714
+ private void RemoveEffectTags(AttributeEffect effect)
715
+ {
716
+ if (effect.effectTags == null)
717
+ {
718
+ return;
719
+ }
720
+
721
+ foreach (string effectTag in effect.effectTags)
722
+ {
723
+ InternalRemoveTag(effectTag, allInstances: false);
724
+ }
725
+ }
372
726
  }
373
727
  }