Game/PowerUpManager.cs
using System;
using System.Collections.Generic;

/// <summary>
/// 道具管理(v0.7.3.0):**host 权威 + 静态 RPC 同步**,与 FoodManager 同款模式
/// (纯数据实体不走网络对象——运行时 live create 不复制到客户端,实测)。
/// 固定槽位制:场上恒 PowerUpOnField 个槽,被捡后同槽延时重刷(换种类换位置),
/// 全量同步只有一个小数组,中途加入/热重载恢复都便宜。
/// 拾取主球与分身都算(v0.7.8.6,存主人背包);效果在拾取瞬间应用到 Ball(buff 挂玩家,质量罐即时加质量)。
/// Init/Tick 仅权威端调用;Apply* 仅供客户端应用远端事件。
/// </summary>
public sealed class PowerUpManager
{
	/// <summary> 道具种类(HUD/渲染/效果 switch 共用;None=255 作 Ball.BuffKind 的"无 buff"哨兵) </summary>
	public enum Kind : byte
	{
		Speed = 0,    // ⚡ 加速:8s 移速 ×1.4(黄)
		Magnet = 1,   // 🧲 磁铁:8s 吸食半径翻倍(青)
		Mass = 2,     // 💊 质量罐:+300 即时(品红)
		Shield = 3,   // 🛡 护盾:5s 不可被大球吃(蓝)
		Spike = 4,    // 💥 尖刺分身:生成跟随主人 20s 的尖刺(紫绿),撞破比自己大的球奖 3s 无敌
	}

	public const byte None = 255;

	/// <summary> 槽位渲染色(与霓虹调色板独立——道具要一眼和食物区分开) </summary>
	public static Color ColorOf( byte kind )
	{
		switch ( kind )
		{
			case (byte)Kind.Speed: return new Color( 1.00f, 0.85f, 0.25f );
			case (byte)Kind.Magnet: return new Color( 0.30f, 0.90f, 1.00f );
			case (byte)Kind.Mass: return new Color( 1.00f, 0.40f, 0.70f );
			case (byte)Kind.Shield: return new Color( 0.40f, 0.55f, 1.00f );
			case (byte)Kind.Spike: return new Color( 0.55f, 1.00f, 0.45f );   // 亮绿(野刺紫=障碍,绿色=可操纵的武器)
			default: return new Color( 0.6f, 0.6f, 0.6f );
		}
	}

	/// <summary> HUD/图例用的道具名 </summary>
	public static string NameOf( byte kind )
	{
		switch ( kind )
		{
			case (byte)Kind.Speed: return "SPEED";
			case (byte)Kind.Magnet: return "MAGNET";
			case (byte)Kind.Shield: return "SHIELD";
			case (byte)Kind.Spike: return "SPIKE";
			case (byte)Kind.Mass: return "+MASS";
			default: return "";
		}
	}

	/// <summary> 道具槽(渲染与 HUD 读;host 权威,客户端为事件镜像) </summary>
	public sealed class Slot
	{
		public bool Alive;
		public byte Kind;
		public Vector2 Pos;
		public TimeSince SincePicked;   // 仅 host:重刷计时
	}

	readonly List<Slot> _slots = new();

	/// <summary> 喂食共享积分池(按槽,仅 host):谁吐丝都往里充,积分 ≥ 本次喂食者体重×PowerUpFeedRatio 即触发给喂食者——
	/// 大球喂到一半会被小球几口抢走(虎口夺食,v0.7.4.0 用户定稿) </summary>
	float[] _feedScores = Array.Empty<float>();

	/// <summary> Q 技能充能计时(按 SteamId,仅 host;v0.7.8.30 Q 改随时间充能,E 仍拾取) </summary>
	readonly Dictionary<long, TimeSince> _qCharge = new();

	/// <summary> Q 槽当前持有技能的球(冷却暂停标记,检测"放走"的转变) </summary>
	readonly HashSet<long> _qHeld = new();

	/// <summary> 全部槽位(NeonRenderer 每帧读,各端内容一致) </summary>
	public IReadOnlyList<Slot> Slots => _slots;

	/// <summary> 客户端/热重载后本地列表未就绪(不渲染也不误判) </summary>
	public bool Ready => _slots.Count > 0;

	/// <summary> host:生成全部槽位并广播全量(开局/演示赛调用) </summary>
	public void Init()
	{
		_slots.Clear();
		ClearCharge();   // 局间清账(v0.7.8.87):见 ClearCharge 注释
		for ( int i = 0; i < GameConfig.PowerUpOnField; i++ )
		{
			_slots.Add( new Slot
			{
				Alive = true,
				Kind = RandomKind(),
				Pos = RandomPos(),
			} );
		}

		if ( Networking.IsActive ) NetworkManager.PowerUpFull( ToWire() );
		_feedScores = new float[_slots.Count];
	}

