First commit for private source control. Older commits available on Github.
This commit is contained in:
366
Assets/Scripts/UI/HUD/CursorTooltip.cs
Normal file
366
Assets/Scripts/UI/HUD/CursorTooltip.cs
Normal file
@@ -0,0 +1,366 @@
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.Gameplay;
|
||||
using BriarQueen.Framework.Events.Input;
|
||||
using BriarQueen.Framework.Events.Save;
|
||||
using BriarQueen.Framework.Managers.Input;
|
||||
using BriarQueen.Framework.Managers.Interaction.Data;
|
||||
using BriarQueen.Framework.Managers.Player.Data;
|
||||
using BriarQueen.Framework.Managers.UI;
|
||||
using BriarQueen.Framework.Services.Settings;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.UI.HUD
|
||||
{
|
||||
/// <summary>
|
||||
/// Cursor-follow tooltip that positions itself in the ROOT canvas coordinate space,
|
||||
/// sizes its background to the text, and clamps to screen/canvas bounds.
|
||||
/// </summary>
|
||||
public class CursorTooltip : MonoBehaviour
|
||||
{
|
||||
[Header("Tooltips")]
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _tooltipText;
|
||||
|
||||
[SerializeField]
|
||||
private Image _tooltipBackground;
|
||||
|
||||
[Header("Follow Cursor")]
|
||||
|
||||
[SerializeField]
|
||||
private bool _clampToScreen = true;
|
||||
|
||||
[SerializeField]
|
||||
private Vector2 _screenPadding = new(12f, 12f);
|
||||
|
||||
[Header("Background Sizing")]
|
||||
[Tooltip("Padding added around the text when sizing the background.")]
|
||||
[SerializeField]
|
||||
private Vector2 _backgroundPadding = new(14f, 8f);
|
||||
|
||||
[Tooltip("Clamp tooltip width so long strings wrap instead of creating a huge background.")]
|
||||
[SerializeField]
|
||||
private float _maxTooltipWidth = 420f;
|
||||
|
||||
[Tooltip("Minimum background size so it doesn't look tiny on short words.")]
|
||||
[SerializeField]
|
||||
private Vector2 _minBackgroundSize = new(60f, 28f);
|
||||
|
||||
[Tooltip("Maximum background size as a final safety clamp.")]
|
||||
[SerializeField]
|
||||
private Vector2 _maxBackgroundSize = new(520f, 200f);
|
||||
|
||||
[Header("Canvas")]
|
||||
[Tooltip("Optional override. If set, this canvas (or its rootCanvas) will be used for positioning/clamping.")]
|
||||
[SerializeField]
|
||||
private Canvas _canvasOverride;
|
||||
|
||||
private RectTransform _bgRect;
|
||||
private EventCoordinator _eventCoordinator;
|
||||
private IInteractable _hoveredInteractable;
|
||||
private InputManager _inputManager;
|
||||
private UICursorService _cursorService;
|
||||
|
||||
// IMPORTANT: We always position relative to the ROOT canvas.
|
||||
private Canvas _rootCanvas;
|
||||
private RectTransform _rootCanvasRect;
|
||||
private RectTransform _rootRect;
|
||||
private ItemDataSo _selectedItem;
|
||||
private ToolID _selectedTool = ToolID.None;
|
||||
private SettingsService _settingsService;
|
||||
private RectTransform _textRect;
|
||||
private Camera _uiCamera;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_rootRect = transform as RectTransform;
|
||||
|
||||
if (_tooltipBackground) _bgRect = _tooltipBackground.rectTransform;
|
||||
if (_tooltipText) _textRect = _tooltipText.rectTransform;
|
||||
|
||||
ResolveRootCanvas();
|
||||
|
||||
if (_tooltipBackground)
|
||||
_tooltipBackground.transform.SetAsFirstSibling();
|
||||
|
||||
SetVisible(false);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!AreTooltipsEnabled())
|
||||
{
|
||||
SetVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
FollowCursor();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_eventCoordinator == null)
|
||||
return;
|
||||
|
||||
_eventCoordinator.Subscribe<SelectedItemChangedEvent>(OnSelectedItemChanged);
|
||||
_eventCoordinator.Subscribe<SelectedToolChangedEvent>(OnSelectedToolChanged);
|
||||
_eventCoordinator.Subscribe<HoverInteractableChangedEvent>(OnHoveredInteractableChanged);
|
||||
_eventCoordinator.Subscribe<SettingsChangedEvent>(OnSettingsChanged);
|
||||
|
||||
UpdateTooltip();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_eventCoordinator == null)
|
||||
return;
|
||||
|
||||
_eventCoordinator.Unsubscribe<SelectedItemChangedEvent>(OnSelectedItemChanged);
|
||||
_eventCoordinator.Unsubscribe<SelectedToolChangedEvent>(OnSelectedToolChanged);
|
||||
_eventCoordinator.Unsubscribe<HoverInteractableChangedEvent>(OnHoveredInteractableChanged);
|
||||
_eventCoordinator.Unsubscribe<SettingsChangedEvent>(OnSettingsChanged);
|
||||
}
|
||||
|
||||
[Inject]
|
||||
private void Construct(EventCoordinator eventCoordinator, InputManager inputManager, SettingsService settingsService,
|
||||
UICursorService cursorService)
|
||||
{
|
||||
_eventCoordinator = eventCoordinator;
|
||||
_inputManager = inputManager;
|
||||
_settingsService = settingsService;
|
||||
_cursorService = cursorService;
|
||||
}
|
||||
|
||||
private void ResolveRootCanvas()
|
||||
{
|
||||
var candidate = _canvasOverride != null ? _canvasOverride : GetComponentInParent<Canvas>();
|
||||
|
||||
if (candidate == null)
|
||||
{
|
||||
_rootCanvas = null;
|
||||
_rootCanvasRect = null;
|
||||
_uiCamera = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_rootCanvas = candidate.rootCanvas;
|
||||
_rootCanvasRect = _rootCanvas.transform as RectTransform;
|
||||
_uiCamera = _rootCanvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : _rootCanvas.worldCamera;
|
||||
}
|
||||
|
||||
private void OnSelectedItemChanged(SelectedItemChangedEvent args)
|
||||
{
|
||||
_selectedItem = args.Item;
|
||||
UpdateTooltip();
|
||||
}
|
||||
|
||||
private void OnSelectedToolChanged(SelectedToolChangedEvent args)
|
||||
{
|
||||
// Change args.Tool if your event exposes a different property name.
|
||||
_selectedTool = args.SelectedTool;
|
||||
UpdateTooltip();
|
||||
}
|
||||
|
||||
private void OnHoveredInteractableChanged(HoverInteractableChangedEvent args)
|
||||
{
|
||||
_hoveredInteractable = args.Interactable;
|
||||
UpdateTooltip();
|
||||
}
|
||||
|
||||
private void OnSettingsChanged(SettingsChangedEvent _)
|
||||
{
|
||||
UpdateTooltip();
|
||||
}
|
||||
|
||||
private bool AreTooltipsEnabled()
|
||||
{
|
||||
return _settingsService == null || _settingsService.AreTooltipsEnabled();
|
||||
}
|
||||
|
||||
private void UpdateTooltip()
|
||||
{
|
||||
if (!_tooltipText)
|
||||
return;
|
||||
|
||||
if (!AreTooltipsEnabled())
|
||||
{
|
||||
_tooltipText.text = string.Empty;
|
||||
SetVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var text = BuildTooltipText();
|
||||
var show = !string.IsNullOrWhiteSpace(text);
|
||||
|
||||
if (!show)
|
||||
{
|
||||
_tooltipText.text = string.Empty;
|
||||
SetVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_tooltipText.text = text;
|
||||
SetVisible(true);
|
||||
|
||||
UpdateLayoutToText();
|
||||
}
|
||||
|
||||
private string BuildTooltipText()
|
||||
{
|
||||
if (_hoveredInteractable == null)
|
||||
return string.Empty;
|
||||
|
||||
var targetName = GetInteractableName(_hoveredInteractable);
|
||||
if (string.IsNullOrWhiteSpace(targetName))
|
||||
return string.Empty;
|
||||
|
||||
var selectedItemName = GetSelectedItemName();
|
||||
if (!string.IsNullOrWhiteSpace(selectedItemName))
|
||||
return $"Use {selectedItemName} on {targetName}";
|
||||
|
||||
var selectedToolName = GetSelectedToolName();
|
||||
if (!string.IsNullOrWhiteSpace(selectedToolName))
|
||||
return $"Use {selectedToolName} on {targetName}";
|
||||
|
||||
return targetName;
|
||||
}
|
||||
|
||||
private string GetSelectedItemName()
|
||||
{
|
||||
return _selectedItem != null && !string.IsNullOrWhiteSpace(_selectedItem.ItemName)
|
||||
? _selectedItem.ItemName
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
private string GetSelectedToolName()
|
||||
{
|
||||
return GetToolDisplayName(_selectedTool);
|
||||
}
|
||||
|
||||
private static string GetInteractableName(IInteractable interactable)
|
||||
{
|
||||
return interactable == null ? string.Empty : interactable.InteractableName;
|
||||
}
|
||||
|
||||
private static string GetToolDisplayName(ToolID toolID)
|
||||
{
|
||||
return toolID switch
|
||||
{
|
||||
ToolID.None => string.Empty,
|
||||
ToolID.Knife => "Knife",
|
||||
_ => toolID.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
private void SetVisible(bool visible)
|
||||
{
|
||||
if (_tooltipText) _tooltipText.enabled = visible;
|
||||
if (_tooltipBackground) _tooltipBackground.enabled = visible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sizes the text rect (clamped) and the background rect (text size + padding),
|
||||
/// so the bubble never looks comically huge or tiny.
|
||||
/// </summary>
|
||||
private void UpdateLayoutToText()
|
||||
{
|
||||
if (!_tooltipText || !_tooltipBackground || !_textRect || !_bgRect)
|
||||
return;
|
||||
|
||||
_tooltipText.textWrappingMode = TextWrappingModes.Normal;
|
||||
|
||||
_tooltipText.ForceMeshUpdate();
|
||||
var preferred = _tooltipText.GetPreferredValues(_tooltipText.text, _maxTooltipWidth, 0f);
|
||||
|
||||
var textW = Mathf.Min(preferred.x, _maxTooltipWidth);
|
||||
var textH = preferred.y;
|
||||
|
||||
_textRect.sizeDelta = new Vector2(textW, textH);
|
||||
|
||||
var bgW = textW + _backgroundPadding.x * 2f;
|
||||
var bgH = textH + _backgroundPadding.y * 2f;
|
||||
|
||||
bgW = Mathf.Clamp(bgW, _minBackgroundSize.x, _maxBackgroundSize.x);
|
||||
bgH = Mathf.Clamp(bgH, _minBackgroundSize.y, _maxBackgroundSize.y);
|
||||
|
||||
_bgRect.sizeDelta = new Vector2(bgW, bgH);
|
||||
}
|
||||
|
||||
private void FollowCursor()
|
||||
{
|
||||
if (_rootRect == null || _inputManager == null)
|
||||
return;
|
||||
|
||||
var pointerScreen = _inputManager.PointerPosition;
|
||||
var cursorOffset = _cursorService.CurrentStyleEntry.TooltipOffset;
|
||||
var targetScreen = pointerScreen + cursorOffset;
|
||||
|
||||
if (_rootCanvasRect != null)
|
||||
{
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
_rootCanvasRect,
|
||||
targetScreen,
|
||||
_uiCamera,
|
||||
out var localPoint
|
||||
);
|
||||
|
||||
_rootRect.anchoredPosition = localPoint;
|
||||
|
||||
if (_clampToScreen)
|
||||
ClampAnchoredToRootCanvas();
|
||||
}
|
||||
else
|
||||
{
|
||||
_rootRect.position = targetScreen;
|
||||
|
||||
if (_clampToScreen)
|
||||
ClampWorldToScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void ClampAnchoredToRootCanvas()
|
||||
{
|
||||
if (_rootCanvasRect == null || _bgRect == null)
|
||||
return;
|
||||
|
||||
var bgSize = _bgRect.rect.size;
|
||||
var canvasRect = _rootCanvasRect.rect;
|
||||
|
||||
var pos = _rootRect.anchoredPosition;
|
||||
|
||||
var minX = canvasRect.xMin + _screenPadding.x;
|
||||
var maxX = canvasRect.xMax - _screenPadding.x - bgSize.x;
|
||||
|
||||
var minY = canvasRect.yMin + _screenPadding.y + bgSize.y;
|
||||
var maxY = canvasRect.yMax - _screenPadding.y;
|
||||
|
||||
pos.x = Mathf.Clamp(pos.x, minX, maxX);
|
||||
pos.y = Mathf.Clamp(pos.y, minY, maxY);
|
||||
|
||||
_rootRect.anchoredPosition = pos;
|
||||
}
|
||||
|
||||
private void ClampWorldToScreen()
|
||||
{
|
||||
if (_bgRect == null)
|
||||
return;
|
||||
|
||||
var bgSize = _bgRect.rect.size;
|
||||
Vector2 pos = _rootRect.position;
|
||||
|
||||
var minX = _screenPadding.x;
|
||||
var maxX = Screen.width - _screenPadding.x - bgSize.x;
|
||||
|
||||
var minY = _screenPadding.y + bgSize.y;
|
||||
var maxY = Screen.height - _screenPadding.y;
|
||||
|
||||
pos.x = Mathf.Clamp(pos.x, minX, maxX);
|
||||
pos.y = Mathf.Clamp(pos.y, minY, maxY);
|
||||
|
||||
_rootRect.position = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/HUD/CursorTooltip.cs.meta
Normal file
3
Assets/Scripts/UI/HUD/CursorTooltip.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: feab9a2f47fa4c39836de3ffbcd9aa7e
|
||||
timeCreated: 1769719100
|
||||
282
Assets/Scripts/UI/HUD/HUDContainer.cs
Normal file
282
Assets/Scripts/UI/HUD/HUDContainer.cs
Normal file
@@ -0,0 +1,282 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Managers.UI;
|
||||
using BriarQueen.Framework.Managers.UI.Base;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.UI.HUD
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimal HUD holder:
|
||||
/// - Registers itself with UIManager
|
||||
/// - Handles HUD visibility (alpha tween only)
|
||||
/// </summary>
|
||||
public class HUDContainer : MonoBehaviour, IHud
|
||||
{
|
||||
private const float DISPLAY_TIME = 4f;
|
||||
|
||||
[Header("UI")]
|
||||
[SerializeField]
|
||||
private CursorTooltip _cursorTooltip;
|
||||
[SerializeField]
|
||||
private InteractTextUI _interactText;
|
||||
|
||||
[Header("Inventory")]
|
||||
[SerializeField]
|
||||
private InventoryBar _inventoryBar;
|
||||
|
||||
[Header("Components")]
|
||||
[SerializeField]
|
||||
private CanvasGroup _mainCanvasGroup;
|
||||
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _interactTextCanvasGroup;
|
||||
|
||||
[Header("Tweens")]
|
||||
[SerializeField]
|
||||
private TweenSettings _tweenSettings = new()
|
||||
{
|
||||
duration = 0.2f,
|
||||
ease = Ease.InOutSine,
|
||||
useUnscaledTime = true
|
||||
};
|
||||
|
||||
[SerializeField]
|
||||
private GraphicRaycaster _graphicRaycaster;
|
||||
public GraphicRaycaster Raycaster => _graphicRaycaster;
|
||||
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
|
||||
private Sequence _hudSequence;
|
||||
private CancellationTokenSource _interactCancellationTokenSource;
|
||||
|
||||
private Sequence _interactErrorSequence;
|
||||
private UIManager _uiManager;
|
||||
|
||||
public CursorTooltip CursorTooltip => _cursorTooltip;
|
||||
public InventoryBar InventoryBar => _inventoryBar;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_mainCanvasGroup != null)
|
||||
{
|
||||
_mainCanvasGroup.alpha = 1f;
|
||||
_mainCanvasGroup.interactable = true;
|
||||
_mainCanvasGroup.blocksRaycasts = true;
|
||||
}
|
||||
|
||||
if (_interactTextCanvasGroup != null)
|
||||
{
|
||||
_interactTextCanvasGroup.alpha = 0f;
|
||||
_interactTextCanvasGroup.blocksRaycasts = false;
|
||||
_interactTextCanvasGroup.interactable = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
StopTween();
|
||||
|
||||
StopInteractErrorTween();
|
||||
}
|
||||
|
||||
[Inject]
|
||||
public void Construct(UIManager uiManager)
|
||||
{
|
||||
_uiManager = uiManager;
|
||||
}
|
||||
|
||||
// --------------------
|
||||
// Visibility
|
||||
// --------------------
|
||||
|
||||
public async UniTask DisplayInteractText(string interactText)
|
||||
{
|
||||
if (_interactTextCanvasGroup == null || _interactText == null)
|
||||
return;
|
||||
|
||||
// Cancel any in-flight error display and restart from current alpha.
|
||||
StopInteractErrorTween();
|
||||
|
||||
// Set text immediately.
|
||||
var text = interactText ?? string.Empty;
|
||||
|
||||
_interactText.SetText(text);
|
||||
|
||||
_interactCancellationTokenSource = new CancellationTokenSource();
|
||||
var token = _interactCancellationTokenSource.Token;
|
||||
|
||||
// Make sure it can show (but don't let it steal clicks).
|
||||
_interactTextCanvasGroup.blocksRaycasts = false;
|
||||
_interactTextCanvasGroup.interactable = false;
|
||||
|
||||
var fadeIn = new TweenSettings<float>
|
||||
{
|
||||
startValue = _interactTextCanvasGroup.alpha,
|
||||
endValue = 1f,
|
||||
settings = _tweenSettings
|
||||
};
|
||||
|
||||
var fadeOut = new TweenSettings<float>
|
||||
{
|
||||
startValue = 1f,
|
||||
endValue = 0f,
|
||||
settings = _tweenSettings
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
// Fade in
|
||||
_interactErrorSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_interactTextCanvasGroup, fadeIn));
|
||||
|
||||
await _interactErrorSequence.ToUniTask(cancellationToken: token);
|
||||
|
||||
_interactTextCanvasGroup.alpha = 1f;
|
||||
|
||||
// Hold
|
||||
if (DISPLAY_TIME > 0f)
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(DISPLAY_TIME), DelayType.UnscaledDeltaTime,
|
||||
cancellationToken: token);
|
||||
|
||||
// Fade out
|
||||
_interactErrorSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_interactTextCanvasGroup, fadeOut));
|
||||
|
||||
await _interactErrorSequence.ToUniTask(cancellationToken: token);
|
||||
|
||||
_interactTextCanvasGroup.alpha = 0f;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// If interrupted by a newer DisplayError call or destroy, just stop cleanly.
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_interactCancellationTokenSource != null)
|
||||
{
|
||||
_interactCancellationTokenSource.Dispose();
|
||||
_interactCancellationTokenSource = null;
|
||||
}
|
||||
|
||||
_interactErrorSequence = default;
|
||||
}
|
||||
}
|
||||
|
||||
public async UniTask Show()
|
||||
{
|
||||
if (_mainCanvasGroup == null)
|
||||
return;
|
||||
|
||||
StopTween();
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
_mainCanvasGroup.blocksRaycasts = true;
|
||||
_mainCanvasGroup.interactable = true;
|
||||
|
||||
var alphaTween = new TweenSettings<float>
|
||||
{
|
||||
startValue = _mainCanvasGroup.alpha,
|
||||
endValue = 1f,
|
||||
settings = _tweenSettings
|
||||
};
|
||||
|
||||
_hudSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_mainCanvasGroup, alphaTween));
|
||||
|
||||
try
|
||||
{
|
||||
await _hudSequence.ToUniTask(cancellationToken: _cancellationTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// interrupted by Hide / destroy — safe to ignore
|
||||
}
|
||||
|
||||
_mainCanvasGroup.alpha = 1f;
|
||||
}
|
||||
public async UniTask Hide()
|
||||
{
|
||||
if (_mainCanvasGroup == null)
|
||||
return;
|
||||
|
||||
StopTween();
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
_mainCanvasGroup.blocksRaycasts = false;
|
||||
_mainCanvasGroup.interactable = false;
|
||||
|
||||
var alphaTween = new TweenSettings<float>
|
||||
{
|
||||
startValue = _mainCanvasGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = _tweenSettings
|
||||
};
|
||||
|
||||
_hudSequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_mainCanvasGroup, alphaTween));
|
||||
|
||||
try
|
||||
{
|
||||
await _hudSequence.ToUniTask(cancellationToken: _cancellationTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// interrupted — safe
|
||||
}
|
||||
|
||||
_mainCanvasGroup.alpha = 0f;
|
||||
}
|
||||
|
||||
private void StopTween()
|
||||
{
|
||||
if (_hudSequence.isAlive)
|
||||
_hudSequence.Stop();
|
||||
|
||||
_hudSequence = default;
|
||||
|
||||
if (_cancellationTokenSource != null)
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
_cancellationTokenSource.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void StopInteractErrorTween()
|
||||
{
|
||||
if (_interactErrorSequence.isAlive)
|
||||
_interactErrorSequence.Stop();
|
||||
|
||||
_interactErrorSequence = default;
|
||||
|
||||
if (_interactCancellationTokenSource != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_interactCancellationTokenSource.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_interactCancellationTokenSource.Dispose();
|
||||
_interactCancellationTokenSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMapClicked()
|
||||
{
|
||||
// Placeholder for later:
|
||||
// - open map window
|
||||
// - toggle map overlay
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/HUD/HUDContainer.cs.meta
Normal file
3
Assets/Scripts/UI/HUD/HUDContainer.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e8b77a48c3af4a8c8858359a537523a9
|
||||
timeCreated: 1769713926
|
||||
433
Assets/Scripts/UI/HUD/InfoPopup.cs
Normal file
433
Assets/Scripts/UI/HUD/InfoPopup.cs
Normal file
@@ -0,0 +1,433 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Managers.UI.Base;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BriarQueen.UI.HUD
|
||||
{
|
||||
public class InfoPopup : MonoBehaviour, IPopup
|
||||
{
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _text;
|
||||
|
||||
[SerializeField]
|
||||
private RectTransform _rectTransform;
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _canvasGroup;
|
||||
|
||||
[Header("Sizing")]
|
||||
[SerializeField]
|
||||
private float _minWidth = 160f;
|
||||
|
||||
[SerializeField]
|
||||
private float _maxWidth = 500f;
|
||||
|
||||
[SerializeField]
|
||||
private float _minHeight = 60f;
|
||||
|
||||
[SerializeField]
|
||||
private float _extraWidthSafety = 4f;
|
||||
|
||||
[SerializeField]
|
||||
private float _extraHeightSafety = 4f;
|
||||
|
||||
[Header("Animation")]
|
||||
[SerializeField]
|
||||
private float _slideDuration = 1.2f;
|
||||
|
||||
[SerializeField]
|
||||
private Vector2 _hiddenAnchoredPosition = new(0f, -250f);
|
||||
|
||||
[SerializeField]
|
||||
private Vector2 _shownAnchoredPosition = new(0f, 0f);
|
||||
|
||||
[SerializeField]
|
||||
private float _shownAlpha = 1f;
|
||||
|
||||
[Header("Queue")]
|
||||
[SerializeField]
|
||||
private bool _suppressDuplicateMessages = true;
|
||||
|
||||
[Header("Editor Debug")]
|
||||
[SerializeField]
|
||||
private bool _debugResizeInEditor = true;
|
||||
|
||||
private readonly Queue<PopupRequest> _queue = new();
|
||||
|
||||
private string _currentMessage = string.Empty;
|
||||
|
||||
private CancellationTokenSource _destroyCts;
|
||||
private bool _isProcessingQueue;
|
||||
private string _lastEditorText = string.Empty;
|
||||
private float _lastHeight = -1f;
|
||||
private float _lastWidth = -1f;
|
||||
|
||||
private Sequence _sequence;
|
||||
private CancellationTokenSource _sequenceCts;
|
||||
|
||||
public bool IsModal => false;
|
||||
public GameObject GameObject => gameObject;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_destroyCts = new CancellationTokenSource();
|
||||
|
||||
if (_rectTransform == null)
|
||||
_rectTransform = GetComponent<RectTransform>();
|
||||
|
||||
if (_canvasGroup == null)
|
||||
_canvasGroup = GetComponent<CanvasGroup>();
|
||||
|
||||
if (_rectTransform != null)
|
||||
_rectTransform.anchoredPosition = _hiddenAnchoredPosition;
|
||||
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void Update()
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
return;
|
||||
|
||||
if (_text == null)
|
||||
return;
|
||||
|
||||
if (_lastEditorText != _text.text)
|
||||
ResizeToFitText();
|
||||
}
|
||||
#endif
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
CancelTweenIfRunning();
|
||||
|
||||
if (_destroyCts != null)
|
||||
{
|
||||
_destroyCts.Cancel();
|
||||
_destroyCts.Dispose();
|
||||
_destroyCts = null;
|
||||
}
|
||||
|
||||
_queue.Clear();
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
if (_rectTransform == null)
|
||||
_rectTransform = GetComponent<RectTransform>();
|
||||
|
||||
if (_canvasGroup == null)
|
||||
_canvasGroup = GetComponent<CanvasGroup>();
|
||||
|
||||
ResizeToFitText();
|
||||
}
|
||||
|
||||
public async UniTask Show()
|
||||
{
|
||||
if (_rectTransform == null || _canvasGroup == null)
|
||||
return;
|
||||
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
|
||||
CancelTweenIfRunning();
|
||||
|
||||
var localTweenCts = new CancellationTokenSource();
|
||||
_sequenceCts = localTweenCts;
|
||||
|
||||
_rectTransform.anchoredPosition = _hiddenAnchoredPosition;
|
||||
_canvasGroup.alpha = 0f;
|
||||
|
||||
_sequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.UIAnchoredPosition(_rectTransform, new TweenSettings<Vector2>
|
||||
{
|
||||
startValue = _hiddenAnchoredPosition,
|
||||
endValue = _shownAnchoredPosition,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _slideDuration,
|
||||
ease = Ease.OutCubic,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}))
|
||||
.Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = 0f,
|
||||
endValue = _shownAlpha,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _slideDuration,
|
||||
ease = Ease.OutCubic,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await _sequence.ToUniTask(cancellationToken: localTweenCts.Token);
|
||||
_rectTransform.anchoredPosition = _shownAnchoredPosition;
|
||||
_canvasGroup.alpha = _shownAlpha;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (ReferenceEquals(_sequenceCts, localTweenCts))
|
||||
_sequenceCts = null;
|
||||
|
||||
localTweenCts.Dispose();
|
||||
_sequence = default;
|
||||
}
|
||||
}
|
||||
|
||||
public async UniTask Hide()
|
||||
{
|
||||
if (_rectTransform == null || _canvasGroup == null)
|
||||
return;
|
||||
|
||||
CancelTweenIfRunning();
|
||||
|
||||
var localTweenCts = new CancellationTokenSource();
|
||||
_sequenceCts = localTweenCts;
|
||||
|
||||
_sequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.UIAnchoredPosition(_rectTransform, new TweenSettings<Vector2>
|
||||
{
|
||||
startValue = _rectTransform.anchoredPosition,
|
||||
endValue = _hiddenAnchoredPosition,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _slideDuration,
|
||||
ease = Ease.InCubic,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}))
|
||||
.Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _canvasGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _slideDuration,
|
||||
ease = Ease.InCubic,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await _sequence.ToUniTask(cancellationToken: localTweenCts.Token);
|
||||
_rectTransform.anchoredPosition = _hiddenAnchoredPosition;
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (ReferenceEquals(_sequenceCts, localTweenCts))
|
||||
_sequenceCts = null;
|
||||
|
||||
localTweenCts.Dispose();
|
||||
_sequence = default;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetText(string text)
|
||||
{
|
||||
if (_text != null)
|
||||
_text.text = text ?? string.Empty;
|
||||
|
||||
ResizeToFitText();
|
||||
}
|
||||
|
||||
public UniTask Play(string text, float duration)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return UniTask.CompletedTask;
|
||||
|
||||
if (_suppressDuplicateMessages && IsDuplicateMessage(text))
|
||||
return UniTask.CompletedTask;
|
||||
|
||||
_queue.Enqueue(new PopupRequest(text, duration));
|
||||
|
||||
if (!_isProcessingQueue)
|
||||
ProcessQueue().Forget();
|
||||
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public void ClearQueue()
|
||||
{
|
||||
_queue.Clear();
|
||||
_currentMessage = string.Empty;
|
||||
|
||||
CancelTweenIfRunning();
|
||||
|
||||
if (_rectTransform != null)
|
||||
_rectTransform.anchoredPosition = _hiddenAnchoredPosition;
|
||||
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
}
|
||||
|
||||
SetText(string.Empty);
|
||||
_isProcessingQueue = false;
|
||||
}
|
||||
|
||||
private bool IsDuplicateMessage(string text)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_currentMessage) &&
|
||||
string.Equals(_currentMessage, text, StringComparison.Ordinal))
|
||||
return true;
|
||||
|
||||
foreach (var queued in _queue)
|
||||
if (string.Equals(queued.Text, text, StringComparison.Ordinal))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async UniTaskVoid ProcessQueue()
|
||||
{
|
||||
if (_isProcessingQueue)
|
||||
return;
|
||||
|
||||
_isProcessingQueue = true;
|
||||
|
||||
try
|
||||
{
|
||||
while (_destroyCts != null && !_destroyCts.IsCancellationRequested && _queue.Count > 0)
|
||||
{
|
||||
var request = _queue.Dequeue();
|
||||
_currentMessage = request.Text;
|
||||
|
||||
SetText(request.Text);
|
||||
await Show();
|
||||
|
||||
if (request.Duration > 0f)
|
||||
await UniTask.Delay(
|
||||
TimeSpan.FromSeconds(request.Duration),
|
||||
cancellationToken: _destroyCts.Token);
|
||||
|
||||
await Hide();
|
||||
_currentMessage = string.Empty;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
_currentMessage = string.Empty;
|
||||
_isProcessingQueue = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ResizeToFitText()
|
||||
{
|
||||
if (_text == null || _rectTransform == null)
|
||||
return;
|
||||
|
||||
_text.ForceMeshUpdate();
|
||||
|
||||
var margin = _text.margin;
|
||||
|
||||
var leftInset = margin.x;
|
||||
var topInset = margin.y;
|
||||
var rightInset = margin.z;
|
||||
var bottomInset = margin.w;
|
||||
|
||||
var totalHorizontalInset = leftInset + rightInset;
|
||||
var totalVerticalInset = topInset + bottomInset;
|
||||
|
||||
var maxTextWidth = Mathf.Max(0f, _maxWidth - totalHorizontalInset - _extraWidthSafety);
|
||||
|
||||
var preferredSize = _text.GetPreferredValues(_text.text, maxTextWidth, Mathf.Infinity);
|
||||
|
||||
var width = Mathf.Clamp(
|
||||
preferredSize.x + totalHorizontalInset + _extraWidthSafety,
|
||||
_minWidth,
|
||||
_maxWidth);
|
||||
|
||||
var availableTextWidth = Mathf.Max(0f, width - totalHorizontalInset - _extraWidthSafety);
|
||||
|
||||
var wrappedPreferredSize = _text.GetPreferredValues(_text.text, availableTextWidth, Mathf.Infinity);
|
||||
|
||||
var height = Mathf.Max(
|
||||
_minHeight,
|
||||
wrappedPreferredSize.y + totalVerticalInset + _extraHeightSafety);
|
||||
|
||||
_rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, width);
|
||||
_rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, height);
|
||||
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(_rectTransform);
|
||||
Canvas.ForceUpdateCanvases();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
var changed =
|
||||
!Mathf.Approximately(width, _lastWidth) ||
|
||||
!Mathf.Approximately(height, _lastHeight) ||
|
||||
_lastEditorText != _text.text;
|
||||
|
||||
if (_debugResizeInEditor && changed)
|
||||
Debug.Log(
|
||||
$"[InfoPopup] '{_text.text}'\n" +
|
||||
$"Preferred: {preferredSize.x:F1} x {preferredSize.y:F1}\n" +
|
||||
$"Wrapped: {wrappedPreferredSize.x:F1} x {wrappedPreferredSize.y:F1}\n" +
|
||||
$"Insets: L{leftInset:F1} T{topInset:F1} R{rightInset:F1} B{bottomInset:F1}\n" +
|
||||
$"Final Size: {width:F1} x {height:F1}",
|
||||
this);
|
||||
#endif
|
||||
|
||||
_lastEditorText = _text.text;
|
||||
_lastWidth = width;
|
||||
_lastHeight = height;
|
||||
}
|
||||
|
||||
private void CancelTweenIfRunning()
|
||||
{
|
||||
if (_sequence.isAlive)
|
||||
_sequence.Stop();
|
||||
|
||||
_sequence = default;
|
||||
|
||||
if (_sequenceCts != null)
|
||||
{
|
||||
_sequenceCts.Cancel();
|
||||
_sequenceCts.Dispose();
|
||||
_sequenceCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private struct PopupRequest
|
||||
{
|
||||
public readonly string Text;
|
||||
public readonly float Duration;
|
||||
|
||||
public PopupRequest(string text, float duration)
|
||||
{
|
||||
Text = text;
|
||||
Duration = duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/HUD/InfoPopup.cs.meta
Normal file
3
Assets/Scripts/UI/HUD/InfoPopup.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 79bc6a48c54849a295fba449628a13ed
|
||||
timeCreated: 1773762669
|
||||
166
Assets/Scripts/UI/HUD/InteractTextUI.cs
Normal file
166
Assets/Scripts/UI/HUD/InteractTextUI.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BriarQueen.UI.HUD
|
||||
{
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
[RequireComponent(typeof(LayoutElement))]
|
||||
public class InteractTextUI : MonoBehaviour
|
||||
{
|
||||
[Header("UI Elements")]
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _text;
|
||||
|
||||
[SerializeField]
|
||||
private Image _background;
|
||||
|
||||
[Header("Sizing")]
|
||||
[SerializeField]
|
||||
private Vector2 _backgroundPadding = new(20f, 10f);
|
||||
|
||||
[SerializeField]
|
||||
private Vector2 _minBackgroundSize = new(80f, 36f);
|
||||
|
||||
[SerializeField]
|
||||
private Vector2 _maxBackgroundSize = new(1400f, 240f);
|
||||
|
||||
private RectTransform _backgroundRect;
|
||||
|
||||
private string _lastText = string.Empty;
|
||||
private LayoutElement _rootLayoutElement;
|
||||
|
||||
private RectTransform _rootRect;
|
||||
private RectTransform _textRect;
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => _text != null ? _text.text : string.Empty;
|
||||
set
|
||||
{
|
||||
if (_text == null)
|
||||
return;
|
||||
|
||||
var safeValue = value ?? string.Empty;
|
||||
if (_text.text == safeValue)
|
||||
return;
|
||||
|
||||
_text.text = safeValue;
|
||||
RefreshLayout();
|
||||
}
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
CacheRefs();
|
||||
RefreshLayout();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (_text == null)
|
||||
return;
|
||||
|
||||
if (_lastText != _text.text)
|
||||
RefreshLayout();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate()
|
||||
{
|
||||
CacheRefs();
|
||||
|
||||
if (!Application.isPlaying)
|
||||
RefreshLayout();
|
||||
}
|
||||
#endif
|
||||
|
||||
private void CacheRefs()
|
||||
{
|
||||
_rootRect = transform as RectTransform;
|
||||
_rootLayoutElement = GetComponent<LayoutElement>();
|
||||
|
||||
if (_text != null)
|
||||
_textRect = _text.rectTransform;
|
||||
|
||||
if (_background != null)
|
||||
_backgroundRect = _background.rectTransform;
|
||||
}
|
||||
|
||||
public void SetText(string value)
|
||||
{
|
||||
Text = value;
|
||||
}
|
||||
|
||||
public void ClearText()
|
||||
{
|
||||
Text = string.Empty;
|
||||
}
|
||||
|
||||
public void RefreshLayout()
|
||||
{
|
||||
if (_rootRect == null || _rootLayoutElement == null || _textRect == null || _backgroundRect == null ||
|
||||
_text == null || _background == null)
|
||||
return;
|
||||
|
||||
_lastText = _text.text ?? string.Empty;
|
||||
|
||||
var hasText = !string.IsNullOrWhiteSpace(_lastText);
|
||||
|
||||
_text.enabled = hasText;
|
||||
_background.enabled = hasText;
|
||||
|
||||
if (!hasText)
|
||||
return;
|
||||
|
||||
_backgroundRect.anchorMin = Vector2.zero;
|
||||
_backgroundRect.anchorMax = Vector2.one;
|
||||
_backgroundRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
_backgroundRect.offsetMin = Vector2.zero;
|
||||
_backgroundRect.offsetMax = Vector2.zero;
|
||||
_backgroundRect.anchoredPosition = Vector2.zero;
|
||||
_background.transform.SetAsFirstSibling();
|
||||
|
||||
_textRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
_textRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
_textRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
_textRect.anchoredPosition = Vector2.zero;
|
||||
|
||||
_text.textWrappingMode = TextWrappingModes.NoWrap;
|
||||
_text.overflowMode = TextOverflowModes.Overflow;
|
||||
_text.enableAutoSizing = false;
|
||||
|
||||
Canvas.ForceUpdateCanvases();
|
||||
_text.ForceMeshUpdate();
|
||||
|
||||
var preferred = _text.GetPreferredValues(_lastText);
|
||||
var textWidth = preferred.x;
|
||||
var textHeight = preferred.y;
|
||||
|
||||
_textRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, textWidth);
|
||||
_textRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, textHeight);
|
||||
|
||||
var rootWidth = Mathf.Clamp(
|
||||
textWidth + _backgroundPadding.x * 2f,
|
||||
_minBackgroundSize.x,
|
||||
_maxBackgroundSize.x);
|
||||
|
||||
var rootHeight = Mathf.Clamp(
|
||||
textHeight + _backgroundPadding.y * 2f,
|
||||
_minBackgroundSize.y,
|
||||
_maxBackgroundSize.y);
|
||||
|
||||
_rootRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, rootWidth);
|
||||
_rootRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, rootHeight);
|
||||
|
||||
_rootLayoutElement.minWidth = rootWidth;
|
||||
_rootLayoutElement.preferredWidth = rootWidth;
|
||||
_rootLayoutElement.flexibleWidth = 0f;
|
||||
_rootLayoutElement.minHeight = rootHeight;
|
||||
_rootLayoutElement.preferredHeight = rootHeight;
|
||||
_rootLayoutElement.flexibleHeight = 0f;
|
||||
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(_rootRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/HUD/InteractTextUI.cs.meta
Normal file
3
Assets/Scripts/UI/HUD/InteractTextUI.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e23149ea93ce4ccf9c43ed9e88896dfc
|
||||
timeCreated: 1773669738
|
||||
566
Assets/Scripts/UI/HUD/InventoryBar.cs
Normal file
566
Assets/Scripts/UI/HUD/InventoryBar.cs
Normal file
@@ -0,0 +1,566 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/HUD/InventoryBar.cs.meta
Normal file
3
Assets/Scripts/UI/HUD/InventoryBar.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21400b75a7244d418a6315a802dc66f7
|
||||
timeCreated: 1769714388
|
||||
291
Assets/Scripts/UI/HUD/TutorialPopup.cs
Normal file
291
Assets/Scripts/UI/HUD/TutorialPopup.cs
Normal file
@@ -0,0 +1,291 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using BriarQueen.Framework.Managers.UI.Base;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BriarQueen.UI.HUD
|
||||
{
|
||||
public class TutorialPopup : MonoBehaviour, IPopup
|
||||
{
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI _text;
|
||||
|
||||
[SerializeField]
|
||||
private CanvasGroup _canvasGroup;
|
||||
|
||||
[Header("Animation")]
|
||||
[SerializeField]
|
||||
private float _fadeDuration = 0.3f;
|
||||
|
||||
[Header("Queue")]
|
||||
[SerializeField]
|
||||
private bool _suppressDuplicateMessages = true;
|
||||
|
||||
private readonly Queue<PopupRequest> _queue = new();
|
||||
private string _currentMessage = string.Empty;
|
||||
|
||||
private CancellationTokenSource _destroyCts;
|
||||
|
||||
private bool _isProcessingQueue;
|
||||
private Sequence _sequence;
|
||||
private CancellationTokenSource _sequenceCts;
|
||||
|
||||
public bool IsModal => false;
|
||||
|
||||
public GameObject GameObject => gameObject;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_destroyCts = new CancellationTokenSource();
|
||||
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
}
|
||||
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
CancelTweenIfRunning();
|
||||
|
||||
if (_destroyCts != null)
|
||||
{
|
||||
_destroyCts.Cancel();
|
||||
_destroyCts.Dispose();
|
||||
_destroyCts = null;
|
||||
}
|
||||
|
||||
_queue.Clear();
|
||||
}
|
||||
|
||||
public async UniTask Show()
|
||||
{
|
||||
if (_canvasGroup == null)
|
||||
return;
|
||||
|
||||
gameObject.SetActive(true);
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
|
||||
CancelTweenIfRunning();
|
||||
|
||||
var localTweenCts = new CancellationTokenSource();
|
||||
_sequenceCts = localTweenCts;
|
||||
|
||||
_canvasGroup.alpha = 0f;
|
||||
|
||||
_sequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _canvasGroup.alpha,
|
||||
endValue = 1f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _fadeDuration,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await _sequence.ToUniTask(cancellationToken: localTweenCts.Token);
|
||||
_canvasGroup.alpha = 1f;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Interrupted by another tween or destroy.
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (ReferenceEquals(_sequenceCts, localTweenCts))
|
||||
_sequenceCts = null;
|
||||
|
||||
localTweenCts.Dispose();
|
||||
_sequence = default;
|
||||
}
|
||||
}
|
||||
|
||||
public async UniTask Hide()
|
||||
{
|
||||
if (_canvasGroup == null)
|
||||
return;
|
||||
|
||||
CancelTweenIfRunning();
|
||||
|
||||
var localTweenCts = new CancellationTokenSource();
|
||||
_sequenceCts = localTweenCts;
|
||||
|
||||
_sequence = Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
|
||||
{
|
||||
startValue = _canvasGroup.alpha,
|
||||
endValue = 0f,
|
||||
settings = new TweenSettings
|
||||
{
|
||||
duration = _fadeDuration,
|
||||
useUnscaledTime = true
|
||||
}
|
||||
}));
|
||||
|
||||
try
|
||||
{
|
||||
await _sequence.ToUniTask(cancellationToken: localTweenCts.Token);
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Interrupted by another tween or destroy.
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (ReferenceEquals(_sequenceCts, localTweenCts))
|
||||
_sequenceCts = null;
|
||||
|
||||
localTweenCts.Dispose();
|
||||
_sequence = default;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClose()
|
||||
{
|
||||
Hide().Forget();
|
||||
}
|
||||
|
||||
public void SetText(string text)
|
||||
{
|
||||
if (_text != null)
|
||||
_text.text = text ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue a popup to be shown in order. Does not interrupt the current popup.
|
||||
/// Duplicate messages can be suppressed if they are currently showing or already queued.
|
||||
/// </summary>
|
||||
public UniTask Play(string text, float duration)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return UniTask.CompletedTask;
|
||||
|
||||
if (_suppressDuplicateMessages && IsDuplicateMessage(text))
|
||||
return UniTask.CompletedTask;
|
||||
|
||||
_queue.Enqueue(new PopupRequest(text, duration));
|
||||
|
||||
if (!_isProcessingQueue)
|
||||
ProcessQueue().Forget();
|
||||
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears pending popups and hides the current popup immediately.
|
||||
/// Useful for scene transitions or hard UI resets.
|
||||
/// </summary>
|
||||
public void ClearQueue()
|
||||
{
|
||||
_queue.Clear();
|
||||
_currentMessage = string.Empty;
|
||||
|
||||
CancelTweenIfRunning();
|
||||
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = 0f;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
_canvasGroup.interactable = false;
|
||||
}
|
||||
|
||||
gameObject.SetActive(false);
|
||||
SetText(string.Empty);
|
||||
_isProcessingQueue = false;
|
||||
}
|
||||
|
||||
private bool IsDuplicateMessage(string text)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_currentMessage) &&
|
||||
string.Equals(_currentMessage, text, StringComparison.Ordinal))
|
||||
return true;
|
||||
|
||||
foreach (var queued in _queue)
|
||||
if (string.Equals(queued.Text, text, StringComparison.Ordinal))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async UniTaskVoid ProcessQueue()
|
||||
{
|
||||
if (_isProcessingQueue)
|
||||
return;
|
||||
|
||||
_isProcessingQueue = true;
|
||||
|
||||
try
|
||||
{
|
||||
while (_destroyCts != null && !_destroyCts.IsCancellationRequested && _queue.Count > 0)
|
||||
{
|
||||
var request = _queue.Dequeue();
|
||||
_currentMessage = request.Text;
|
||||
|
||||
SetText(request.Text);
|
||||
await Show();
|
||||
|
||||
if (request.Duration > 0f)
|
||||
await UniTask.Delay(
|
||||
TimeSpan.FromSeconds(request.Duration),
|
||||
cancellationToken: _destroyCts.Token);
|
||||
|
||||
await Hide();
|
||||
_currentMessage = string.Empty;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Object destroyed or queue processing canceled.
|
||||
}
|
||||
finally
|
||||
{
|
||||
_currentMessage = string.Empty;
|
||||
_isProcessingQueue = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelTweenIfRunning()
|
||||
{
|
||||
if (_sequence.isAlive)
|
||||
_sequence.Stop();
|
||||
|
||||
_sequence = default;
|
||||
|
||||
if (_sequenceCts != null)
|
||||
{
|
||||
_sequenceCts.Cancel();
|
||||
_sequenceCts.Dispose();
|
||||
_sequenceCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private struct PopupRequest
|
||||
{
|
||||
public readonly string Text;
|
||||
public readonly float Duration;
|
||||
|
||||
public PopupRequest(string text, float duration)
|
||||
{
|
||||
Text = text;
|
||||
Duration = duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/HUD/TutorialPopup.cs.meta
Normal file
3
Assets/Scripts/UI/HUD/TutorialPopup.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b1451fbb217493e8e7d5fb086195021
|
||||
timeCreated: 1772821477
|
||||
114
Assets/Scripts/UI/HUD/UIInventorySlot.cs
Normal file
114
Assets/Scripts/UI/HUD/UIInventorySlot.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using BriarQueen.Framework.Coordinators.Events;
|
||||
using BriarQueen.Framework.Events.UI;
|
||||
using BriarQueen.Framework.Managers.Interaction.Data;
|
||||
using BriarQueen.Framework.Managers.Player;
|
||||
using BriarQueen.Framework.Managers.Player.Data;
|
||||
using BriarQueen.Framework.Managers.UI;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using VContainer;
|
||||
|
||||
namespace BriarQueen.UI.HUD
|
||||
{
|
||||
/// <summary>
|
||||
/// UI slot is now an IInteractable so InteractManager (UI raycast path) can drive hover + click.
|
||||
/// No IPointerEnter/Exit, no OnClickEvent subscription.
|
||||
/// </summary>
|
||||
public class UIInventorySlot : MonoBehaviour, IInteractable
|
||||
{
|
||||
[SerializeField]
|
||||
private Image _slotBase;
|
||||
|
||||
[SerializeField]
|
||||
private Image _selectedVisual;
|
||||
|
||||
[SerializeField]
|
||||
private Image _icon;
|
||||
|
||||
public ItemDataSo Item;
|
||||
private EventCoordinator _eventCoordinator;
|
||||
|
||||
private InventoryBar _owner;
|
||||
|
||||
private PlayerManager _playerManager;
|
||||
|
||||
// Cursor while hovering inventory slots (tweak if you have a “hand” cursor etc.)
|
||||
public UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.UseItem;
|
||||
public string InteractableName => Item.ItemName;
|
||||
|
||||
public UniTask EnterHover()
|
||||
{
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public UniTask ExitHover()
|
||||
{
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public async UniTask OnInteract(ItemDataSo selectedItem = null)
|
||||
{
|
||||
Debug.Log("[UI Inventory Slot] Interacted");
|
||||
|
||||
if (Item == null)
|
||||
{
|
||||
_eventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(UIInteractKey.EmptySlot)));
|
||||
return;
|
||||
}
|
||||
|
||||
// normal click: selection
|
||||
if (selectedItem == null)
|
||||
{
|
||||
_owner?.OnSlotInteracted(this);
|
||||
return;
|
||||
}
|
||||
|
||||
// use selected item on this item (prefer selected item's logic)
|
||||
if (selectedItem.Interaction != null)
|
||||
{
|
||||
var handled = await selectedItem.Interaction.TryUseWith(
|
||||
selectedItem, Item, _playerManager, _eventCoordinator);
|
||||
|
||||
if (handled)
|
||||
return;
|
||||
}
|
||||
|
||||
// fallback: allow target to handle it
|
||||
if (Item.Interaction != null)
|
||||
{
|
||||
var handled = await Item.Interaction.TryUseWith(
|
||||
Item, selectedItem, _playerManager, _eventCoordinator);
|
||||
|
||||
if (handled)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
[Inject]
|
||||
public void Construct(PlayerManager playerManager, EventCoordinator eventCoordinator)
|
||||
{
|
||||
_playerManager = playerManager;
|
||||
_eventCoordinator = eventCoordinator;
|
||||
}
|
||||
|
||||
public void Initialize(InventoryBar owner, ItemDataSo item)
|
||||
{
|
||||
Debug.Log("Initializing Slot");
|
||||
_owner = owner;
|
||||
Item = item;
|
||||
|
||||
if (_icon != null && Item != null)
|
||||
_icon.sprite = Item.Icon;
|
||||
|
||||
SetSelected(false);
|
||||
Debug.Log($"Set Slot to {Item}");
|
||||
}
|
||||
|
||||
public void SetSelected(bool selected)
|
||||
{
|
||||
if (_selectedVisual != null) _selectedVisual.gameObject.SetActive(selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/UI/HUD/UIInventorySlot.cs.meta
Normal file
3
Assets/Scripts/UI/HUD/UIInventorySlot.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 75a4776058b6492394b866b5e04bc73f
|
||||
timeCreated: 1769714789
|
||||
Reference in New Issue
Block a user