Files
A-Fairytale-Gone-Bad-Briar-…/Assets/Scripts/UI/HUD/InventoryBar.cs

566 lines
16 KiB
C#

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<UIInventorySlot> _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<InventoryChangedEvent>(OnInventoryChanged);
_eventCoordinator.Subscribe<SelectedItemChangedEvent>(OnSelectedItemCleared);
_eventCoordinator.Subscribe<OnNextItemClickedEvent>(OnNextItem);
_eventCoordinator.Subscribe<OnPreviousItemClickedEvent>(OnPreviousItem);
}
UpdateButtonState();
}
private void OnDisable()
{
if (_eventCoordinator != null)
{
_eventCoordinator.Unsubscribe<InventoryChangedEvent>(OnInventoryChanged);
_eventCoordinator.Unsubscribe<SelectedItemChangedEvent>(OnSelectedItemCleared);
_eventCoordinator.Unsubscribe<OnNextItemClickedEvent>(OnNextItem);
_eventCoordinator.Unsubscribe<OnPreviousItemClickedEvent>(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<ItemDataSo>();
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<UIInventorySlot>();
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<Vector2> vecSettings = new TweenSettings<Vector2>
{
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);
}
}
}