671 lines
21 KiB
C#
671 lines
21 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using BriarQueen.Data.Identifiers;
|
|
using BriarQueen.Framework.Coordinators.Events;
|
|
using BriarQueen.Framework.Events.UI;
|
|
using BriarQueen.Framework.Extensions;
|
|
using BriarQueen.Framework.Managers.Interaction;
|
|
using BriarQueen.Framework.Managers.IO;
|
|
using BriarQueen.Framework.Managers.Player;
|
|
using BriarQueen.Framework.Managers.UI.Base;
|
|
using BriarQueen.Framework.Managers.UI.Events;
|
|
using BriarQueen.Framework.Services.Settings;
|
|
using Cysharp.Threading.Tasks;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using VContainer;
|
|
|
|
namespace BriarQueen.Framework.Managers.UI
|
|
{
|
|
/// <summary>
|
|
/// UIManager:
|
|
/// - Modal windows use the window stack
|
|
/// - Non-modal UI (popups / fader) does not use the stack
|
|
/// - HUD visibility is event-driven
|
|
/// - Concrete UI implementations are hidden behind interfaces
|
|
/// </summary>
|
|
public class UIManager : IDisposable, IManager
|
|
{
|
|
private readonly EventCoordinator _eventCoordinator;
|
|
private readonly InteractManager _interactManager;
|
|
private readonly SaveManager _saveManager;
|
|
private readonly SettingsService _settingsService;
|
|
private readonly PlayerManager _playerManager;
|
|
|
|
private readonly Dictionary<WindowType, IUIWindow> _windows = new();
|
|
private readonly Stack<IUIWindow> _windowStack = new();
|
|
private readonly SemaphoreSlim _windowTransitionGate = new(1, 1);
|
|
|
|
private bool _disposed;
|
|
|
|
public bool Initialized { get; private set; }
|
|
|
|
private sealed record OverlayResumeContext(WindowType OverlayWindowType, IUIOverlayHost Host);
|
|
|
|
private IHud _hudContainer;
|
|
private IPopup _infoPopup;
|
|
private IPopup _tutorialPopup;
|
|
private IScreenFader _screenFader;
|
|
private IUIOverlayHost _mainMenuOverlayHost;
|
|
private readonly Stack<OverlayResumeContext> _overlayResumeStack = new();
|
|
|
|
[Inject]
|
|
public UIManager(
|
|
EventCoordinator eventCoordinator,
|
|
InteractManager interactManager,
|
|
SettingsService settingsService,
|
|
SaveManager saveManager,
|
|
PlayerManager playerManager)
|
|
{
|
|
_eventCoordinator = eventCoordinator;
|
|
_interactManager = interactManager;
|
|
_settingsService = settingsService;
|
|
_saveManager = saveManager;
|
|
_playerManager = playerManager;
|
|
}
|
|
|
|
private IUIWindow ActiveWindow => _windowStack.Count > 0 ? _windowStack.Peek() : null;
|
|
|
|
public bool IsAnyUIOpen => _windowStack.Count > 0;
|
|
|
|
public void Initialize()
|
|
{
|
|
if (Initialized)
|
|
return;
|
|
|
|
_disposed = false;
|
|
SubscribeToEvents();
|
|
Initialized = true;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (!Initialized || _disposed)
|
|
return;
|
|
|
|
_disposed = true;
|
|
|
|
UnsubscribeFromEvents();
|
|
ResetUIStateHard();
|
|
|
|
Initialized = false;
|
|
}
|
|
|
|
private void SubscribeToEvents()
|
|
{
|
|
_eventCoordinator.Subscribe<PauseButtonClickedEvent>(OnPauseClickReceived);
|
|
_eventCoordinator.Subscribe<UIBackRequestedEvent>(OnBackRequested);
|
|
_eventCoordinator.Subscribe<ToggleCodexEvent>(ToggleCodexWindow);
|
|
_eventCoordinator.Subscribe<UIToggleSettingsWindow>(ToggleSettingsWindow);
|
|
_eventCoordinator.Subscribe<FadeEvent>(OnFadeEvent);
|
|
_eventCoordinator.Subscribe<DisplayInteractEvent>(OnDisplayInteractText);
|
|
_eventCoordinator.Subscribe<DisplayTutorialPopupEvent>(OnTutorialDisplayPopup);
|
|
_eventCoordinator.Subscribe<CodexChangedEvent>(OnCodexChangedEvent);
|
|
_eventCoordinator.Subscribe<UIToggleHudEvent>(OnHudToggleEvent);
|
|
_eventCoordinator.Subscribe<ToolbeltChangedEvent>(OnToolbeltChangedEvent);
|
|
}
|
|
|
|
private void UnsubscribeFromEvents()
|
|
{
|
|
_eventCoordinator.Unsubscribe<PauseButtonClickedEvent>(OnPauseClickReceived);
|
|
_eventCoordinator.Unsubscribe<UIBackRequestedEvent>(OnBackRequested);
|
|
_eventCoordinator.Unsubscribe<ToggleCodexEvent>(ToggleCodexWindow);
|
|
_eventCoordinator.Unsubscribe<UIToggleSettingsWindow>(ToggleSettingsWindow);
|
|
_eventCoordinator.Unsubscribe<FadeEvent>(OnFadeEvent);
|
|
_eventCoordinator.Unsubscribe<DisplayInteractEvent>(OnDisplayInteractText);
|
|
_eventCoordinator.Unsubscribe<DisplayTutorialPopupEvent>(OnTutorialDisplayPopup);
|
|
_eventCoordinator.Unsubscribe<CodexChangedEvent>(OnCodexChangedEvent);
|
|
_eventCoordinator.Unsubscribe<UIToggleHudEvent>(OnHudToggleEvent);
|
|
_eventCoordinator.Unsubscribe<ToolbeltChangedEvent>(OnToolbeltChangedEvent);
|
|
}
|
|
|
|
public void RegisterWindow(IUIWindow window)
|
|
{
|
|
if (window == null)
|
|
return;
|
|
|
|
_windows[window.WindowType] = window;
|
|
window.Hide().Forget();
|
|
}
|
|
|
|
public void UnregisterWindow(IUIWindow window)
|
|
{
|
|
if (window == null)
|
|
return;
|
|
|
|
if (_windows.TryGetValue(window.WindowType, out var registered) && ReferenceEquals(registered, window))
|
|
_windows.Remove(window.WindowType);
|
|
|
|
if (window is IUIOverlayHost overlayHost)
|
|
RemoveOverlayResumeContextsForHost(overlayHost);
|
|
}
|
|
|
|
public void RegisterHUD(IHud hudContainer)
|
|
{
|
|
_hudContainer = hudContainer;
|
|
|
|
if (_hudContainer != null)
|
|
{
|
|
_hudContainer.Hide().Forget();
|
|
_interactManager.AddUIRaycaster(_hudContainer.Raycaster);
|
|
}
|
|
}
|
|
|
|
public void RegisterInfoPopup(IPopup infoPopup)
|
|
{
|
|
_infoPopup = infoPopup;
|
|
|
|
if (_infoPopup != null)
|
|
_infoPopup.Hide().Forget();
|
|
}
|
|
|
|
public void RegisterTutorialPopup(IPopup tutorialPopup)
|
|
{
|
|
_tutorialPopup = tutorialPopup;
|
|
|
|
if (_tutorialPopup != null)
|
|
_tutorialPopup.Hide().Forget();
|
|
}
|
|
|
|
public void RegisterScreenFader(IScreenFader screenFader)
|
|
{
|
|
_screenFader = screenFader;
|
|
}
|
|
|
|
public void RegisterMainMenuOverlayHost(IUIOverlayHost host)
|
|
{
|
|
_mainMenuOverlayHost = host;
|
|
}
|
|
|
|
public void UnregisterMainMenuOverlayHost(IUIOverlayHost host)
|
|
{
|
|
if (!ReferenceEquals(_mainMenuOverlayHost, host))
|
|
return;
|
|
|
|
RemoveOverlayResumeContextsForHost(host);
|
|
_mainMenuOverlayHost = null;
|
|
}
|
|
|
|
private IUIWindow GetWindow(WindowType windowType)
|
|
{
|
|
return _windows.TryGetValue(windowType, out var window) ? window : null;
|
|
}
|
|
|
|
public bool IsWindowOpen(WindowType windowType)
|
|
{
|
|
var target = GetWindow(windowType);
|
|
return target != null && _windowStack.Contains(target);
|
|
}
|
|
|
|
private void RemoveOverlayResumeContextsForHost(IUIOverlayHost host)
|
|
{
|
|
if (host == null || _overlayResumeStack.Count == 0)
|
|
return;
|
|
|
|
var contextsToKeep = new List<OverlayResumeContext>();
|
|
foreach (var context in _overlayResumeStack)
|
|
{
|
|
if (!ReferenceEquals(context.Host, host))
|
|
contextsToKeep.Add(context);
|
|
}
|
|
|
|
_overlayResumeStack.Clear();
|
|
|
|
for (var i = contextsToKeep.Count - 1; i >= 0; i--)
|
|
_overlayResumeStack.Push(contextsToKeep[i]);
|
|
}
|
|
|
|
private async UniTask<bool> TrySuspendActiveWindowFor(WindowType incomingWindowType)
|
|
{
|
|
if (ActiveWindow is IUIOverlayHost overlayHost &&
|
|
overlayHost.CanSuspendFor(incomingWindowType))
|
|
{
|
|
await overlayHost.SuspendForOverlay();
|
|
_overlayResumeStack.Push(new OverlayResumeContext(incomingWindowType, overlayHost));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private async UniTask RestoreUnderlyingUi(WindowType closedWindowType)
|
|
{
|
|
if (_overlayResumeStack.Count > 0 &&
|
|
_overlayResumeStack.Peek().OverlayWindowType == closedWindowType)
|
|
{
|
|
var resumeContext = _overlayResumeStack.Pop();
|
|
await resumeContext.Host.ResumeFromOverlay();
|
|
return;
|
|
}
|
|
|
|
if (ActiveWindow != null)
|
|
await ActiveWindow.Show();
|
|
}
|
|
|
|
private async UniTask ApplyHudVisibility(bool visible)
|
|
{
|
|
if (_disposed || _hudContainer == null)
|
|
return;
|
|
|
|
try
|
|
{
|
|
if (visible)
|
|
await _hudContainer.Show();
|
|
else
|
|
await _hudContainer.Hide();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogError($"[UIManager] ApplyHudVisibility error: {ex}");
|
|
}
|
|
}
|
|
|
|
private void OnPauseClickReceived(PauseButtonClickedEvent _)
|
|
{
|
|
if (ActiveWindow == null)
|
|
{
|
|
OpenWindow(WindowType.PauseMenuWindow);
|
|
return;
|
|
}
|
|
|
|
if (ActiveWindow.WindowType == WindowType.PauseMenuWindow)
|
|
{
|
|
TryHandleBackRequest();
|
|
return;
|
|
}
|
|
|
|
if (ActiveWindow.PauseBehavior == UIPauseBehavior.OpenPauseOverlay)
|
|
{
|
|
OpenWindow(WindowType.PauseMenuWindow);
|
|
return;
|
|
}
|
|
|
|
TryHandleBackRequest();
|
|
}
|
|
|
|
private void OnBackRequested(UIBackRequestedEvent _)
|
|
{
|
|
TryHandleBackRequest();
|
|
}
|
|
|
|
private void ToggleSettingsWindow(UIToggleSettingsWindow eventData)
|
|
{
|
|
if (eventData.Show)
|
|
OpenSettingsWindow(eventData.Source).Forget();
|
|
else
|
|
CloseWindow(WindowType.SettingsWindow);
|
|
}
|
|
|
|
private void ToggleCodexWindow(ToggleCodexEvent eventData)
|
|
{
|
|
if(!_playerManager.CodexUnlocked())
|
|
return;
|
|
|
|
if (eventData.Shown)
|
|
{
|
|
if (!IsWindowOpen(WindowType.CodexWindow))
|
|
OpenWindow(WindowType.CodexWindow);
|
|
}
|
|
else
|
|
{
|
|
if (IsWindowOpen(WindowType.CodexWindow))
|
|
CloseWindow(WindowType.CodexWindow);
|
|
}
|
|
}
|
|
|
|
private void OnCodexChangedEvent(CodexChangedEvent eventData)
|
|
{
|
|
if (_infoPopup == null)
|
|
return;
|
|
|
|
var duration = _settingsService?.Game?.PopupDisplayDuration ?? 3f;
|
|
var codexText = GetCodexTextForEntry(eventData.EntryType);
|
|
|
|
_infoPopup.Play(codexText, duration).Forget();
|
|
}
|
|
|
|
private void OnToolbeltChangedEvent(ToolbeltChangedEvent eventData)
|
|
{
|
|
if (_infoPopup == null)
|
|
return;
|
|
|
|
var duration = _settingsService?.Game?.PopupDisplayDuration ?? 3f;
|
|
var toolText = GetToolbeltTextForEntry(eventData.ToolID, eventData.Lost);
|
|
|
|
_infoPopup.Play(toolText, duration).Forget();
|
|
}
|
|
|
|
private string GetToolbeltTextForEntry(ToolID toolID, bool lost)
|
|
{
|
|
if (lost)
|
|
return $"You lost the {toolID.GetDisplayName()}.";
|
|
else
|
|
return $"You gained the {toolID.GetDisplayName()}.";
|
|
}
|
|
private string GetCodexTextForEntry(CodexType codexType)
|
|
{
|
|
return codexType switch
|
|
{
|
|
CodexType.DocumentEntry => "You've acquired a new document.",
|
|
CodexType.PuzzleClue => "You've acquired a new puzzle clue.",
|
|
CodexType.Photo => "You've acquired a new photo.",
|
|
_ => string.Empty
|
|
};
|
|
}
|
|
|
|
private void OnTutorialDisplayPopup(DisplayTutorialPopupEvent eventData)
|
|
{
|
|
if (_tutorialPopup == null)
|
|
return;
|
|
|
|
if (!_settingsService.AreTutorialsEnabled())
|
|
return;
|
|
|
|
if (string.IsNullOrWhiteSpace(eventData.ResolvedText))
|
|
{
|
|
Debug.LogWarning($"[UIManager] Empty resolved text for tutorial '{eventData.TutorialID}'.");
|
|
return;
|
|
}
|
|
|
|
var duration = _settingsService?.Game?.PopupDisplayDuration ?? 3f;
|
|
_tutorialPopup.Play(eventData.ResolvedText, duration).Forget();
|
|
}
|
|
|
|
private void OnDisplayInteractText(DisplayInteractEvent eventData)
|
|
{
|
|
if (_hudContainer == null)
|
|
return;
|
|
|
|
_hudContainer.DisplayInteractText(eventData.Message).Forget();
|
|
}
|
|
|
|
private void OnFadeEvent(FadeEvent eventData)
|
|
{
|
|
if (_screenFader == null)
|
|
return;
|
|
|
|
if (eventData.Hidden)
|
|
_screenFader.FadeFromAsync(eventData.Duration).Forget();
|
|
else
|
|
_screenFader.FadeToAsync(eventData.Duration).Forget();
|
|
}
|
|
|
|
private void OnHudToggleEvent(UIToggleHudEvent eventData)
|
|
{
|
|
ApplyHudVisibility(eventData.Show).Forget();
|
|
}
|
|
|
|
public void OpenWindow(WindowType windowType)
|
|
{
|
|
OpenWindowInternal(windowType).Forget();
|
|
}
|
|
|
|
private async UniTask OpenSettingsWindow(SettingsOpenSource source)
|
|
{
|
|
await _windowTransitionGate.WaitAsync();
|
|
try
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
var window = GetWindow(WindowType.SettingsWindow);
|
|
if (window == null)
|
|
{
|
|
Debug.LogError("[UIManager] Window of type SettingsWindow not registered.");
|
|
return;
|
|
}
|
|
|
|
if (_windowStack.Contains(window))
|
|
return;
|
|
|
|
var suspended = false;
|
|
|
|
if (source == SettingsOpenSource.MainMenu &&
|
|
_mainMenuOverlayHost != null &&
|
|
_mainMenuOverlayHost.CanSuspendFor(WindowType.SettingsWindow))
|
|
{
|
|
await _mainMenuOverlayHost.SuspendForOverlay();
|
|
_overlayResumeStack.Push(new OverlayResumeContext(WindowType.SettingsWindow, _mainMenuOverlayHost));
|
|
suspended = true;
|
|
}
|
|
else
|
|
{
|
|
suspended = await TrySuspendActiveWindowFor(WindowType.SettingsWindow);
|
|
}
|
|
|
|
if (!suspended && ActiveWindow != null)
|
|
{
|
|
await ActiveWindow.Hide();
|
|
}
|
|
|
|
_windowStack.Push(window);
|
|
await window.Show();
|
|
|
|
NotifyWindowStateChanged(window.WindowType, true);
|
|
NotifyUIStackChanged();
|
|
}
|
|
finally
|
|
{
|
|
_windowTransitionGate.Release();
|
|
}
|
|
}
|
|
|
|
private async UniTask OpenWindowInternal(WindowType windowType)
|
|
{
|
|
await _windowTransitionGate.WaitAsync();
|
|
try
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
var window = GetWindow(windowType);
|
|
|
|
if (window == null)
|
|
{
|
|
Debug.LogError($"[UIManager] Window of type {windowType} not registered.");
|
|
return;
|
|
}
|
|
|
|
if (_windowStack.Contains(window))
|
|
return;
|
|
|
|
var suspended = await TrySuspendActiveWindowFor(windowType);
|
|
|
|
if (!suspended && ActiveWindow != null)
|
|
await ActiveWindow.Hide();
|
|
|
|
_windowStack.Push(window);
|
|
await window.Show();
|
|
|
|
NotifyWindowStateChanged(window.WindowType, true);
|
|
NotifyUIStackChanged();
|
|
}
|
|
finally
|
|
{
|
|
_windowTransitionGate.Release();
|
|
}
|
|
}
|
|
|
|
public void CloseWindow(WindowType windowType)
|
|
{
|
|
CloseWindowInternal(windowType).Forget();
|
|
}
|
|
|
|
private async UniTask CloseWindowInternal(WindowType windowType)
|
|
{
|
|
await _windowTransitionGate.WaitAsync();
|
|
try
|
|
{
|
|
if (_disposed || _windowStack.Count == 0)
|
|
return;
|
|
|
|
var target = GetWindow(windowType);
|
|
if (target == null || !_windowStack.Contains(target))
|
|
return;
|
|
|
|
while (_windowStack.Count > 0)
|
|
{
|
|
var current = _windowStack.Pop();
|
|
if (current != null)
|
|
{
|
|
await current.Hide();
|
|
NotifyWindowStateChanged(current.WindowType, false);
|
|
}
|
|
|
|
if (current == target)
|
|
break;
|
|
}
|
|
|
|
await RestoreUnderlyingUi(target.WindowType);
|
|
|
|
NotifyUIStackChanged();
|
|
}
|
|
finally
|
|
{
|
|
_windowTransitionGate.Release();
|
|
}
|
|
}
|
|
|
|
public void CloseTopWindow()
|
|
{
|
|
CloseTopWindowInternal().Forget();
|
|
}
|
|
|
|
private void TryHandleBackRequest()
|
|
{
|
|
if (_disposed || _windowStack.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ActiveWindow is IUIBackHandler backHandler && backHandler.HandleBackRequest())
|
|
{
|
|
return;
|
|
}
|
|
|
|
CloseTopWindow();
|
|
}
|
|
|
|
private async UniTask CloseTopWindowInternal()
|
|
{
|
|
await _windowTransitionGate.WaitAsync();
|
|
try
|
|
{
|
|
if (_disposed || _windowStack.Count == 0)
|
|
return;
|
|
|
|
var top = _windowStack.Pop();
|
|
|
|
if (top != null)
|
|
{
|
|
await top.Hide();
|
|
NotifyWindowStateChanged(top.WindowType, false);
|
|
}
|
|
|
|
if (top != null)
|
|
await RestoreUnderlyingUi(top.WindowType);
|
|
|
|
NotifyUIStackChanged();
|
|
}
|
|
finally
|
|
{
|
|
_windowTransitionGate.Release();
|
|
}
|
|
}
|
|
|
|
public void ResetUIState()
|
|
{
|
|
ResetUIStateAsync().Forget();
|
|
}
|
|
|
|
public async UniTask ResetUIStateAsync()
|
|
{
|
|
await _windowTransitionGate.WaitAsync();
|
|
try
|
|
{
|
|
while (_windowStack.Count > 0)
|
|
{
|
|
var window = _windowStack.Pop();
|
|
if (window == null)
|
|
continue;
|
|
|
|
try
|
|
{
|
|
await window.Hide();
|
|
NotifyWindowStateChanged(window.WindowType, false);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
_overlayResumeStack.Clear();
|
|
|
|
if (_tutorialPopup != null)
|
|
{
|
|
try
|
|
{
|
|
await _tutorialPopup.Hide();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
if (_infoPopup != null)
|
|
{
|
|
try
|
|
{
|
|
await _infoPopup.Hide();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
NotifyUIStackChanged();
|
|
}
|
|
finally
|
|
{
|
|
_windowTransitionGate.Release();
|
|
}
|
|
}
|
|
|
|
private void ResetUIStateHard()
|
|
{
|
|
foreach (var kv in _windows)
|
|
{
|
|
if (kv.Value is Component component && component != null)
|
|
component.gameObject.SetActive(false);
|
|
}
|
|
|
|
if (_tutorialPopup?.GameObject != null)
|
|
_tutorialPopup.GameObject.SetActive(false);
|
|
|
|
if (_infoPopup?.GameObject != null)
|
|
_infoPopup.GameObject.SetActive(false);
|
|
|
|
if (_hudContainer is Component hudComponent && hudComponent != null)
|
|
hudComponent.gameObject.SetActive(false);
|
|
|
|
if (_screenFader is Component faderComponent && faderComponent != null)
|
|
faderComponent.gameObject.SetActive(false);
|
|
|
|
_windowStack.Clear();
|
|
_overlayResumeStack.Clear();
|
|
_mainMenuOverlayHost = null;
|
|
}
|
|
|
|
private void NotifyUIStackChanged()
|
|
{
|
|
_eventCoordinator.Publish(new UIStackChangedEvent(_windowStack.Count > 0));
|
|
}
|
|
|
|
private void NotifyWindowStateChanged(WindowType windowType, bool isOpen)
|
|
{
|
|
_eventCoordinator.Publish(new UIWindowStateChangedEvent(windowType, isOpen));
|
|
}
|
|
}
|
|
}
|