Game/MatchState.cs
using System;

/// <summary>
/// 比赛规则与进行状态(M5):host 在主菜单选规则,开局生效并经静态 RPC 下发;
/// 倒计时 host 权威(1Hz 校时 + 各端本地续走);比赛结束由 host 判定并广播结算榜。
/// 全端只读 Current*,写入只来自权威端与本文件(RPC 应用幂等)。
/// </summary>
public static class MatchState
{
	public enum Mode
	{
		Ffa = 0,     // 普通赛:各自为战
		Team2 = 1,   // 团队赛:2 人一队
		Team3 = 2,   // 团队赛:3 人一队
		Team4 = 3,   // 团队赛:4 人一队
	}

	/// <summary> 团队赛每队人数(模式字节直接编码规模:Team2=1 → 2 人 ... Team4=3 → 4 人) </summary>
	public static int TeamSize => 1 + (int)CurrentMode;

	// ---- 菜单里选的待生效设置(host 侧;开局 ActivateForMatch 落地)----

	/// <summary> 菜单当前选择的模式(下一局生效) </summary>
	public static Mode PendingMode { get; set; } = Mode.Ffa;

	/// <summary> 菜单当前选择的时长(秒) </summary>
	public static float PendingDuration { get; set; } = GameConfig.DefaultMatchSeconds;

	/// <summary> 菜单当前选择的场上目标人数(真人 + bot) </summary>
	public static int PendingPlayerTarget { get; set; } = GameConfig.DefaultPlayerTarget;

	// ---- 进行中的规则(全端一致;host 写,客户端经 RPC 收)----

	public static Mode CurrentMode { get; private set; } = Mode.Ffa;
	public static float Duration { get; private set; } = GameConfig.DefaultMatchSeconds;
	public static int PlayerTarget { get; private set; } = GameConfig.DefaultPlayerTarget;

	/// <summary> 剩余秒数(host 权威递减;客户端 1Hz 校时 + 本地每帧续走) </summary>
	public static float TimeLeft { get; internal set; }

	/// <summary> host 心跳(v0.6.8.0):Apply/ApplyTick 到达即归零。对局中 host 以 1Hz 广播校时,
	/// 客户端据此检测"host 静默挂了"——host 进程被杀时引擎会话可能不自动失效(僵尸态),
	/// 只能靠应用层心跳发现(CircleroyaleGame.Tick 超时强制断线) </summary>
	public static TimeSince SinceLastTick;

	/// <summary> 本局是否已结束(结算面板展示中,全场模拟冻结) </summary>
	public static bool MatchOver { get; internal set; }

	public static bool IsTeam => CurrentMode >= Mode.Team2;

	/// <summary> 队伍显示色(团队赛:球色/比分条/播报按队伍取色;队伍最多 24 个,调色板循环取用) </summary>
	public static Color TeamColor( int teamIndex ) =>
		GameConfig.Palette[( ( teamIndex % GameConfig.Palette.Length ) + GameConfig.Palette.Length ) % GameConfig.Palette.Length];

	/// <summary> 权威端开局生效并广播(OnNetworkReady 调用;菜单选择原样落地)。
	/// 无会话(菜单背景演示赛)时跳过广播——Rpc.Broadcast 需要激活的会话。 </summary>
	public static void ActivateForMatch()
	{
		Apply( (byte)PendingMode, (int)PendingDuration, PendingPlayerTarget );
		if ( Networking.IsActive )
			NetworkManager.MatchSettings( (byte)PendingMode, (int)PendingDuration, PendingPlayerTarget );
	}

	/// <summary> 全端应用规则(host 本地直调 / 客户端经 RPC;重复应用幂等) </summary>
	public static void Apply( byte mode, int durationSeconds, int playerTarget )
	{
		CurrentMode = (Mode)mode;
		Duration = MathF.Max( 10f, durationSeconds );
		PlayerTarget = Math.Clamp( playerTarget, 2, 64 );
		TimeLeft = Duration;
		MatchOver = false;
		SinceLastTick = 0;   // 规则下发也算 host 活着(武装心跳,防开局瞬间误判)
	}

	/// <summary> 1Hz 校时(客户端;本地每帧继续递减,误差 << 1s 观感平滑) </summary>
	public static void ApplyTick( float timeLeft )
	{
		TimeLeft = MathF.Max( 0f, timeLeft );
		SinceLastTick = 0;   // host 心跳
	}

	/// <summary> 回主菜单:清比赛进行态,保留菜单里选的规则 </summary>
	public static void Reset()
	{
		MatchOver = false;
		TimeLeft = 0f;
		CurrentMode = PendingMode;
		Duration = PendingDuration;
		PlayerTarget = PendingPlayerTarget;
	}
}