// 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.Attributes; using UnityEngine; /// /// Abstract base class for components that contain Attribute fields to be modified by effects. /// Subclasses should define public or private Attribute fields that can be dynamically modified. /// /// /// /// This component automatically: /// - Discovers all Attribute fields via reflection (optimized with caching) /// - Registers with the EffectHandler to receive attribute modifications /// - Applies and removes modifications based on effect handles /// - Notifies listeners when attributes change /// /// /// Example usage: /// /// public class CharacterStats : AttributesComponent /// { /// public Attribute Health = new Attribute(100f); /// public Attribute Speed = new Attribute(5f); /// public Attribute Damage = new Attribute(10f); /// /// protected override void Awake() /// { /// base.Awake(); /// OnAttributeModified += (attrName, oldVal, newVal) => /// { /// Debug.Log($"{attrName} changed from {oldVal} to {newVal}"); /// }; /// } /// } /// /// /// [RequireComponent(typeof(TagHandler))] [RequireComponent(typeof(EffectHandler))] public abstract class AttributesComponent : MonoBehaviour { /// /// Invoked when an attribute's value changes due to an effect being applied or removed. /// Provides the attribute name, old value, and new value. /// public event Action OnAttributeModified; private Dictionary> _attributeFieldGetters; private readonly HashSet _effectHandles; [SiblingComponent] protected TagHandler _tagHandler; [SiblingComponent] protected EffectHandler _effectHandler; /// /// Initializes the AttributesComponent by discovering all Attribute fields in the derived class. /// protected AttributesComponent() { _effectHandles = new HashSet(); } /// /// Initializes sibling components and registers with the EffectHandler. /// Override this method in derived classes, but always call base.Awake(). /// protected virtual void Awake() { EnsureAttributeFieldGettersInitialized(); this.AssignSiblingComponents(); _effectHandler.Register(this); } /// /// Unregisters from the EffectHandler when destroyed. /// Override this method in derived classes, but always call base.OnDestroy(). /// protected virtual void OnDestroy() { if (_effectHandler != null) { _effectHandler.Remove(this); } } /// /// Applies a collection of attribute modifications, either instantly or with an effect handle. /// /// The modifications to apply. /// Optional effect handle for tracking. If null, modifications are permanent. public void ApplyAttributeModifications( IEnumerable attributeModifications, EffectHandle? handle ) { if (handle.HasValue) { ForceApplyAttributeModifications(handle.Value); return; } InternalApplyAttributeModifications(attributeModifications); } /// /// Removes all attribute modifications associated with the specified effect handle. /// Called automatically by the EffectHandler when an effect is removed. /// /// The effect handle whose modifications should be removed. public void ForceRemoveAttributeModifications(EffectHandle handle) { InternalRemoveAttributeModifications(handle); } private void InternalApplyAttributeModifications( IEnumerable attributeModifications ) { if (attributeModifications is IReadOnlyList readonlyList) { for (int i = 0; i < readonlyList.Count; ++i) { AttributeModification modification = readonlyList[i]; if (!TryGetAttribute(modification.attribute, out Attribute attribute)) { continue; } float oldValue = attribute; attribute.ApplyAttributeModification(modification); float currentValue = attribute; OnAttributeModified?.Invoke(modification.attribute, oldValue, currentValue); } return; } foreach (AttributeModification modification in attributeModifications) { if (!TryGetAttribute(modification.attribute, out Attribute attribute)) { continue; } float oldValue = attribute; attribute.ApplyAttributeModification(modification); float currentValue = attribute; OnAttributeModified?.Invoke(modification.attribute, oldValue, currentValue); } } /// /// Applies all attribute modifications from an effect handle. /// Called automatically by the EffectHandler when an effect is applied. /// /// The effect handle containing modifications to apply. public void ForceApplyAttributeModifications(EffectHandle handle) { AttributeEffect effect = handle.effect; if (effect.modifications is not { Count: > 0 }) { return; } bool isNewEffect = false; foreach (AttributeModification modification in effect.modifications) { if (!TryGetAttribute(modification.attribute, out Attribute attribute)) { continue; } isNewEffect |= _effectHandles.Add(handle); if (isNewEffect) { float oldValue = attribute; attribute.ApplyAttributeModification(modification, handle); float currentValue = attribute; OnAttributeModified?.Invoke(modification.attribute, oldValue, currentValue); } } } /// /// Applies all attribute modifications from an effect without tracking a handle. /// Used for instant effects. /// /// The effect containing modifications to apply. public void ForceApplyAttributeModifications(AttributeEffect effect) { if (effect.modifications is not { Count: > 0 }) { return; } foreach (AttributeModification modification in effect.modifications) { if (!TryGetAttribute(modification.attribute, out Attribute attribute)) { continue; } float oldValue = attribute; attribute.ApplyAttributeModification(modification); float currentValue = attribute; OnAttributeModified?.Invoke(modification.attribute, oldValue, currentValue); } } private void InternalRemoveAttributeModifications(EffectHandle handle) { AttributeEffect effect = handle.effect; if (effect.modifications is not { Count: > 0 }) { return; } foreach (AttributeModification modification in effect.modifications) { if (!TryGetAttribute(modification.attribute, out Attribute attribute)) { continue; } float oldValue = attribute; _ = attribute.RemoveAttributeModification(handle); float currentValue = attribute; _ = _effectHandles.Remove(handle); OnAttributeModified?.Invoke(modification.attribute, oldValue, currentValue); } } // ReSharper disable once MemberCanBePrivate.Global protected bool TryGetAttribute(string attributeName, out Attribute attribute) { EnsureAttributeFieldGettersInitialized(); if ( !_attributeFieldGetters.TryGetValue( attributeName, out Func getter ) ) { attribute = default; return false; } attribute = getter(this); return true; } // ReSharper disable once MemberCanBePrivate.Global protected void EnsureAttributeFieldGettersInitialized() { if (_attributeFieldGetters != null) { return; } _attributeFieldGetters = AttributeUtilities.GetOptimizedAttributeFields(GetType()); } } }