1285 lines
48 KiB
C#
1285 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;
|
|
public UIPauseBehavior PauseBehavior => UIPauseBehavior.TreatAsBackRequest;
|
|
|
|
// ── IUIWindow ─────────────────────────────────────────────────
|
|
|
|
public async UniTask Show()
|
|
{
|
|
ResetOperationCts();
|
|
|
|
gameObject.SetActive(true);
|
|
EnsureExclusiveRaycaster();
|
|
|
|
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 = true;
|
|
_canvasGroup.interactable = false;
|
|
}
|
|
|
|
_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;
|
|
}
|
|
|
|
TryUnregisterRaycaster();
|
|
gameObject.SetActive(false);
|
|
Debug.Log($"[CodexWindow] Codex Window Hide Complete.");
|
|
}
|
|
|
|
// ── Raycaster ─────────────────────────────────────────────────
|
|
|
|
private void EnsureExclusiveRaycaster()
|
|
{
|
|
Debug.Log($"[CodexWindow] TryRegisterRaycaster " +
|
|
|
|
$"registered={_raycasterRegistered} " +
|
|
|
|
$"interactManager={_interactManager != null} " +
|
|
|
|
$"raycaster={_graphicRaycaster != null}");
|
|
|
|
Debug.Log("[CodexWindow] Try register raycaster.");
|
|
|
|
if (_interactManager == null || _graphicRaycaster == null) return;
|
|
|
|
if (!_raycasterRegistered)
|
|
{
|
|
_interactManager.AddUIRaycaster(_graphicRaycaster);
|
|
_raycasterRegistered = true;
|
|
}
|
|
|
|
_interactManager.SetExclusiveRaycaster(_graphicRaycaster);
|
|
|
|
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.ReleaseExclusiveRaycaster(_graphicRaycaster);
|
|
_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 }
|
|
}
|
|
}
|