using UnityEngine; using DG.Tweening; using Ashwild.Player; namespace Ashwild.Inventory { public class HotbarUI : MonoBehaviour { [Header("References")] [SerializeField] private SlotUI[] hotbarSlot; [SerializeField] private CanvasGroup canvasGroup; [Header("Animation")] [SerializeField] private float idleScale = 0.7f; [SerializeField] private float activeScale = 1f; [SerializeField] private float idleAlpha = 0.5f; [SerializeField] private float activeAlpha = 1f; [SerializeField] private float zoomInDuration = 0.15f; [SerializeField] private float zoomOutDuration = 0.4f; [SerializeField] private float activeHoldDuration = 1.5f; [SerializeField] private Ease zoomInEase = Ease.OutBack; [SerializeField] private Ease zoomOutEase = Ease.InQuad; [Header("Context Menu")] [SerializeField] private SlotContextMenu contextMenu; [Header("Hover Description")] [Tooltip("Same panel used by the inventory — assign the shared HoverDescriptionUI here too.")] [SerializeField] private HoverDescriptionUI hoverDescription; private PlayerInventory inventory; private Tweener scaleTween; private Tweener alphaTween; private float zoomOutTimer; private bool inventoryLocked; private bool bound; /// /// Waits for the networked local player to spawn before binding to its inventory. /// private void OnEnable() { PlayerEvents.LocalPlayerSpawned += HandleLocalPlayerSpawned; } /// /// Unsubscribes — mirrors OnEnable exactly. /// private void OnDisable() { PlayerEvents.LocalPlayerSpawned -= HandleLocalPlayerSpawned; } private void Start() { // Visual setup that does not need the player. for (int i = 0; i < hotbarSlot.Length; i++) hotbarSlot[i].Initialize(SlotContainer.Inventory, i, InventoryUI.HandleSlotDrop, OnSlotClicked, OnSlotHoverEnter, OnSlotHoverExit); transform.localScale = Vector3.one * idleScale; if (canvasGroup != null) canvasGroup.alpha = idleAlpha; // The player may already exist (late UI init); otherwise we wait for the spawn event. if (PlayerInventory.Instance != null) BindToInventory(); } /// /// Binds to the inventory as soon as the local player has spawned on the network. /// private void HandleLocalPlayerSpawned() => BindToInventory(); /// /// Hooks the hotbar UI onto the local player's inventory; safe to call more than once. /// private void BindToInventory() { if (bound) return; inventory = PlayerInventory.Instance; if (inventory == null) return; bound = true; inventory.onSlotChanged.AddListener(OnSlotChanged); inventory.onSelectedSlotChanged.AddListener(OnSelectedSlotChanged); RefreshAll(); UpdateSelection(inventory.SelectedHotbarIndex); } private void OnDestroy() { scaleTween?.Kill(); alphaTween?.Kill(); if (inventory != null) { inventory.onSlotChanged.RemoveListener(OnSlotChanged); inventory.onSelectedSlotChanged.RemoveListener(OnSelectedSlotChanged); } } private void Update() { if (inventoryLocked || zoomOutTimer <= 0f) return; zoomOutTimer -= Time.deltaTime; if (zoomOutTimer <= 0f) ZoomOut(); } private void OnSelectedSlotChanged(int selectedIndex) { UpdateSelection(selectedIndex); ZoomIn(); } private void ZoomIn() { zoomOutTimer = activeHoldDuration; scaleTween?.Kill(); scaleTween = transform.DOScale(activeScale, zoomInDuration).SetEase(zoomInEase); if (canvasGroup != null) { alphaTween?.Kill(); alphaTween = canvasGroup.DOFade(activeAlpha, zoomInDuration); } } private void ZoomOut() { scaleTween?.Kill(); scaleTween = transform.DOScale(idleScale, zoomOutDuration).SetEase(zoomOutEase); if (canvasGroup != null) { alphaTween?.Kill(); alphaTween = canvasGroup.DOFade(idleAlpha, zoomOutDuration); } } public void OnInventoryOpen() { inventoryLocked = true; zoomOutTimer = 0f; ZoomIn(); } public void OnInventoryClose() { inventoryLocked = false; ZoomOut(); } /// /// Right-click on a hotbar slot: a quick deposit into the open chest, otherwise the slot context /// menu (only while the inventory window is open — the hotbar must not pop a menu during play). /// private void OnSlotClicked(SlotContainer container, int index, bool rightClick) { if (!rightClick) return; if (PlayerEvents.IsChestOpen) { if (InventoryUI.Instance != null) InventoryUI.Instance.RequestQuickTransfer(SlotContainer.Inventory, index); return; } if (!PlayerEvents.IsInventoryOpen) return; if (contextMenu != null) contextMenu.Show(index); else inventory.UseItem(index); } private void OnSlotChanged(int index) { if (index >= 0 && index < hotbarSlot.Length) hotbarSlot[index].UpdateVisual(inventory.GetSlot(index)); } /// /// Shows the shared description panel for a hovered hotbar slot, but only while the inventory /// is open and no chest is open (chest mode hides the description entirely) — during normal play /// the hotbar must not pop a description. The duration bar reflects the item's remaining uses. /// private void OnSlotHoverEnter(SlotContainer container, int index) { if (PlayerEvents.IsChestOpen) return; if (hoverDescription == null || inventory == null || !PlayerEvents.IsInventoryOpen) return; InventorySlot slot = inventory.GetSlot(index); if (slot == null || slot.IsEmpty) { hoverDescription.Hide(); return; } hoverDescription.Show(ItemDescriptionView.From(slot)); } /// /// Hides the description panel when the pointer leaves a hotbar slot. /// private void OnSlotHoverExit() { if (hoverDescription != null) hoverDescription.Hide(); } private void UpdateSelection(int selectedIndex) { for (int i = 0; i < hotbarSlot.Length; i++) hotbarSlot[i].SetSelected(i == selectedIndex); } private void RefreshAll() { for (int i = 0; i < hotbarSlot.Length; i++) hotbarSlot[i].UpdateVisual(inventory.GetSlot(i)); } } }