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 <noreply@anthropic.com>
231 lines
9.8 KiB
C#
231 lines
9.8 KiB
C#
using System.Collections.Generic;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
|
|
/// <summary>
|
|
/// The standalone match results menu (modal pause overlay, same pattern as
|
|
/// <see cref="OnlineSessionBrowserUI"/>): opened via the overview menu's Match Results button
|
|
/// (<see cref="Open"/>), auto-opened when the Resolution sub-phase begins, closed by Esc through
|
|
/// <see cref="OverviewMenu"/> (all Esc handling lives there). Binds the scoreboard to the result rows
|
|
/// <see cref="MatchManager"/> 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 (<see cref="MatchManager.ResultVersion"/>). Only the visual
|
|
/// <see cref="_panelRoot"/> is toggled - this binder stays alive on its (always-active) object so it
|
|
/// keeps tracking match state while the panel is hidden.
|
|
/// </summary>
|
|
[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<Row> _rows = new List<Row>();
|
|
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<GameModeManager>();
|
|
if (_panelRoot) _panelRoot.SetActive(false);
|
|
}
|
|
|
|
/// <summary>True while the modal overlay is open. The overview menu yields Esc to this overlay
|
|
/// (close-only) while it is open.</summary>
|
|
public bool IsOpen => _open;
|
|
|
|
/// <summary>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.</summary>
|
|
public bool HasResults => MatchManager.Instance && MatchManager.Instance.LocalResults.Count > 0;
|
|
|
|
/// <summary>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.</summary>
|
|
public bool Open()
|
|
{
|
|
if (!enabled || !HasResults) return false;
|
|
SetOpen(true);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>Closes the modal overlay and resumes gameplay (Esc handling in <see cref="OverviewMenu"/>).</summary>
|
|
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<MatchManager.PlayerResult> 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<TMP_Text>() : 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 "";
|
|
}
|
|
}
|
|
}
|