Files
2026-06-22 16:18:34 +02:00

196 lines
6.0 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Ashwild.Player;
namespace Ashwild.UI
{
/// <summary>
/// Generic panel-stack engine shared by every scene that drives a stack of <see cref="UIPanel"/>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 <see cref="OnEscape"/>.
///
/// This is abstract: put the concrete <c>MenuUIManager</c> or <c>GameUIManager</c> 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 <see cref="Instance"/> of the base type, which lets panels reach "their"
/// manager through <c>UIManager.Instance</c> regardless of which concrete subclass is running.
/// </summary>
public abstract class UIManager : MonoBehaviour
{
#region State
/// <summary>
/// The UI manager for the currently loaded scene (menu or game).
/// </summary>
public static UIManager Instance { get; private set; }
[Header("Panels")]
[SerializeField] protected List<UIPanel> panels;
[Tooltip("Panel shown on start. Leave empty for scenes that begin with no panel (e.g. gameplay).")]
[SerializeField] protected UIPanel defaultPanel;
private Dictionary<Type, UIPanel> map;
private Stack<UIPanel> history;
private UIPanel current;
/// <summary>
/// The panel currently on top of the stack (null when nothing is open).
/// </summary>
public UIPanel Current => current;
/// <summary>
/// True while at least one panel is open.
/// </summary>
protected bool HasOpenPanel => current != null;
/// <summary>
/// True when there is a panel below the current one to go back to.
/// </summary>
protected bool CanGoBack => history != null && history.Count > 1;
#endregion
#region Unity Lifecycle
/// <summary>
/// Establishes the singleton and builds the type→panel lookup.
/// </summary>
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<UIPanel>();
}
/// <summary>
/// Subscribes to the Cancel (Escape) input from the bus.
/// </summary>
protected virtual void OnEnable()
{
PlayerEvents.CancelPressed += OnEscape;
}
/// <summary>
/// Unsubscribes — mirrors OnEnable.
/// </summary>
protected virtual void OnDisable()
{
PlayerEvents.CancelPressed -= OnEscape;
}
/// <summary>
/// Hides every panel, then opens the default one (if any).
/// </summary>
protected virtual void Start()
{
foreach (UIPanel p in panels)
if (p != null) p.HideInstant();
if (defaultPanel != null)
OpenPanel(defaultPanel.GetType());
}
/// <summary>
/// Clears the singleton reference.
/// </summary>
protected virtual void OnDestroy()
{
if (Instance == this) Instance = null;
}
#endregion
#region Public API
/// <summary>
/// Opens the registered panel of type T.
/// </summary>
public void OpenPanel<T>() where T : UIPanel => OpenPanel(typeof(T));
/// <summary>
/// Opens the given panel instance (resolved by its concrete type).
/// </summary>
public void OpenPanel(UIPanel panel)
{
if (panel == null) return;
OpenPanel(panel.GetType());
}
/// <summary>
/// Goes back one level in the navigation history. Never closes the last panel — use
/// <see cref="CloseAll"/> for that (the menu always keeps its default panel visible).
/// </summary>
public void Back()
{
if (history.Count <= 1) return;
UIPanel closing = history.Pop();
closing.Hide();
current = history.Peek();
current.Show();
OnCurrentChanged();
}
/// <summary>
/// Closes every open panel, returning to the "no panel" state (used by gameplay to leave pause).
/// </summary>
public void CloseAll()
{
if (history.Count == 0) return;
while (history.Count > 0)
history.Pop().Hide();
current = null;
OnCurrentChanged();
}
#endregion
#region Internal
/// <summary>
/// Core open: hides the current panel, pushes and shows the requested one.
/// </summary>
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();
}
/// <summary>
/// Scene-specific Escape behaviour (menu = navigate back, game = toggle pause).
/// </summary>
protected abstract void OnEscape();
/// <summary>
/// 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.
/// </summary>
protected virtual void OnCurrentChanged() { }
#endregion
}
}