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 _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 { 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 { 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; } /// /// 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. /// 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; } /// /// Clears pending popups and hides the current popup immediately. /// Useful for scene transitions or hard UI resets. /// 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; } } } }