From 7669ae1b664cce516ce9eda1374179748c06d978 Mon Sep 17 00:00:00 2001 From: MelekTaus Moribundus Date: Sun, 12 Jul 2026 12:55:05 +0200 Subject: [PATCH] Complete Match Results modal: standalone overlay, auto-open on resolution MatchResultScreen becomes a self-contained modal (IsOpen/Open/Close, _panelRoot, pause handling); resolution auto-open moves there from the session browser. OverviewMenu integrates it into the Esc chain and hides the Match Results button until results exist (same pattern as the role-scoped mode buttons). Co-Authored-By: Claude Fable 5 --- Assets/Scripts/Components/UI/OverviewMenu.cs | 37 +++++++-- .../Scripts/Network/UI/MatchResultScreen.cs | 76 +++++++++++++++++-- .../Network/UI/OnlineSessionBrowserUI.cs | 16 +--- 3 files changed, 101 insertions(+), 28 deletions(-) diff --git a/Assets/Scripts/Components/UI/OverviewMenu.cs b/Assets/Scripts/Components/UI/OverviewMenu.cs index fa76674..734313a 100644 --- a/Assets/Scripts/Components/UI/OverviewMenu.cs +++ b/Assets/Scripts/Components/UI/OverviewMenu.cs @@ -8,10 +8,10 @@ using UnityEngine.InputSystem; /// /// 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 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, and every click -/// closes the panel. Mode buttons are only shown for modes present in the current role-scoped set +/// 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. /// @@ -24,6 +24,10 @@ public class OverviewMenu : MonoBehaviour "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.")] @@ -31,6 +35,7 @@ public class OverviewMenu : MonoBehaviour [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; @@ -43,12 +48,14 @@ public class OverviewMenu : MonoBehaviour { 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); @@ -58,6 +65,7 @@ public class OverviewMenu : MonoBehaviour 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); @@ -67,12 +75,13 @@ public class OverviewMenu : MonoBehaviour private void Update() { bool browserOpen = sessionBrowser && sessionBrowser.IsOpen; + bool resultsOpen = matchResultScreen && matchResultScreen.IsOpen; if (_open) { - // The browser overlay took over (its Resolution auto-open) -> hide this panel; the pause - // is the browser's now, so don't release it. - if (browserOpen) + // 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. @@ -84,6 +93,8 @@ public class OverviewMenu : MonoBehaviour 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 @@ -129,6 +140,13 @@ public class OverviewMenu : MonoBehaviour 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(); @@ -151,6 +169,11 @@ public class OverviewMenu : MonoBehaviour 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); diff --git a/Assets/Scripts/Network/UI/MatchResultScreen.cs b/Assets/Scripts/Network/UI/MatchResultScreen.cs index 9fb7a26..22114eb 100644 --- a/Assets/Scripts/Network/UI/MatchResultScreen.cs +++ b/Assets/Scripts/Network/UI/MatchResultScreen.cs @@ -1,18 +1,28 @@ using System.Collections.Generic; using TMPro; using UnityEngine; +using UnityEngine.SceneManagement; /// -/// Binds the match scoreboard section inside the pause/session panel (Online Session Manager) to the -/// result rows replicates: LIVE while a match runs (roster from Hide start, -/// re-sent on every hit / mid-match leave; winner still None -> pending hiders read "Hiding") and frozen -/// with the winner banner when Seek ends (the browser UI auto-opens the panel then). The section root is -/// hidden while no result data exists (Main / lobby). Rows are pooled clones of the disabled row -/// template, rebuilt only when a new result version arrives (). +/// The standalone match results menu (modal pause overlay, same pattern as +/// ): opened via the overview menu's Match Results button +/// (), auto-opened when the Resolution sub-phase begins, closed by Esc through +/// (all Esc handling lives there). Binds the scoreboard to the result rows +/// replicates: LIVE while a match runs (roster from Hide start, re-sent on +/// every hit / mid-match leave; winner still None -> pending hiders read "Hiding") and frozen with the +/// winner banner when Seek ends. Rows are pooled clones of the disabled row template, rebuilt only when +/// a new result version arrives (). Only the visual +/// is toggled - this binder stays alive on its (always-active) object so it +/// keeps tracking match state while the panel is hidden. /// [DisallowMultipleComponent] public class MatchResultScreen : MonoBehaviour { + [Header("Overlay")] + [Tooltip("Visual container toggled via Open/Close. Must be a CHILD of this binder's object so the " + + "binder keeps tracking match state while the panel is hidden.")] + [SerializeField] private GameObject _panelRoot; + [Tooltip("Big winner banner (\"Hunters win!\" / \"Hiders win!\").")] [SerializeField] private TMP_Text _winnerText; @@ -38,6 +48,11 @@ public class MatchResultScreen : MonoBehaviour private readonly List _rows = new List(); private int _boundVersion = -1; + private GameModeManager _gameMode; // pause hook for the modal overlay; found at runtime + private bool _open; // overlay visibility (toggled via Open/Close) + private Scene _lastScene; // last-seen active scene (to auto-close on a scene change) + private MatchManager.MatchSubPhase _lastSubPhase; // last-seen sub-phase (auto-open on Resolution) + private void Awake() { bool templateOk = _rowTemplate && FindText(_rowTemplate, "Name") && FindText(_rowTemplate, "Outcome") && @@ -49,16 +64,65 @@ public class MatchResultScreen : MonoBehaviour enabled = false; return; } + + _gameMode = FindFirstObjectByType(); + if (_panelRoot) _panelRoot.SetActive(false); + } + + /// True while the modal overlay is open. The overview menu yields Esc to this overlay + /// (close-only) while it is open. + public bool IsOpen => _open; + + /// True once any result data exists (LIVE mid-match roster or the frozen final scoreboard) - + /// there is nothing to show before the first Hide phase. + public bool HasResults => MatchManager.Instance && MatchManager.Instance.LocalResults.Count > 0; + + /// Opens the modal overlay (used by the overview menu's Match Results button); false while + /// no result data exists, so the caller can drop the pause instead of stranding it. + public bool Open() + { + if (!enabled || !HasResults) return false; + SetOpen(true); + return true; + } + + /// Closes the modal overlay and resumes gameplay (Esc handling in ). + public void Close() => SetOpen(false); + + private void SetOpen(bool open) + { + _open = open; + if (_panelRoot) _panelRoot.SetActive(open); + if (_gameMode) _gameMode.SetPaused(open); } private void Update() { + // Auto-close if the scene changed underneath the open overlay (match ended -> lobby swap, or a + // remote started the match) so it never lingers as a stale pause across a transition. + Scene scene = SceneManager.GetActiveScene(); + if (_open && _lastScene.IsValid() && scene != _lastScene) + SetOpen(false); + _lastScene = scene; + var match = MatchManager.Instance; bool hasResults = match && match.LocalResults.Count > 0; + + // Resolution just began -> auto-open so the final scoreboard is presented (this used to live on + // the session browser while the scoreboard was a section of its panel). Esc can still close it. + MatchManager.MatchSubPhase subPhase = match ? match.LocalSubPhase : MatchManager.MatchSubPhase.None; + if (subPhase != _lastSubPhase) + { + _lastSubPhase = subPhase; + if (subPhase == MatchManager.MatchSubPhase.Resolution && !_open && hasResults) + SetOpen(true); + } + if (_content && _content.activeSelf != hasResults) _content.SetActive(hasResults); if (!hasResults) { _boundVersion = -1; + if (_open) SetOpen(false); // data vanished (disconnect) -> don't linger as an empty pause return; } if (_boundVersion != match.ResultVersion) Bind(match); diff --git a/Assets/Scripts/Network/UI/OnlineSessionBrowserUI.cs b/Assets/Scripts/Network/UI/OnlineSessionBrowserUI.cs index 160d272..5c3d81d 100644 --- a/Assets/Scripts/Network/UI/OnlineSessionBrowserUI.cs +++ b/Assets/Scripts/Network/UI/OnlineSessionBrowserUI.cs @@ -13,9 +13,7 @@ using UnityEngine.UI; /// lobby/map the overview (Esc) menu reopens it via as a modal pause overlay /// (cursor freed, gameplay suspended via ) - all Esc key /// handling lives in , which closes this overlay through . -/// It also auto-opens when the Resolution sub-phase begins so -/// the final match scoreboard (a section of this panel) is presented. The Disconnect button is -/// context-sensitive: in Main it is an +/// The Disconnect button is context-sensitive: in Main it is an /// application "Exit"; on the map it becomes "Back to Lobby" for the match authority (ends the match); /// everyone else gets a plain disconnect. Only the visual is toggled - this binder /// stays alive on its (always-active) canvas so it keeps tracking state while the panel is hidden. @@ -61,7 +59,6 @@ public class OnlineSessionBrowserUI : MonoBehaviour private bool _connected; // last-seen connection state (to detect transitions) private Scene _lastScene; // last-seen active scene (to auto-close on a scene change) private string _disconnectText; // last applied caption (avoids per-frame TMP churn) - private MatchManager.MatchSubPhase _lastSubPhase; // last-seen sub-phase (auto-open on Resolution) private void Awake() { @@ -124,16 +121,6 @@ public class OnlineSessionBrowserUI : MonoBehaviour if (!connected) return; // Main: panel always shown - // Resolution just began -> auto-open the overlay so the final scoreboard is presented. Esc can - // still close it (via the overview menu); the lobby scene swap afterwards auto-closes it (see above). - MatchManager.MatchSubPhase subPhase = _match ? _match.LocalSubPhase : MatchManager.MatchSubPhase.None; - if (subPhase != _lastSubPhase) - { - _lastSubPhase = subPhase; - if (subPhase == MatchManager.MatchSubPhase.Resolution && !_open) - SetOpen(true); - } - RefreshDisconnectLabel(); } @@ -202,7 +189,6 @@ public class OnlineSessionBrowserUI : MonoBehaviour // Connection up (hosted/joined) -> hide the panel; Esc reopens it in the lobby/map. // Back to Main (disconnect/shutdown) -> show it again and drop any pause. _open = false; - _lastSubPhase = MatchManager.MatchSubPhase.None; // rejoining into Resolution must re-trigger if (!connected && _gameMode) _gameMode.SetPaused(false); ApplyVisibility(); }