Rendering/TerritoryRenderer.cs
using System;

/// <summary>
/// 领土战争地面渲染(M7,v0.7.8.79 像素风重写):占领格 = 队色底 + 粗边框 + 旗点旗子(flag.png
/// 白模染队色),大本营 = 城堡(castle.png 白模染队色);争夺中的中立格 = 白边框 + 争夺方色进度条;
/// 分数越高底色越实。状态版本变化才重建几何(16 格顶点量极小);贴图批次 1s 自愈补载
/// (NeonRenderer 同款)。CreateBackdrop 先于 Neon 建本组件 → 同层绘制在球/食物之下。非领土模式清空。
/// ⚠️ 相机 90° 滚转:旗/城堡这类有方向的贴图必须 rot=π/2 才屏幕正立(表情/头像同款教训)。
/// </summary>
public sealed class TerritoryRenderer : Component
{
	/// <summary> 全局引用(cr_perf 渲染 A/B 用;NeonRenderer.Instance 同款) </summary>
	public static TerritoryRenderer Instance { get; private set; }

	SceneDynamicObject _shape;    // 格底/粗边框/进度条(普通 alpha 三角形)
	SceneDynamicObject _flags;    // flag.png(白模 tint)
	SceneDynamicObject _castles;  // castle.png(白模 tint)
	int _builtVersion = -1;
	float _builtHalf = -1f;
	TimeSince _sinceRebuild = 999f;   // 重建节流(喂旗高峰 host 5Hz 快照,全清重写三批几何过频会闪)

	sealed class TexBatch
	{
		public SceneDynamicObject Dyno;
		public string Path;
		public Texture Tex;
		public Material Mat;   // 材质强引用(托管包装被 GC 会连带回收原生材质 → 红几何,实测)
	}

	/// <summary> 材质强引用池:材质只被原生对象引用时,托管包装被 GC 后原生材质随之回收,
	/// 场景对象画成引擎错误材质(红几何)。所有创建的材质在此留档。 </summary>
	readonly List<Material> _mats = new();

	Material TrackMat( Material m )
	{
		if ( m is not null ) _mats.Add( m );
		return m;
	}

	readonly List<TexBatch> _texBatches = new();
	TimeSince _sinceTexHeal = 1f;

	/// <summary> 调试 A/B(v0.7.8.104,cr_perf 渲染减负定位):隐藏/恢复全部领土批次
	/// (组件 Enabled 关不掉已构建的原生批次——SceneObject.RenderingEnabled 才是真开关) </summary>
	public void SetDebugVisible( bool visible )
	{
		if ( _shape is not null ) _shape.RenderingEnabled = visible;
		if ( _flags is not null ) _flags.RenderingEnabled = visible;
		if ( _castles is not null ) _castles.RenderingEnabled = visible;
		foreach ( var b in _texBatches )
			if ( b.Dyno is not null ) b.Dyno.RenderingEnabled = visible;
	}

	const float BorderThickness = 18f;         // 格边框粗细(世界单位,像素粗边观感)
	const float FlagWorldHeight = 400f;        // 旗子世界高度(约 1/7.5 格宽)
	const float CastleWorldFraction = 0.30f;   // 城堡占格宽比例
	const float BarWidth = 380f;               // 进度条宽
	const float BarHeight = 36f;               // 进度条高
	const float FlagTexAspect = 32f / 52f;     // flag.png 宽高比

	protected override void OnStart()
	{
		base.OnStart();
		Instance = this;
		_shape = CreateShapeBatch();
		_flags = CreateTexBatch( "ui/pixel/flag.png" );
		_castles = CreateTexBatch( "ui/pixel/castle.png" );
	}

	protected override void OnDestroy()
	{
		base.OnDestroy();

		// 组件销毁时必须显式删除原生批次:世界重建只销毁子 GameObject,遗留批次会带最后一帧
		// 几何继续渲染,旧材质回收后回退错误材质——满屏红几何 + Vulkan 管线崩溃的元凶(实测)
		DeleteBatch( _shape );
		DeleteBatch( _flags );
		DeleteBatch( _castles );
	}

	static void DeleteBatch( SceneDynamicObject dyno )
	{
		if ( dyno is null ) return;
		try { dyno.Delete(); } catch { }
	}

