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 人一队
Territory = 4, // 领土战争(M7):4 队 × 4 人,4×4 格驻军模型占点
}
/// <summary> 团队赛每队人数(查表,v0.7.8.34:Territory 加入后不能再从枚举值直推——Team4/Territory 都是 4 人队) </summary>
public static int TeamSize => CurrentMode switch
{
Mode.Ffa => 1,
Mode.Team2 => 2,
Mode.Team3 => 3,
_ => 4,
};
// ---- 菜单里选的待生效设置(host 侧;开局 ActivateForMatch 落地)----
/// <summary> 菜单当前选择的模式(下一局生效;默认领土战争,v0.7.8.85 用户定稿) </summary>
public static Mode PendingMode { get; set; } = Mode.Territory;
/// <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> 是否领土战争模式(M7) </summary>
public static bool IsTerritory => CurrentMode == Mode.Territory;
/// <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;重复应用幂等)。
/// mode 字节经网络到达,钳到合法枚举域——TeamSize 由 CurrentMode 推导,坏值不能漏进来(v0.7.8.33) </summary>
public static void Apply( byte mode, int durationSeconds, int playerTarget )
{
CurrentMode = (Mode)Math.Clamp( (int)mode, (int)Mode.Ffa, (int)Mode.Territory );
Duration = MathF.Max( 10f, durationSeconds );
PlayerTarget = Math.Clamp( playerTarget, 2, 64 );
if ( CurrentMode == Mode.Territory )
PlayerTarget = GameConfig.TerritoryGrid * TeamSize; // 领土固定 4 队 × 4 人 = 16(人数菜单对本模式无效,v0.7.8.35——32 目标曾分出 8 队)
TimeLeft = Duration;
MatchOver = false;
SinceLastTick = 0; // 规则下发也算 host 活着(武装心跳,防开局瞬间误判)
// 规则生效点 = 领土格复位点(全端一致:host 直调与客户端 RPC 都走这里;非领土模式置空态)
TerritoryManager.Reset();
}
/// <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;
}
}