// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Tags
{
using System;
using System.Collections.Generic;
using Core.Helper;
using UnityEngine;
using Utils;
///
/// Prefab-like container for visual/audio behaviors that represent an effect's cosmetic feedback.
/// Groups one or more s and declares if instancing is required.
///
///
///
/// Role in the system: references one or more CosmeticEffectData assets.
/// When the effect is applied, will either:
/// - Reuse the existing CosmeticEffectData on the target (RequiresInstancing = false), OR
/// - Instantiate a copy and parent it to the target (RequiresInstancing = true).
/// On removal, corresponding cosmetic components receive .
///
///
/// Problems solved:
/// - Decouple gameplay logic from presentation.
/// - Support shared cosmetic presenters (e.g., a single status icon) or per‑instance visuals (e.g., particle emitters).
/// - Automatic lifecycle management (instantiation and cleanup) alongside effect application/removal.
///
///
/// Authoring pattern:
/// - Create a prefab with a CosmeticEffectData + one or more CosmeticEffectComponent scripts.
/// - Mark a component's true if a unique instance per effect is needed.
/// - Reference the prefab in your list.
///
///
/// Example:
///
/// // PoisonEffectData (Prefab)
/// // - CosmeticEffectData
/// // - PoisonParticles : CosmeticEffectComponent (RequiresInstance = true)
/// // - PoisonIcon : CosmeticEffectComponent (shared UI, RequiresInstance = false)
///
/// // In AttributeEffect: cosmeticEffects = [ PoisonEffectData ]
/// // EffectHandler will instance PoisonParticles per application and reuse PoisonIcon as needed.
///
///
///
/// Tips:
/// - Prefer shared presenters when possible (fewer instantiations).
/// - If a component animates its own teardown, set to true.
/// - Keep CosmeticEffectData lightweight; heavy content belongs in the child components.
///
///
/// Warning: All methods on this class reflect the current component state.
/// If components are added or removed after an instance is placed in a hash-based collection
/// (e.g., or ), the collection
/// may behave unexpectedly because and
/// will return different values than when the instance was added.
///
///
[DisallowMultipleComponent]
public sealed class CosmeticEffectData : MonoBehaviour, IEquatable
{
///
/// Indicates whether this cosmetic effect requires a new instance for each application.
/// Returns true if any child CosmeticEffectComponent requires instancing.
///
///
///
/// This property always reflects the current state of attached components.
/// It uses Unity's non-allocating GetComponents(List) overload with pooled lists
/// to achieve zero allocations while ensuring correctness when components are added or removed.
///
///
/// Destroyed Unity objects are safely skipped during iteration.
///
///
public bool RequiresInstancing
{
get
{
using PooledResource> lease =
Buffers.List.Get(
out List cosmetics
);
GetComponents(cosmetics);
for (int i = 0; i < cosmetics.Count; i++)
{
CosmeticEffectComponent cosmetic = cosmetics[i];
if (cosmetic == null)
{
continue;
}
if (cosmetic.RequiresInstance)
{
return true;
}
}
return false;
}
}
///
/// Populates the provided set with the types of all currently attached s.
///
/// The set to populate. Will be cleared before adding types.
///
/// Uses pooled lists for zero-allocation queries. Destroyed Unity objects are safely skipped.
///
private void GetCurrentCosmeticTypes(HashSet types)
{
types.Clear();
using PooledResource> lease =
Buffers.List.Get(
out List cosmetics
);
GetComponents(cosmetics);
for (int i = 0; i < cosmetics.Count; i++)
{
CosmeticEffectComponent cosmetic = cosmetics[i];
if (cosmetic == null)
{
continue;
}
types.Add(cosmetic.GetType());
}
}
///
/// Determines whether this instance is equal to another object.
///
/// The object to compare with.
/// true when is a with matching components and name; otherwise, false.
public override bool Equals(object other)
{
return other is CosmeticEffectData cosmeticEffectData && Equals(cosmeticEffectData);
}
///
/// Determines whether this instance is equal to another .
/// Equality compares the current set of contained types and the GameObject name.
///
/// The other cosmetic effect data to compare.
/// true if both assets expose the same component types and share the same name; otherwise, false.
///
/// This method reflects the current component state at the time of the call.
/// Uses pooled HashSets for zero-allocation comparison.
///
public bool Equals(CosmeticEffectData other)
{
if (ReferenceEquals(this, other))
{
return true;
}
if (other == null)
{
return false;
}
using PooledResource> thisLease = Buffers.HashSet.Get(
out HashSet thisTypes
);
using PooledResource> otherLease = Buffers.HashSet.Get(
out HashSet otherTypes
);
GetCurrentCosmeticTypes(thisTypes);
other.GetCurrentCosmeticTypes(otherTypes);
if (!thisTypes.SetEquals(otherTypes))
{
return false;
}
return Helpers.NameEquals(this, other);
}
///
/// Returns a hash code based on the current number of valid cosmetic components.
///
/// A hash code suitable for use in hash-based collections.
///
/// This method reflects the current component state at the time of the call.
/// If components are added or removed, the hash code will change.
///
public override int GetHashCode()
{
using PooledResource> lease =
Buffers.List.Get(
out List cosmetics
);
GetComponents(cosmetics);
int count = 0;
for (int i = 0; i < cosmetics.Count; i++)
{
if (cosmetics[i] != null)
{
count++;
}
}
return Objects.HashCode(count);
}
}
}