First commit for private source control. Older commits available on Github.

This commit is contained in:
2026-03-26 12:52:52 +00:00
parent a04c602626
commit 2d449c4a17
2176 changed files with 408185 additions and 0 deletions

View 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;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: feab9a2f47fa4c39836de3ffbcd9aa7e
timeCreated: 1769719100

View 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
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e8b77a48c3af4a8c8858359a537523a9
timeCreated: 1769713926

View 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;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 79bc6a48c54849a295fba449628a13ed
timeCreated: 1773762669

View 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);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e23149ea93ce4ccf9c43ed9e88896dfc
timeCreated: 1773669738

View 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);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 21400b75a7244d418a6315a802dc66f7
timeCreated: 1769714388

View 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;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9b1451fbb217493e8e7d5fb086195021
timeCreated: 1772821477

View 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);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 75a4776058b6492394b866b5e04bc73f
timeCreated: 1769714789