	/// <summary> 清空(回主菜单;客户端经 PowerUpFull 空数组同步) </summary>
	public void Reset()
	{
		_slots.Clear();
		_feedScores = Array.Empty<float>();
		ClearCharge();
	}

	/// <summary> 清 Q 充能/持有时长记账(v0.7.8.87):`_qCharge` 按 SteamId 记而 `TimeSince` 一直在走,
	/// 不随局清空的话,上一局充能到一半的玩家在新局开局几乎立刻白拿一个技能 </summary>
	void ClearCharge()
	{
		_qCharge.Clear();
		_qHeld.Clear();
	}

	/// <summary> host:拾取判定(主球 + 分身共用)+ 到期槽位重刷。仅权威端每帧调用。
	/// v0.7.5.0 起拾取**存入背包**(bot 例外:拾取即用)——背包满则道具留在场上不被捡;
	/// v0.7.8.6 起分身(SplitPiece)也走同一判定,捡到存主人背包 </summary>
	public void Tick( List<Ball> balls, List<CellPiece> cells = null )
	{
		if ( _slots.Count == 0 ) return;

		foreach ( var ball in balls )
		{
			if ( !ball.IsValid() || !ball.Alive ) continue;
			PickupPass( ball, ball.WorldPosition, ball.Radius );
		}

		// 分身拾取(v0.7.8.6):捡到存主人背包(同一 Q/E 背包);孢子不捡(中性食物团),
		// 尖刺分身不捡(武器不兼职取货)。主人死亡的分身下一帧 TickCells 才清,这里顺带过滤。
		// ⚠️ 倒序 for:bot 拿着(20s 后转正的)尖刺分身踩到尖刺道具 → 拾取即用 →
		// SpawnSpikeMinion 往 _cells 追加——foreach 枚举中改集合会炸(v0.7.8.20 修)
		if ( cells is not null )
		{
			for ( int i = cells.Count - 1; i >= 0; i-- )
			{
				var c = cells[i];
				if ( c.PieceKind != CellPiece.Kind.SplitPiece ) continue;
				if ( !c.OwnerBall.IsValid() || !c.OwnerBall.Alive ) continue;
				PickupPass( c.OwnerBall, c.Pos, c.Radius );
			}
		}

		// 空槽重刷
		for ( int i = 0; i < _slots.Count; i++ )
		{
			var s = _slots[i];
			if ( s.Alive || s.SincePicked < GameConfig.PowerUpRefillSeconds ) continue;

			s.Alive = true;
			s.Kind = RandomKind();
			s.Pos = RandomPos();
			if ( i < _feedScores.Length ) _feedScores[i] = 0f;   // 重刷积分清零(培养进度不作数)
			if ( Networking.IsActive ) NetworkManager.PowerUpRespawned( i, s.Kind, s.Pos.x, s.Pos.y );
		}

		// Q 技能随时间充能(v0.7.8.30 用户定稿):真人球 Q 槽空时按**当前质量**计时充能
		// (15~60s,越轻越快),到点自动发一个随机技能。bot 拾取即用无背包,不参与;
		// 持有未放时冷却暂停(技能躺在槽里等按键)。计时按 SteamId 记(跨热重载稳定)。
		// 领土模式(M7.4):Q 键让给职业技能——充能 pass 冻结清零,PowerA 保持空槽
		if ( MatchState.IsTerritory )
		{
			_qCharge.Clear();
			_qHeld.Clear();
			return;
		}
		foreach ( var b in balls )
		{
			if ( !b.IsValid() || !b.Alive || b.IsBot ) continue;

			var sid = b.OwnerSteamId;
			if ( b.PowerA != None )
			{
				_qHeld.Add( sid );   // 技能待放:冷却暂停
				continue;
			}
			if ( _qHeld.Remove( sid ) )
			{
				_qCharge[sid] = 0;   // 刚放走/复活清空:冷却从现在起算
				continue;
			}
			if ( !_qCharge.TryGetValue( sid, out var ts ) )
			{
				_qCharge[sid] = 0;   // 首见(入局)
				continue;
			}
			if ( ts < GameConfig.QSkillCooldownFor( b.Mass ) ) continue;

			_qCharge[sid] = 0;
			b.PowerA = RandomKind();
			if ( GameSfx.IsMine( b.OwnerSteamId ) )
			{
				GameSfx.Pickup();
				CircleroyaleGame.Current?.Hud?.AddBanner( $"{NameOf( b.PowerA )} READY (Q)", ColorOf( b.PowerA ) );
			}
			// 客户端玩家的 Q 到点:横幅+音效走广播(mode2),host 回声由 OnPowerBannerRemote 的 IsAuthority 挡掉(v0.7.8.31)
			if ( Networking.IsActive ) NetworkManager.PowerBanner( b.PowerA, b.OwnerSteamId, 2 );
			GameLog.Info( $"[game] q skill granted: {NameOf( b.PowerA )} -> {b.PlayerName}" );
		}
	}

