using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; #if ENABLE_INPUT_SYSTEM using UnityEngine.InputSystem; #endif /// /// The Esc overview menu (the "Overview UI" canvas in Main). Owns ALL Escape handling: Esc toggles /// this panel as a modal pause overlay (gameplay suspended via ); /// while the session browser or match results overlay is open instead, Esc closes that, and while paint /// mode is active Esc exits to the camera mode (Player Control, or First Person for the hunter). Buttons /// switch game modes (via ) or open the session browser / /// match results overlay, and every click closes the panel. Mode buttons are only shown for modes present in the current role-scoped set /// (); the active mode's button is disabled. Attach to the /// always-active canvas root; is the toggled visual child. /// public class OverviewMenu : MonoBehaviour { [Tooltip("Mode manager driving pause + mode switches. Empty = found automatically.")] [SerializeField] private GameModeManager gameMode; [Tooltip("Session browser binder. While its overlay is open it owns Esc (close-only) and this menu " + "stays closed. Empty = found automatically.")] [SerializeField] private OnlineSessionBrowserUI sessionBrowser; [Tooltip("Match results screen binder. While its overlay is open it owns Esc (close-only) and this " + "menu stays closed. Empty = found automatically.")] [SerializeField] private MatchResultScreen matchResultScreen; [Header("Panel")] [Tooltip("Visual container toggled by Esc. Must be a CHILD of this binder's object so the binder " + "keeps reading Esc while the panel is hidden.")] [SerializeField] private GameObject panelRoot; [Header("Buttons")] [SerializeField] private Button sessionButton; [SerializeField] private Button matchResultButton; [Tooltip("Enters Player Control (third-person orbit) mode.")] [SerializeField] private Button playerControlButton; [SerializeField] private Button paintButton; [SerializeField] private Button firstPersonButton; [SerializeField] private Button freeCamButton; private bool _open; private void Awake() { if (!gameMode) gameMode = FindFirstObjectByType(); if (!sessionBrowser) sessionBrowser = FindFirstObjectByType(); if (!matchResultScreen) matchResultScreen = FindFirstObjectByType(); if (panelRoot) panelRoot.SetActive(false); } private void OnEnable() { if (sessionButton) sessionButton.onClick.AddListener(OnSession); if (matchResultButton) matchResultButton.onClick.AddListener(OnMatchResult); if (playerControlButton) playerControlButton.onClick.AddListener(OnPlayerControl); if (paintButton) paintButton.onClick.AddListener(OnPaint); if (firstPersonButton) firstPersonButton.onClick.AddListener(OnFirstPerson); if (freeCamButton) freeCamButton.onClick.AddListener(OnFreeCam); } private void OnDisable() { if (sessionButton) sessionButton.onClick.RemoveListener(OnSession); if (matchResultButton) matchResultButton.onClick.RemoveListener(OnMatchResult); if (playerControlButton) playerControlButton.onClick.RemoveListener(OnPlayerControl); if (paintButton) paintButton.onClick.RemoveListener(OnPaint); if (firstPersonButton) firstPersonButton.onClick.RemoveListener(OnFirstPerson); if (freeCamButton) freeCamButton.onClick.RemoveListener(OnFreeCam); } private void Update() { bool browserOpen = sessionBrowser && sessionBrowser.IsOpen; bool resultsOpen = matchResultScreen && matchResultScreen.IsOpen; if (_open) { // Another overlay took over (results Resolution auto-open) -> hide this panel; the pause // is that overlay's now, so don't release it. if (browserOpen || resultsOpen) HidePanel(); // An external state reset (respawn/bind, disconnect) dropped the pause underneath the // open panel -> don't linger as a stale overlay over live gameplay. else if (gameMode && !gameMode.IsPaused) HidePanel(); } if (!EscapePressed()) return; if (browserOpen) sessionBrowser.Close(); else if (resultsOpen) matchResultScreen.Close(); else if (_open) Close(resume: true); // Paint mode: Esc exits back to the camera mode instead of opening the menu. Player Control // first; the hunter's role-scoped set has none, so it falls through to First Person. else if (gameMode && gameMode.EffectiveMode is PaintMode) { if (!gameMode.TrySwitchTo() && !gameMode.TrySwitchTo()) Open(); } // No active/paused mode = the inert no-player state (pre-lobby Main, where the session panel // is always shown) - nothing to pause, don't open. else if (gameMode && gameMode.EffectiveMode != null) Open(); } private void Open() { _open = true; if (panelRoot) panelRoot.SetActive(true); RefreshButtons(); if (gameMode) gameMode.SetPaused(true); } /// Hides the panel; = false hands the pause over (session /// browser opening) instead of resuming gameplay. private void Close(bool resume) { HidePanel(); if (resume && gameMode) gameMode.SetPaused(false); } private void HidePanel() { _open = false; if (panelRoot) panelRoot.SetActive(false); } private void OnSession() { Close(resume: false); // Not connected (defensive - the menu should not be openable then): don't strand the pause. if (!(sessionBrowser && sessionBrowser.Open()) && gameMode) gameMode.SetPaused(false); } private void OnMatchResult() { Close(resume: false); if (!(matchResultScreen && matchResultScreen.Open()) && gameMode) gameMode.SetPaused(false); } private void OnPlayerControl() => SwitchMode(); private void OnPaint() => SwitchMode(); private void OnFirstPerson() => SwitchMode(); private void OnFreeCam() => SwitchMode(); private void SwitchMode() where T : GameMode { // Resume first so the switch runs on a live cycle (SwitchTo on a paused manager would fight // the pause bookkeeping); picking the already-active mode is a no-op inside SwitchTo. Close(resume: true); if (gameMode) gameMode.TrySwitchTo(); } /// Shows only the buttons whose mode exists in the current role-scoped set and disables /// the active mode's button. Ran on every open - the set only changes with a (re)bind, which /// force-closes the panel anyway. private void RefreshButtons() { if (!gameMode) return; IReadOnlyList modes = gameMode.Modes; GameMode active = gameMode.EffectiveMode; // Match results only exist from the first Hide phase on - hide the button until then // (same pattern as the role-scoped mode buttons). if (matchResultButton) matchResultButton.gameObject.SetActive(matchResultScreen && matchResultScreen.HasResults); SetModeButton(playerControlButton, modes, active); SetModeButton(paintButton, modes, active); SetModeButton(firstPersonButton, modes, active); SetModeButton(freeCamButton, modes, active); } private static void SetModeButton(Button button, IReadOnlyList modes, GameMode active) where T : GameMode { if (!button) return; bool present = false; for (int i = 0; i < modes.Count; i++) if (modes[i] is T) { present = true; break; } button.gameObject.SetActive(present); button.interactable = !(active is T); } // NOT TypingGuard-gated (GameplayKeyPressed): Esc must work even while a text field has focus. private static bool EscapePressed() { #if ENABLE_INPUT_SYSTEM return InputUtil.KeyPressedThisFrame(Key.Escape); #else return InputUtil.KeyPressedThisFrame(KeyCode.Escape); #endif } }