Game/TerritoryManager.cs
using System;
/// <summary>
/// 领土战争模式(M7,用户定稿 V2.1):4×4 格,四角大本营开局属己方且永不沦陷。
/// 驻军模型占领:吐球进旗圈计分——中立格喂满 100 变色占领(驻军满 100);
/// 敌方吐球先啃驻军,啃光回中立(溢出转为自己进度)。无自动衰减、无补给线;
/// 只能抢与己方领土 4 邻接的格(大本营免疫敌方喂食)。
/// host 权威结算(TickCells 吐球命中即记 + 5Hz 变化快照广播);客户端 ApplyRemote 只读。
/// 格子几何由 ArenaHalfSize 决定(Replicated 下发,双端一致),无额外网络状态。
/// </summary>
public static class TerritoryManager
{
const int TeamCount = 4;
struct Cell
{
public int Owner; // 占领队(-1 = 中立)
public int ScoreTeam; // 分数归属队(中立格的争夺进度方;-1 = 无)
public float Score; // 中立格 = 该队的占领进度;己方格 = 驻军值
}
static readonly Cell[] _cells = new Cell[GameConfig.TerritoryGrid * GameConfig.TerritoryGrid];
static readonly int[] _owned = new int[TeamCount];
static readonly float[] _points = new float[TeamCount]; // 占旗积分(每旗每 3s +1,用户定稿)
static float _pointTimer; // 积分发放节拍
static int _conquerTeam = -1; // 先到征服胜利的队(TickTerritory 读,EndMatch 后随 Reset 清)
static int _sentVersion; // 已广播过的版本号(host 节流:变了才发)
public static bool Active => MatchState.IsTerritory;
/// <summary> 状态版本号:任何格变化 +1(TerritoryRenderer 据此重建几何) </summary>
public static int StateVersion { get; private set; }
/// <summary> host:自上次快照后状态有变(TickTerritory 节流广播用) </summary>
public static bool NeedsSync => StateVersion != _sentVersion;
/// <summary> 规则生效点调用(MatchState.Apply):全端复位为开局态——四角大本营属己方满驻军,其余全中立 </summary>
public static void Reset()
{
for ( int i = 0; i < _cells.Length; i++ )
{
_cells[i].Owner = -1;
_cells[i].ScoreTeam = -1;
_cells[i].Score = 0f;
}
for ( var t = 0; t < TeamCount; t++ )
{
var ( cx, cy ) = HqCell( t );
ref var c = ref _cells[cy * GameConfig.TerritoryGrid + cx];
c.Owner = t;
c.ScoreTeam = t;
c.Score = GameConfig.TerritoryCaptureScore;
}
_conquerTeam = -1;
_sentVersion = 0;
_pointTimer = 0f;
for ( var t = 0; t < TeamCount; t++ ) _points[t] = 0f;
Recount();
StateVersion++;
}
/// <summary> 队伍的大本营格(固定角映射:0 左上、1 右上、2 右下、3 左下) </summary>
public static ( int Cx, int Cy ) HqCell( int team ) => team switch
{
0 => ( 0, 0 ),
1 => ( GameConfig.TerritoryGrid - 1, 0 ),
2 => ( GameConfig.TerritoryGrid - 1, GameConfig.TerritoryGrid - 1 ),
_ => ( 0, GameConfig.TerritoryGrid - 1 ),
};
static bool IsHq( int cx, int cy )
{
for ( var t = 0; t < TeamCount; t++ )
{
var hq = HqCell( t );
if ( hq.Cx == cx && hq.Cy == cy ) return true;
}
return false;
}
// ---- 几何(双端一致,只依赖 ArenaHalfSize)----
public static float CellSize => GameConfig.ArenaHalfSize * 2f / GameConfig.TerritoryGrid;
public static Vector3 CellCenter( int cx, int cy )
{
var half = GameConfig.ArenaHalfSize;
var s = CellSize;
return new Vector3( -half + ( cx + 0.5f ) * s, -half + ( cy + 0.5f ) * s, 0f );
}
public static void CellOf( Vector3 pos, out int cx, out int cy )
{
var half = GameConfig.ArenaHalfSize;
var s = CellSize;
cx = Math.Clamp( (int)MathF.Floor( ( pos.x + half ) / s ), 0, GameConfig.TerritoryGrid - 1 );
cy = Math.Clamp( (int)MathF.Floor( ( pos.y + half ) / s ), 0, GameConfig.TerritoryGrid - 1 );
}
/// <summary> 队伍大本营内的出生采样范围(贴边内缩,别出生到旗圈边上挨打) </summary>
public static void HqBounds( int team, out float minX, out float maxX, out float minY, out float maxY )
{
var ( cx, cy ) = HqCell( team );
var half = GameConfig.ArenaHalfSize;
var s = CellSize;
var inset = s * 0.28f;
minX = -half + cx * s + inset;
maxX = -half + ( cx + 1 ) * s - inset;
minY = -half + cy * s + inset;
maxY = -half + ( cy + 1 ) * s - inset;
}
/// <summary> 球是否在己方大本营内(大体型衰减豁免判定,Ball 衰减处调用) </summary>
public static bool IsOwnHq( int team, Vector3 pos )
{
if ( !Active || team < 0 ) return false;
CellOf( pos, out var cx, out var cy );
var hq = HqCell( team );
return cx == hq.Cx && cy == hq.Cy;
}
/// <summary> 球是否在敌营大本营内(M7.2 安全屋:减速+掉重;无队/非领土恒 false) </summary>
public static bool InHostileHq( int team, Vector3 pos )
{
if ( !Active || team < 0 ) return false;
CellOf( pos, out var cx, out var cy );
ref var c = ref _cells[cy * GameConfig.TerritoryGrid + cx];
return IsHq( cx, cy ) && c.Owner >= 0 && c.Owner != team;
}
/// <summary> 是否存在前线格(大本营以外的己方占领格——死亡面板 FRONT 按钮亮暗用) </summary>
public static bool HasFrontCell( int team ) => TryFrontSpawn( team, default, out _ );
/// <summary> 该位置的旗是否允许该队动作(己方格,或邻接己方领土的非大本营格)——UI 引导用;
/// 与 Feed 的入侵门同一规则 </summary>
public static bool CanActOn( int team, Vector3 pos )
{
CellOf( pos, out var cx, out var cy );
return CanActOnCell( cx, cy, team );
}
public static bool CanActOnCell( int cx, int cy, int team )
{
if ( !Active || team < 0 ) return false;
ref var c = ref _cells[cy * GameConfig.TerritoryGrid + cx];
return c.Owner == team || ( !IsHq( cx, cy ) && AdjacentToOwned( cx, cy, team ) );
}
/// <summary> 前线复活点(M7.2 复活二选一):离 nearPos 最近的己方非大本营占领格内随机点;
/// 没有前线格返回 false(只剩大本营可复活) </summary>
public static bool TryFrontSpawn( int team, Vector3 nearPos, out Vector3 pos )
{
pos = default;
if ( !Active || team < 0 ) return false;
var best = float.MaxValue;
var found = false;
var grid = GameConfig.TerritoryGrid;
for ( int cy = 0; cy < grid; cy++ )
{
for ( int cx = 0; cx < grid; cx++ )
{
ref var c = ref _cells[cy * grid + cx];
if ( c.Owner != team || IsHq( cx, cy ) ) continue;
var d = CellCenter( cx, cy ).Distance( nearPos );
if ( d < best )
{
best = d;
pos = CellCenter( cx, cy );
found = true;
}
}
}
if ( !found ) return false;
var s = CellSize;
pos += new Vector3( Game.Random.Float( -s * 0.18f, s * 0.18f ), Game.Random.Float( -s * 0.18f, s * 0.18f ), 0f );
return true;
}
// ---- 喂旗结算(host 权威)----
/// <summary> 一颗孢子落进旗圈(TickCells 吐球命中调用)。
/// 返回是否消耗该孢子:进旗圈且规则允许就消耗计分;没进圈或规则不允许(非邻接/大本营)
/// 则 false,孢子继续当普通孢子飞,吐出的质量不白费 </summary>
public static bool Feed( int team, Vector3 pos, float blobMass )
{
if ( !Active || team < 0 || blobMass <= 0f ) return false;
CellOf( pos, out var cx, out var cy );
if ( CellCenter( cx, cy ).Distance( pos ) >= GameConfig.TerritoryFlagRadius ) return false; // 没进旗圈
ref var c = ref _cells[cy * GameConfig.TerritoryGrid + cx];
// 入侵规则:动别人/无主的格,必须与己方领土 4 邻接(稳扎稳打,无飞地)
if ( c.Owner != team && ( !AdjacentToOwned( cx, cy, team ) || IsHq( cx, cy ) ) )
return false;
var pts = MathF.Min( blobMass * GameConfig.TerritoryScorePerMass, GameConfig.TerritoryScoreCapPerBlob );
if ( pts <= 0f ) return false;
var changed = false;
if ( c.Owner == team )
{
// 己方格:补驻军(无自动衰减,只会被敌方啃掉后重占)
if ( c.Score < GameConfig.TerritoryCaptureScore )
{
c.Score = MathF.Min( GameConfig.TerritoryCaptureScore, c.Score + pts );
changed = true;
}
}
else if ( c.Owner < 0 )
{
// 中立格:无主进度归自己累加;有别人的进度先啃,啃光转为自己累加(溢出计入)
if ( c.ScoreTeam < 0 || c.ScoreTeam == team )
{
c.ScoreTeam = team;
c.Score += pts;
}
else
{
c.Score -= pts;
if ( c.Score <= 0f )
{
c.ScoreTeam = team;
c.Score = -c.Score;
}
}
if ( c.Score >= GameConfig.TerritoryCaptureScore )
{
c.Owner = team;
c.Score = GameConfig.TerritoryCaptureScore;
_owned[team]++;
changed = true;
NeonRenderer.Puff( CellCenter( cx, cy ), team );
GameLog.Info( $"[territory] team {team + 1} captured cell ({cx},{cy}) — {_owned[team]}/{GameConfig.TerritoryWinCells}" );
}
else
{
changed = true;
}
}
else
{
// 敌方格:先啃驻军,啃光回中立(溢出转为自己进度,接着喂就能顺势占领)
c.Score -= pts;
if ( c.Score <= 0f )
{
_owned[c.Owner]--;
c.Owner = -1;
c.ScoreTeam = team;
c.Score = -c.Score;
NeonRenderer.Puff( CellCenter( cx, cy ), team );
GameLog.Info( $"[territory] team {team + 1} neutralized cell ({cx},{cy})" );
}
changed = true;
}
if ( changed )
{
StateVersion++;
if ( _conquerTeam < 0 && _owned[team] >= GameConfig.TerritoryWinCells )
{
_conquerTeam = team;
GameLog.Info( $"[territory] team {team + 1} reaches {GameConfig.TerritoryWinCells} cells — conquest!" );
}
}
return true;
}
static bool AdjacentToOwned( int cx, int cy, int team )
{
return Owns( cx - 1, cy, team ) || Owns( cx + 1, cy, team )
|| Owns( cx, cy - 1, team ) || Owns( cx, cy + 1, team );
}
static bool Owns( int cx, int cy, int team )
{
if ( cx < 0 || cy < 0 || cx >= GameConfig.TerritoryGrid || cy >= GameConfig.TerritoryGrid ) return false;
return _cells[cy * GameConfig.TerritoryGrid + cx].Owner == team;
}
static void Recount()
{
for ( var i = 0; i < _owned.Length; i++ ) _owned[i] = 0;
foreach ( var c in _cells )
{
if ( c.Owner >= 0 && c.Owner < _owned.Length ) _owned[c.Owner]++;
}
}
/// <summary> 某队当前占领格数(HUD/结算/胜利判定共用) </summary>
public static int OwnedCount( int team ) => team >= 0 && team < TeamCount ? _owned[team] : 0;
/// <summary> host 每帧:占旗积分发放(用户定稿——每占领一旗每 3 秒 +1 分;点数只在 host 累积,随快照下发) </summary>
public static void TickPoints( float dt )
{
if ( !Active ) return;
_pointTimer += dt;
var step = GameConfig.TerritoryPointSeconds;
while ( _pointTimer >= step )
{
_pointTimer -= step;
for ( var t = 0; t < TeamCount; t++ ) _points[t] += _owned[t];
}
}
/// <summary> 某队当前占旗积分(胜利判定/HUD/结算共用;客户端经 TerritoryState 快照同步) </summary>
public static float Points( int team ) => team >= 0 && team < TeamCount ? _points[team] : 0f;
/// <summary> 单格状态读取(TerritoryRenderer 每次重建逐格取) </summary>
public static void CellState( int cx, int cy, out int owner, out int scoreTeam, out float score )
{
if ( cx < 0 || cy < 0 || cx >= GameConfig.TerritoryGrid || cy >= GameConfig.TerritoryGrid )
{
owner = -1;
scoreTeam = -1;
score = 0f;
return;
}
ref var c = ref _cells[cy * GameConfig.TerritoryGrid + cx];
owner = c.Owner;
scoreTeam = c.ScoreTeam;
score = c.Score;
}
/// <summary> 达成征服胜利的队伍(-1 = 尚无);host 检查到即 EndMatch </summary>
public static int ConquerWinner => _conquerTeam;
// ---- 网络同步 ----
/// <summary> host:当前四队占旗积分(与格子快照同拍下发) </summary>
public static float[] PointsWire()
{
var arr = new float[TeamCount];
for ( var t = 0; t < TeamCount; t++ ) arr[t] = _points[t];
return arr;
}
/// <summary> host:当前 16 格状态快照(发出即记录版本,NeedsSync 归零) </summary>
public static TerritoryWire[] Snapshot()
{
var wires = new TerritoryWire[_cells.Length];
for ( int i = 0; i < _cells.Length; i++ )
{
wires[i] = new TerritoryWire
{
Owner = _cells[i].Owner < 0 ? (byte)255 : (byte)_cells[i].Owner,
ScoreTeam = _cells[i].ScoreTeam < 0 ? (byte)255 : (byte)_cells[i].ScoreTeam,
Score = _cells[i].Score,
};
}
_sentVersion = StateVersion;
return wires;
}
/// <summary> 客户端应用 host 快照(host 回声由 IsAuthority 挡掉;重复应用幂等)。
/// points = 四队占旗积分(null 容错:旧包/空包只刷格子) </summary>
public static void ApplyRemote( TerritoryWire[] wires, float[] points )
{
if ( NetworkManager.IsAuthority ) return;
if ( wires is null || wires.Length != _cells.Length ) return;
// 逐项比对,完全没变就跳过——host 每 2s 保活快照带相同数据,
// 无脑 StateVersion++ 会让客户端领土几何频繁重建(闪,v0.7.8.86 修)
var changed = false;
for ( int i = 0; i < _cells.Length; i++ )
{
var owner = wires[i].Owner == 255 ? -1 : wires[i].Owner;
var scoreTeam = wires[i].ScoreTeam == 255 ? -1 : wires[i].ScoreTeam;
var score = wires[i].Score;
ref var c = ref _cells[i];
if ( c.Owner == owner && c.ScoreTeam == scoreTeam && c.Score == score ) continue;
c.Owner = owner;
c.ScoreTeam = scoreTeam;
c.Score = score;
changed = true;
}
if ( points is not null )
{
for ( var t = 0; t < TeamCount && t < points.Length; t++ )
{
if ( _points[t] != points[t] )
{
_points[t] = points[t];
changed = true;
}
}
}
if ( !changed ) return;
Recount();
StateVersion++;
}
}
/// <summary> 领土格网络快照线格式(255 = 中立/无归属;与 CellWire/ScoreWire 同顶层惯例) </summary>
public struct TerritoryWire
{
public byte Owner;
public byte ScoreTeam;
public float Score;
}