using UnityEngine; using Ashwild.Network; using Ashwild.Player; namespace Ashwild.UI { /// /// In-game UI manager. Every openable window is a panel on the shared stack /// (): the pause menu, its sub-screens, and the inventory. "Paused" means /// a non-inventory panel is open — that freezes the world (optionally) and raises /// ; the inventory instead keeps the world running but /// locks input and shows the cursor via . /// /// One Escape closes whatever is open (inventory or a sub-panel), then opens pause when nothing /// is. The inventory toggle key opens/closes the inventory through the same stack, so the two can /// never fight. Ignored while dead. /// [DisallowMultipleComponent] public class GameUIManager : UIManager { #region Serialized Fields [Header("Pause")] [Tooltip("The pause menu panel opened by Escape.")] [SerializeField] private UIPanel pausePanel; #endregion #region State /// /// The registered inventory panel (resolved by its kind), toggled by the inventory key. /// private UIPanel inventoryPanel; /// /// The registered construction menu panel (resolved by its kind), toggled by the build /// hammer's right-click through . /// private UIPanel buildPanel; /// /// True while a panel that pauses the world is open — i.e. anything but the inventory (which a /// chest reuses) or the construction menu, both local windows the world keeps running under. /// public bool IsPaused => Current != null && Current.Kind != PanelKind.Inventory && Current.Kind != PanelKind.Build; // Last bus values pushed, so we only fire on real transitions. private bool lastPaused; private bool lastInventoryOpen; private bool lastBuildOpen; #endregion #region Unity Lifecycle /// /// Subscribes to the inventory toggle input (Cancel/Escape is handled by the base). /// protected override void OnEnable() { base.OnEnable(); PlayerEvents.InventoryTogglePressed += HandleInventoryToggle; PlayerEvents.BuildMenuToggleRequested += HandleBuildMenuToggle; } /// /// Unsubscribes and makes sure we never leave the game stuck paused on scene unload. /// protected override void OnDisable() { base.OnDisable(); PlayerEvents.InventoryTogglePressed -= HandleInventoryToggle; PlayerEvents.BuildMenuToggleRequested -= HandleBuildMenuToggle; if (lastPaused) { lastPaused = false; PlayerEvents.RaiseGamePaused(false); } if (lastInventoryOpen) { lastInventoryOpen = false; PlayerEvents.RaiseInventoryOpenChanged(false); } if (lastBuildOpen) { lastBuildOpen = false; PlayerEvents.RaiseBuildMenuOpenChanged(false); } } /// /// Resolves the inventory panel and starts unpaused with the cursor locked. /// protected override void Start() { base.Start(); ResolvePanelsByKind(); ApplyUIState(force: true); } #endregion #region Escape Policy /// /// Escape, from the most local context outward: close whatever panel is open (inventory or /// sub-panel), else back out of an in-world build placement, else open the pause menu. /// /// The placement step matters because a ghost being positioned is a mode with no panel behind it: /// the construction menu closes as soon as a card is picked, so HasOpenPanel is already false and /// Escape used to fall straight through to the pause menu — leaving the player still holding a /// ghost. It is routed through BuildCancelRequested rather than reaching for BuildManager, so /// ending a placement stays the bus contract the hammer's right-click tap already uses. /// /// Demolition mode is deliberately not handled here: it lasts only while right-click is held, so /// it ends on release, and cancelling it from the outside would leave the hammer thinking it is /// still held. /// protected override void OnEscape() { if (PlayerEvents.IsDead) return; if (HasOpenPanel) { if (CanGoBack) Back(); else CloseAll(); return; } if (PlayerEvents.IsPlacingBuild) { PlayerEvents.RaiseBuildCancelRequested(); return; } OpenPause(); } #endregion #region Public API /// /// Opens the pause menu (no-op if a panel is already open). /// public void OpenPause() { if (pausePanel == null) { Debug.LogError("[GameUIManager] No pause panel assigned.", this); return; } OpenPanel(pausePanel); } /// /// Closes every panel and returns to gameplay. Hook this to the pause menu "Resume" button. /// public void Resume() => CloseAll(); /// /// Opens the Steam overlay to invite a friend to the current session. /// public void InviteFriend() { if (SteamInviteService.Instance == null) { Debug.LogError("[GameUIManager] SteamInviteService introuvable — ajoute-le sur le NetworkManager.", this); return; } SteamInviteService.Instance.OpenInviteOverlay(); } /// /// Leaves the session and returns to the main menu scene behind the loading curtain. The /// transition — and the menu scene name — is owned by the persistent NetworkSessionManager (it /// survives the scene swap and is the single source of truth for scene names; this manager does /// neither). Logs and no-ops if it is somehow missing rather than tearing down uncovered. /// public void QuitToMenu() { if (NetworkSessionManager.Instance == null) { Debug.LogError("[GameUIManager] NetworkSessionManager missing — cannot return to menu.", this); return; } NetworkSessionManager.Instance.ReturnToMenu(); } #endregion #region Inventory /// /// Inventory key: close the inventory if it is open, otherwise open it (never over a pause menu). /// private void HandleInventoryToggle() { if (PlayerEvents.IsDead) return; if (Current != null && Current.Kind == PanelKind.Inventory) { CloseAll(); return; } // Don't open the inventory on top of the pause menu or the construction menu. if (IsPaused) return; if (Current != null && Current.Kind == PanelKind.Build) return; if (inventoryPanel != null) OpenPanel(inventoryPanel); } /// /// Build hammer right-click: close the construction menu if it is open, otherwise open it /// (never over the pause menu or the inventory). Mirrors the inventory toggle so the two /// windows can never fight for the stack. /// private void HandleBuildMenuToggle() { if (PlayerEvents.IsDead) return; if (Current != null && Current.Kind == PanelKind.Build) { CloseAll(); return; } if (IsPaused) return; if (Current != null && Current.Kind == PanelKind.Inventory) return; if (buildPanel != null) OpenPanel(buildPanel); } /// /// Caches the registered inventory and construction-menu panels by their kind, so the /// toggle handlers can open them without a hard type reference. /// private void ResolvePanelsByKind() { if (panels == null) return; foreach (UIPanel p in panels) { if (p == null) continue; if (p.Kind == PanelKind.Inventory) inventoryPanel = p; else if (p.Kind == PanelKind.Build) buildPanel = p; } } #endregion #region UI State /// /// Re-applies cursor, time freeze and the pause/inventory bus states whenever the stack changes. /// protected override void OnCurrentChanged() => ApplyUIState(); /// /// Drives the side effects of the open panel: cursor for any panel, world freeze + GamePaused /// for pausing panels, and the InventoryOpenChanged / BuildMenuOpenChanged bus states for the /// two local windows. The inventory (which a chest reuses) and the construction menu are windows /// the world keeps running under, so only "real" menus pause. The ChestOpenChanged sub-state is /// owned by the inventory window itself, since a chest is just the inventory panel in chest mode. /// All these bus states feed PlayerEvents.InputLocked, so player control is cut whenever a panel /// is open. /// private void ApplyUIState(bool force = false) { UIPanel cur = Current; bool anyOpen = cur != null; bool inventory = anyOpen && cur.Kind == PanelKind.Inventory; bool build = anyOpen && cur.Kind == PanelKind.Build; bool paused = anyOpen && !inventory && !build; Cursor.lockState = anyOpen ? CursorLockMode.None : CursorLockMode.Locked; Cursor.visible = anyOpen; if (force || paused != lastPaused) { lastPaused = paused; PlayerEvents.RaiseGamePaused(paused); } if (force || inventory != lastInventoryOpen) { lastInventoryOpen = inventory; PlayerEvents.RaiseInventoryOpenChanged(inventory); } if (force || build != lastBuildOpen) { lastBuildOpen = build; PlayerEvents.RaiseBuildMenuOpenChanged(build); } } #endregion } }