Rendering/NeonRenderer.cs
using System;

/// <summary>
/// 霓虹批渲染:每帧重写两组顶点(线段 / 三角形),全场实体一帧两个 DrawCall。
/// 球的构图按用户要求:<b>玩家头像为圆心,科幻线条为外侧</b>——
/// M0 内芯用低透明度色盘占位,M3 换成 Steam 头像四边形(Texture.LoadAvatar,bot 用几何图案)。
/// </summary>
	public sealed class NeonRenderer : Component
	{
		SceneDynamicObject _lines;
		SceneDynamicObject _tris;

		/// <summary> 全局引用(特效生成入口 NeonRenderer.Pop/Spark/Puff 走静态调用) </summary>
		public static NeonRenderer Instance { get; private set; }

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

			Instance = this;
			_lines = CreateBatch();
			_tris = CreateBatch();
		}

		protected override void OnDestroy()
		{
			base.OnDestroy();
			if ( Instance == this ) Instance = null;
		}

	SceneDynamicObject CreateBatch()
	{
		var dyno = new SceneDynamicObject( Scene.SceneWorld );
		dyno.Material = Material.FromShader( "shaders/line.shader" );   // 顶点色 + 加色混合
		dyno.Flags.CastShadows = false;
		dyno.RenderLayer = SceneRenderLayer.OverlayWithDepth;
		dyno.Attributes.SetCombo( "D_BLEND", 1 );                       // SrcAlpha/One 加色,发光感
		return dyno;
	}

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

		var game = CircleroyaleGame.Current;
		if ( game is null || _lines is null || _tris is null ) return;

		_lines.Clear();
		_lines.Init( Graphics.PrimitiveType.Lines );
		_tris.Clear();
		_tris.Init( Graphics.PrimitiveType.Triangles );

		UpdateParticles();

		foreach ( var ball in game.Balls )
		{
			if ( !ball.IsValid() || !ball.Alive ) continue;
			WriteBall( game, ball );

			// 瞄准箭头(v0.7.0.0 用户需求):本机球外缘小三角指向光标——分裂/吐孢子方向指示。
			// 仅对局中显示(房间页停泊的球不画,箭头方向已冻结没有意义)
			if ( game.IsMatchStarted && ball == game.LocalBall && ball.AimDir.Length > 0.01f )
				WriteAimArrow( ball );
		}

		// 分身/孢子(纯数据实体,非 GameObject):分身=无头像迷你球,孢子=小盘
		foreach ( var c in game.Cells )
		{
			var color = GameConfig.Palette[Math.Clamp( c.ColorIndex, 0, GameConfig.Palette.Length - 1 )];
			var pos = new Vector3( c.DrawPos.x, c.DrawPos.y, 0f );
			var r = c.Radius;

			if ( c.PieceKind == CellPiece.Kind.EjectedMass )
			{
				AddDisc( _tris, pos, r * 0.85f, color * 0.5f, 0.9f, 10 );
				AddRing( _lines, pos, r, color * 0.7f, 14 );
			}
			else
			{
				AddDisc( _tris, pos, r * 0.92f, color * 0.32f, 0.9f, GameConfig.DiscSegments );
				AddRing( _lines, pos, r, color * 0.9f, GameConfig.RingSegments );
			}
		}

		// 尖刺:12 角星形(撞刺炸裂的障碍;小的从底下穿过)。**紫色——用户定稿**:
		// 绿色顶点色在此管线里帧间表现不稳定(同一代码一会儿绿一会儿品红),紫/品红族始终稳定
		var spikes = game.Spikes;
		if ( spikes is not null )
		{
			var purple = new Color( 0.85f, 0.45f, 1f );
			foreach ( var s in spikes )
			{
				// 实心(用户定稿):填充拉满,加色混合下星形内部完整发亮,不再只是描边+淡芯
				WriteStar( new Vector3( s.x, s.y, 0f ), GameConfig.SpikeRadius, purple, purple * 0.85f );
			}
		}

		// 食物:低模小圆盘(道具 M4+ 按 Type 换形状/图标)
		var foods = game.Food?.Foods;
		if ( foods is not null )
		{
			// 视口剔除(v0.6.3.0):3 倍场地 4140 颗食物只有 ~20% 在画面里——
			// 用相机世界包围盒(正交半高 + 竖向余量按宽高比折算)跳过画面外的
			var palette = GameConfig.Palette;
			var fr = GameConfig.FoodRadius;
			var cullCx = Scene.Camera.WorldPosition.x;
			var cullCy = Scene.Camera.WorldPosition.y;
			var cullHw = Scene.Camera.OrthographicHeight * 0.5f * 2.4f + 64f;   // 宽余量按最大宽高比
			var cullHh = Scene.Camera.OrthographicHeight * 0.5f + 64f;

			for ( int i = 0; i < foods.Length; i++ )
			{
				if ( !foods[i].Alive ) continue;

				var fx = foods[i].Pos.x;
				var fy = foods[i].Pos.y;
				if ( MathF.Abs( fx - cullCx ) > cullHw || MathF.Abs( fy - cullCy ) > cullHh ) continue;

				var c = palette[foods[i].ColorIndex % palette.Length] * 0.55f;
				AddDisc( _tris, new Vector3( fx, fy, 0f ), fr, c, 0.75f, GameConfig.FoodSegments );
			}
		}

		DrawParticles();
	}

	// ---- CPU 粒子(M3 表现力):走同一霓虹加色批次,风格统一、零资产依赖 ----

	/// <summary> 瞄准箭头(v0.7.0.0):本机球外缘的实心小三角,指向光标(分裂/吐孢子方向)。
	/// agar 式贴着球缘外侧,白色半透明;尺寸随球半径微缩放,不随球转 </summary>
	void WriteAimArrow( Ball ball )
	{
		var dir = ball.AimDir;
		var c = ball.WorldPosition;
		var r = ball.Radius;

		var len = Math.Clamp( r * 0.22f, 18f, 64f );   // 箭头长度随球径微缩放
		var halfW = len * 0.42f;
		var gap = r * 1.04f;

		var dir3 = new Vector3( dir.x, dir.y, 0f );
		var perp = new Vector3( -dir.y, dir.x, 0f );

		var baseC = c + dir3 * gap;
		var tip = c + dir3 * ( gap + len );
		var p1 = baseC + perp * halfW;
		var p2 = baseC - perp * halfW;

		AddTri( _tris, p1, p2, tip, new Color( 1f, 1f, 1f, 0.7f ) );
	}

	/// <summary> 单个实心三角 </summary>
	void AddTri( SceneDynamicObject batch, Vector3 a, Vector3 b, Vector3 c, Color color )
	{
		batch.AddVertex( new Vertex( a, color ) );
		batch.AddVertex( new Vertex( b, color ) );
		batch.AddVertex( new Vertex( c, color ) );
	}

	sealed class Particle
	{
		public Vector3 Pos;
		public Vector3 Vel;
		public float Life;
		public float MaxLife;
		public float Size;
		public Color Color;
	}

	readonly List<Particle> _particles = new();

	/// <summary> 爆裂(死亡/撞刺):中大粒子径向飞散 </summary>
	public static void Pop( Vector3 pos, int colorIndex, float radius )
	{
		Spawn( pos, colorIndex, 26, 90f + radius * 0.9f, 0.45f, 0.9f, 3f, 9f );
	}

	/// <summary> 火花(吃食物):几颗小碎屑一闪 </summary>
	public static void Spark( Vector3 pos, int colorIndex )
	{
		Spawn( pos, colorIndex, 5, 100f, 0.2f, 0.4f, 2f, 4f );
	}

	/// <summary> 喷发(分裂/吐孢子生成瞬间) </summary>
	public static void Puff( Vector3 pos, int colorIndex )
	{
		Spawn( pos, colorIndex, 8, 150f, 0.25f, 0.5f, 2f, 5f );
	}

	static void Spawn( Vector3 pos, int colorIndex, int count, float maxSpeed, float minLife, float maxLife, float minSize, float maxSize )
	{
		var inst = Instance;
		if ( inst is null || !inst.IsValid() ) return;

		var color = GameConfig.Palette[Math.Clamp( colorIndex, 0, GameConfig.Palette.Length - 1 )];

		for ( int i = 0; i < count; i++ )
		{
			if ( inst._particles.Count >= GameConfig.FxMaxParticles )
				inst._particles.RemoveAt( 0 );   // 挤掉最老的,保帧率优先

			var a = Game.Random.Float( 0f, MathF.PI * 2f );
			var sp = maxSpeed * Game.Random.Float( 0.35f, 1f );
			var life = Game.Random.Float( minLife, maxLife );
			inst._particles.Add( new Particle
			{
				Pos = pos + new Vector3( Game.Random.Float( -6f, 6f ), Game.Random.Float( -6f, 6f ), 0f ),
				Vel = new Vector3( MathF.Cos( a ) * sp, MathF.Sin( a ) * sp, 0f ),
				Life = life,
				MaxLife = life,
				Size = Game.Random.Float( minSize, maxSize ),
				Color = color,
			} );
		}
	}

	void UpdateParticles()
	{
		var dt = Time.Delta;
		for ( int i = _particles.Count - 1; i >= 0; i-- )
		{
			var p = _particles[i];
			p.Life -= dt;
			if ( p.Life <= 0f )
			{
				_particles.RemoveAt( i );
				continue;
			}
			p.Pos += p.Vel * dt;
			p.Vel *= MathF.Exp( -3.5f * dt );   // 阻尼,飞散后减速悬浮消散
		}
	}

	void DrawParticles()
	{
		foreach ( var p in _particles )
		{
			var a = p.Life / p.MaxLife;
			AddDisc( _tris, p.Pos, p.Size * ( 0.5f + a * 0.5f ), p.Color * ( 0.25f + a * 0.75f ), 1.2f, 6 );
		}
	}

	void WriteBall( CircleroyaleGame game, Ball ball )
	{
		var c = ball.WorldPosition;
		var r = ball.Radius;
		var color = ball.NeonColor;

		// 出生保护:整球一闪一闪(用户定稿)——方波闪烁,暗相保留 35% 不至于看不见
		var blink = 1f;
		if ( ball.IsProtected )
			blink = MathF.Sin( Time.Now * GameConfig.SpawnBlinkHz * MathF.PI * 2f ) > 0f ? 1f : 0.35f;

		// 内芯:实心圆背景填充,颜色取自玩家头像主色(bot 用霓虹配色)。
		// z 抬高保证盖在网格之上、外环之下
		AddDisc( _tris, c, r * 0.92f, ball.BackgroundColor * ( ball.IsProtected ? 0.25f * blink : 0.45f ), 1f, GameConfig.DiscSegments );

		// 外环:霓虹线圈
		AddRing( _lines, c, r, color * 0.9f * ( ball.IsProtected ? blink : 1f ), GameConfig.RingSegments );

		// 护盾虚环:出生保护 = 闪烁亮环(v0.6.2.0 起分身不再护体,护体暗环移除)
		if ( ball.IsProtected )
		{
			DrawShieldRing( c, r, color * 0.5f * blink );
		}
	}

	void DrawShieldRing( Vector3 c, float r, Color shield )
	{
		var r2 = r * 1.25f;
		var seg = GameConfig.RingSegments;
		for ( int i = 0; i < seg; i += 2 )
		{
			var a0 = i * MathF.PI * 2f / seg;
			var a1 = ( i + 1 ) * MathF.PI * 2f / seg;
			_lines.AddVertex( new Vertex( RingPoint( c, r2, a0 ), shield ) );
			_lines.AddVertex( new Vertex( RingPoint( c, r2, a1 ), shield ) );
		}
	}

	/// <summary> 绿刺星形:12 角内尖星,描边 + 低透明度填充(固定相位不旋转) </summary>
	void WriteStar( Vector3 c, float r, Color edge, Color fill )
	{
		const int points = 12;
		const float inner = 0.55f;
		var n = points * 2;
		var center = c + Vector3.Up * 0.85f;
		var pts = new Vector3[n];
		for ( int i = 0; i < n; i++ )
		{
			var rr = ( i % 2 == 0 ) ? r : r * inner;
			var a = i * MathF.PI * 2f / n + 0.26f;
			pts[i] = c + new Vector3( MathF.Cos( a ) * rr, MathF.Sin( a ) * rr, 0.85f );
		}

		for ( int i = 0; i < n; i++ )
		{
			var p2 = pts[( i + 1 ) % n];
			_tris.AddVertex( new Vertex( center, fill ) );
			_tris.AddVertex( new Vertex( pts[i], fill ) );
			_tris.AddVertex( new Vertex( p2, fill ) );

			_lines.AddVertex( new Vertex( pts[i], edge ) );
			_lines.AddVertex( new Vertex( p2, edge ) );
		}
	}

	static Vector3 RingPoint( Vector3 c, float r, float a )
	{
		return c + new Vector3( MathF.Cos( a ) * r, MathF.Sin( a ) * r, 2f );
	}

	/// <summary> 霓虹圆环(线段批):球外环 / 分身外环 / 孢子描边公用 </summary>
	static void AddRing( SceneDynamicObject dyno, Vector3 c, float r, Color color, int segments )
	{
		segments = Math.Max( 3, segments );
		var prev = RingPoint( c, r, 0f );
		for ( int i = 1; i <= segments; i++ )
		{
			var p = RingPoint( c, r, i * MathF.PI * 2f / segments );
			dyno.AddVertex( new Vertex( prev, color ) );
			dyno.AddVertex( new Vertex( p, color ) );
			prev = p;
		}
	}

	static void AddDisc( SceneDynamicObject dyno, Vector3 c, float r, Color color, float z, int segments )
	{
		segments = Math.Max( 3, segments );
		var center = c + Vector3.Up * z;
		var prev = c + new Vector3( r, 0f, z );
		for ( int i = 1; i <= segments; i++ )
		{
			var p = RingPoint( c, r, i * MathF.PI * 2f / segments );
			p.z = z;
			dyno.AddVertex( new Vertex( center, color ) );
			dyno.AddVertex( new Vertex( prev, color ) );
			dyno.AddVertex( new Vertex( p, color ) );
			prev = p;
		}
	}
}