Files
A-Fairytale-Gone-Bad-Briar-…/Assets/Scripts/UI/Codex/CodexWindow.cs
2026-05-13 20:29:56 +01:00

1279 lines
48 KiB
C#

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<CodexEntryButton> _activeEntryButtons = new();
private readonly List<CodexLocationButton> _activeLocationButtons = new();
private readonly Dictionary<string, float> _entryScrollByCategoryAndLocation = new();
private readonly Dictionary<CodexType, string> _lastEntryByCategory = new();
private readonly Dictionary<CodexType, Location> _lastLocationByCategory = new();
private readonly Dictionary<CodexType, float> _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<CodexEntryButton> _entryButtonPool;
private AssetReference _entryButtonReference;
private CodexEntryButton _entryButtonTemplate;
private InteractManager _interactManager;
private Sequence _leftPanelSequence;
private LeftPanelState _leftPanelState = LeftPanelState.Categories;
private Sequence _leftPanelTitleSequence;
private ObjectPool<CodexLocationButton> _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<UnityEngine.EventSystems.RaycastResult>();
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<float>
{
startValue = 0f,
endValue = 1f,
settings = _windowTweenSettings
}))
.Group(Tween.Scale(_windowRect, new TweenSettings<Vector3>
{
startValue = Vector3.one * _hiddenScale,
endValue = Vector3.one,
settings = _windowTweenSettings
}));
try
{
var canvasTask = Tween.Alpha(_canvasGroup, new TweenSettings<float>
{
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 = false;
_canvasGroup.interactable = false;
}
TryUnregisterRaycaster();
_windowSequence = Sequence.Create(useUnscaledTime: true)
.Group(Tween.Alpha(_backgroundGroup, new TweenSettings<float>
{
startValue = _backgroundGroup != null ? _backgroundGroup.alpha : 1f,
endValue = 0f,
settings = _windowTweenSettings
}))
.Group(Tween.Scale(_windowRect, new TweenSettings<Vector3>
{
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;
}
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<CodexLocationButton>();
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<CodexEntryButton>();
if (_entryButtonTemplate != null)
_entryButtonTemplate.gameObject.SetActive(false);
else
await _destructionService.Destroy(instance);
}
}
if (_locationButtonPool == null && _locationButtonTemplate != null)
{
_locationButtonPool = new ObjectPool<CodexLocationButton>(
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<CodexEntryButton>(
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<float>
{
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<float>
{
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<float>
{
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<float>
{
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<float>
{
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<float>
{
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<float>
{
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 }
}
}