	protected override void OnUpdate()
	{
		base.OnUpdate();
		if ( !Game.IsPlaying ) return;

		var half = GameConfig.ArenaHalfSize;

		if ( TerritoryManager.Active )
		{
			// 重建节流(≥0.25s):喂旗高峰 host 5Hz 快照,全清重写三批几何过频会闪;
			// 错过的版本不追,下一拍自然画上(进度差 1/5 格分不清)
			if ( ( _builtVersion != TerritoryManager.StateVersion || MathF.Abs( _builtHalf - half ) > 0.5f )
				&& _sinceRebuild > 0.25f )
				Rebuild( half );
		}
		else if ( _builtVersion >= 0 )
		{
			Clear();
		}

		// 贴图自愈(1s 节流):热重载/资产刷新期间加载失败补载,否则整场错误红纹理(NeonRenderer 实测)
		if ( _sinceTexHeal > 1f )
		{
			_sinceTexHeal = 0;
			foreach ( var b in _texBatches )
			{
				if ( b.Tex is not null ) continue;
				LoadTex( b );
			}
		}
	}

	void Clear()
	{
		_builtVersion = -1;
		if ( _shape.IsValid() ) _shape.Clear();
		if ( _flags.IsValid() ) _flags.Clear();
		if ( _castles.IsValid() ) _castles.Clear();
	}

	SceneDynamicObject CreateShapeBatch()
	{
		var dyno = new SceneDynamicObject( Scene.SceneWorld );
		dyno.Material = TrackMat( LineMaterial.Create() );
		dyno.Flags.CastShadows = false;
		dyno.RenderLayer = SceneRenderLayer.OverlayWithDepth;
		dyno.Attributes.SetCombo( "D_BLEND", 0 );   // 像素风普通 alpha(加色发光已随霓虹风废弃)
		return dyno;
	}

	SceneDynamicObject CreateTexBatch( string path )
	{
		var b = new TexBatch { Dyno = CreateShapeBatch(), Path = path };
		_texBatches.Add( b );
		LoadTex( b );
		return b.Dyno;
	}

	void LoadTex( TexBatch b )
	{
		b.Tex = Texture.Load( b.Path, false );
		if ( b.Tex is null )
			Log.Warning( $"[pixel] texture not ready: {b.Path} (1s 后自动重试)" );
		b.Mat = TrackMat( NeonRenderer.MakeTexMaterial( b.Tex, pixel: true ) );   // Material 只写:先备好再赋
		b.Dyno.Material = b.Mat;
	}

	void Rebuild( float half )
	{
		_builtVersion = TerritoryManager.StateVersion;
		_builtHalf = half;
		_sinceRebuild = 0;

		// 诊断(临时,客户端 HUD 爆闪排查):记录每次重建的时刻与版本,用 Log(客户端日志也打)
		Log.Info( $"[terr] rebuild v={TerritoryManager.StateVersion} half={half:0}" );

		_shape.Clear();
		_shape.Init( Graphics.PrimitiveType.Triangles );
		_flags.Clear();
		_flags.Init( Graphics.PrimitiveType.Triangles );
		_castles.Clear();
		_castles.Init( Graphics.PrimitiveType.Triangles );

		var grid = GameConfig.TerritoryGrid;
		var s = TerritoryManager.CellSize;
		var flagHalfH = FlagWorldHeight * 0.5f;
		var flagHalfW = flagHalfH * FlagTexAspect;
		var castleHalf = s * CastleWorldFraction * 0.5f;

		for ( int cy = 0; cy < grid; cy++ )
		{
			for ( int cx = 0; cx < grid; cx++ )
			{
				TerritoryManager.CellState( cx, cy, out var owner, out var scoreTeam, out var score );
				var frac = Math.Clamp( score / GameConfig.TerritoryCaptureScore, 0f, 1f );
				var center = TerritoryManager.CellCenter( cx, cy );

				var x0 = -half + cx * s;
				var x1 = -half + ( cx + 1 ) * s;
				var y0 = -half + cy * s;
				var y1 = -half + ( cy + 1 ) * s;
				var isHq = IsHq( cx, cy );

				// 主色:归属方队色;争夺中中立=白边框+争夺方进度;无人问津=灰旗
				Color teamColor;
				bool contested = false;
				if ( owner >= 0 )
					teamColor = MatchState.TeamColor( owner );
				else if ( scoreTeam >= 0 && score > 0f )
				{
					teamColor = MatchState.TeamColor( scoreTeam );
					contested = true;
				}
				else
					teamColor = new Color( 0.62f, 0.65f, 0.73f );

				if ( owner >= 0 || contested )
				{
					// 格底:越满越实(争夺中更淡——还没到手)
					var fillA = owner >= 0 ? 0.16f + 0.18f * frac : 0.07f + 0.10f * frac;
					var fill = new Color( teamColor.r, teamColor.g, teamColor.b, fillA );
					AddQuadShape( x0 + BorderThickness, y0 + BorderThickness, x1 - BorderThickness, y1 - BorderThickness, 0.20f, fill );

					// 粗边框(像素领地感;争夺中=白色虚位以待)
					var lineC = owner >= 0
						? new Color( teamColor.r, teamColor.g, teamColor.b, 0.85f )
						: new Color( 1f, 1f, 1f, 0.55f );
					AddBorder( x0, y0, x1, y1, 0.30f, lineC );

					// 进度条(驻军/占领进度,旗/城堡下方)
					if ( frac > 0.01f )
					{
						var barTop = center.y + FlagWorldHeight * 0.62f + BarHeight;
						AddQuadShape( center.x - BarWidth * 0.5f, barTop - BarHeight, center.x + BarWidth * 0.5f, barTop, 0.35f,
							new Color( 0.16f, 0.15f, 0.22f, 0.5f ) );
						AddQuadShape( center.x - BarWidth * 0.5f, barTop - BarHeight, center.x - BarWidth * 0.5f + BarWidth * frac, barTop, 0.35f,
							new Color( teamColor.r, teamColor.g, teamColor.b, 0.95f ) );
					}
				}

				// 旗/城堡:大本营=城堡,普通格=旗子(rot=π/2 对齐屏幕正立)
				Color spriteTint = owner >= 0 ? new Color( teamColor.r, teamColor.g, teamColor.b, 1f )
					: contested ? new Color( 1f, 1f, 1f, 1f )
					: new Color( 0.72f, 0.74f, 0.82f, 0.45f );

				if ( isHq )
					AddTexQuad( _castles, center, castleHalf, castleHalf, MathF.PI * 0.5f, spriteTint, 0.52f );
				else
					AddTexQuad( _flags, center, flagHalfW, flagHalfH, MathF.PI * 0.5f, spriteTint, 0.52f );
			}
		}
	}