	/// <summary>
	/// 一个"拾取体"(主球或分身)扫一遍槽位。bot 拾取即用;真人存入 Q/E 背包
	/// (效果等按键释放),背包满则道具留在场上。判定圈封顶反马太(大球要刻意碾上去)。
	/// </summary>
	void PickupPass( Ball receiver, Vector3 pos, float radius )
	{
		// 拾取判定封顶(v0.7.4.0 反马太):大球不再"路过顺走"——判定圈按封顶半径算,
		// 必须刻意碾到道具正上方才吃得到;小球反而轻松
		var rr = MathF.Min( radius, GameConfig.PowerUpPickRadiusCap ) + GameConfig.PowerUpRadius;

		for ( int i = 0; i < _slots.Count; i++ )
		{
			var s = _slots[i];
			if ( !s.Alive ) continue;

			var dx = s.Pos.x - pos.x;
			var dy = s.Pos.y - pos.y;
			if ( dx * dx + dy * dy > rr * rr ) continue;

			if ( receiver.IsBot )
			{
				Consume( i, receiver );   // bot 不囤:拾取即触发效果(banner 由 Consume 内 IsMine 挡掉)
			}
			else if ( receiver.StorePower( s.Kind ) >= 0 )
			{
				// 真人:存入 Q/E 背包槽(效果等玩家按键释放),道具槽重刷
				s.Alive = false;
				s.SincePicked = 0;

				NeonRenderer.Spark( new Vector3( s.Pos.x, s.Pos.y, 0f ), ColorOf( s.Kind ) );
				if ( GameSfx.IsMine( receiver.OwnerSteamId ) )
				{
					GameSfx.EatFood( new Vector3( s.Pos.x, s.Pos.y, 0f ) );
					GameAchievements.PowerStored();   // 成就:累计捡 5 个入包(host 本机侧)
				}
				if ( Networking.IsActive ) NetworkManager.PowerUpPicked( i, s.Kind, receiver.OwnerSteamId );

				ShowBanner( receiver, s.Kind, stored: true );
				GameLog.Info( $"[game] powerup stored: {NameOf( s.Kind )} -> {receiver.PlayerName}" );
			}
			// 背包满:不消耗,道具留在场上(下一颗球/下个帧再判)
		}
	}

	/// <summary> 消耗道具并触发效果(bot 拾取即用 / 喂食触发共用路径;槽重刷) </summary>
	void Consume( int index, Ball receiver )
	{
		if ( (uint)index >= (uint)_slots.Count ) return;
		var s = _slots[index];
		if ( !s.Alive ) return;

		s.Alive = false;
		s.SincePicked = 0;
		if ( index < _feedScores.Length ) _feedScores[index] = 0f;

		ApplyEffect( receiver, s.Kind );
		NeonRenderer.Spark( new Vector3( s.Pos.x, s.Pos.y, 0f ), ColorOf( s.Kind ) );
		if ( GameSfx.IsMine( receiver.OwnerSteamId ) )
			GameSfx.EatFood( new Vector3( s.Pos.x, s.Pos.y, 0f ) );

		ShowBanner( receiver, s.Kind, stored: false );
		if ( Networking.IsActive ) NetworkManager.PowerUpPicked( index, s.Kind, receiver.OwnerSteamId );
	}

	/// <summary>
	/// 个人高光横幅(v0.7.6.0):host 本地播自己的 + 广播给其他端(客户端按 IsMine 再显示)。
	/// stored=true "XXX STORED"(捡到入背包)/ false 激活文案(喂养触发/bot 拾取即用)。
	/// v0.7.7.0:横幅配套"叮咚"音(本机耳朵)。
	/// </summary>
	void ShowBanner( Ball receiver, byte kind, bool stored )
	{
		var text = stored ? $"{NameOf( kind )} STORED" : GameHud.ActivationText( kind );
		if ( GameSfx.IsMine( receiver.OwnerSteamId ) )
		{
			CircleroyaleGame.Current?.Hud?.AddBanner( text, ColorOf( kind ) );
			GameSfx.Pickup();
		}

		if ( Networking.IsActive ) NetworkManager.PowerBanner( kind, receiver.OwnerSteamId, (byte)( stored ? 0 : 1 ) );
	}

