Add host-configurable match settings persisted in settings

- GameSettings: 7 match fields (mode, hunter count, hide/seek/resolve
  durations, radar ping interval/lifetime) with authored defaults
- MatchSettingSlider (new): enum-driven panel binder, seeds from
  settings, saves on change; lifetime label shows one decimal
  (invariant culture)
- MatchManager: ApplyHostMatchSettings() on StartMatch (clamped);
  phase message now carries ping interval so clients tick correctly

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Fable 5 committed 2026-07-08 22:04:54 +02:00
1 parent 1468921ca2
commit 270c42d85b
4 files changed
+200 -14

No files matched your search

+37 -14
View File
@@ -198,6 +198,7 @@ public class MatchManager : MonoBehaviour
private float _phaseEndTime; // Time.time when the current sub-phase ends (for the HUD countdown)
private float _seekStartTime; // Time.time Seek began locally (anchors the ping-countdown cadence, START-aligned)
private MatchMode _localMode = MatchMode.Classic; // mirror of the server's matchMode, carried in the phase message
private float _localPingInterval; // mirror of the server's radarPingInterval, carried in the phase message (seeded from the inspector value in Awake)
private const string PreMatchTag = "PreMatch"; // scene props destroyed when seek starts
@@ -210,10 +211,11 @@ public class MatchManager : MonoBehaviour
/// <summary>Seconds left in the current sub-phase for the HUD (clamped to 0; cosmetic).</summary>
public float LocalPhaseRemaining => Mathf.Max(0f, _phaseEndTime - Time.time);
/// <summary>Seconds between radar ping sweeps (0 = pings disabled). Baked identically on every client
/// (persistent MatchManager, no replication - like <see cref="EmbeddedRevealPercent"/>), so the HUD can
/// drive the ping countdown locally.</summary>
public float RadarPingInterval => radarPingInterval;
/// <summary>Seconds between radar ping sweeps (0 = pings disabled). Mirror of the SERVER's value, carried
/// in the phase message - the host overrides its inspector default from the Match Panel settings on match
/// start, so the value can no longer be assumed identical on every install. The HUD drives the ping
/// countdown locally from this mirror.</summary>
public float RadarPingInterval => _localPingInterval;
/// <summary>Seconds until the next radar ping sweep for the HUD (counts down from <see cref="RadarPingInterval"/>
/// and repeats). 0 outside Seek or when pings are disabled. Computed locally from the local Seek-start +
@@ -223,9 +225,9 @@ public class MatchManager : MonoBehaviour
{
get
{
if (_subPhase != MatchSubPhase.Seek || radarPingInterval <= 0f) return 0f;
float intoInterval = (Time.time - _seekStartTime) % radarPingInterval;
return radarPingInterval - intoInterval;
if (_subPhase != MatchSubPhase.Seek || _localPingInterval <= 0f) return 0f;
float intoInterval = (Time.time - _seekStartTime) % _localPingInterval;
return _localPingInterval - intoInterval;
}
}
@@ -256,8 +258,9 @@ public class MatchManager : MonoBehaviour
// the other client->server ("please end the match"); the server only honors it from the authority id.
private const string MsgAuthorityGranted = "Match_AuthorityGranted";
private const string MsgRequestEndMatch = "Match_RequestEndMatch";
// Server -> all clients: the sub-phase changed (byte phase + float duration). Client -> server: a hunter
// reports a hit on a hider (ulong target client id); the server validates and marks the hider found.
// Server -> all clients: the sub-phase changed (byte phase + float duration + byte mode + float radar ping
// interval). Client -> server: a hunter reports a hit on a hider (ulong target client id); the server
// validates and marks the hider found.
private const string MsgPhase = "Match_Phase";
private const string MsgReportHit = "Match_ReportHit";
// Client -> server, right after the client connects: the sender's persistent per-install id (survives its
@@ -346,6 +349,7 @@ public class MatchManager : MonoBehaviour
{
if (Instance && Instance != this) { Destroy(this); return; }
Instance = this;
_localPingInterval = radarPingInterval; // pre-match HUD default; overwritten by every phase message
_net = GetComponent<NetworkManager>();
if (!_net) _net = FindFirstObjectByType<NetworkManager>();
@@ -481,7 +485,7 @@ public class MatchManager : MonoBehaviour
_hasAuthorityClient = false;
_isLocalAuthority = false;
Phase = MatchPhase.Lobby;
ApplyPhaseLocal(MatchSubPhase.None, 0f, _localMode);
ApplyPhaseLocal(MatchSubPhase.None, 0f, _localMode, _localPingInterval);
_pendingHiders.Clear();
_persistentIds.Clear();
// Drop any in-flight loading-barrier state (StopAllCoroutines above already killed the polls).
@@ -676,6 +680,8 @@ public class MatchManager : MonoBehaviour
var ids = new List<ulong>(_net.ConnectedClientsIds);
if (ids.Count == 0) return;
ApplyHostMatchSettings();
Phase = MatchPhase.Transitioning;
// Roll N distinct random Hunters via partial Fisher-Yates (swap a random not-yet-picked id into
@@ -695,6 +701,20 @@ public class MatchManager : MonoBehaviour
StartCoroutine(MatchSequence(hunterIds));
}
// Server-only, called once per match start. Pulls the host-authored match rules (Esc-menu Match Panel,
// persisted via GameSettings/Settings.json) over the inspector defaults, so the machine that starts the
// match decides the rules. Defensively clamped - the JSON is hand-editable.
private void ApplyHostMatchSettings()
{
matchMode = GameSettings.MatchModeIndex == (int)MatchMode.Conversion ? MatchMode.Conversion : MatchMode.Classic;
hunterCount = Mathf.Max(0, GameSettings.MatchHunterCount);
hideDuration = Mathf.Max(0f, GameSettings.MatchHideDuration);
seekDuration = Mathf.Max(0f, GameSettings.MatchSeekDuration);
resolveDuration = Mathf.Max(0f, GameSettings.MatchResolveDuration);
radarPingInterval = Mathf.Max(0f, GameSettings.MatchRadarPingInterval);
radarPingLifetime = Mathf.Max(0f, GameSettings.MatchRadarPingLifetime);
}
private IEnumerator MatchSequence(HashSet<ulong> hunterIds)
{
_conversionRecords.Clear(); // fresh match inherits no stale statue captures (EndMatch also clears)
@@ -1027,17 +1047,18 @@ public class MatchManager : MonoBehaviour
private void SetPhaseServer(MatchSubPhase phase, float duration)
{
ApplyPhaseLocal(phase, duration, matchMode);
ApplyPhaseLocal(phase, duration, matchMode, radarPingInterval);
BroadcastPhase(phase, duration, matchMode, 0, true);
}
// Shared by the server's SetPhaseServer and a client's OnPhaseMessage handler.
private void ApplyPhaseLocal(MatchSubPhase phase, float duration, MatchMode mode)
private void ApplyPhaseLocal(MatchSubPhase phase, float duration, MatchMode mode, float pingInterval)
{
bool changed = _subPhase != phase;
_subPhase = phase;
_phaseEndTime = Time.time + duration;
_localMode = mode;
_localPingInterval = pingInterval;
// Props staged only for the hide phase are gone the instant seek begins; anchor the local ping-countdown
// cadence to the same moment (START-aligned, matching the server's sweep timer).
if (changed && phase == MatchSubPhase.Seek)
@@ -1064,11 +1085,12 @@ public class MatchManager : MonoBehaviour
{
var cmm = _net ? _net.CustomMessagingManager : null;
if (cmm == null) return;
using (var writer = new FastBufferWriter(sizeof(byte) + sizeof(float) + sizeof(byte), Allocator.Temp))
using (var writer = new FastBufferWriter(sizeof(byte) + sizeof(float) + sizeof(byte) + sizeof(float), Allocator.Temp))
{
writer.WriteValueSafe((byte)phase);
writer.WriteValueSafe(duration);
writer.WriteValueSafe((byte)mode);
writer.WriteValueSafe(radarPingInterval);
if (toAll)
{
_phaseRecipients.Clear();
@@ -1090,7 +1112,8 @@ public class MatchManager : MonoBehaviour
reader.ReadValueSafe(out byte phase);
reader.ReadValueSafe(out float duration);
reader.ReadValueSafe(out byte mode);
ApplyPhaseLocal((MatchSubPhase)phase, duration, (MatchMode)mode);
reader.ReadValueSafe(out float pingInterval);
ApplyPhaseLocal((MatchSubPhase)phase, duration, (MatchMode)mode, pingInterval);
}
// Server-only. One radar sweep during Seek: registers every still-un-found hider as a blip on each HUNTER's