	static bool IsHq( int cx, int cy )
	{
		for ( var t = 0; t < 4; t++ )
		{
			var hq = TerritoryManager.HqCell( t );
			if ( hq.Cx == cx && hq.Cy == cy ) return true;
		}
		return false;
	}

	// ---- 顶点写入(shape=纯色 quad;tex=UV quad,写法同 NeonRenderer.AddQuad)----

	void AddQuadShape( float x0, float y0, float x1, float y1, float z, Color color )
	{
		var a = new Vertex( new Vector3( x0, y0, z ), color );
		var b = new Vertex( new Vector3( x1, y0, z ), color );
		var c = new Vertex( new Vector3( x1, y1, z ), color );
		var d = new Vertex( new Vector3( x0, y1, z ), color );
		_shape.AddVertex( a ); _shape.AddVertex( b ); _shape.AddVertex( c );
		_shape.AddVertex( a ); _shape.AddVertex( c ); _shape.AddVertex( d );
	}

	void AddBorder( float x0, float y0, float x1, float y1, float z, Color color )
	{
		// 四条粗边向内铺,不越过格界
		AddQuadShape( x0, y0, x1, y0 + BorderThickness, z, color );
		AddQuadShape( x0, y1 - BorderThickness, x1, y1, z, color );
		AddQuadShape( x0, y0 + BorderThickness, x0 + BorderThickness, y1 - BorderThickness, z, color );
		AddQuadShape( x1 - BorderThickness, y0 + BorderThickness, x1, y1 - BorderThickness, z, color );
	}

	void AddTexQuad( SceneDynamicObject batch, Vector3 center, float halfX, float halfY, float rot, Color color, float z )
	{
		var cos = MathF.Cos( rot );
		var sin = MathF.Sin( rot );
		var rx = cos * halfX; var ry = sin * halfX;      // right 轴
		var ux = -sin * halfY; var uy = cos * halfY;     // up 轴

		var v0 = new Vertex( new Vector3( center.x - rx - ux, center.y - ry - uy, z ), new Vector4( 0f, 0f, 0f, 0f ), color );
		var v1 = new Vertex( new Vector3( center.x + rx - ux, center.y + ry - uy, z ), new Vector4( 1f, 0f, 0f, 0f ), color );
		var v2 = new Vertex( new Vector3( center.x + rx + ux, center.y + ry + uy, z ), new Vector4( 1f, 1f, 0f, 0f ), color );
		var v3 = new Vertex( new Vector3( center.x - rx + ux, center.y - ry + uy, z ), new Vector4( 0f, 1f, 0f, 0f ), color );

		batch.AddVertex( v0 ); batch.AddVertex( v1 ); batch.AddVertex( v2 );
		batch.AddVertex( v0 ); batch.AddVertex( v2 ); batch.AddVertex( v3 );
	}
}