using System; using System.Collections.Generic; using System.Linq; using System.Threading; using BriarQueen.Data.Identifiers; using BriarQueen.Framework.Extensions; using BriarQueen.Framework.Managers.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.Framework.Services.Destruction; using BriarQueen.UI.Components; using BriarQueen.UI.Menus; using BriarQueen.UI.Menus.Components; using Cysharp.Threading.Tasks; using PrimeTween; using TMPro; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.Pool; using UnityEngine.UI; using VContainer; namespace BriarQueen.UI.Codex { public class CodexWindow : MonoBehaviour, IUIWindow, IUIBackHandler { [Header("Root UI")] [SerializeField] private CanvasGroup _canvasGroup; [SerializeField] private RectTransform _windowRect; [Header("Content")] [SerializeField] private CanvasGroup _backgroundGroup; // Direct child — faded instead of root [Header("Left Panel — Header")] [SerializeField] private TextMeshProUGUI _leftPanelTitleText; [SerializeField] private CanvasGroup _leftPanelTitleGroup; [Header("Left Panel — Categories")] [SerializeField] private CanvasGroup _categoriesGroup; [SerializeField] private UnderlineButton _booksButton; [SerializeField] private UnderlineButton _cluesButton; [SerializeField] private UnderlineButton _photosButton; [Header("Left Panel — Locations")] [SerializeField] private CanvasGroup _locationListGroup; [SerializeField] private RectTransform _locationListContainer; [SerializeField] private VerticalScrollbar _locationScrollbar; [SerializeField] private Button _backToCategoriesButton; [Header("Left Panel — Entries")] [SerializeField] private CanvasGroup _entryListGroup; [SerializeField] private RectTransform _entryListContainer; [SerializeField] private VerticalScrollbar _entryScrollbar; [SerializeField] private Button _backToLocationsButton; [SerializeField] private UnderlineButtonGroup _entryButtonGroup; [Header("Right Panel — Display")] [SerializeField] private CanvasGroup _rightPanelGroup; [SerializeField] private RectTransform _rightPanelRoot; [SerializeField] private TextMeshProUGUI _titleText; [SerializeField] private CanvasGroup _titleGroup; [SerializeField] private CanvasGroup _displayAreaGroup; [SerializeField] private ScrollableTextBox _bodyText; [SerializeField] private ScrollableTextBox _photoDescription; [SerializeField] private TextMeshProUGUI _polaroidWriting; [SerializeField] private Image _polaroid; [SerializeField] private Image _displayImage; [SerializeField] private GameObject _displayAreaRoot; [SerializeField] private GameObject _emptyStateRoot; [Header("Tween Settings")] [SerializeField] private TweenSettings _windowTweenSettings = new() { duration = 1.2f, ease = Ease.OutQuad, useUnscaledTime = true }; [SerializeField] private TweenSettings _panelFadeSettings = new() { duration = 0.3f, ease = Ease.OutQuad, useUnscaledTime = true }; [SerializeField] private TweenSettings _titleFadeSettings = new() { duration = 0.2f, ease = Ease.OutQuad, useUnscaledTime = true }; [SerializeField] private TweenSettings _contentFadeSettings = new() { duration = 0.25f, ease = Ease.OutQuad, useUnscaledTime = true }; [SerializeField] private TweenSettings _leftPanelTitleFadeSettings = new() { duration = 0.2f, 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 _lastEntryByCategory = new(); private readonly Dictionary _lastLocationByCategory = new(); private readonly Dictionary _locationScrollByCategory = new(); private AddressableManager _addressableManager; private AssetRegistry _assetRegistry; private bool _cached; private CodexType _currentCategory = CodexType.DocumentEntry; private CodexEntrySo _currentEntry; private Location _currentLocation = Location.None; private DestructionService _destructionService; private Sequence _displaySequence; private ObjectPool _entryButtonPool; private AssetReference _entryButtonReference; private CodexEntryButton _entryButtonTemplate; private InteractManager _interactManager; private Sequence _leftPanelSequence; private LeftPanelState _leftPanelState = LeftPanelState.Categories; private Sequence _leftPanelTitleSequence; private ObjectPool _locationButtonPool; private AssetReference _locationButtonReference; private CodexLocationButton _locationButtonTemplate; private CancellationTokenSource _operationCts; private PlayerManager _playerManager; private bool _raycasterRegistered; private bool _started; private Sequence _windowSequence; public bool IsModal => true; // ── Unity lifecycle ─────────────────────────────────────────── private void Awake() { if (_canvasGroup != null) { _canvasGroup.alpha = 0f; _canvasGroup.blocksRaycasts = false; _canvasGroup.interactable = false; } // Background group controls visual fading if (_backgroundGroup != null) { _backgroundGroup.alpha = 0f; _backgroundGroup.blocksRaycasts = false; _backgroundGroup.interactable = false; } if (_windowRect != null) _windowRect.localScale = Vector3.one * _hiddenScale; SetPanelImmediate(_categoriesGroup, false); SetPanelImmediate(_locationListGroup, false); SetPanelImmediate(_entryListGroup, false); SetDisplayImmediate(false); SetEmptyStateVisible(false); SetLeftPanelTitleImmediate("Codex"); } private async UniTaskVoid Start() { CacheButtonReferences(); await EnsurePoolsReady(); } private void Update() { var mouse = UnityEngine.InputSystem.Mouse.current; if (mouse == null || !mouse.leftButton.wasPressedThisFrame) { return; } var ev = UnityEngine.EventSystems.EventSystem.current; if (ev == null) { Debug.Log("[Click] No EventSystem.current!"); return; } var pos = mouse.position.ReadValue(); var ped = new UnityEngine.EventSystems.PointerEventData(ev) { position = pos }; var results = new System.Collections.Generic.List(); ev.RaycastAll(ped, results); } private void OnEnable() { BindCategoryButtons(); BindNavButtons(); } private void OnDisable() { UnbindCategoryButtons(); UnbindNavButtons(); TryUnregisterRaycaster(); ReleaseAllLocationButtons(); ReleaseAllEntryButtons(); CancelAllOperations(); } private void OnDestroy() { CancelAllOperations(); _locationButtonPool?.Dispose(); _entryButtonPool?.Dispose(); TryUnregisterRaycaster(); } // ── IUIBackHandler ──────────────────────────────────────────── public bool HandleBackRequest() { switch (_leftPanelState) { case LeftPanelState.Entries: NavigateBackToLocations().Forget(); return true; case LeftPanelState.Locations: NavigateBackToCategories().Forget(); return true; case LeftPanelState.Categories: return false; default: return false; } } public WindowType WindowType => WindowType.CodexWindow; // ── IUIWindow ───────────────────────────────────────────────── public async UniTask Show() { ResetOperationCts(); gameObject.SetActive(true); TryRegisterRaycaster(); if (_canvasGroup != null) { _canvasGroup.blocksRaycasts = false; _canvasGroup.interactable = false; } if (_backgroundGroup != null) { _backgroundGroup.alpha = 0f; _backgroundGroup.blocksRaycasts = false; _backgroundGroup.interactable = false; } if (_windowRect != null) _windowRect.localScale = Vector3.one * _hiddenScale; SetPanelImmediate(_categoriesGroup, false); SetPanelImmediate(_locationListGroup, false); SetPanelImmediate(_entryListGroup, false); SetDisplayImmediate(false); SetLeftPanelTitleImmediate("Codex"); _windowSequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(_backgroundGroup, new TweenSettings { startValue = 0f, endValue = 1f, settings = _windowTweenSettings })) .Group(Tween.Scale(_windowRect, new TweenSettings { startValue = Vector3.one * _hiddenScale, endValue = Vector3.one, settings = _windowTweenSettings })); try { var canvasTask = Tween.Alpha(_canvasGroup, new TweenSettings { startValue = 0f, endValue = 1f, settings = _windowTweenSettings }).ToUniTask(); var backgroundTask = _windowSequence.ToUniTask(); await UniTask.WhenAll(canvasTask, backgroundTask).AttachExternalCancellation(_operationCts.Token); _backgroundGroup.blocksRaycasts = true; _backgroundGroup.interactable = true; } catch (OperationCanceledException) { return; } finally { _windowSequence = default; } _canvasGroup.interactable = true; _canvasGroup.blocksRaycasts = true; _canvasGroup.alpha = 1f; await TransitionToCategories(instant: true); } public async UniTask Hide() { Debug.Log($"[CodexWindow] Codex Window Hide Started."); ResetOperationCts(); if (_canvasGroup != null) { _canvasGroup.blocksRaycasts = true; _canvasGroup.interactable = false; } _windowSequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(_backgroundGroup, new TweenSettings { startValue = _backgroundGroup != null ? _backgroundGroup.alpha : 1f, endValue = 0f, settings = _windowTweenSettings })) .Group(Tween.Scale(_windowRect, new TweenSettings { startValue = _windowRect.localScale, endValue = Vector3.one * _hiddenScale, settings = _windowTweenSettings })); try { await _windowSequence.ToUniTask(cancellationToken: _operationCts.Token); } catch (OperationCanceledException) { return; } finally { _windowSequence = default; } if (_canvasGroup != null) { _canvasGroup.alpha = 0f; _canvasGroup.blocksRaycasts = false; _canvasGroup.interactable = false; } TryUnregisterRaycaster(); gameObject.SetActive(false); Debug.Log($"[CodexWindow] Codex Window Hide Complete."); } // ── Raycaster ───────────────────────────────────────────────── private void TryRegisterRaycaster() { Debug.Log($"[CodexWindow] TryRegisterRaycaster " + $"registered={_raycasterRegistered} " + $"interactManager={_interactManager != null} " + $"raycaster={_graphicRaycaster != null}"); Debug.Log("[CodexWindow] Try register raycaster."); if (_raycasterRegistered || _interactManager == null || _graphicRaycaster == null) return; _interactManager.AddUIRaycaster(_graphicRaycaster); _interactManager.SetExclusiveRaycaster(_graphicRaycaster); _raycasterRegistered = true; Debug.Log("[CodexWindow] Registered raycaster."); } private void TryUnregisterRaycaster() { Debug.Log("[CodexWindow] Try unregister raycaster."); if (!_raycasterRegistered || _interactManager == null || _graphicRaycaster == null) return; _interactManager.RemoveUIRaycaster(_graphicRaycaster); _interactManager.ClearExclusiveRaycaster(); _raycasterRegistered = false; Debug.Log("[CodexWindow] Raycaster unregistered."); } // ── DI ──────────────────────────────────────────────────────── [Inject] public void Construct( InteractManager interactManager, PlayerManager playerManager, AssetRegistry assetRegistry, AddressableManager addressableManager, DestructionService destructionService) { _interactManager = interactManager; _playerManager = playerManager; _assetRegistry = assetRegistry; _addressableManager = addressableManager; _destructionService = destructionService; } // ── Pool setup ──────────────────────────────────────────────── private async UniTask EnsurePoolsReady() { if (_locationButtonPool != null && _entryButtonPool != null) return; var ct = this.GetCancellationTokenOnDestroy(); if (_locationButtonTemplate == null && _locationButtonReference != null) { var instance = await _addressableManager.InstantiateAsync( _locationButtonReference, parent: _locationListContainer, cancellationToken: ct); if (instance != null) { _locationButtonTemplate = instance.GetComponent(); if (_locationButtonTemplate != null) _locationButtonTemplate.gameObject.SetActive(false); else await _destructionService.Destroy(instance); } } if (_entryButtonTemplate == null && _entryButtonReference != null) { var instance = await _addressableManager.InstantiateAsync( _entryButtonReference, parent: _entryListContainer, cancellationToken: ct); if (instance != null) { _entryButtonTemplate = instance.GetComponent(); if (_entryButtonTemplate != null) _entryButtonTemplate.gameObject.SetActive(false); else await _destructionService.Destroy(instance); } } if (_locationButtonPool == null && _locationButtonTemplate != null) { _locationButtonPool = new ObjectPool( createFunc: () => { var btn = Instantiate(_locationButtonTemplate, _locationListContainer); btn.gameObject.SetActive(false); return btn; }, actionOnGet: btn => { btn.transform.SetParent(_locationListContainer, false); btn.gameObject.SetActive(true); }, actionOnRelease: btn => { btn.OnLocationClicked -= OnLocationClicked; btn.gameObject.SetActive(false); }, actionOnDestroy: btn => { if (btn != null) Destroy(btn.gameObject); }, collectionCheck: false, defaultCapacity: 8, maxSize: 32); } if (_entryButtonPool == null && _entryButtonTemplate != null) { _entryButtonPool = new ObjectPool( createFunc: () => { var btn = Instantiate(_entryButtonTemplate, _entryListContainer); btn.gameObject.SetActive(false); return btn; }, actionOnGet: btn => { btn.transform.SetParent(_entryListContainer, false); btn.gameObject.SetActive(true); }, actionOnRelease: btn => { if (_entryButtonGroup != null) _entryButtonGroup.RemoveButton(btn.UnderlineButton); btn.OnEntryClicked -= OnEntryClicked; btn.SetSelected(false); btn.gameObject.SetActive(false); }, actionOnDestroy: btn => { if (btn != null) Destroy(btn.gameObject); }, collectionCheck: false, defaultCapacity: 16, maxSize: 64); } } // ── Navigation ──────────────────────────────────────────────── private void OnCluesClicked(UnderlineButton _) => NavigateToLocations(CodexType.PuzzleClue).Forget(); private void OnDocumentsClicked(UnderlineButton _) => NavigateToLocations(CodexType.DocumentEntry).Forget(); private void OnPhotosClicked(UnderlineButton _) => NavigateToLocations(CodexType.Photo).Forget(); private void OnLocationClicked(Location location) => NavigateToEntries(location).Forget(); private void OnEntryClicked(CodexEntrySo entry) => DisplayEntry(entry).Forget(); private void OnBackToCategoriesClicked() => NavigateBackToCategories().Forget(); private void OnBackToLocationsClicked() => NavigateBackToLocations().Forget(); // ── Left panel transitions ──────────────────────────────────── private async UniTask TransitionToCategories(bool instant = false) { _leftPanelState = LeftPanelState.Categories; if (instant) { SetPanelImmediate(_locationListGroup, false); SetPanelImmediate(_entryListGroup, false); SetPanelImmediate(_categoriesGroup, true); SetLeftPanelTitleImmediate("Codex"); ShowEmptyDisplay(); return; } await FadeLeftPanelTitle("Codex"); await FadePanelOut(_locationListGroup); await FadePanelOut(_entryListGroup); await FadePanelIn(_categoriesGroup); ShowEmptyDisplay(); } private async UniTask NavigateToLocations(CodexType category) { ResetOperationCts(); var token = _operationCts.Token; try { _currentCategory = category; _currentLocation = Location.None; _currentEntry = null; await FadeLeftPanelTitle(GetCategoryDisplayName(category)); await FadeOutCurrentLeftPanel(); await BuildLocationButtons(token); token.ThrowIfCancellationRequested(); _leftPanelState = LeftPanelState.Locations; await RefreshLayout(_locationListContainer); RestoreLocationScroll(); await FadePanelIn(_locationListGroup); await FadeOutDisplay(); ShowEmptyDisplay(); } catch (OperationCanceledException) { } } private async UniTask NavigateToEntries(Location location) { ResetOperationCts(); var token = _operationCts.Token; try { SaveLocationScroll(); _currentLocation = location; _lastLocationByCategory[_currentCategory] = location; _currentEntry = null; await FadeLeftPanelTitle(GetLocationDisplayName(location)); await FadePanelOut(_locationListGroup); await BuildEntryButtons(token); token.ThrowIfCancellationRequested(); _leftPanelState = LeftPanelState.Entries; await RefreshLayout(_entryListContainer); RestoreEntryScroll(); await FadePanelIn(_entryListGroup); await FadeOutDisplay(); ShowEmptyDisplay(); } catch (OperationCanceledException) { } } private async UniTask NavigateBackToCategories() { ResetOperationCts(); try { await FadeOutDisplay(); SetEmptyStateVisible(false); await TransitionToCategories(instant: false); ReleaseAllLocationButtons(); } catch (OperationCanceledException) { } } private async UniTask NavigateBackToLocations() { ResetOperationCts(); var token = _operationCts.Token; try { SaveEntryScroll(); _currentEntry = null; await FadeLeftPanelTitle(GetCategoryDisplayName(_currentCategory)); await FadePanelOut(_entryListGroup); ReleaseAllEntryButtons(); token.ThrowIfCancellationRequested(); _leftPanelState = LeftPanelState.Locations; await RefreshLayout(_locationListContainer); RestoreLocationScroll(); await FadePanelIn(_locationListGroup); await FadeOutDisplay(); ShowEmptyDisplay(); } catch (OperationCanceledException) { } } private async UniTask FadeOutCurrentLeftPanel() { switch (_leftPanelState) { case LeftPanelState.Categories: await FadePanelOut(_categoriesGroup); break; case LeftPanelState.Locations: await FadePanelOut(_locationListGroup); break; case LeftPanelState.Entries: await FadePanelOut(_entryListGroup); break; } } // ── Left panel title ────────────────────────────────────────── private void SetLeftPanelTitleImmediate(string title) { if (_leftPanelTitleText != null) _leftPanelTitleText.text = title; if (_leftPanelTitleGroup != null) { _leftPanelTitleGroup.alpha = 1f; _leftPanelTitleGroup.interactable = true; _leftPanelTitleGroup.blocksRaycasts = true; } } private async UniTask FadeLeftPanelTitle(string newTitle) { if (_leftPanelTitleGroup == null) { if (_leftPanelTitleText != null) _leftPanelTitleText.text = newTitle; return; } StopLeftPanelTitleTween(); var token = _operationCts?.Token ?? this.GetCancellationTokenOnDestroy(); _leftPanelTitleSequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(_leftPanelTitleGroup, new TweenSettings { startValue = _leftPanelTitleGroup.alpha, endValue = 0f, settings = _leftPanelTitleFadeSettings })); try { await _leftPanelTitleSequence.ToUniTask(cancellationToken: token); } catch (OperationCanceledException) { return; } finally { _leftPanelTitleSequence = default; } if (_leftPanelTitleText != null) _leftPanelTitleText.text = newTitle; _leftPanelTitleSequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(_leftPanelTitleGroup, new TweenSettings { startValue = 0f, endValue = 1f, settings = _leftPanelTitleFadeSettings })); try { await _leftPanelTitleSequence.ToUniTask(cancellationToken: token); _leftPanelTitleGroup.alpha = 1f; } catch (OperationCanceledException) { } finally { _leftPanelTitleSequence = default; } } private static string GetCategoryDisplayName(CodexType category) => category switch { CodexType.DocumentEntry => "Documents", CodexType.PuzzleClue => "Clues", CodexType.Photo => "Photos", _ => string.Empty }; private static string GetLocationDisplayName(Location location) { return location.ToString().Prettify(); } // ── Display area ────────────────────────────────────────────── private async UniTaskVoid DisplayEntry(CodexEntrySo entry) { if (entry == null) return; ResetOperationCts(); var token = _operationCts.Token; try { if (_currentEntry != null && _rightPanelGroup.alpha > 0.001f) await FadeOutDisplay(); token.ThrowIfCancellationRequested(); _currentEntry = entry; _lastEntryByCategory[_currentCategory] = entry.UniqueID; UpdateEntryButtonSelection(); ApplyEntryToDisplay(entry); await RefreshLayout(_rightPanelRoot); SetTitleVisible(false); SetContentVisible(false); SetDisplayImmediate(true); await FadeInTitle(token); token.ThrowIfCancellationRequested(); await FadeInContent(token); } catch (OperationCanceledException) { } } private void ShowEmptyDisplay() { SetDisplayImmediate(false); ClearDisplay(); var hasEntries = _playerManager.AnyCodexEntriesForCategory(_currentCategory); SetEmptyStateVisible(!hasEntries); } private async UniTask FadeInTitle(CancellationToken token) { if (_titleGroup == null) return; _titleGroup.alpha = 0f; _titleGroup.interactable = true; _titleGroup.blocksRaycasts = true; await Tween.Alpha(_titleGroup, new TweenSettings { startValue = 0f, endValue = 1f, settings = _titleFadeSettings }).ToUniTask(cancellationToken: token); _titleGroup.alpha = 1f; } private async UniTask FadeInContent(CancellationToken token) { if (_displayAreaGroup == null) return; _displayAreaGroup.alpha = 0f; _displayAreaGroup.interactable = true; _displayAreaGroup.blocksRaycasts = true; await Tween.Alpha(_displayAreaGroup, new TweenSettings { startValue = 0f, endValue = 1f, settings = _contentFadeSettings }).ToUniTask(cancellationToken: token); _displayAreaGroup.alpha = 1f; } private async UniTask FadeOutDisplay() { if (_rightPanelGroup == null || _rightPanelGroup.alpha < 0.001f) return; StopDisplayTween(); _displaySequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(_rightPanelGroup, new TweenSettings { startValue = _rightPanelGroup.alpha, endValue = 0f, settings = _contentFadeSettings })); try { await _displaySequence.ToUniTask( cancellationToken: _operationCts?.Token ?? this.GetCancellationTokenOnDestroy()); } catch (OperationCanceledException) { return; } finally { _displaySequence = default; } SetDisplayImmediate(false); } private void SetTitleVisible(bool visible) { if (_titleGroup == null) return; _titleGroup.alpha = visible ? 1f : 0f; _titleGroup.interactable = visible; _titleGroup.blocksRaycasts = visible; } private void SetContentVisible(bool visible) { if (_displayAreaGroup == null) return; _displayAreaGroup.alpha = visible ? 1f : 0f; _displayAreaGroup.interactable = visible; _displayAreaGroup.blocksRaycasts = visible; } // ── Panel fade helpers ──────────────────────────────────────── private async UniTask FadePanelIn(CanvasGroup group) { if (group == null) return; StopLeftPanelTween(); group.alpha = 0f; group.blocksRaycasts = false; group.interactable = false; _leftPanelSequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(group, new TweenSettings { startValue = 0f, endValue = 1f, settings = _panelFadeSettings })); try { await _leftPanelSequence.ToUniTask( cancellationToken: _operationCts?.Token ?? this.GetCancellationTokenOnDestroy()); } catch (OperationCanceledException) { return; } finally { _leftPanelSequence = default; } group.alpha = 1f; group.blocksRaycasts = true; group.interactable = true; } private async UniTask FadePanelOut(CanvasGroup group) { if (group == null || group.alpha < 0.001f) { if (group != null) { group.alpha = 0f; group.interactable = false; group.blocksRaycasts = false; } return; } StopLeftPanelTween(); group.blocksRaycasts = false; group.interactable = false; _leftPanelSequence = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(group, new TweenSettings { startValue = group.alpha, endValue = 0f, settings = _panelFadeSettings })); try { await _leftPanelSequence.ToUniTask( cancellationToken: _operationCts?.Token ?? this.GetCancellationTokenOnDestroy()); } catch (OperationCanceledException) { return; } finally { _leftPanelSequence = default; } group.alpha = 0f; group.interactable = false; group.blocksRaycasts = false; } // ── Button building ─────────────────────────────────────────── private async UniTask BuildLocationButtons(CancellationToken token) { ReleaseAllLocationButtons(); if (_playerManager == null || _locationButtonPool == null) return; var locations = _playerManager .GetDiscoveredCodexEntriesByType(_currentCategory) .Where(e => e != null) .Select(e => e.Location) .Where(l => l != Location.None) .Distinct() .OrderBy(l => l.ToString()) .ToList(); foreach (var location in locations) { token.ThrowIfCancellationRequested(); var button = _locationButtonPool.Get(); if (button == null) continue; button.Initialize(location); button.OnLocationClicked += OnLocationClicked; _activeLocationButtons.Add(button); } await UniTask.Yield(cancellationToken: token); _locationScrollbar?.Rebuild(); } private async UniTask BuildEntryButtons(CancellationToken token) { ReleaseAllEntryButtons(); if (_playerManager == null || _currentLocation == Location.None || _entryButtonPool == null) return; var entries = _playerManager .GetDiscoveredCodexEntriesByType(_currentCategory) .Where(e => e != null && e.Location == _currentLocation) .OrderBy(e => e.Title) .ToList(); foreach (var entry in entries) { token.ThrowIfCancellationRequested(); var button = _entryButtonPool.Get(); if (button == null) continue; button.Initialize(entry); button.OnEntryClicked += OnEntryClicked; _activeEntryButtons.Add(button); if (_entryButtonGroup != null && button.UnderlineButton != null) _entryButtonGroup.AddButton(button.UnderlineButton); } await UniTask.Yield(cancellationToken: token); _entryScrollbar?.Rebuild(); } // ── Display content ─────────────────────────────────────────── private void ApplyEntryToDisplay(CodexEntrySo entry) { if (entry == null) { ClearDisplay(); return; } if (_titleText != null) _titleText.text = entry.Title; var isPhoto = entry.EntryType == CodexType.Photo || entry.IsPhotoOverride; var showImage = isPhoto && entry.DisplayImage != null; var showText = !string.IsNullOrWhiteSpace(entry.BodyText) || entry.IsBodyTextOverride; var showPolaroid = isPhoto && !string.IsNullOrWhiteSpace(entry.PolaroidWriting); var showPhotoDesc = isPhoto && !string.IsNullOrWhiteSpace(entry.PhotoDescription); if (_polaroid != null) _polaroid.gameObject.SetActive(isPhoto); if (_displayImage != null) { _displayImage.sprite = showImage ? entry.DisplayImage : null; _displayImage.enabled = showImage; _displayImage.gameObject.SetActive(showImage); } if (_polaroidWriting != null) { _polaroidWriting.text = showPolaroid ? entry.PolaroidWriting : string.Empty; _polaroidWriting.gameObject.SetActive(showPolaroid); } if (_photoDescription != null) { if (showPhotoDesc) { _photoDescription.gameObject.SetActive(true); _photoDescription.SetText(entry.PhotoDescription); _photoDescription.ScrollToTop(); } else { _photoDescription.Clear(); _photoDescription.gameObject.SetActive(false); } } if (_bodyText != null) { if (showText) { _bodyText.gameObject.SetActive(true); _bodyText.SetText(entry.BodyText); } else { _bodyText.Clear(); _bodyText.gameObject.SetActive(false); } } SetEmptyStateVisible(false); if (_displayAreaRoot != null) _displayAreaRoot.SetActive(true); } private void ClearDisplay() { if (_titleText != null) _titleText.text = string.Empty; if (_bodyText != null) { _bodyText.Clear(); _bodyText.gameObject.SetActive(false); } if (_photoDescription != null) { _photoDescription.Clear(); _photoDescription.gameObject.SetActive(false); } if (_polaroid != null) _polaroid.gameObject.SetActive(false); if (_displayImage != null) { _displayImage.sprite = null; _displayImage.enabled = false; _displayImage.gameObject.SetActive(false); } if (_polaroidWriting != null) { _polaroidWriting.text = string.Empty; _polaroidWriting.gameObject.SetActive(false); } } // ── Selection state ─────────────────────────────────────────── 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); if (_entryButtonGroup != null && button.UnderlineButton != null && isSelected) _entryButtonGroup.SelectButton(button.UnderlineButton); } } // ── Scroll position ─────────────────────────────────────────── private void SaveLocationScroll() { if (_locationScrollbar != null) _locationScrollByCategory[_currentCategory] = _locationScrollbar.Normalized; } private void SaveEntryScroll() { if (_entryScrollbar != null && _currentLocation != Location.None) _entryScrollByCategoryAndLocation[ScrollKey()] = _entryScrollbar.Normalized; } private void RestoreLocationScroll() { if (_locationScrollbar == null) return; _locationScrollbar.Rebuild(); _locationScrollbar.SetNormalized( _locationScrollByCategory.TryGetValue(_currentCategory, out var v) ? v : 1f); } private void RestoreEntryScroll() { if (_entryScrollbar == null || _currentLocation == Location.None) return; _entryScrollbar.Rebuild(); _entryScrollbar.SetNormalized( _entryScrollByCategoryAndLocation.TryGetValue(ScrollKey(), out var v) ? v : 1f); } private string ScrollKey() => $"{_currentCategory}_{_currentLocation}"; // ── Pool release helpers ────────────────────────────────────── private void ReleaseAllLocationButtons() { foreach (var button in _activeLocationButtons) { if (button == null) continue; _locationButtonPool?.Release(button); } _activeLocationButtons.Clear(); _locationScrollbar?.Rebuild(); } private void ReleaseAllEntryButtons() { foreach (var button in _activeEntryButtons) { if (button == null) continue; _entryButtonPool?.Release(button); } _activeEntryButtons.Clear(); _entryScrollbar?.Rebuild(); } // ── Binding ─────────────────────────────────────────────────── private void BindCategoryButtons() { if (_booksButton != null) _booksButton.SelectionRequested += OnDocumentsClicked; if (_cluesButton != null) _cluesButton.SelectionRequested += OnCluesClicked; if (_photosButton != null) _photosButton.SelectionRequested += OnPhotosClicked; } private void UnbindCategoryButtons() { if (_booksButton != null) _booksButton.SelectionRequested -= OnDocumentsClicked; if (_cluesButton != null) _cluesButton.SelectionRequested -= OnCluesClicked; if (_photosButton != null) _photosButton.SelectionRequested -= OnPhotosClicked; } private void BindNavButtons() { if (_backToCategoriesButton != null) _backToCategoriesButton.onClick.AddListener(OnBackToCategoriesClicked); if (_backToLocationsButton != null) _backToLocationsButton.onClick.AddListener(OnBackToLocationsClicked); } private void UnbindNavButtons() { if (_backToCategoriesButton != null) _backToCategoriesButton.onClick.RemoveListener(OnBackToCategoriesClicked); if (_backToLocationsButton != null) _backToLocationsButton.onClick.RemoveListener(OnBackToLocationsClicked); } // ── Layout ──────────────────────────────────────────────────── private async UniTask RefreshLayout(RectTransform root) { Canvas.ForceUpdateCanvases(); if (root != null) LayoutRebuilder.ForceRebuildLayoutImmediate(root); await UniTask.Yield(PlayerLoopTiming.LastPostLateUpdate); Canvas.ForceUpdateCanvases(); if (root != null) LayoutRebuilder.ForceRebuildLayoutImmediate(root); } // ── Addressable cache ───────────────────────────────────────── private void CacheButtonReferences() { if (_cached || _assetRegistry == null) return; if (!_assetRegistry.TryGetReference( AssetKeyIdentifiers.Get(UIKey.CodexLocationButton), out _locationButtonReference)) Debug.LogError("[CodexWindow] Failed to resolve location button reference."); if (!_assetRegistry.TryGetReference( AssetKeyIdentifiers.Get(UIKey.CodexEntryButton), out _entryButtonReference)) Debug.LogError("[CodexWindow] Failed to resolve entry button reference."); _cached = true; } // ── Immediate state helpers ─────────────────────────────────── private static void SetPanelImmediate(CanvasGroup group, bool visible) { if (group == null) return; group.alpha = visible ? 1f : 0f; group.interactable = visible; group.blocksRaycasts = visible; } private static void SetCanvasGroupImmediate(CanvasGroup group, float alpha, bool interactable) { if (group == null) return; group.alpha = alpha; group.interactable = interactable; group.blocksRaycasts = interactable; } private static void SetCanvasGroupInteractivity(CanvasGroup group, bool interactable) { if (group == null) return; group.interactable = interactable; group.blocksRaycasts = interactable; } private void SetDisplayImmediate(bool visible) { if (_rightPanelGroup == null) return; _rightPanelGroup.alpha = visible ? 1f : 0f; _rightPanelGroup.interactable = visible; _rightPanelGroup.blocksRaycasts = visible; } private void SetEmptyStateVisible(bool visible) { if (_emptyStateRoot != null) _emptyStateRoot.SetActive(visible); if (_displayAreaRoot != null) _displayAreaRoot.SetActive(!visible); } // ── CTS helpers ─────────────────────────────────────────────── private void CancelAllOperations() { if (_windowSequence.isAlive) _windowSequence.Stop(); StopLeftPanelTween(); StopDisplayTween(); StopLeftPanelTitleTween(); if (_operationCts == null) return; try { _operationCts.Cancel(); } catch { } _operationCts.Dispose(); _operationCts = null; } private void ResetOperationCts() { CancelAllOperations(); _operationCts = new CancellationTokenSource(); } private void StopLeftPanelTween() { if (_leftPanelSequence.isAlive) _leftPanelSequence.Stop(); _leftPanelSequence = default; } private void StopDisplayTween() { if (_displaySequence.isAlive) _displaySequence.Stop(); _displaySequence = default; } private void StopLeftPanelTitleTween() { if (_leftPanelTitleSequence.isAlive) _leftPanelTitleSequence.Stop(); _leftPanelTitleSequence = default; } private enum LeftPanelState { Categories, Locations, Entries } } }