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 { /// /// 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 /// 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 _windows = new(); private readonly Stack _windowStack = new(); private readonly SemaphoreSlim _windowTransitionGate = new(1, 1); private bool _disposed; public bool Initialized { get; private set; } private IHud _hudContainer; private IPopup _infoPopup; private IPopup _tutorialPopup; private IScreenFader _screenFader; private IUIOverlayHost _mainMenuOverlayHost; private IUIOverlayHost _activeSettingsOverlayHost; [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(OnPauseClickReceived); _eventCoordinator.Subscribe(OnBackRequested); _eventCoordinator.Subscribe(ToggleCodexWindow); _eventCoordinator.Subscribe(ToggleSettingsWindow); _eventCoordinator.Subscribe(OnFadeEvent); _eventCoordinator.Subscribe(OnDisplayInteractText); _eventCoordinator.Subscribe(OnTutorialDisplayPopup); _eventCoordinator.Subscribe(OnCodexChangedEvent); _eventCoordinator.Subscribe(OnHudToggleEvent); _eventCoordinator.Subscribe(OnToolbeltChangedEvent); } private void UnsubscribeFromEvents() { _eventCoordinator.Unsubscribe(OnPauseClickReceived); _eventCoordinator.Unsubscribe(OnBackRequested); _eventCoordinator.Unsubscribe(ToggleCodexWindow); _eventCoordinator.Unsubscribe(ToggleSettingsWindow); _eventCoordinator.Unsubscribe(OnFadeEvent); _eventCoordinator.Unsubscribe(OnDisplayInteractText); _eventCoordinator.Unsubscribe(OnTutorialDisplayPopup); _eventCoordinator.Unsubscribe(OnCodexChangedEvent); _eventCoordinator.Unsubscribe(OnHudToggleEvent); _eventCoordinator.Unsubscribe(OnToolbeltChangedEvent); } public void RegisterWindow(IUIWindow window) { if (window == null) return; _windows[window.WindowType] = window; window.Hide().Forget(); } 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; if (ReferenceEquals(_activeSettingsOverlayHost, host)) _activeSettingsOverlayHost = null; _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 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 (_windowStack.Count > 0) { TryHandleBackRequest(); return; } OpenWindow(WindowType.PauseMenuWindow); } 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; _activeSettingsOverlayHost = null; var openingSettingsOverPause = source == SettingsOpenSource.PauseMenu && ActiveWindow?.WindowType == WindowType.PauseMenuWindow && ActiveWindow is IUIOverlayHost; var openingSettingsOverMainMenu = source == SettingsOpenSource.MainMenu && _mainMenuOverlayHost != null; if (openingSettingsOverPause) { _activeSettingsOverlayHost = (IUIOverlayHost)ActiveWindow; await _activeSettingsOverlayHost.SuspendForOverlay(); } else if (openingSettingsOverMainMenu) { _activeSettingsOverlayHost = _mainMenuOverlayHost; await _activeSettingsOverlayHost.SuspendForOverlay(); } else if (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; if (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; } if (target.WindowType == WindowType.SettingsWindow && _activeSettingsOverlayHost != null) { await _activeSettingsOverlayHost.ResumeFromOverlay(); _activeSettingsOverlayHost = null; } else if (ActiveWindow != null) { await ActiveWindow.Show(); } 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 && top.WindowType == WindowType.SettingsWindow && _activeSettingsOverlayHost != null) { await _activeSettingsOverlayHost.ResumeFromOverlay(); _activeSettingsOverlayHost = null; } else if (ActiveWindow != null) { await ActiveWindow.Show(); } NotifyUIStackChanged(); } finally { _windowTransitionGate.Release(); } } public void ResetUIState() { ResetUIStateAsync().Forget(); } public async UniTask ResetUIStateAsync() { await _windowTransitionGate.WaitAsync(); try { var shouldResumeSettingsHost = false; while (_windowStack.Count > 0) { var window = _windowStack.Pop(); if (window == null) continue; if (window.WindowType == WindowType.SettingsWindow && _activeSettingsOverlayHost != null) shouldResumeSettingsHost = true; try { await window.Hide(); NotifyWindowStateChanged(window.WindowType, false); } catch { } } if (shouldResumeSettingsHost) { try { await _activeSettingsOverlayHost.ResumeFromOverlay(); } catch { } } _activeSettingsOverlayHost = null; 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(); _activeSettingsOverlayHost = null; _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)); } } }