	/// <summary>
	/// host:孢子喂道具("血量池")——共享积分池按槽累计(谁吐丝都往里充),
	/// 积分 ≥ 本次喂食者当前体重 × PowerUpFeedRatio 即触发效果给喂食者(**立即生效,不入背包**)。
	/// 大球要喂到 5%×大体重才轮到自己达标,小球几口就够——中途截胡即"虎口夺食"。
	/// feeder 无效(断线/死亡瞬间)只充能不判定。
	/// </summary>
	public void Feed( Ball feeder, int index, float blobMass )
	{
		if ( (uint)index >= (uint)_slots.Count || !_slots[index].Alive ) return;
		if ( index >= _feedScores.Length ) Array.Resize( ref _feedScores, _slots.Count );

		_feedScores[index] += blobMass;

		if ( feeder is null || !feeder.IsValid() || !feeder.Alive ) return;
		if ( _feedScores[index] < feeder.Mass * GameConfig.PowerUpFeedRatio ) return;

		var kind = _slots[index].Kind;
		Consume( index, feeder );
		GameAchievements.FeedTriggered( feeder.OwnerSteamId );   // 成就:首次喂食触发(host 本机侧)
		GameLog.Info( $"[game] powerup fed to trigger: {NameOf( kind )} -> {feeder.PlayerName}" );
	}

	/// <summary> 道具效果(host 权威;质量罐即时到账,其余挂 buff,尖刺分身在游戏系统生成)。
	/// 拾取存背包(v0.7.5.0):真人按键 UsePower 时才走到这里 </summary>
	public static void ApplyEffect( Ball ball, byte kind )
	{
		switch ( kind )
		{
			case (byte)Kind.Speed:
				ball.ApplyBuff( kind, GameConfig.PowerUpSpeedSeconds );
				break;
			case (byte)Kind.Magnet:
				ball.ApplyBuff( kind, GameConfig.PowerUpMagnetSeconds );
				break;
			case (byte)Kind.Shield:
				ball.ApplyBuff( kind, GameConfig.PowerUpShieldSeconds );
				break;
			case (byte)Kind.Mass:
				ball.Mass = MathF.Min( GameConfig.MaxMass, ball.Mass + GameConfig.PowerUpMassBonus );
				break;
			case (byte)Kind.Spike:
				CircleroyaleGame.Current?.SpawnSpikeMinion( ball );
				break;
		}

		GameLog.Info( $"[game] powerup picked: {NameOf( kind )} -> {ball.PlayerName}" );
	}

	byte RandomKind() => (byte)Game.Random.Float( 0f, 4.999f );

	Vector2 RandomPos()
	{
		var half = GameConfig.ArenaHalfSize - GameConfig.PowerUpMargin;
		return new Vector2( Game.Random.Float( -half, half ), Game.Random.Float( -half, half ) );
	}

	/// <summary> 全量快照(开局补发/新连接定向用;host 侧调用) </summary>
	public PowerUpWire[] ToWireForSync() => ToWire();

	PowerUpWire[] ToWire()
	{
		var wires = new PowerUpWire[_slots.Count];
		for ( int i = 0; i < _slots.Count; i++ )
		{
			var s = _slots[i];
			wires[i] = new PowerUpWire
			{
				Kind = s.Alive ? s.Kind : None,
				X = s.Pos.x,
				Y = s.Pos.y,
			};
		}
		return wires;
	}

	// ---- 客户端应用(远端事件 → 本地槽位)----

	public void ApplyFull( PowerUpWire[] wires )
	{
		_slots.Clear();
		if ( wires is null ) return;

		foreach ( var w in wires )
		{
			_slots.Add( new Slot
			{
				Alive = w.Kind != None,
				Kind = w.Kind,
				Pos = new Vector2( w.X, w.Y ),
			} );
		}
	}

	public void ApplyPicked( int index )
	{
		if ( (uint)index >= (uint)_slots.Count ) return;   // 热重载后未同步全量前,忽略增量
		_slots[index].Alive = false;
	}

	public void ApplyRespawned( int index, byte kind, Vector2 pos )
	{
		if ( (uint)index >= (uint)_slots.Count ) return;

		var s = _slots[index];
		s.Alive = true;
		s.Kind = kind;
		s.Pos = pos;
	}
}