using UnityEngine; using Ashwild.Player; namespace Ashwild.Inventory { /// /// Keeps the item in the player's hand in sync with the selected hotbar slot. It owns the *timing* /// of a swap, not its look: the put-away and draw animations live in the arms rig, and this /// controller simply waits for them. /// /// The wait is what makes the swap read correctly. Destroying the old prefab the instant the /// selection changes makes the item vanish mid-motion, so instead the sequence is: ask for the /// put-away animation, wait for its Animation Event, then destroy, announce the new item (which /// lets the animation binder load its clips first), spawn it, and ask for the draw animation. /// /// That wait is guarded by a timeout, because the completion event only exists if someone plays the /// clip. An item whose set has no put-away clip, a rig that was never wired, a disabled arms /// object — any of those would otherwise leave the player stuck holding an item he already /// switched away from, with no error to explain it. The timeout turns a silent deadlock into a /// slightly abrupt swap. /// [DisallowMultipleComponent] public class HotbarController : MonoBehaviour { #region Serialized Fields [Header("References")] [Tooltip("Attachment joint in the arms rig's right hand (RightHand_Holder_JTN). Held prefabs are parented here so they follow the animated hand.")] [SerializeField] private Transform handHolder; [Tooltip("Origin and forward of the aim ray handed to held items — normally the first-person camera.")] [SerializeField] private Transform raycastOrigin; [Header("Swap")] [Tooltip("Seconds to wait for the put-away animation before swapping anyway. Safety net only — a wired rig always completes first.")] [SerializeField] private float stowTimeout = 0.5f; #endregion #region State private PlayerInventory inventory; private HeldItemContext heldContext; private GameObject currentHeldObject; private ItemData currentHeldItem; private bool isStowing; private ItemData pendingItem; private float stowDeadline; private bool stowTimeoutReported; #endregion #region Unity Lifecycle /// /// Binds to the local inventory and draws whatever is already selected, without animation — /// spawning into the world should not look like the player just swapped weapons. /// private void Start() { inventory = PlayerInventory.Instance; if (inventory == null) { Debug.LogError("[HotbarController] No local PlayerInventory — the hand will stay empty.", this); return; } inventory.onSelectedSlotChanged.AddListener(HandleSelectionChanged); inventory.onSlotChanged.AddListener(HandleSlotChanged); heldContext = new HeldItemContext { RaycastOrigin = raycastOrigin }; UpdateHeldItem(false); } /// /// Listens for the put-away animation finishing. Paired with OnDisable. /// private void OnEnable() { PlayerEvents.UnequipAnimComplete += HandleUnequipAnimComplete; } /// /// Unsubscribes — mirrors OnEnable exactly. /// private void OnDisable() { PlayerEvents.UnequipAnimComplete -= HandleUnequipAnimComplete; } /// /// Drops the inventory listeners; the bus subscription is already handled by OnDisable. /// private void OnDestroy() { if (inventory == null) return; inventory.onSelectedSlotChanged.RemoveListener(HandleSelectionChanged); inventory.onSlotChanged.RemoveListener(HandleSlotChanged); } /// /// Forces the swap through when the put-away animation never reports back. Runs only while a /// swap is actually pending, so an idle player costs nothing. The diagnostic is logged once per /// session rather than per swap: the cause is always a wiring or authoring gap that will repeat /// on every single hotbar change, and a console flooded with the same warning hides the rest. /// private void Update() { if (!isStowing) return; if (Time.time < stowDeadline) return; if (!stowTimeoutReported) { stowTimeoutReported = true; Debug.LogWarning("[HotbarController] The put-away animation never completed — swapping anyway " + "(logged once). Check that the arms rig has an ArmsAnimationEvents component and " + "that the Unequip clip carries an AnimUnequipComplete event. Until those clips " + "exist, lower Stow Timeout so the swap stays snappy.", this); } CommitSwap(); } #endregion #region Event Handlers private void HandleSelectionChanged(int index) => UpdateHeldItem(true); /// /// The held item's own stack changed (it was consumed, worn out, or refilled) — redraw only when /// it is the selected slot. /// private void HandleSlotChanged(int index) { if (index == inventory.SelectedHotbarIndex) UpdateHeldItem(true); } /// /// The arms finished putting the old item away, so the swap can complete. /// private void HandleUnequipAnimComplete() { if (isStowing) CommitSwap(); } #endregion #region Swap Sequence /// /// Entry point for every reason the hand might need to change. Swaps straight away when there is /// nothing to put away (empty hands, or the very first draw), otherwise starts the put-away /// animation and defers. A selection that changes again mid-stow only updates the target: the /// player who scrolls three slots quickly plays one put-away, not three. /// private void UpdateHeldItem(bool animate) { InventorySlot selected = inventory.GetSelectedSlot(); ItemData newItem = selected.IsEmpty ? null : selected.ItemData; if (newItem == currentHeldItem && !isStowing) return; pendingItem = newItem; if (isStowing) return; if (!animate || currentHeldObject == null) { CommitSwap(); return; } isStowing = true; stowDeadline = Time.time + stowTimeout; PlayerEvents.RaiseUnequipStarted(); } /// /// Performs the actual exchange. HeldItemChanged is raised *before* the new prefab is spawned so /// the animation binder has already loaded the item's clips by the time the draw trigger fires — /// otherwise the first frame of the draw would play the previous item's animation. /// private void CommitSwap() { isStowing = false; if (currentHeldObject != null) { Destroy(currentHeldObject); currentHeldObject = null; } currentHeldItem = pendingItem; PlayerEvents.RaiseHeldItemChanged(currentHeldItem); SpawnItem(currentHeldItem); if (currentHeldItem != null) PlayerEvents.RaiseEquipStarted(); } /// /// Instantiates the item's hand prefab on the rig's hand joint and links its behaviour to the /// player. Parenting to the joint rather than to a free-floating holder is what keeps a tool /// locked in the palm through every animation, with no code following the hand each frame. /// private void SpawnItem(ItemData item) { if (item == null || item.HandPrefab == null) return; if (handHolder == null) { Debug.LogError($"[HotbarController] No hand holder assigned — '{item.ItemName}' cannot be drawn. " + "Wire the arms rig's RightHand_Holder_JTN.", this); return; } currentHeldObject = Instantiate(item.HandPrefab, handHolder); IHeldItemBehaviour behaviour = currentHeldObject.GetComponentInChildren(true); behaviour?.Setup(heldContext, item); } #endregion } }