// 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); } } }