Player/Ball.cs
using System;

/// <summary>
/// 球(真人/bot 共用):**owner 本地模拟移动**(真人=键盘输入,bot=host 的 BotBrain),
/// 权威数值(Mass/IsBot/ColorIndex/PlayerName/OwnerSteamId)由 host 经 [Sync(FromHost)] 下发,
/// 位置随网络 transform 自动同步(owner → 其他端)。
/// 视觉:霓虹线条由 NeonRenderer 批量绘制;头像为本地 SpriteRenderer(各端按 OwnerSteamId 自取)。
/// </summary>
public sealed class Ball : Component
{
	[Sync( SyncFlags.FromHost )] public float Mass { get; set; } = GameConfig.StartMass;

	[Sync( SyncFlags.FromHost )] public bool IsBot { get; set; }

	[Sync( SyncFlags.FromHost )] public int ColorIndex { get; set; }

	[Sync( SyncFlags.FromHost )] public string PlayerName { get; set; } = "Player";

	/// <summary> 拥有者 SteamId(bot 为 0);host 写入,各端据此加载对应头像 </summary>
	[Sync( SyncFlags.FromHost )] public long OwnerSteamId { get; set; }

	/// <summary> 队伍号(团队赛 0..N-1,普通赛 -1);host 开局轮转分配,重生不变 </summary>
	[Sync( SyncFlags.FromHost )] public int TeamIndex { get; set; } = -1;

	/// <summary>
	/// 是否存活。**球对象永不销毁**:死亡 = Alive=false(同步字段,渲染/AI/判定全部跳过),
	/// 重生 = host 置回 true。绕开"进行中 NetworkSpawn 不复制到已连接客户端"的引擎行为(实测大坑)。
	/// </summary>
	[Sync( SyncFlags.FromHost )]
	public bool Alive { get; set; } = true;

	public float Radius => GameConfig.StartRadius * MathF.Sqrt( Mass / GameConfig.StartMass );

	/// <summary> 本球的霓虹色(由 ColorIndex 从调色板取) </summary>
	public Color NeonColor => GameConfig.Palette[Math.Clamp( ColorIndex, 0, GameConfig.Palette.Length - 1 )];

	public float Speed => MathF.Max( GameConfig.MinSpeed,
		GameConfig.BaseSpeed * MathF.Pow( GameConfig.StartMass / Mass, GameConfig.SpeedCurve ) );

	/// <summary> 是否出生保护中(虚环护盾表现,双向免战) </summary>
	public bool IsProtected => _sinceSpawn < GameConfig.SpawnProtectSeconds;

	/// <summary> 球体背景填充色:真人=头像主色取样,bot=霓虹配色 </summary>
	public Color BackgroundColor => _avatarColor ?? NeonColor;

	Vector3 _velocity;
	TimeSince _sinceSpawn;
	SpriteRenderer _avatar;
	Color? _avatarColor;
	Vector2 _botDir;
	Vector2 _lastMoveDir = Vector2.Right;   // 静止时分裂/吐孢子的方向兜底
	Vector2 _lastAimDir = Vector2.Zero;     // 光标瞄准方向(分裂/吐孢子用,v0.7.0.0)

	/// <summary> 当前瞄准方向(本机球:渲染外缘箭头用;bot 恒 Zero 不画) </summary>
	public Vector2 AimDir => _lastAimDir;
	TimeSince _sinceEject;                  // 客户端按住连吐的节流(host 还有同款校验)
	TimeSince _sinceAvatarRetry = 1f;   // 首帧立即尝试
	int _avatarRetryCount;
	bool _avatarReady;
	bool _wasAlive = true;

	/// <summary> 死亡时刻(host 记录,用于 bot 原地复活的延时) </summary>
	public TimeSince SinceDeath { get; private set; }

	/// <summary>
	/// host 侧标记:这颗球是为远程连接生成的(仅权威端有意义,不联网)。
	/// ScanBalls/SetLocalBall 用它把远程球(含竞态下未网络化的幽灵球)排除出本机球候选,
	// 否则同账号双开时幽灵球会顶掉 host 自己的球、相机来回跳(实测)。
	/// </summary>
	public bool IsRemoteOwned { get; set; }

