Files
chameleon/Assets/Scripts/Network/UI/MatchHud.cs
T
adminandClaude Opus 4.8 fba4780c0e Add match-start readiness barrier (Loading phase)
Gate the Lobby-to-world transition on a new MatchSubPhase.Loading before
Hide: movement stays frozen and the Hide countdown does not start until
every connected client has spawned all match avatars and applied their
paintings.

- MatchManager: Loading phase; matchLoadTimeout tunable; Match_BeginLoad
  broadcasts the respawned avatar NetworkObjectIds (+ generation tag),
  Match_Ready collects per-client ACKs (host self-checks locally); barrier
  coroutine in RunMatchPhases before Hide; disconnect/teardown cleanup.
- GameModeManager: freeze local player (movement/paint/tab) during Loading,
  restored via ActiveMode.Enter() on transition to Hide (no camera jump).
- NetworkPlayerSetup: treat Loading like Hide in ComputeVisible so a
  fast-loading client cannot reveal a hider early.
- MatchHud: "Loading" phase label.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 22:07:59 +02:00

115 lines
4.5 KiB
C#

using TMPro;
using UnityEngine;
/// <summary>
/// On-screen match HUD binder: drives the authored <c>MatchHudCanvas</c> widgets from live match state -
/// the current sub-phase + countdown (top centre) and a hunter crosshair during the seek phase. The visual
/// layout lives in the scene; this component creates nothing at runtime, it only binds. Attach it to the
/// MatchHudCanvas and wire the references in the Inspector.
///
/// Passive overlay: the authored canvas has no <see cref="UnityEngine.UI.GraphicRaycaster"/> and every graphic
/// is raycastTarget=false, so it never consumes pointer events, and its sortingOrder (90) sits below PaintHud's
/// (100) so tool panels always win. There is no change event for the phase/timer, so the values are polled once
/// per frame; the visible strings are only rebuilt when the displayed second / phase actually changes.
/// </summary>
[DisallowMultipleComponent]
public class MatchHud : MonoBehaviour
{
[Tooltip("HUD canvas, toggled with the match. Empty = the Canvas on this GameObject.")]
[SerializeField] private Canvas _canvas;
[Tooltip("Sub-phase label in the top bar.")]
[SerializeField] private TMP_Text _phaseText;
[Tooltip("Countdown label in the top bar.")]
[SerializeField] private TMP_Text _timeText;
[Tooltip("Seconds until the next radar ping sweep; counts down from the interval and repeats. Shown only " +
"during the seek phase (blank otherwise).")]
[SerializeField] private TMP_Text _pingTimeText;
[SerializeField] private TMP_Text _hunterCount;
[SerializeField] private TMP_Text _hiderCount;
[Tooltip("Crosshair, shown only for the hunter during the seek phase.")]
[SerializeField] private GameObject _crosshair;
private int _lastSecond = -1;
private int _lastPingSecond = -1;
private int _lastHunters = -1;
private int _lastHiders = -1;
private MatchManager.MatchSubPhase _lastPhase = (MatchManager.MatchSubPhase)(-1);
private void Awake()
{
if (!_canvas) _canvas = GetComponent<Canvas>();
if (!_canvas || !_phaseText || !_timeText || !_pingTimeText || !_crosshair || !_hunterCount || !_hiderCount)
{
Debug.LogError("[MatchHud] Unwired reference(s) - assign Canvas/Phase/Time/PingTime/Crosshair/HunterCount/HiderCount in the Inspector.", this);
enabled = false;
return;
}
// Toggling the Canvas component (not the GameObject) keeps this binder's Update alive while hidden.
_canvas.enabled = false; // shown once a match is active
}
private void Update()
{
var match = MatchManager.Instance;
bool active = match && match.MatchActive;
if (_canvas.enabled != active) _canvas.enabled = active;
if (!active) return;
var phase = match.LocalSubPhase;
if (phase != _lastPhase)
{
_lastPhase = phase;
_phaseText.text = PhaseLabel(phase);
}
int s = Mathf.CeilToInt(Mathf.Max(0f, match.LocalPhaseRemaining));
if (s != _lastSecond)
{
_lastSecond = s;
_timeText.text = $"{s / 60:0}:{s % 60:00}";
}
// Next-radar-sweep countdown: shown for everyone during Seek (pings only fire then), 0:00 otherwise.
bool showPing = phase == MatchManager.MatchSubPhase.Seek && match.RadarPingInterval > 0f;
int p = showPing ? Mathf.CeilToInt(Mathf.Max(0f, match.LocalNextPingIn)) : 0;
if (p != _lastPingSecond)
{
_lastPingSecond = p;
_pingTimeText.text = $"{p / 60:0}:{p % 60:00}";
}
NetworkPlayerSetup.CountRoles(out int hunters, out int hiders);
if (hunters != _lastHunters)
{
_lastHunters = hunters;
_hunterCount.text = hunters.ToString();
}
if (hiders != _lastHiders)
{
_lastHiders = hiders;
_hiderCount.text = hiders.ToString();
}
bool showCross = phase == MatchManager.MatchSubPhase.Seek && NetworkPlayerSetup.LocalViewerIsHunter();
if (_crosshair.activeSelf != showCross) _crosshair.SetActive(showCross);
}
private static string PhaseLabel(MatchManager.MatchSubPhase phase)
{
switch (phase)
{
case MatchManager.MatchSubPhase.Loading: return "Loading";
case MatchManager.MatchSubPhase.Hide: return "Hide";
case MatchManager.MatchSubPhase.Seek: return "Seek";
case MatchManager.MatchSubPhase.Resolution: return "Resolution";
default: return "";
}
}
}