// 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