	/// <summary> host 侧标记死亡(对象保留,原地复用) </summary>
	public void MarkDead()
	{
		Alive = false;
		SinceDeath = 0;
		_velocity = Vector3.Zero;
	}

	/// <summary> 重置出生保护计时(host 在玩家真正入局时调用——pre-snapshot 球的
	/// 保护期在握手期间就空跑完了,实测加入 1.6s 即被吃;IsProtected 按各端本地计时判定,
	/// host 重置即恢复权威判定,客户端自己的计时随快照应用另算) </summary>
	public void RefreshSpawnProtection() => _sinceSpawn = 0;

	/// <summary> 瞬移(分身晋升主球时用):位置归 owner 模拟,owner 端必须自己搬过去;
	/// 不触发保护期/重播音效——这是身份延续,不是重生 </summary>
	public void TeleportTo( Vector3 pos )
	{
		WorldPosition = new Vector3( pos.x, pos.y, 0f );
		_velocity = Vector3.Zero;
		_lastMoveDir = Vector2.Zero;
	}

	/// <summary> host 侧初始化(仅权威端调用;客户端字段全部走网络同步)。
	/// teamIndex ≥ 0 时同时写入队伍号;团队赛颜色锁队伍色,普通赛随机霓虹色 </summary>
	public void Init( bool isBot, int teamIndex = -1 )
	{
		IsBot = isBot;
		if ( teamIndex >= 0 ) TeamIndex = teamIndex;

		ColorIndex = MatchState.IsTeam
			? ( ( TeamIndex % GameConfig.Palette.Length ) + GameConfig.Palette.Length ) % GameConfig.Palette.Length
			: (int)Game.Random.Float( 0f, GameConfig.Palette.Length - 0.001f );

		_sinceSpawn = 0;
		_velocity = Vector3.Zero;
	}

	/// <summary> bot 由 BotBrain 每帧写入的移动方向(真人走 BallInput,不用这个) </summary>
	public void SetBotDirection( Vector2 dir ) => _botDir = dir;

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

		// 所有机器统一从网络对象的 OnStart 注册(host 生成 / 客户端随复制出现)
		CircleroyaleGame.Current?.RegisterBall( this );
		if ( !IsProxy && !IsBot )
			CircleroyaleGame.Current?.SetLocalBall( this );

		// 头像圆心(用户决策:头像作圆心,霓虹线条作外侧)
		var go = new GameObject( true, "Avatar" );
		go.Parent = GameObject;

		_avatar = go.AddComponent<SpriteRenderer>();
		_avatar.Lighting = false;
		_avatar.Shadows = false;

