using System; using System.Collections.Generic; using System.Linq; using System.Threading; using BriarQueen.Data.Identifiers; using BriarQueen.Framework.Assets; using BriarQueen.Framework.Managers.Interaction; using BriarQueen.Framework.Managers.Player; using BriarQueen.Framework.Managers.Player.Data; using BriarQueen.Framework.Managers.UI.Base; using BriarQueen.Framework.Registries; using BriarQueen.UI.Menus; using Cysharp.Threading.Tasks; using PrimeTween; using TMPro; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.UI; using VContainer; namespace BriarQueen.UI.Codex { public class CodexWindow : MonoBehaviour, IUIWindow { [Header("Root UI")] [SerializeField] private CanvasGroup _canvasGroup; [SerializeField] private RectTransform _windowRect; [Header("Category Buttons")] [SerializeField] private CodexCategoryButton _booksButton; [SerializeField] private CodexCategoryButton _cluesButton; [SerializeField] private CodexCategoryButton _photosButton; [Header("Left Panel - Location View")] [SerializeField] private CanvasGroup _locationListCanvasGroup; [SerializeField] private RectTransform _locationListContainer; [SerializeField] private VerticalScrollbar _locationScrollbar; [Header("Left Panel - Entry View")] [SerializeField] private CanvasGroup _entryListCanvasGroup; [SerializeField] private RectTransform _entryListContainer; [SerializeField] private VerticalScrollbar _entryScrollbar; [SerializeField] private Button _backToLocationsButton; [Header("Display Area")] [SerializeField] private CanvasGroup _displayCanvasGroup; [SerializeField] private RectTransform _displayLayoutRoot; [SerializeField] private TextMeshProUGUI _titleText; [SerializeField] private TextMeshProUGUI _bodyText; [SerializeField] private TextMeshProUGUI _photoDescription; [SerializeField] private TextMeshProUGUI _polaroidWriting; [SerializeField] private Image _polaroid; [SerializeField] private Image _displayImage; [SerializeField] private GameObject _contentRoot; [SerializeField] private GameObject _emptyStateRoot; [Header("Tween Settings")] [SerializeField] private TweenSettings _windowTweenSettings = new() { duration = 1.2f, ease = Ease.OutQuad, useUnscaledTime = true }; [SerializeField] private TweenSettings _leftPanelFadeSettings = new() { duration = 0.3f, ease = Ease.OutQuad, useUnscaledTime = true }; [SerializeField] private TweenSettings _displayFadeSettings = new() { duration = 0.3f, ease = Ease.OutQuad, useUnscaledTime = true }; [Header("Scale")] [SerializeField] private float _hiddenScale = 0.85f; [Header("Internal")] [SerializeField] private GraphicRaycaster _graphicRaycaster; private readonly List _activeEntryButtons = new(); private readonly List _activeLocationButtons = new(); private readonly Dictionary _entryScrollByCategoryAndLocation = new(); private readonly Dictionary _lastSelectedEntryByCategory = new(); private readonly Dictionary _lastSelectedLocationByCategory = new(); private readonly Dictionary _locationScrollByCategory = new(); private readonly Stack _pooledEntryButtons = new(); private readonly Stack _pooledLocationButtons = new(); private AddressableManager _addressableManager; private AssetRegistry _assetRegistry; private bool _cached; private CodexType _currentCategory = CodexType.BookEntry; private CodexEntrySo _currentEntry; private Location _currentLocation = Location.None; private Sequence _displaySequence; private AssetReference _entryButtonReference; private InteractManager _interactManager; private Sequence _leftPanelSequence; private AssetReference _locationButtonReference; private CancellationTokenSource _operationCts; private PlayerManager _playerManager; private bool _raycasterRegistered; private bool _showingEntriesForLocation; private bool _started; private Sequence _windowSequence; public bool IsModal => true; public WindowType WindowType => WindowType.CodexWindow; private void Awake() { if (_canvasGroup != null) { _canvasGroup.alpha = 0f; _canvasGroup.blocksRaycasts = false; _canvasGroup.interactable = false; } if (_windowRect != null) _windowRect.localScale = Vector3.one * _hiddenScale; SetLocationPanelVisibleImmediate(true); SetEntryPanelVisibleImmediate(false); SetDisplayVisibleImmediate(false); ClearDisplay(); SetEmptyStateVisible(true); } private void Start() { InitializeStaticUi(); _started = true; } private void OnEnable() { if (_booksButton != null) _booksButton.OnCategoryClicked += OnCategoryClicked; if (_cluesButton != null) _cluesButton.OnCategoryClicked += OnCategoryClicked; if (_photosButton != null) _photosButton.OnCategoryClicked += OnCategoryClicked; if (_backToLocationsButton != null) _backToLocationsButton.onClick.AddListener(OnBackToLocationsClicked); if (_started) TryRegisterRaycaster(); } private void OnDisable() { if (_booksButton != null) _booksButton.OnCategoryClicked -= OnCategoryClicked; if (_cluesButton != null) _cluesButton.OnCategoryClicked -= OnCategoryClicked; if (_photosButton != null) _photosButton.OnCategoryClicked -= OnCategoryClicked; if (_backToLocationsButton != null) _backToLocationsButton.onClick.RemoveListener(OnBackToLocationsClicked); ReleaseAllActiveLocationButtons(); ReleaseAllActiveEntryButtons(); CancelActiveOperations(); _interactManager?.ClearExclusiveRaycaster(); } private void OnDestroy() { CancelActiveOperations(); } public async UniTask Show() { Debug.Log("[CodexWindow] Showing CodexWindow"); Debug.Log($"[CodexWindow] Started - {_started}"); CacheButtonReferences(); TryRegisterRaycaster(); if (!_started) await UniTask.WaitUntil(() => _started, cancellationToken: this.GetCancellationTokenOnDestroy()); if (_canvasGroup == null || _windowRect == null) { Debug.LogError("[CodexWindow] Missing CanvasGroup or WindowRect reference.", this); gameObject.SetActive(true); await RefreshCurrentCategoryAsync(false, false); return; } ResetOperationCancellation(); gameObject.SetActive(true); _canvasGroup.alpha = 0f; _canvasGroup.blocksRaycasts = false; _canvasGroup.interactable = false; _windowRect.localScale = Vector3.one * _hiddenScale; await RefreshCurrentCategoryAsync(false, false); var token = _operationCts.Token; _windowSequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(_canvasGroup, new TweenSettings { startValue = 0f, endValue = 1f, settings = _windowTweenSettings })) .Group(Tween.Scale(_windowRect, new TweenSettings { startValue = Vector3.one * _hiddenScale, endValue = Vector3.one, settings = _windowTweenSettings })); try { await _windowSequence.ToUniTask(cancellationToken: token); } catch (OperationCanceledException) { return; } finally { _windowSequence = default; } _canvasGroup.alpha = 1f; _windowRect.localScale = Vector3.one; _canvasGroup.blocksRaycasts = true; _canvasGroup.interactable = true; if (_currentEntry != null || IsShowingEmptyState()) { await RefreshLayoutAsync(_displayLayoutRoot); await FadeInDisplayAsync(); } } public async UniTask Hide() { if (_canvasGroup == null || _windowRect == null) { gameObject.SetActive(false); return; } TryUnregisterRaycaster(); SaveCurrentScrollPosition(); ResetOperationCancellation(); _canvasGroup.blocksRaycasts = false; _canvasGroup.interactable = false; var token = _operationCts.Token; _windowSequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(_canvasGroup, new TweenSettings { startValue = _canvasGroup.alpha, endValue = 0f, settings = _windowTweenSettings })) .Group(Tween.Scale(_windowRect, new TweenSettings { startValue = _windowRect.localScale, endValue = Vector3.one * _hiddenScale, settings = _windowTweenSettings })); try { await _windowSequence.ToUniTask(cancellationToken: token); } catch (OperationCanceledException) { return; } finally { _windowSequence = default; } _canvasGroup.alpha = 0f; _windowRect.localScale = Vector3.one * _hiddenScale; gameObject.SetActive(false); } [Inject] public void Construct( InteractManager interactManager, PlayerManager playerManager, AssetRegistry assetRegistry, AddressableManager addressableManager) { _interactManager = interactManager; _playerManager = playerManager; _assetRegistry = assetRegistry; _addressableManager = addressableManager; } private void InitializeStaticUi() { if (_booksButton != null) _booksButton.Initialize(CodexType.BookEntry); if (_cluesButton != null) _cluesButton.Initialize(CodexType.PuzzleClue); if (_photosButton != null) _photosButton.Initialize(CodexType.Photo); } private void CacheButtonReferences() { if (_cached) return; if (_assetRegistry == null) { Debug.LogError("[CodexWindow] AssetRegistry was not injected before Start.", this); return; } if (!_assetRegistry.TryGetReference(AssetKeyIdentifiers.Get(UIKey.CodexLocationButton), out _locationButtonReference) || _locationButtonReference == null) Debug.LogError("[CodexWindow] Failed to resolve location button addressable reference.", this); if (!_assetRegistry.TryGetReference(AssetKeyIdentifiers.Get(UIKey.CodexEntryButton), out _entryButtonReference) || _entryButtonReference == null) Debug.LogError("[CodexWindow] Failed to resolve entry button addressable reference.", this); _cached = true; } private void TryRegisterRaycaster() { if (_raycasterRegistered) return; if (_interactManager == null || _graphicRaycaster == null) return; _interactManager.AddUIRaycaster(_graphicRaycaster); _interactManager?.SetExclusiveRaycaster(_graphicRaycaster); _raycasterRegistered = true; } private void TryUnregisterRaycaster() { if (!_raycasterRegistered) return; if (_interactManager == null || _graphicRaycaster == null) return; _interactManager.RemoveUIRaycaster(_graphicRaycaster); _interactManager?.ClearExclusiveRaycaster(); _raycasterRegistered = false; } private void OnCategoryClicked(CodexType category) { SetCategoryAsync(category).Forget(); } private void OnBackToLocationsClicked() { ShowLocationListAsync().Forget(); } private async UniTask SetCategoryAsync(CodexType category) { if (_currentCategory == category) return; SaveCurrentScrollPosition(); _currentCategory = category; _currentLocation = Location.None; _currentEntry = null; _showingEntriesForLocation = false; await RefreshCurrentCategoryAsync(true, true); } private async UniTask RefreshCurrentCategoryAsync(bool animateLeftPanel, bool animateDisplay) { if (_playerManager == null) return; if (_locationListContainer == null || _entryListContainer == null) { Debug.LogError("[CodexWindow] Missing left-panel container references.", this); ClearDisplay(); SetEmptyStateVisible(true); SetDisplayVisibleImmediate(true); return; } if (animateLeftPanel && animateDisplay) await UniTask.WhenAll( FadeOutCurrentLeftPanelAsync(), FadeOutDisplayAsync()); else if (animateLeftPanel) await FadeOutCurrentLeftPanelAsync(); else if (animateDisplay) await FadeOutDisplayAsync(); UpdateCategoryButtonSelection(); var entriesInCategory = _playerManager .GetDiscoveredCodexEntriesByType(_currentCategory) .Where(entry => entry != null) .OrderBy(entry => entry.Title) .ToList(); ReleaseAllActiveLocationButtons(); ReleaseAllActiveEntryButtons(); if (entriesInCategory.Count == 0) { ShowEmptyState(); if (animateLeftPanel) { await RefreshLayoutAsync(_locationListContainer); await FadeInLocationsPanelAsync(); } if (animateDisplay) { await RefreshLayoutAsync(_displayLayoutRoot); await FadeInDisplayAsync(); } return; } SetEmptyStateVisible(false); var locations = entriesInCategory .Select(entry => entry.Location) .Where(location => location != Location.None) .Distinct() .OrderBy(location => location.ToString()) .ToList(); if (locations.Count == 0) { ShowEmptyState(); if (animateLeftPanel) { await RefreshLayoutAsync(_locationListContainer); await FadeInLocationsPanelAsync(); } if (animateDisplay) { await RefreshLayoutAsync(_displayLayoutRoot); await FadeInDisplayAsync(); } return; } foreach (var location in locations) { var button = await GetOrCreateLocationButtonAsync(); if (button == null) continue; button.Initialize(location); button.OnLocationClicked -= OnLocationClicked; button.OnLocationClicked += OnLocationClicked; _activeLocationButtons.Add(button); } _locationScrollbar?.Rebuild(); _currentLocation = GetLocationToSelect(locations); await BuildEntryButtonsForCurrentLocationAsync(); ClearDisplaySelectionForBrowsing(); _showingEntriesForLocation = false; SetEntryPanelVisibleImmediate(false); SetLocationPanelVisibleImmediate(true); RestoreLocationScrollPosition(1f); await RefreshLayoutAsync(_locationListContainer); if (animateLeftPanel) await FadeInLocationsPanelAsync(); } private void ShowEmptyState() { _currentLocation = Location.None; _currentEntry = null; _showingEntriesForLocation = false; ClearDisplay(); SetEmptyStateVisible(true); SetDisplayVisibleImmediate(true); SetLocationPanelVisibleImmediate(true); SetEntryPanelVisibleImmediate(false); RestoreLocationScrollPosition(1f); } private bool IsShowingEmptyState() { return _emptyStateRoot != null && _emptyStateRoot.activeSelf; } private Location GetLocationToSelect(List locations) { if (_currentLocation != Location.None && locations.Contains(_currentLocation)) return _currentLocation; if (_lastSelectedLocationByCategory.TryGetValue(_currentCategory, out var rememberedLocation) && rememberedLocation != Location.None && locations.Contains(rememberedLocation)) return rememberedLocation; return locations[0]; } private void OnLocationClicked(Location location) { OnLocationClickedAsync(location).Forget(); } private async UniTask OnLocationClickedAsync(Location location) { if (location == Location.None) return; SaveCurrentScrollPosition(); _currentLocation = location; _lastSelectedLocationByCategory[_currentCategory] = location; _currentEntry = null; await UniTask.WhenAll( FadeOutLocationsPanelAsync(), FadeOutDisplayAsync()); await BuildEntryButtonsForCurrentLocationAsync(); ClearDisplaySelectionForBrowsing(); await RefreshLayoutAsync(_entryListContainer); await RefreshLayoutAsync(_displayLayoutRoot); await FadeInEntriesPanelAsync(); } private async UniTask BuildEntryButtonsForCurrentLocationAsync() { ReleaseAllActiveEntryButtons(); if (_currentLocation == Location.None || _playerManager == null) return; var entries = _playerManager .GetDiscoveredCodexEntriesByType(_currentCategory) .Where(entry => entry != null && entry.Location == _currentLocation) .OrderBy(entry => entry.Title) .ToList(); foreach (var entry in entries) { var button = await GetOrCreateEntryButtonAsync(); if (button == null) continue; button.Initialize(entry); button.SetSelected(false); button.OnEntryClicked -= OnEntryClicked; button.OnEntryClicked += OnEntryClicked; _activeEntryButtons.Add(button); } _entryScrollbar?.Rebuild(); } private void ClearDisplaySelectionForBrowsing() { _currentEntry = null; ClearDisplay(); SetEmptyStateVisible(false); SetDisplayVisibleImmediate(false); UpdateEntryButtonSelection(); } private void OnEntryClicked(CodexEntrySo entry) { ChangeDisplayedEntryAsync(entry).Forget(); } private async UniTask ChangeDisplayedEntryAsync(CodexEntrySo entry) { if (entry == null) return; var hasCurrentVisibleEntry = _currentEntry != null && _displayCanvasGroup != null && _displayCanvasGroup.alpha > 0.001f; var isChangingEntry = _currentEntry != null && _currentEntry.UniqueID != entry.UniqueID; if (hasCurrentVisibleEntry && isChangingEntry) await FadeOutDisplayAsync(); ApplyEntryToDisplay(entry); UpdateEntryButtonSelection(); await RefreshLayoutAsync(_displayLayoutRoot); await FadeInDisplayAsync(); } private void ApplyEntryToDisplay(CodexEntrySo entry) { _currentEntry = entry; if (entry == null) { ClearDisplay(); SetDisplayVisibleImmediate(false); return; } _lastSelectedEntryByCategory[_currentCategory] = entry.UniqueID; if (_titleText != null) _titleText.text = entry.Title; var isPhoto = entry.EntryType == CodexType.Photo || entry.IsPhotoOverride; var showImage = isPhoto && entry.DisplayImage != null; var showText = !showImage && !string.IsNullOrWhiteSpace(entry.BodyText) || entry.IsBodyTextOverride; var showPhotoDescription = isPhoto && !string.IsNullOrWhiteSpace(entry.PhotoDescription); var showPolaroidWriting = isPhoto && !string.IsNullOrWhiteSpace(entry.PolaroidWriting); if (_polaroid != null) _polaroid.gameObject.SetActive(isPhoto); if (_displayImage != null) { _displayImage.sprite = showImage ? entry.DisplayImage : null; _displayImage.enabled = showImage; _displayImage.gameObject.SetActive(showImage); } if (_photoDescription != null) { _photoDescription.text = showPhotoDescription ? entry.PhotoDescription : string.Empty; _photoDescription.gameObject.SetActive(showPhotoDescription); } if (_polaroidWriting != null) { _polaroidWriting.text = showPolaroidWriting ? entry.PolaroidWriting : string.Empty; _polaroidWriting.gameObject.SetActive(showPolaroidWriting); } if (_bodyText != null) { _bodyText.text = showText ? entry.BodyText : string.Empty; _bodyText.gameObject.SetActive(showText); } SetEmptyStateVisible(false); } private void ClearDisplay() { if (_titleText != null) _titleText.text = string.Empty; if (_polaroid != null) _polaroid.gameObject.SetActive(false); if (_bodyText != null) { _bodyText.text = string.Empty; _bodyText.gameObject.SetActive(false); } if (_displayImage != null) { _displayImage.sprite = null; _displayImage.enabled = false; _displayImage.gameObject.SetActive(false); } if (_photoDescription != null) { _photoDescription.text = string.Empty; _photoDescription.gameObject.SetActive(false); } if (_polaroidWriting != null) { _polaroidWriting.text = string.Empty; _polaroidWriting.gameObject.SetActive(false); } } private void SetEmptyStateVisible(bool visible) { if (_emptyStateRoot != null) _emptyStateRoot.SetActive(visible); if (_contentRoot != null) _contentRoot.SetActive(!visible); } private async UniTask GetOrCreateLocationButtonAsync() { if (_locationButtonReference == null) return null; if (_pooledLocationButtons.Count > 0) { var pooledButton = _pooledLocationButtons.Pop(); pooledButton.transform.SetParent(_locationListContainer, false); pooledButton.gameObject.SetActive(true); return pooledButton; } var instance = await _addressableManager.InstantiateAsync(_locationButtonReference, parent: _locationListContainer); if (instance == null) return null; var button = instance.GetComponent(); if (button == null) { Debug.LogError("[CodexWindow] Spawned location button is missing CodexLocationButton component.", instance); return null; } button.transform.SetParent(_locationListContainer, false); button.gameObject.SetActive(true); return button; } private async UniTask GetOrCreateEntryButtonAsync() { if (_entryButtonReference == null) return null; if (_pooledEntryButtons.Count > 0) { var pooledButton = _pooledEntryButtons.Pop(); pooledButton.transform.SetParent(_entryListContainer, false); pooledButton.gameObject.SetActive(true); return pooledButton; } var instance = await _addressableManager.InstantiateAsync(_entryButtonReference, parent: _entryListContainer); if (instance == null) return null; var button = instance.GetComponent(); if (button == null) { Debug.LogError("[CodexWindow] Spawned entry button is missing CodexEntryButton component.", instance); return null; } button.transform.SetParent(_entryListContainer, false); button.gameObject.SetActive(true); return button; } private void ReleaseAllActiveLocationButtons() { foreach (var button in _activeLocationButtons) { if (button == null) continue; button.OnLocationClicked -= OnLocationClicked; button.gameObject.SetActive(false); _pooledLocationButtons.Push(button); } _activeLocationButtons.Clear(); _locationScrollbar?.Rebuild(); } private void ReleaseAllActiveEntryButtons() { foreach (var button in _activeEntryButtons) { if (button == null) continue; button.OnEntryClicked -= OnEntryClicked; button.SetSelected(false); button.gameObject.SetActive(false); _pooledEntryButtons.Push(button); } _activeEntryButtons.Clear(); _entryScrollbar?.Rebuild(); } private void UpdateEntryButtonSelection() { foreach (var button in _activeEntryButtons) { if (button == null) continue; var isSelected = _currentEntry != null && button.Entry != null && button.Entry.UniqueID == _currentEntry.UniqueID; button.SetSelected(isSelected); } } private void UpdateCategoryButtonSelection() { if (_booksButton != null) _booksButton.SetSelected(_currentCategory == CodexType.BookEntry); if (_cluesButton != null) _cluesButton.SetSelected(_currentCategory == CodexType.PuzzleClue); if (_photosButton != null) _photosButton.SetSelected(_currentCategory == CodexType.Photo); } private void SaveCurrentScrollPosition() { if (_showingEntriesForLocation) { if (_entryScrollbar != null && _currentLocation != Location.None) _entryScrollByCategoryAndLocation[GetEntryScrollKey(_currentCategory, _currentLocation)] = _entryScrollbar.Normalized; } else { if (_locationScrollbar != null) _locationScrollByCategory[_currentCategory] = _locationScrollbar.Normalized; } } private void RestoreLocationScrollPosition(float fallbackValue) { if (_locationScrollbar == null) return; _locationScrollbar.Rebuild(); var target = _locationScrollByCategory.TryGetValue(_currentCategory, out var saved) ? saved : fallbackValue; _locationScrollbar.SetNormalized(target); } private void RestoreEntryScrollPosition(float fallbackValue) { if (_entryScrollbar == null || _currentLocation == Location.None) return; _entryScrollbar.Rebuild(); var key = GetEntryScrollKey(_currentCategory, _currentLocation); var target = _entryScrollByCategoryAndLocation.TryGetValue(key, out var saved) ? saved : fallbackValue; _entryScrollbar.SetNormalized(target); } private static string GetEntryScrollKey(CodexType category, Location location) { return $"{category}_{location}"; } private async UniTask RefreshLayoutAsync(RectTransform layoutRoot) { Canvas.ForceUpdateCanvases(); if (layoutRoot != null) LayoutRebuilder.ForceRebuildLayoutImmediate(layoutRoot); await UniTask.Yield(PlayerLoopTiming.LastPostLateUpdate); Canvas.ForceUpdateCanvases(); if (layoutRoot != null) LayoutRebuilder.ForceRebuildLayoutImmediate(layoutRoot); } private async UniTask FadeOutCurrentLeftPanelAsync() { if (_showingEntriesForLocation) await FadeOutEntriesPanelAsync(); else await FadeOutLocationsPanelAsync(); } private async UniTask FadeOutLocationsPanelAsync() { if (_locationListCanvasGroup == null) return; ResetLeftPanelTweenOnly(); var token = _operationCts?.Token ?? this.GetCancellationTokenOnDestroy(); _leftPanelSequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(_locationListCanvasGroup, new TweenSettings { startValue = _locationListCanvasGroup.alpha, endValue = 0f, settings = _leftPanelFadeSettings })); try { await _leftPanelSequence.ToUniTask(cancellationToken: token); } catch (OperationCanceledException) { return; } finally { _leftPanelSequence = default; } SetLocationPanelVisibleImmediate(false); } private async UniTask FadeOutEntriesPanelAsync() { if (_entryListCanvasGroup == null) return; ResetLeftPanelTweenOnly(); var token = _operationCts?.Token ?? this.GetCancellationTokenOnDestroy(); _leftPanelSequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(_entryListCanvasGroup, new TweenSettings { startValue = _entryListCanvasGroup.alpha, endValue = 0f, settings = _leftPanelFadeSettings })); try { await _leftPanelSequence.ToUniTask(cancellationToken: token); } catch (OperationCanceledException) { return; } finally { _leftPanelSequence = default; } SetEntryPanelVisibleImmediate(false); } private async UniTask FadeInLocationsPanelAsync() { if (_locationListCanvasGroup == null) return; SetEntryPanelVisibleImmediate(false); SetLocationPanelVisibleImmediate(true); _showingEntriesForLocation = false; ResetLeftPanelTweenOnly(); _locationListCanvasGroup.alpha = 0f; var token = _operationCts?.Token ?? this.GetCancellationTokenOnDestroy(); _leftPanelSequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(_locationListCanvasGroup, new TweenSettings { startValue = 0f, endValue = 1f, settings = _leftPanelFadeSettings })); try { await _leftPanelSequence.ToUniTask(cancellationToken: token); } catch (OperationCanceledException) { } finally { _leftPanelSequence = default; } RestoreLocationScrollPosition(1f); } private async UniTask FadeInEntriesPanelAsync() { if (_entryListCanvasGroup == null) return; SetLocationPanelVisibleImmediate(false); SetEntryPanelVisibleImmediate(true); _showingEntriesForLocation = true; ResetLeftPanelTweenOnly(); _entryListCanvasGroup.alpha = 0f; var token = _operationCts?.Token ?? this.GetCancellationTokenOnDestroy(); _leftPanelSequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(_entryListCanvasGroup, new TweenSettings { startValue = 0f, endValue = 1f, settings = _leftPanelFadeSettings })); try { await _leftPanelSequence.ToUniTask(cancellationToken: token); } catch (OperationCanceledException) { } finally { _leftPanelSequence = default; } RestoreEntryScrollPosition(1f); } private async UniTask ShowLocationListAsync() { SaveCurrentScrollPosition(); _currentEntry = null; await FadeOutEntriesPanelAsync(); await FadeOutDisplayAsync(); ClearDisplaySelectionForBrowsing(); await RefreshLayoutAsync(_locationListContainer); await FadeInLocationsPanelAsync(); } private async UniTask FadeOutDisplayAsync() { if (_displayCanvasGroup == null) return; ResetDisplayTweenOnly(); var token = _operationCts?.Token ?? this.GetCancellationTokenOnDestroy(); _displaySequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(_displayCanvasGroup, new TweenSettings { startValue = _displayCanvasGroup.alpha, endValue = 0f, settings = _displayFadeSettings })); try { await _displaySequence.ToUniTask(cancellationToken: token); } catch (OperationCanceledException) { return; } finally { _displaySequence = default; } SetDisplayVisibleImmediate(false); } private async UniTask FadeInDisplayAsync() { if (_displayCanvasGroup == null) return; SetDisplayVisibleImmediate(true); ResetDisplayTweenOnly(); _displayCanvasGroup.alpha = 0f; var token = _operationCts?.Token ?? this.GetCancellationTokenOnDestroy(); _displaySequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(_displayCanvasGroup, new TweenSettings { startValue = 0f, endValue = 1f, settings = _displayFadeSettings })); try { await _displaySequence.ToUniTask(cancellationToken: token); } catch (OperationCanceledException) { } finally { _displaySequence = default; } } private void SetLocationPanelVisibleImmediate(bool visible) { if (_locationListCanvasGroup == null) return; _locationListCanvasGroup.alpha = visible ? 1f : 0f; _locationListCanvasGroup.blocksRaycasts = visible; _locationListCanvasGroup.interactable = visible; _locationListCanvasGroup.gameObject.SetActive(visible); } private void SetEntryPanelVisibleImmediate(bool visible) { if (_entryListCanvasGroup == null) return; _entryListCanvasGroup.alpha = visible ? 1f : 0f; _entryListCanvasGroup.blocksRaycasts = visible; _entryListCanvasGroup.interactable = visible; _entryListCanvasGroup.gameObject.SetActive(visible); if (_backToLocationsButton != null) _backToLocationsButton.gameObject.SetActive(visible); } private void SetDisplayVisibleImmediate(bool visible) { if (_displayCanvasGroup == null) return; _displayCanvasGroup.alpha = visible ? 1f : 0f; _displayCanvasGroup.blocksRaycasts = visible; _displayCanvasGroup.interactable = visible; _displayCanvasGroup.gameObject.SetActive(visible); } private void ResetLeftPanelTweenOnly() { if (_leftPanelSequence.isAlive) _leftPanelSequence.Stop(); _leftPanelSequence = default; } private void ResetDisplayTweenOnly() { if (_displaySequence.isAlive) _displaySequence.Stop(); _displaySequence = default; } private void CancelActiveOperations() { if (_windowSequence.isAlive) _windowSequence.Stop(); ResetLeftPanelTweenOnly(); ResetDisplayTweenOnly(); if (_operationCts == null) return; try { _operationCts.Cancel(); } catch { } _operationCts.Dispose(); _operationCts = null; } private void ResetOperationCancellation() { CancelActiveOperations(); _operationCts = new CancellationTokenSource(); } } }