using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.SceneManagement; /// /// 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; [Tooltip("One-line summary under the banner (counts: survived / found / converted).")] [SerializeField] private TMP_Text _summaryText; [Tooltip("Parent the result rows are instantiated under (a static header row may live here too).")] [SerializeField] private RectTransform _rowContainer; [Tooltip("Disabled template row with TMP children named Name / Outcome / Time / FoundBy; cloned per result row.")] [SerializeField] private GameObject _rowTemplate; [Tooltip("Optional: root of the whole result section; hidden while no result data exists (in Main / " + "the lobby) so the pause panel shows no empty scoreboard scaffold.")] [SerializeField] private GameObject _content; private sealed class Row { public GameObject Root; public TMP_Text Name, Outcome, Time, FoundBy; } 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") && FindText(_rowTemplate, "Time") && FindText(_rowTemplate, "FoundBy"); if (!_winnerText || !_summaryText || !_rowContainer || !templateOk) { Debug.LogError("[MatchResultScreen] Unwired reference(s) or row template missing a TMP child " + "(Name/Outcome/Time/FoundBy) - fix the wiring in the Inspector.", this); 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); } private void Bind(MatchManager match) { _boundVersion = match.ResultVersion; // Winner == None means the rows are the LIVE mid-match scoreboard: un-hit hiders read "Hiding" // (their survival time is still open), everything else is already frozen at its hit moment. bool live = match.LocalWinner == MatchManager.MatchWinner.None; _winnerText.text = live ? "Match in progress" : match.LocalWinner == MatchManager.MatchWinner.Hunters ? "Hunters win!" : "Hiders win!"; IReadOnlyList results = match.LocalResults; int survived = 0, found = 0, converted = 0; for (int i = 0; i < results.Count; i++) { switch (results[i].Outcome) { case MatchManager.PlayerOutcome.Survived: survived++; break; case MatchManager.PlayerOutcome.Found: found++; break; case MatchManager.PlayerOutcome.Converted: converted++; break; } } _summaryText.text = BuildSummary(survived, found, converted, live); for (int i = 0; i < results.Count; i++) { Row row = i < _rows.Count ? _rows[i] : CreateRow(); MatchManager.PlayerResult r = results[i]; bool pending = live && r.Outcome == MatchManager.PlayerOutcome.Survived; row.Root.SetActive(true); row.Name.text = r.Name; row.Outcome.text = pending ? "Hiding" : OutcomeLabel(r.Outcome); row.Time.text = !pending && HasTime(r.Outcome) ? FormatTime(r.SurvivalTime) : "-"; row.FoundBy.text = string.IsNullOrEmpty(r.FoundBy) ? "-" : r.FoundBy; } for (int i = results.Count; i < _rows.Count; i++) _rows[i].Root.SetActive(false); } private Row CreateRow() { GameObject go = Instantiate(_rowTemplate, _rowContainer); var row = new Row { Root = go, Name = FindText(go, "Name"), Outcome = FindText(go, "Outcome"), Time = FindText(go, "Time"), FoundBy = FindText(go, "FoundBy") }; _rows.Add(row); return row; } private static TMP_Text FindText(GameObject root, string childName) { Transform t = root.transform.Find(childName); return t ? t.GetComponent() : null; } private static string BuildSummary(int survived, int found, int converted, bool live) { var sb = new System.Text.StringBuilder(64); AppendCount(sb, survived, live ? "hiding" : "survived"); AppendCount(sb, found, "found"); AppendCount(sb, converted, "converted"); return sb.Length > 0 ? sb.ToString() : "No hiders in this match"; } private static void AppendCount(System.Text.StringBuilder sb, int count, string label) { if (count == 0) return; if (sb.Length > 0) sb.Append(" • "); sb.Append(count).Append(' ').Append(label); } private static bool HasTime(MatchManager.PlayerOutcome outcome) => outcome == MatchManager.PlayerOutcome.Survived || outcome == MatchManager.PlayerOutcome.Found || outcome == MatchManager.PlayerOutcome.Converted; private static string FormatTime(float seconds) { int s = Mathf.Max(0, Mathf.RoundToInt(seconds)); return $"{s / 60:0}:{s % 60:00}"; } private static string OutcomeLabel(MatchManager.PlayerOutcome outcome) { switch (outcome) { case MatchManager.PlayerOutcome.Hunter: return "Hunter"; case MatchManager.PlayerOutcome.Survived: return "Survived"; case MatchManager.PlayerOutcome.Found: return "Found"; case MatchManager.PlayerOutcome.Converted: return "Converted"; case MatchManager.PlayerOutcome.Left: return "Left"; default: return ""; } } }