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 (Tab acts as an /// alternative to Esc): Esc toggles /// this panel as a modal pause overlay (gameplay suspended via ); /// while the session browser, match results or pose menu 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. Hotkeys mirror the /// buttons: C toggles the pose menu (or cycles the pose directly with 2 or fewer poses), V switches to /// paint mode (E = taunt lives in GameModeManager). 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; [Tooltip("Pose selection menu binder. While its overlay is open it owns Esc (close-only) and this " + "menu stays closed. Empty = found automatically.")] [SerializeField] private PoseMenu poseMenu; [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; [Tooltip("Triggers the hider's manual radar ping (same as the E key) and closes the menu. Only shown " + "for a hider during Seek.")] [SerializeField] private Button tauntButton; [Tooltip("Opens the pose selection menu (or cycles the pose directly with 2 or fewer poses). Only " + "shown while the local player has selectable poses.")] [SerializeField] private Button poseButton; private bool _open; private void Awake() { if (!gameMode) gameMode = FindFirstObjectByType(); if (!sessionBrowser) sessionBrowser = FindFirstObjectByType(); if (!matchResultScreen) matchResultScreen = FindFirstObjectByType(); if (!poseMenu) poseMenu = 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); if (tauntButton) tauntButton.onClick.AddListener(OnTaunt); if (poseButton) poseButton.onClick.AddListener(OnPose); } 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); if (tauntButton) tauntButton.onClick.RemoveListener(OnTaunt); if (poseButton) poseButton.onClick.RemoveListener(OnPose); } private void Update() { bool browserOpen = sessionBrowser && sessionBrowser.IsOpen; bool resultsOpen = matchResultScreen && matchResultScreen.IsOpen; bool poseOpen = poseMenu && poseMenu.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 || poseOpen) 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 (HandleHotkeys(browserOpen, resultsOpen, poseOpen)) return; if (!EscapePressed()) return; if (browserOpen) sessionBrowser.Close(); else if (resultsOpen) matchResultScreen.Close(); else if (poseOpen) poseMenu.Close(); else if (_open) Close(resume: true); // Loading (joining a session, or the pre-match Loading barrier): the scene/avatar is mid-setup, // pausing or switching modes here corrupts the camera rigs - Esc/Tab do nothing. else if (MatchManager.Instance && MatchManager.Instance.IsLoadingLocally) { } // 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 OnPose() { // Small pose set (hunter): no radial menu, the button/hotkey just cycles to the next pose. if (poseMenu && poseMenu.CyclesDirectly) { Close(resume: true); poseMenu.CyclePose(); return; } Close(resume: false); if (!(poseMenu && poseMenu.Open()) && gameMode) gameMode.SetPaused(false); } private void OnTaunt() { Close(resume: true); if (MatchManager.Instance) MatchManager.Instance.RequestManualPing(); } 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); // Taunt/ping only does something for a hider during Seek (the server re-gates found hiders) - // hide it otherwise, same pattern as the role-scoped mode buttons. if (tauntButton) tauntButton.gameObject.SetActive(gameMode.LocalPlayerIsHider && MatchManager.Instance && MatchManager.Instance.LocalSubPhase == MatchManager.MatchSubPhase.Seek); // Pose selection needs a bound local player with poses - hide the button otherwise. if (poseButton) poseButton.gameObject.SetActive(poseMenu && poseMenu.CanOpen); 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); } /// Hotkeys mirroring the overlay buttons: C = pose menu (toggle), V = paint mode. /// Same gating as the buttons: inert while another overlay owns input, while loading, and in the /// no-player state. Returns true when a hotkey acted this frame. private bool HandleHotkeys(bool browserOpen, bool resultsOpen, bool poseOpen) { if (browserOpen || resultsOpen) return false; if (MatchManager.Instance && MatchManager.Instance.IsLoadingLocally) return false; if (!gameMode || gameMode.EffectiveMode == null) return false; #if ENABLE_INPUT_SYSTEM bool posePressed = InputUtil.GameplayKeyPressed(Key.C); bool paintPressed = InputUtil.GameplayKeyPressed(Key.V); #else bool posePressed = InputUtil.GameplayKeyPressed(KeyCode.C); bool paintPressed = InputUtil.GameplayKeyPressed(KeyCode.V); #endif if (posePressed) { if (poseOpen) poseMenu.Close(); else if (poseMenu && poseMenu.CanOpen) OnPose(); return true; } if (paintPressed && !poseOpen) { SwitchMode(); return true; } return false; } // Esc is NOT TypingGuard-gated: it must work even while a text field has focus. // Tab (alternative binding) IS gated so it stays usable for typing/field navigation. private static bool EscapePressed() { #if ENABLE_INPUT_SYSTEM return InputUtil.KeyPressedThisFrame(Key.Escape) || InputUtil.GameplayKeyPressed(Key.Tab); #else return InputUtil.KeyPressedThisFrame(KeyCode.Escape) || InputUtil.GameplayKeyPressed(KeyCode.Tab); #endif } }