Files
chameleon/Assets/Scripts/Network/UI/OnlineSessionBrowserUI.cs
T
adminandClaude Fable 5 b71cd61533 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>
2026-07-08 20:40:36 +02:00

321 lines
12 KiB
C#

using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif
/// <summary>
/// uGUI binding for <see cref="OnlineSessionManager"/>: Host / Refresh / row select / Join / Disconnect
/// plus a status line, mirroring the manager through its <see cref="OnlineSessionManager.Changed"/> /
/// <see cref="OnlineSessionManager.SessionsChanged"/> events; the session logic stays on the manager.
///
/// 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"/>). 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.
/// </summary>
public class OnlineSessionBrowserUI : MonoBehaviour
{
[SerializeField] private OnlineSessionManager manager;
[Header("Buttons")]
[SerializeField] private Button hostButton;
[SerializeField] private Button refreshButton;
[SerializeField] private Button joinButton;
[SerializeField] private Button disconnectButton;
[Header("List")]
[Tooltip("Content RectTransform of the session ScrollRect (rows are added here).")]
[SerializeField] private RectTransform listContent;
[Tooltip("A row prototype kept disabled; cloned once per session. Children named 'Name' and 'Players' carry the text.")]
[SerializeField] private GameObject entryTemplate;
[Header("Status")]
[SerializeField] private TMP_Text statusLabel;
[Header("Overlay")]
[Tooltip("Visual container toggled by Esc while connected (the panel root). Must be a CHILD of this " +
"binder's object so the binder keeps running - and reading Esc - while the panel is hidden.")]
[SerializeField] private GameObject panelRoot;
[Tooltip("Background tint of the selected row.")]
[SerializeField] private Color selectedTint = new Color(0.26f, 0.52f, 0.96f, 1f);
[Tooltip("Background tint of an unselected row.")]
[SerializeField] private Color normalTint = new Color(1f, 1f, 1f, 0.05f);
private struct Row { public string id; public Image image; public GameObject go; }
private readonly List<Row> _rows = new List<Row>();
private string _selectedId;
private MatchManager _match; // local match state (scene kind + authority); found at runtime
private GameModeManager _gameMode; // pause hook for the modal overlay; found at runtime
private TMP_Text _disconnectLabel; // disconnect button caption (relabelled to "Back to Lobby")
private bool _open; // Esc-toggled visibility while connected
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()
{
// The template is the prototype, never shown itself - layout ignores it while disabled.
if (entryTemplate) entryTemplate.SetActive(false);
_match = FindFirstObjectByType<MatchManager>();
_gameMode = FindFirstObjectByType<GameModeManager>();
}
private void OnEnable()
{
if (hostButton) hostButton.onClick.AddListener(OnHost);
if (refreshButton) refreshButton.onClick.AddListener(OnRefresh);
if (joinButton) joinButton.onClick.AddListener(OnJoin);
if (disconnectButton)
{
disconnectButton.onClick.AddListener(OnDisconnect);
_disconnectLabel = disconnectButton.GetComponentInChildren<TMP_Text>(true);
}
if (manager)
{
manager.Changed += OnManagerChanged;
manager.SessionsChanged += RebuildList;
}
_connected = manager && manager.IsConnected;
RebuildList();
OnManagerChanged();
ApplyVisibility();
}
private void OnDisable()
{
if (hostButton) hostButton.onClick.RemoveListener(OnHost);
if (refreshButton) refreshButton.onClick.RemoveListener(OnRefresh);
if (joinButton) joinButton.onClick.RemoveListener(OnJoin);
if (disconnectButton) disconnectButton.onClick.RemoveListener(OnDisconnect);
if (manager)
{
manager.Changed -= OnManagerChanged;
manager.SessionsChanged -= RebuildList;
}
}
private void Update()
{
if (!manager) return;
bool connected = manager.IsConnected;
// Auto-close the overlay if the scene changed underneath it (a remote ended/started the match, or
// we walked into Start Match) so it never lingers as a stale pause across a transition.
// Compared as Scene structs (handles), not names, so nothing is allocated per frame.
Scene scene = SceneManager.GetActiveScene();
if (connected && _open && _lastScene.IsValid() && scene != _lastScene)
SetOpen(false);
_lastScene = scene;
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);
RefreshDisconnectLabel();
}
private void OnHost() { if (manager) manager.Host(); }
private void OnRefresh() { if (manager) manager.Refresh(); }
private void OnJoin() { if (manager && !string.IsNullOrEmpty(_selectedId)) manager.Join(_selectedId); }
private void OnDisconnect()
{
if (!manager) return;
// In Main (no session) the button is an application Exit, so the player can quit the build
// without Alt+F4.
if (!manager.IsConnected)
{
QuitApplication();
return;
}
// On the map, the match AUTHORITY's button means "Back to Lobby" (end the match for everyone)
// rather than leaving the session. Everyone else - in the lobby, or a non-authority client on the
// map - does a plain disconnect.
if (_match && _match.IsLocalSceneMap && _match.IsLocalMatchAuthority)
{
_match.RequestEndMatch();
SetOpen(false); // heading back to the lobby; close the overlay + resume input
return;
}
manager.Disconnect();
}
private static void QuitApplication()
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
}
private void OnManagerChanged()
{
if (!manager) return;
bool connected = manager.IsConnected;
if (connected != _connected)
{
_connected = connected;
// 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();
}
if (statusLabel) statusLabel.text = manager.Status;
UpdateButtons();
RefreshDisconnectLabel();
}
private void SetOpen(bool open)
{
_open = open;
ApplyVisibility();
if (_gameMode) _gameMode.SetPaused(open);
}
private void ApplyVisibility()
{
bool show = !(manager && manager.IsConnected) || _open;
if (panelRoot && panelRoot.activeSelf != show)
panelRoot.SetActive(show);
}
private void RefreshDisconnectLabel()
{
if (!_disconnectLabel) return;
string text;
if (!(manager && manager.IsConnected))
text = "Exit"; // Main: quit the application
else if (_match && _match.IsLocalSceneMap && _match.IsLocalMatchAuthority)
text = "Back to Lobby";
else
text = "Disconnect";
if (text != _disconnectText)
{
_disconnectText = text;
_disconnectLabel.text = text;
}
}
private void UpdateButtons()
{
if (!manager) return;
bool busy = manager.IsBusy;
bool connected = manager.IsConnected;
bool hasSelection = !string.IsNullOrEmpty(_selectedId);
if (hostButton) hostButton.interactable = !busy && !connected;
if (refreshButton) refreshButton.interactable = !busy && !connected;
if (joinButton) joinButton.interactable = !busy && !connected && hasSelection;
// Usable whenever idle: connected -> Disconnect / Back to Lobby; in Main -> Exit.
if (disconnectButton) disconnectButton.interactable = !busy;
}
private void RebuildList()
{
for (int i = 0; i < _rows.Count; i++)
if (_rows[i].go) Destroy(_rows[i].go);
_rows.Clear();
if (manager && entryTemplate && listContent)
{
var sessions = manager.Sessions;
bool selectionStillPresent = false;
for (int i = 0; i < sessions.Count; i++)
{
var s = sessions[i];
var go = Instantiate(entryTemplate, listContent);
go.SetActive(true);
var nameText = FindText(go.transform, "Name");
if (nameText) nameText.text = s.Name;
var playersText = FindText(go.transform, "Players");
if (playersText)
{
int taken = Mathf.Max(0, s.MaxPlayers - s.AvailableSlots);
playersText.text = $"{taken}/{s.MaxPlayers}";
}
string id = s.Id; // capture per-row for the click closure
var button = go.GetComponent<Button>();
if (button) button.onClick.AddListener(() => Select(id));
_rows.Add(new Row { id = id, image = go.GetComponent<Image>(), go = go });
if (id == _selectedId) selectionStillPresent = true;
}
if (!selectionStillPresent) _selectedId = null;
}
ApplyTints();
UpdateButtons();
}
private void Select(string id)
{
_selectedId = id;
ApplyTints();
UpdateButtons();
}
private void ApplyTints()
{
for (int i = 0; i < _rows.Count; i++)
if (_rows[i].image)
_rows[i].image.color = _rows[i].id == _selectedId ? selectedTint : normalTint;
}
private static TMP_Text FindText(Transform root, string childName)
{
var t = root.Find(childName);
return t ? t.GetComponent<TMP_Text>() : null;
}
// NOT TypingGuard-gated (GameplayKeyPressed): Esc must close the panel even while a text field has focus.
private static bool EscapePressed()
{
#if ENABLE_INPUT_SYSTEM
return InputUtil.KeyPressedThisFrame(Key.Escape);
#else
return InputUtil.KeyPressedThisFrame(KeyCode.Escape);
#endif
}
}