Add match result evaluation with live scoreboard in the Esc menu
MatchManager tracks per-participant outcomes from match start (hunter /
hiding / found / converted / left), freezes hit rows with seek time and
finder name, and decides the winner at seek end (any untouched hider ->
hiders win). Results replicate via a Match_Result named message,
broadcast live on hide start, every hit and mid-match leave (no-op once
the winner is frozen), with late joiners served the current state.
MatchResultScreen renders the scoreboard as a section of the session
panel: winner banner ("Match in progress" while live), summary counts
and pooled rows (name, outcome, time, found by); the section hides when
no result data exists. OnlineSessionBrowserUI auto-opens the Esc
overlay when the Resolution sub-phase begins.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
262a20326a
commit
b71cd61533
4 files changed
+436
-5
No files matched your search
@@ -0,0 +1,166 @@
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Binds the match scoreboard section inside the pause/session panel (Online Session Manager) 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 (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 (<see cref="MatchManager.ResultVersion"/>).
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class MatchResultScreen : MonoBehaviour
|
||||
{
|
||||
[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 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;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
var match = MatchManager.Instance;
|
||||
bool hasResults = match && match.LocalResults.Count > 0;
|
||||
if (_content && _content.activeSelf != hasResults) _content.SetActive(hasResults);
|
||||
if (!hasResults)
|
||||
{
|
||||
_boundVersion = -1;
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 17b13eac332f8184d9a5a09e1c08651b
|
||||
@@ -14,7 +14,9 @@ using UnityEngine.InputSystem;
|
||||
///
|
||||
/// The panel is always shown in Main (not connected); once a connection is up it hides, and in the
|
||||
/// lobby/map Esc toggles it back as a modal pause overlay (cursor freed, gameplay suspended via
|
||||
/// <see cref="GameModeManager.SetPaused"/>). The Disconnect button is context-sensitive: in Main it is an
|
||||
/// <see cref="GameModeManager.SetPaused"/>). 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
|
||||
/// 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 <see cref="panelRoot"/> is toggled - this binder
|
||||
/// stays alive on its (always-active) canvas so it keeps reading Esc while the panel is hidden.
|
||||
@@ -60,6 +62,7 @@ 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()
|
||||
{
|
||||
@@ -122,6 +125,16 @@ public class OnlineSessionBrowserUI : MonoBehaviour
|
||||
|
||||
if (!connected) return; // Main: panel always shown, Esc ignored
|
||||
|
||||
// Resolution just began -> auto-open the overlay so the final scoreboard is presented. Esc can
|
||||
// still close it; 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);
|
||||
}
|
||||
|
||||
bool inLobbyOrMap = _match && (_match.IsLocalSceneLobby || _match.IsLocalSceneMap);
|
||||
if (inLobbyOrMap && EscapePressed())
|
||||
SetOpen(!_open);
|
||||
@@ -178,6 +191,7 @@ 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();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user