		TryLoadAvatar();
	}

	/// <summary>
	/// 加载头像:bot 用占位图;真人按 OwnerSteamId 拉 Steam 头像
	/// (下载有延迟,失败每秒重试,15 次后转占位图)。
	/// </summary>
	void TryLoadAvatar()
	{
		if ( IsBot )
		{
			// 几何图案头像:按名字+色号确定性生成(各端算出同一张,零同步)
			if ( string.IsNullOrEmpty( PlayerName ) ) return;   // 名字同步未到,等重试

			Texture tex = null;
			try
			{
				tex = BotAvatar.GetOrCreate( PlayerName, ColorIndex );
			}
			catch ( Exception e )
			{
				Log.Warning( $"bot avatar generate failed for '{PlayerName}': {e.Message}" );
			}
			ApplyAvatarTexture( tex ?? Texture.Load( GameConfig.AvatarPlaceholderPath, false ) );
			_avatarReady = true;
			return;
		}

		if ( OwnerSteamId == 0 ) return;   // 同步未到,等下一轮重试

		var avatar = Texture.LoadAvatar( OwnerSteamId, 128 );
		if ( avatar is null ) return;

		ApplyAvatarTexture( avatar );
		_avatarReady = true;
	}

	void ApplyAvatarTexture( Texture tex )
	{
		if ( tex is null )
		{
			Log.Warning( $"avatar load failed: '{PlayerName}' bot={IsBot} steam={OwnerSteamId}" );
			_avatar.Enabled = false;
			return;
		}

		_avatar.Sprite = Sprite.FromTexture( tex );   // Texture setter 已废弃,必须走 Sprite 资源
		_avatarColor = IsBot ? null : SampleAverageColor( tex );
	}

	/// <summary> 头像主色:全图不透明像素的均值(用作球底填充色) </summary>
	static Color? SampleAverageColor( Texture tex )
	{
		try
		{
			var pixels = tex.GetPixels( 0 );
			if ( pixels is null || pixels.Length == 0 ) return null;

			float r = 0f, g = 0f, b = 0f;
			int n = 0;
			for ( int i = 0; i < pixels.Length; i++ )
			{
				var p = pixels[i];
				if ( p.a < 32 ) continue;
				r += p.r;
				g += p.g;
				b += p.b;
				n++;
			}
			if ( n == 0 ) return null;

			return new Color( r / n / 255f, g / n / 255f, b / n / 255f );
		}
		catch
		{
			return null;
		}
	}

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

		// 头像直径 = 半径(占玩家形状直径的 50%,用户指定),居中;
		// 死亡时一并隐藏——此前只藏了霓虹环,头像方块留在原地像"尸体"(实测)
		if ( _avatar.IsValid() )
		{
			var d = Radius;
			_avatar.Size = new Vector2( d, d );
			_avatar.Enabled = Alive;
		}

		// 头像重试:每秒一次,15 次未拿到转占位图(bot 也走:名字/色号同步可能晚于 OnStart)
		if ( !_avatarReady && _sinceAvatarRetry > 1f )
		{
			_sinceAvatarRetry = 0;
			if ( _avatarRetryCount++ >= 15 )
			{
				ApplyAvatarTexture( Texture.Load( GameConfig.AvatarPlaceholderPath, false ) );
				_avatarReady = true;
			}
			else
			{
				TryLoadAvatar();
			}
		}

		// ---- 分裂/吐孢子输入(M4.11:每帧路径,仅本机真人、活球)----
		// 权威端直接执行;客户端发实例 RPC([Rpc.Host],host 侧校验后执行,分身是 host 权威数据)。
		// 音效在按键瞬间即播(本地即时反馈;host 校验不过顶多空响一次)
		if ( IsProxy || IsRemoteOwned || IsBot || !Alive ) return;
		if ( MatchState.MatchOver ) return;   // 比赛结束:全场冻结,输入不响应

		// 停在等待页(P 退局/掉线恢复)且不在对局时停止操控:球留在原地,防隔屏盲开。
		// ⚠️ 必须带 !IsMatchStarted:只要还在对局(_gameStarted)就绝不禁输入——
		// _lobbyWaiting 若因异常残留为 true,会把对局中的移动/分裂/吐孢子/瞄准全锁死
		// (实测:不能分身/吐丝、瞄准箭头冻在旧方向)
		{
			var g = CircleroyaleGame.Current;
			if ( g is not null && g.IsClientWaiting && !g.IsMatchStarted )
			{
				_lastMoveDir = Vector2.Zero;
				return;
			}
		}

		var inputDir = BallInput.GetMoveDir( Scene.Camera );
		if ( inputDir.Length > 0.001f ) _lastMoveDir = inputDir;

		// 瞄准(v0.7.0.0 用户需求):分裂/吐孢子指向光标,光标正贴球心时回退移动方向
		var aimDir = BallInput.GetAimDir( Scene.Camera, WorldPosition );
		if ( aimDir.Length > 0.001f ) _lastAimDir = aimDir;
		var actionDir = _lastAimDir.Length > 0.001f ? _lastAimDir : _lastMoveDir;

		if ( GameConfig.EnableSplit && Input.Pressed( "Jump" ) )
		{
			var splitDir = actionDir;
			if ( Mass >= GameConfig.SplitMinMass ) GameSfx.Split( WorldPosition );

			if ( NetworkManager.IsAuthority )
				CircleroyaleGame.Current?.DoSplit( this, splitDir );
			else
				RequestSplit( splitDir );
		}

		if ( GameConfig.EnableEject && Input.Down( "Reload" ) && _sinceEject > GameConfig.EjectCooldownSeconds )
		{
			_sinceEject = 0;
			var ejectDir = actionDir;
			if ( Mass >= 2f ) GameSfx.Eject( WorldPosition );   // 至少能吐最小一颗(下限 1 + 消耗 1)

			if ( NetworkManager.IsAuthority )
				CircleroyaleGame.Current?.DoEject( this, ejectDir );
			else
				RequestEject( ejectDir );
		}
	}

	protected override void OnFixedUpdate()
	{
		if ( IsProxy ) return;   // 只有拥有者(真人)或 host(bot)模拟
		if ( IsRemoteOwned ) return;   // host 侧远程球由其主人模拟;未网络化的幽灵球绝不本地模拟

		// 死亡状态:停住不动(对象保留);Alive 翻回 true 时本地重生(随机换位+保护期)
		if ( !Alive )
		{
			_velocity = Vector3.Zero;
			_wasAlive = false;
			return;
		}
		if ( !_wasAlive )
		{
			_wasAlive = true;
			_sinceSpawn = 0;          // 复活保护期(本地表现;权威保护期在 host 的 Init 里)
			_velocity = Vector3.Zero;
			WorldPosition = RandomSpawnPos();   // owner 自己随机换位(位置归 owner 模拟)
			GameSfx.Respawn( WorldPosition );
			GameSfx.ResetEatCount();   // 下一条命吃食物音从 0 重新计
			Log.Info( $"[ball] local respawn '{PlayerName}'" );
		}

		// 比赛结束:全场冻结(结算面板接管,谁也不许再动)
		if ( MatchState.MatchOver )
		{
			_velocity = Vector3.Zero;
			return;
		}

		// 真人 = 本地输入(owner 模拟);bot = BotBrain 写入的方向。
		// 分裂/吐孢子的输入判定已移到 OnUpdate(每帧路径)——Input 按键边沿按渲染帧刷新,
		// 固定步进的 FixedUpdate 可能错过 Pressed 边沿(按了没反应的隐患)
		var dir = IsBot ? _botDir : BallInput.GetMoveDir( Scene.Camera );
		if ( dir.Length > 0.001f ) _lastMoveDir = dir;

		// 速度与转向都随质量衰减(批18①):体积越大极速越低、惯性越强转向越笨拙,
		// 大球必须提前预判走位。响应时间常数 = BaseTurnSpeed × (StartMass/Mass)^TurnCurve
		var turn = MathF.Max( GameConfig.MinTurnSpeed,
			GameConfig.BaseTurnSpeed * MathF.Pow( GameConfig.StartMass / Mass, GameConfig.TurnCurve ) );
		var t = 1f - MathF.Exp( -turn * Time.Delta );
		_velocity = Vector3.Lerp( _velocity, dir * Speed, t );

		var pos = WorldPosition + _velocity * Time.Delta;

		// 场地边界钳制(半径异常涨破场地时 half 兜底为 0,绝不让 Clamp 的 min>max 抛异常)
		var half = MathF.Max( 0f, GameConfig.ArenaHalfSize - Radius );
		pos.x = Math.Clamp( pos.x, -half, half );
		pos.y = Math.Clamp( pos.y, -half, half );
		pos.z = 0f;

		WorldPosition = pos;
	}

	/// <summary> 场地内出生点(owner 复活时本地选位):走游戏系统的安全采样,无系统时纯随机兜底 </summary>
	static Vector3 RandomSpawnPos()
	{
		var game = CircleroyaleGame.Current;
		if ( game is not null ) return game.RandomSpawnPos();

		var half = GameConfig.ArenaHalfSize - 128f;
		return new Vector3( Game.Random.Float( -half, half ), Game.Random.Float( -half, half ), 0f );
	}

	// ---- 分裂/吐孢子 RPC(M3):客户端 → host,host 校验归属后执行 ----

	[Rpc.Host]
	public void RequestSplit( Vector2 dir )
	{
		var caller = Rpc.Caller;
		if ( caller is not null && caller.SteamId.Value != OwnerSteamId ) return;   // 只能操作自己的球

		CircleroyaleGame.Current?.DoSplit( this, dir );
	}

	[Rpc.Host]
	public void RequestEject( Vector2 dir )
	{
		var caller = Rpc.Caller;
		if ( caller is not null && caller.SteamId.Value != OwnerSteamId ) return;

		CircleroyaleGame.Current?.DoEject( this, dir );
	}

	protected override void OnDestroy()
	{
		base.OnDestroy();
		CircleroyaleGame.Current?.RemoveBall( this );
	}
}