using System; using System.Threading; using BriarQueen.Framework.Managers.Levels.Data; using BriarQueen.Framework.Managers.Player.Data; using Cysharp.Threading.Tasks; using PrimeTween; using UnityEngine; using UnityEngine.UI; namespace BriarQueen.Game.Levels.ChapterOne.LaxleyHouse.FireplaceLockbox { public class LockboxSlot : BaseItem { private const int MinDigit = 1; private const int MaxDigit = 9; [Header("Components")] [SerializeField] private Image _currentNumberImage; [SerializeField] private Image _nextNumberImage; [Header("Animation")] [SerializeField] private float _spinPadding = 8f; [SerializeField] private float _spinDuration = 0.15f; [SerializeField] private Ease _spinEase = Ease.OutCubic; [SerializeField] [Range(0f, 1f)] private float _fadedAlpha = 0.4f; private RectTransform _currentRect; private RectTransform _nextRect; private Sequence _slotSequence; private CancellationTokenSource _slotCancellationTokenSource; private Func _spriteResolver; private Action _onValueChanged; public int CurrentValue { get; private set; } = MinDigit; public void Initialize(Func spriteResolver, Action onValueChanged) { _spriteResolver = spriteResolver; _onValueChanged = onValueChanged; _currentRect = _currentNumberImage != null ? _currentNumberImage.rectTransform : null; _nextRect = _nextNumberImage != null ? _nextNumberImage.rectTransform : null; SyncHelperImageLayout(); ApplyValueImmediate(CurrentValue); } public void ApplyValueImmediate(int value) { CurrentValue = WrapValue(value); CancelTweenIfRunning(); SyncHelperImageLayout(); var sprite = ResolveSprite(CurrentValue); if (_currentNumberImage != null) { _currentNumberImage.sprite = sprite; _currentNumberImage.enabled = sprite != null; _currentNumberImage.color = Color.white; } if (_nextNumberImage != null) { _nextNumberImage.sprite = null; _nextNumberImage.enabled = false; _nextNumberImage.color = Color.white; } if (_currentRect != null) _currentRect.anchoredPosition = Vector2.zero; if (_nextRect != null) _nextRect.anchoredPosition = GetHiddenPosition(); } public override async UniTask OnInteract(ItemDataSo item = null) { if (item != null) return; await SpinToNextValue(); } public async UniTask SpinToNextValue() { if (_currentNumberImage == null || _nextNumberImage == null || _currentRect == null || _nextRect == null) return; int nextValue = WrapValue(CurrentValue + 1); var nextSprite = ResolveSprite(nextValue); if (nextSprite == null) { Debug.LogWarning($"[LockboxSlot] Missing sprite for value {nextValue}.", this); return; } CancelTweenIfRunning(); SyncHelperImageLayout(); _slotCancellationTokenSource = new CancellationTokenSource(); _nextNumberImage.sprite = nextSprite; _nextNumberImage.enabled = true; _nextNumberImage.color = new Color(1f, 1f, 1f, _fadedAlpha); _currentNumberImage.enabled = true; _currentNumberImage.color = Color.white; _currentRect.anchoredPosition = Vector2.zero; _nextRect.anchoredPosition = GetHiddenPosition(); _slotSequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.UIAnchoredPosition( _currentRect, GetVisibleExitPosition(), _spinDuration, _spinEase, useUnscaledTime: true)) .Group(Tween.UIAnchoredPosition( _nextRect, Vector2.zero, _spinDuration, _spinEase, useUnscaledTime: true)) .Group(Tween.Alpha( _currentNumberImage, new TweenSettings { endValue = _fadedAlpha, settings = new TweenSettings { duration = _spinDuration, useUnscaledTime = true } })) .Group(Tween.Alpha( _nextNumberImage, new TweenSettings { endValue = 1f, settings = new TweenSettings { duration = _spinDuration, useUnscaledTime = true } })); try { await _slotSequence.ToUniTask(cancellationToken: _slotCancellationTokenSource.Token); } catch (OperationCanceledException) { return; } finally { if (_slotSequence.isAlive) _slotSequence.Stop(); _slotSequence = default; _slotCancellationTokenSource?.Dispose(); _slotCancellationTokenSource = null; } SwapImages(); CurrentValue = nextValue; PrepareHiddenImage(); _onValueChanged?.Invoke(); } private Sprite ResolveSprite(int value) { return _spriteResolver?.Invoke(WrapValue(value)); } private int WrapValue(int value) { value %= MaxDigit; if (value <= 0) value += MaxDigit; return value; } private void SwapImages() { (_currentNumberImage, _nextNumberImage) = (_nextNumberImage, _currentNumberImage); (_currentRect, _nextRect) = (_nextRect, _currentRect); } private void PrepareHiddenImage() { if (_currentNumberImage != null) { _currentNumberImage.enabled = true; _currentNumberImage.color = Color.white; } if (_currentRect != null) _currentRect.anchoredPosition = Vector2.zero; if (_nextNumberImage != null) { _nextNumberImage.sprite = null; _nextNumberImage.enabled = false; _nextNumberImage.color = Color.white; } if (_nextRect != null) _nextRect.anchoredPosition = GetHiddenPosition(); } private void SyncHelperImageLayout() { if (_currentRect == null || _nextRect == null) return; _nextRect.anchorMin = _currentRect.anchorMin; _nextRect.anchorMax = _currentRect.anchorMax; _nextRect.pivot = _currentRect.pivot; _nextRect.sizeDelta = _currentRect.sizeDelta; _nextRect.localScale = _currentRect.localScale; _nextRect.localRotation = _currentRect.localRotation; } private Vector2 GetHiddenPosition() { float travelDistance = GetTravelDistance(); return new Vector2(0f, -travelDistance); } private Vector2 GetVisibleExitPosition() { float travelDistance = GetTravelDistance(); return new Vector2(0f, travelDistance); } private float GetTravelDistance() { if (_currentRect == null) return _spinPadding; float rectHeight = _currentRect.rect.height; if (rectHeight <= 0f && _currentNumberImage != null) rectHeight = _currentNumberImage.preferredHeight; return rectHeight + _spinPadding; } private void CancelTweenIfRunning() { if (_slotCancellationTokenSource != null && !_slotCancellationTokenSource.IsCancellationRequested) _slotCancellationTokenSource.Cancel(); if (_slotSequence.isAlive) _slotSequence.Stop(); _slotSequence = default; _slotCancellationTokenSource?.Dispose(); _slotCancellationTokenSource = null; } private void OnDisable() { CancelTweenIfRunning(); } private void OnDestroy() { CancelTweenIfRunning(); } } }