using System; using System.Collections.Generic; using System.Threading; using BriarQueen.Data.Identifiers; using BriarQueen.Framework.Assets; using BriarQueen.Framework.Coordinators.Events; using BriarQueen.Framework.Events.Gameplay; using BriarQueen.Framework.Managers.Player; using BriarQueen.Framework.Managers.Player.Data; using BriarQueen.Framework.Registries; using BriarQueen.Framework.Services.Tutorials; using Cysharp.Threading.Tasks; using PrimeTween; using UnityEngine; using UnityEngine.UI; using VContainer; namespace BriarQueen.UI.HUD { public class InventoryBar : MonoBehaviour { private const int ITEMS_PER_PAGE = 4; [Header("Buttons")] [SerializeField] private Button _nextButton; [SerializeField] private Button _prevButton; [Header("Paging")] [SerializeField] private RectTransform _content; [SerializeField] private float _pageTweenSeconds = 5f; private readonly List _inventorySlots = new(); private AddressableManager _addressableManager; private AssetRegistry _assetRegistry; private EventCoordinator _eventCoordinator; private PlayerManager _playerManager; private TutorialService _tutorialService; private int _currentPage; private int _selectedIndex = -1; private CancellationTokenSource _pageCts; private CancellationTokenSource _rebuildCts; private Sequence _pageSequence; private float _pageZeroFirstSlotX; private int PageCount { get { if (_inventorySlots.Count <= 0) return 0; return (_inventorySlots.Count + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE; } } [Inject] public void Construct( EventCoordinator eventCoordinator, PlayerManager playerManager, AddressableManager addressableManager, AssetRegistry assetRegistry, TutorialService tutorialService) { _eventCoordinator = eventCoordinator; _playerManager = playerManager; _addressableManager = addressableManager; _assetRegistry = assetRegistry; _tutorialService = tutorialService; } private void Awake() { if (_nextButton) _nextButton.onClick.AddListener(OnNextClicked); if (_prevButton) _prevButton.onClick.AddListener(OnPrevClicked); } private void OnEnable() { if (_eventCoordinator != null) { _eventCoordinator.Subscribe(OnInventoryChanged); _eventCoordinator.Subscribe(OnSelectedItemCleared); _eventCoordinator.Subscribe(OnNextItem); _eventCoordinator.Subscribe(OnPreviousItem); } UpdateButtonState(); } private void OnDisable() { if (_eventCoordinator != null) { _eventCoordinator.Unsubscribe(OnInventoryChanged); _eventCoordinator.Unsubscribe(OnSelectedItemCleared); _eventCoordinator.Unsubscribe(OnNextItem); _eventCoordinator.Unsubscribe(OnPreviousItem); } StopPageTween(); StopRebuild(); } private void OnDestroy() { if (_nextButton) _nextButton.onClick.RemoveListener(OnNextClicked); if (_prevButton) _prevButton.onClick.RemoveListener(OnPrevClicked); StopPageTween(); StopRebuild(); } private void OnInventoryChanged(InventoryChangedEvent evt) { RebuildInventoryAsync().Forget(); } private void OnSelectedItemCleared(SelectedItemChangedEvent evt) { if (evt.Item != null) return; ClearSelectionVisualOnly(); } public void ClearSelection() { ClearSelectionVisualOnly(); _eventCoordinator?.Publish(new SelectedItemChangedEvent(null)); } public void SelectIndex(int index) { SetSelectedIndex(index, true); } public void OnSlotInteracted(UIInventorySlot slot) { if (slot == null || slot.Item == null) return; var index = _inventorySlots.IndexOf(slot); if (index < 0) return; if (_selectedIndex == index) { ClearSelection(); return; } SelectIndex(index); } private void ClearContentChildrenImmediateVisual() { if (!_content) return; for (var i = _content.childCount - 1; i >= 0; i--) { var child = _content.GetChild(i); if (child == null) continue; child.gameObject.SetActive(false); Destroy(child.gameObject); } } private async UniTask RebuildInventoryAsync() { StopRebuild(); _rebuildCts = new CancellationTokenSource(); var token = _rebuildCts.Token; try { if (!_content) { ClearSelectionVisualOnly(); _eventCoordinator?.Publish(new SelectedItemChangedEvent(null)); UpdateButtonState(); return; } if (!_assetRegistry || !_assetRegistry.TryGetReference(AssetKeyIdentifiers.Get(UIKey.InventorySlot), out var slotRef)) { ClearSelectionVisualOnly(); _eventCoordinator?.Publish(new SelectedItemChangedEvent(null)); UpdateButtonState(); return; } StopPageTween(); var inventoryItems = _playerManager.GetInventoryItems(); var items = new List(); if (inventoryItems != null) { for (var i = 0; i < inventoryItems.Count; i++) { var item = inventoryItems[i]; if (item != null) items.Add(item); } } Debug.Log($"Rebuilding inventory with {items.Count} valid items"); _inventorySlots.Clear(); ClearContentChildrenImmediateVisual(); await UniTask.NextFrame(token); Canvas.ForceUpdateCanvases(); LayoutRebuilder.ForceRebuildLayoutImmediate(_content); for (var i = 0; i < items.Count; i++) { token.ThrowIfCancellationRequested(); var item = items[i]; if (item == null) continue; var slotObj = await _addressableManager.InstantiateAsync(slotRef, parent: _content); token.ThrowIfCancellationRequested(); if (slotObj == null) { Debug.LogWarning("[InventoryBar] AddressableManager returned null slot object."); continue; } var slot = slotObj.GetComponent(); if (slot == null) { Debug.LogWarning("[InventoryBar] Instantiated slot prefab is missing UIInventorySlot."); Destroy(slotObj); continue; } slot.Initialize(this, item); _inventorySlots.Add(slot); } await UniTask.Yield(PlayerLoopTiming.LastPostLateUpdate, token); Canvas.ForceUpdateCanvases(); LayoutRebuilder.ForceRebuildLayoutImmediate(_content); CachePageAnchorData(); _currentPage = Mathf.Clamp(_currentPage, 0, Mathf.Max(0, PageCount - 1)); SnapToPage(_currentPage); ClearSelectionVisualOnly(); _eventCoordinator.Publish(new SelectedItemChangedEvent(null)); UpdateButtonState(); } catch (OperationCanceledException) { } } private void StopRebuild() { _rebuildCts?.Cancel(); _rebuildCts?.Dispose(); _rebuildCts = null; } private void SetSelectedIndex(int index, bool scrollToSelection) { if (_inventorySlots.Count == 0) { ClearSelectionVisualOnly(); _eventCoordinator.Publish(new SelectedItemChangedEvent(null)); UpdateButtonState(); return; } if (index < 0) { ClearSelection(); return; } index = Mathf.Clamp(index, 0, _inventorySlots.Count - 1); if (_selectedIndex >= 0 && _selectedIndex < _inventorySlots.Count) _inventorySlots[_selectedIndex].SetSelected(false); _selectedIndex = index; _inventorySlots[_selectedIndex].SetSelected(true); _eventCoordinator.Publish(new SelectedItemChangedEvent(_inventorySlots[_selectedIndex].Item)); CheckSelectedItemTutorial(); if (scrollToSelection) { var targetPage = IndexToPage(_selectedIndex); GoToPage(targetPage).Forget(); } UpdateButtonState(); } private void CheckSelectedItemTutorial() { _tutorialService.DisplayTutorial(TutorialPopupID.ItemsAway); } private void ClearSelectionVisualOnly() { if (_selectedIndex >= 0 && _selectedIndex < _inventorySlots.Count) _inventorySlots[_selectedIndex].SetSelected(false); _selectedIndex = -1; UpdateButtonState(); } private int IndexToPage(int index) { if (index < 0) return 0; return index / ITEMS_PER_PAGE; } private void CachePageAnchorData() { _pageZeroFirstSlotX = 0f; if (_inventorySlots.Count == 0) return; Canvas.ForceUpdateCanvases(); LayoutRebuilder.ForceRebuildLayoutImmediate(_content); var first = _inventorySlots[0].transform as RectTransform; if (!first) return; _pageZeroFirstSlotX = first.anchoredPosition.x; } private float PageToAnchoredX(int pageIndex) { if (_inventorySlots.Count == 0) return 0f; pageIndex = Mathf.Clamp(pageIndex, 0, Mathf.Max(0, PageCount - 1)); var firstIndexOnPage = pageIndex * ITEMS_PER_PAGE; if (firstIndexOnPage < 0 || firstIndexOnPage >= _inventorySlots.Count) return 0f; var pageFirstSlot = _inventorySlots[firstIndexOnPage].transform as RectTransform; if (!pageFirstSlot) return 0f; return _pageZeroFirstSlotX - pageFirstSlot.anchoredPosition.x; } private async UniTask GoToPage(int pageIndex) { if (!_content) return; var pages = PageCount; if (pages <= 1) { _currentPage = 0; SnapToPage(0); UpdateButtonState(); return; } Canvas.ForceUpdateCanvases(); LayoutRebuilder.ForceRebuildLayoutImmediate(_content); var clamped = Mathf.Clamp(pageIndex, 0, pages - 1); if (clamped == _currentPage && !_pageSequence.isAlive) { UpdateButtonState(); return; } _currentPage = clamped; StopPageTween(); _pageCts = new CancellationTokenSource(); var targetX = PageToAnchoredX(_currentPage); var from = _content.anchoredPosition; var to = from; to.x = targetX; var tweenSettings = new TweenSettings { duration = Mathf.Max(0f, _pageTweenSeconds), ease = Ease.OutCubic, useUnscaledTime = true }; TweenSettings vecSettings = new TweenSettings { startValue = from, endValue = to, settings = tweenSettings }; _pageSequence = Sequence.Create(useUnscaledTime: true). Group(Tween.Custom(vecSettings, newValue => _content.anchoredPosition = newValue)); try { await _pageSequence.ToUniTask(cancellationToken: _pageCts.Token); } catch (OperationCanceledException) { } finally { _pageSequence = default; UpdateButtonState(); } } private void SnapToPage(int pageIndex) { if (!_content) return; var pages = PageCount; if (pages <= 1) { var p0 = _content.anchoredPosition; p0.x = 0f; _content.anchoredPosition = p0; return; } Canvas.ForceUpdateCanvases(); LayoutRebuilder.ForceRebuildLayoutImmediate(_content); pageIndex = Mathf.Clamp(pageIndex, 0, pages - 1); var pos = _content.anchoredPosition; pos.x = PageToAnchoredX(pageIndex); _content.anchoredPosition = pos; } private void StopPageTween() { if (_pageSequence.isAlive) _pageSequence.Stop(); _pageSequence = default; _pageCts?.Cancel(); _pageCts?.Dispose(); _pageCts = null; } private void OnNextClicked() { if (PageCount <= 1) return; GoToPage(_currentPage + 1).Forget(); } private void OnPrevClicked() { if (PageCount <= 1) return; GoToPage(_currentPage - 1).Forget(); } private void UpdateButtonState() { var pages = PageCount; var showNav = pages > 1; if (_prevButton) { var showPrev = showNav && _currentPage > 0; _prevButton.gameObject.SetActive(showPrev); } if (_nextButton) { var showNext = showNav && _currentPage < pages - 1; _nextButton.gameObject.SetActive(showNext); } } private void OnNextItem(OnNextItemClickedEvent e) { if (_inventorySlots.Count == 0) return; int nextIndex; if (_selectedIndex < 0) { nextIndex = 0; } else { nextIndex = _selectedIndex + 1; if (nextIndex >= _inventorySlots.Count) nextIndex = 0; } SetSelectedIndex(nextIndex, true); } private void OnPreviousItem(OnPreviousItemClickedEvent e) { if (_inventorySlots.Count == 0) return; int prevIndex; if (_selectedIndex < 0) { prevIndex = _inventorySlots.Count - 1; } else { prevIndex = _selectedIndex - 1; if (prevIndex < 0) prevIndex = _inventorySlots.Count - 1; } SetSelectedIndex(prevIndex, true); } } }