using System.Collections.Generic; using UnityEngine; using Ashwild.Inventory; namespace Ashwild.Player { /// /// Keeps a rig's Animator loaded with the clips of whatever item the player is holding. It owns /// exactly one concern — *which clips are in the controller right now* — while PlayerAnimatorDriver /// owns *which parameters are set*. Splitting them is what lets a new weapon ship without touching /// either: the driver keeps firing the same "Attack" trigger, the binder just points that slot at a /// different clip. /// /// It never assigns a fresh controller per item. Assigning runtimeAnimatorController rebinds the /// Animator, which resets the state machine to Entry and wipes the parameters — the player would /// visibly snap to idle every time he scrolled the hotbar. Instead one AnimatorOverrideController is /// built in Awake and only its overrides are mutated afterwards, which leaves the current state and /// all parameters untouched. That same property is what keeps FishNet's NetworkAnimator working on /// the third-person body: an override controller preserves the base controller's parameter list, so /// replication never sees the swap. /// [DisallowMultipleComponent] public class PlayerAnimationSetBinder : MonoBehaviour { #region Serialized Fields [Header("Target")] [SerializeField] private Animator animator; [Tooltip("Which rig this binder drives — it picks the matching set off the held ItemData.")] [SerializeField] private PlayerAnimationRig rig = PlayerAnimationRig.Arms; [Header("Sets")] [Tooltip("Set used with bare hands, and as the fallback for any slot the held item leaves empty.")] [SerializeField] private PlayerAnimationSet defaultSet; #endregion #region State private AnimatorOverrideController overrideController; private AnimationClip[] placeholders; private readonly List> overrides = new List>(); private PlayerAnimationSet currentSet; private bool ready; #endregion #region Unity Lifecycle /// /// Wraps the authored controller in a single override controller and caches its placeholder /// clips, whose names are the slot keys every set is matched against. Done in Awake so the /// rebind happens once, before any driver seeds a parameter. /// private void Awake() { if (animator == null) { Debug.LogError($"[PlayerAnimationSetBinder] '{name}' has no Animator assigned — clips will never swap.", this); return; } RuntimeAnimatorController master = animator.runtimeAnimatorController; if (master == null) { Debug.LogError($"[PlayerAnimationSetBinder] '{name}' — the Animator has no controller assigned.", this); return; } overrideController = new AnimatorOverrideController(master) { name = $"{master.name} (Runtime)" }; overrideController.GetOverrides(overrides); placeholders = new AnimationClip[overrides.Count]; for (int i = 0; i < overrides.Count; i++) placeholders[i] = overrides[i].Key; animator.runtimeAnimatorController = overrideController; ready = true; ApplySet(defaultSet); } /// /// Subscribes to the held-item bus. HotbarController re-raises HeldItemChanged on its own first /// resolve, so a rig enabled after the player already drew an item still lands on the right set. /// private void OnEnable() { PlayerEvents.HeldItemChanged += HandleHeldItemChanged; } /// /// Unsubscribes — mirrors OnEnable exactly. /// private void OnDisable() { PlayerEvents.HeldItemChanged -= HandleHeldItemChanged; } #endregion #region Event Handlers /// /// Loads the set the newly drawn item authors for this rig (or the bare-hand set when the player /// empties his hands). /// private void HandleHeldItemChanged(ItemData item) { ApplySet(item != null ? item.GetAnimationSet(rig) : defaultSet); } #endregion #region Internal Helpers /// /// Rewrites every slot in one ApplyOverrides call rather than one indexer assignment per clip, /// because each individual assignment forces the controller to regenerate. Skips redundant work /// when the set has not actually changed — several items can legitimately share one set. /// private void ApplySet(PlayerAnimationSet set) { if (!ready) return; if (set == currentSet) return; currentSet = set; for (int i = 0; i < placeholders.Length; i++) { AnimationClip placeholder = placeholders[i]; overrides[i] = new KeyValuePair(placeholder, Resolve(placeholder.name, set)); } overrideController.ApplyOverrides(overrides); } /// /// Picks the clip for a slot: the held item's set first, then the default set, then null — which /// means "no override" and leaves the master controller's own placeholder playing. That last /// step is the safety net: a slot nobody authored still animates instead of freezing the rig. /// private AnimationClip Resolve(string slotName, PlayerAnimationSet set) { AnimationClip clip = set != null ? set.GetClip(slotName) : null; if (clip == null && defaultSet != null) clip = defaultSet.GetClip(slotName); return clip; } #endregion } }