using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using Ashwild.Player; namespace Ashwild.UI { /// /// Generic panel-stack engine shared by every scene that drives a stack of s /// (the menu and the in-game UI). It owns the panel registry, the navigation history and the /// show/hide flow, and dispatches the Cancel (Escape) input from the bus to a scene-specific /// policy via . /// /// This is abstract: put the concrete MenuUIManager or GameUIManager on the object, /// never this. Menu and game live in separate scenes, so only one manager is active at a time — /// hence a single shared of the base type, which lets panels reach "their" /// manager through UIManager.Instance regardless of which concrete subclass is running. /// public abstract class UIManager : MonoBehaviour { #region State /// /// The UI manager for the currently loaded scene (menu or game). /// public static UIManager Instance { get; private set; } [Header("Panels")] [SerializeField] protected List panels; [Tooltip("Panel shown on start. Leave empty for scenes that begin with no panel (e.g. gameplay).")] [SerializeField] protected UIPanel defaultPanel; private Dictionary map; private Stack history; private UIPanel current; /// /// The panel currently on top of the stack (null when nothing is open). /// public UIPanel Current => current; /// /// True while at least one panel is open. /// protected bool HasOpenPanel => current != null; /// /// True when there is a panel below the current one to go back to. /// protected bool CanGoBack => history != null && history.Count > 1; #endregion #region Unity Lifecycle /// /// Establishes the singleton and builds the type→panel lookup. /// protected virtual void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; map = panels.Where(p => p != null).ToDictionary(p => p.GetType()); history = new Stack(); } /// /// Subscribes to the Cancel (Escape) input from the bus. /// protected virtual void OnEnable() { PlayerEvents.CancelPressed += OnEscape; } /// /// Unsubscribes — mirrors OnEnable. /// protected virtual void OnDisable() { PlayerEvents.CancelPressed -= OnEscape; } /// /// Hides every panel, then opens the default one (if any). /// protected virtual void Start() { foreach (UIPanel p in panels) if (p != null) p.HideInstant(); if (defaultPanel != null) OpenPanel(defaultPanel.GetType()); } /// /// Clears the singleton reference. /// protected virtual void OnDestroy() { if (Instance == this) Instance = null; } #endregion #region Public API /// /// Opens the registered panel of type T. /// public void OpenPanel() where T : UIPanel => OpenPanel(typeof(T)); /// /// Opens the given panel instance (resolved by its concrete type). /// public void OpenPanel(UIPanel panel) { if (panel == null) return; OpenPanel(panel.GetType()); } /// /// Goes back one level in the navigation history. Never closes the last panel — use /// for that (the menu always keeps its default panel visible). /// public void Back() { if (history.Count <= 1) return; UIPanel closing = history.Pop(); closing.Hide(); current = history.Peek(); current.Show(); OnCurrentChanged(); } /// /// Closes every open panel, returning to the "no panel" state (used by gameplay to leave pause). /// public void CloseAll() { if (history.Count == 0) return; while (history.Count > 0) history.Pop().Hide(); current = null; OnCurrentChanged(); } #endregion #region Internal /// /// Core open: hides the current panel, pushes and shows the requested one. /// protected void OpenPanel(Type t) { if (map == null) return; if (!map.TryGetValue(t, out UIPanel next)) { Debug.LogWarning($"[{GetType().Name}] no panel of type {t.Name} registered.", this); return; } if (current == next) return; if (current != null) current.Hide(); history.Push(next); current = next; next.Show(); OnCurrentChanged(); } /// /// Scene-specific Escape behaviour (menu = navigate back, game = toggle pause). /// protected abstract void OnEscape(); /// /// Hook fired whenever the top panel changes (opened / went back / closed all). /// Subclasses use it to react to the stack becoming empty or non-empty. /// protected virtual void OnCurrentChanged() { } #endregion } }