UI/LobbyPanel.cs
using System;
using System.Collections.Generic;
using Sandbox.UI;

/// <summary>
/// 游戏房间/大厅(M5 二级界面):HOST / VS BOTS 点开后进入——host 可调比赛参数、看玩家列表、
/// 点 START 开局;客户端 JOIN 连上后进同一界面,参数与玩家列表由 host 经 LobbyState RPC 推送,
/// 显示"等待房主开始",房主开局后经 MatchStarted RPC 自动进游戏。
/// 主菜单的设置区已移到这里(房间才是设置参数的地方)。
/// 纯 C# Panel + 同目录 LobbyPanel.cs.scss。
/// </summary>
public sealed class LobbyPanel : PanelComponent
{
	Label _status;
	Label[] _settings;       // 0=模式 1=时长 2=人数(host 可点击循环,client 只读展示)
	Label[] _playerRows;     // 玩家列表:2 列 × 12 行
	Label _botsNote;
	Label _start;
	Label _leave;
	Label _join;             // 客户端"JOIN GAME"按钮:对局进行中时显示,点击后 3-2-1 进入(v0.6.4.1)
	Label _countdown;        // 加入倒计时(客户端收到 host 开局指令后 3-2-1,v0.6.4.0)
	bool _matchRunning;      // 客户端视角:host 的对局进行中(JOIN GAME 按钮亮起)

	bool _built;
	bool _retryOpen;         // Panel 未就绪时的开门重试(EnsureWorld 会先于 OnStart 调用 ShowClient/ShowHost)
	bool _isHost;
	bool _shown;
	bool _connectionLost;    // 掉线重连中:状态行锁定提示(否则每帧 RefreshStatus 会覆盖掉)
	TimeSince _sinceListRefresh;

	// 客户端:房主推送的房间信息(LobbyState RPC)
	byte _clientMode;
	int _clientDuration;
	int _clientPlayers;
	List<string> _clientNames = new List<string>();

	const int RowsPerCol = 12;

	protected override void OnStart()
	{
		base.OnStart();
		BuildUi();
		Hide();
	}

	void BuildUi()
	{
		var root = Panel;
		if ( root is null ) return;
		if ( _built && _status.IsValid() && root.ChildrenCount > 0 ) return;

		root.DeleteChildren( true );

		root.Style.Position = PositionMode.Absolute;
		root.Style.Left = 0f;
		root.Style.Top = 0f;
		root.Style.Right = 0f;
		root.Style.Bottom = 0f;
		root.Style.BackgroundColor = new Color( 0.008f, 0.012f, 0.05f, 0.55f );   // 半透明:网格/背景对战透出来(用户反馈"黑幕")
		root.Style.PointerEvents = PointerEvents.All;

		var title = new Label() { Classes = "lp-title" };
		title.Text = "GAME LOBBY";
		root.AddChild( title );

		_status = new Label() { Classes = "lp-status" };
		root.AddChild( _status );

		var settingsHead = new Label() { Classes = "lp-head" };
		settingsHead.Text = "MATCH SETTINGS";
		root.AddChild( settingsHead );

		_settings = new Label[3];
		string[] ids = { "set-mode", "set-time", "set-players" };
		for ( int i = 0; i < _settings.Length; i++ )
		{
			var l = new Label() { Classes = $"lp-setting {ids[i]}" };
			int index = i;
			l.AddEventListener( "onclick", () => CycleSetting( index ) );
			root.AddChild( l );
			_settings[i] = l;
		}

		var playersHead = new Label() { Classes = "lp-head players" };
		playersHead.Text = "PLAYERS";
		root.AddChild( playersHead );

		// 玩家列表 2 列 × 12 行(左列 0..11,右列 12..23)
		_playerRows = new Label[RowsPerCol * 2];
		for ( int i = 0; i < _playerRows.Length; i++ )
		{
			var row = new Label() { Classes = "lp-player" };
			row.Style.Position = PositionMode.Absolute;
			row.Style.Left = i < RowsPerCol ? 560f : 1010f;
			row.Style.Width = 440f;
			row.Style.Top = 384f + ( i % RowsPerCol ) * 30f;
			root.AddChild( row );
			_playerRows[i] = row;
		}

		_botsNote = new Label() { Classes = "lp-note" };
		_botsNote.Text = "BOTS FILL REMAINING SLOTS AT START";
		root.AddChild( _botsNote );

		// 加入倒计时(客户端):收到 host 开局指令后显示 3-2-1
		_countdown = new Label() { Classes = "lp-countdown" };
		_countdown.Style.Display = DisplayMode.None;
		root.AddChild( _countdown );

		// JOIN GAME(客户端):对局进行中时点此进入(3-2-1 倒计时)
		_join = new Label() { Classes = "lp-join" };
		_join.Text = "JOIN GAME";
		_join.AddEventListener( "onclick", () => OnJoinClicked() );
		root.AddChild( _join );

		_start = new Label() { Classes = "lp-start" };
		_start.Text = "START GAME";
		_start.AddEventListener( "onclick", () => OnStartClicked() );
		root.AddChild( _start );

		_leave = new Label() { Classes = "lp-leave" };
		_leave.Text = "LEAVE";
		_leave.AddEventListener( "onclick", () => OnLeaveClicked() );
		root.AddChild( _leave );

		_built = true;
	}

	/// <summary> host 进入房间(HOST / VS BOTS 开房后调用) </summary>
	public void ShowHost()
	{
		_isHost = true;
		Open();
	}

	/// <summary> 客户端连入后进入房间(等待房主开局;列表等 LobbyState 推送) </summary>
	public void ShowClient()
	{
		_isHost = false;
		Open();
	}

	void Open()
	{
		BuildUi();
		if ( !_built )
		{
			_retryOpen = true;   // Panel 未就绪(EnsureWorld 立即调 ShowClient/ShowHost 时),OnUpdate 重试
			return;
		}

		_retryOpen = false;
		Panel.Style.Display = DisplayMode.Flex;
		if ( _shown )
		{
			// 已显示:不重置状态(ReconcileUi 每帧调,JOIN 按钮/倒计时不能被清掉)。
			// 但状态行要恢复——掉线重连成功回到房间时,"CONNECTION LOST" 不能一直挂着
			if ( !_isHost ) RefreshStatus();
			return;
		}
		_shown = true;
		_matchRunning = false;   // 每次进房间重置(MatchStarted 到达后再亮 JOIN 按钮)
		HideJoinCountdown();
		_sinceListRefresh = 1f;   // 首帧立即刷
		RefreshAll();
	}

	/// <summary> 收起(开局 / 回菜单) </summary>
	public void Hide()
	{
		_shown = false;
		_retryOpen = false;
		if ( Panel.IsValid() ) Panel.Style.Display = DisplayMode.None;
	}

	/// <summary> 客户端应用房主推送的房间信息(CircleroyaleGame.OnLobbyStateRemote 转发) </summary>
	public void UpdateFromHost( byte mode, int duration, int players, string[] names )
	{
		_connectionLost = false;   // host 有消息 = 连接活着,解除掉线提示
		_clientMode = mode;
		_clientDuration = duration;
		_clientPlayers = players;
		_clientNames = names is not null ? new List<string>( names ) : new List<string>();

		if ( _shown && !_isHost )
		{
			RefreshSettings();
			RefreshPlayers();
		}
	}

	/// <summary> 显示加入倒计时(CircleroyaleGame 每帧调用) </summary>
	public void ShowJoinCountdown( float secondsLeft )
	{
		if ( _countdown is null || !_countdown.IsValid() ) return;

		_countdown.Style.Display = DisplayMode.Flex;
		_countdown.Text = $"JOINING MATCH IN {MathF.Ceiling( MathF.Max( 0f, secondsLeft ) ):0}";

		// 倒计时期间收起 JOIN 按钮
		if ( _join.IsValid() ) _join.Style.Display = DisplayMode.None;
	}

	/// <summary> 收起加入倒计时 </summary>
	public void HideJoinCountdown()
	{
		if ( _countdown.IsValid() ) _countdown.Style.Display = DisplayMode.None;
	}

	/// <summary> 客户端视角:host 的对局进行中(房间页亮起 JOIN GAME 按钮) </summary>
	public void SetMatchRunning()
	{
		_matchRunning = true;
		RefreshButtons();

		SetText( _status, "MATCH IN PROGRESS — JOIN NOW" );
	}

	/// <summary> 文本防抖(v0.7.1.2):值没变就不赋——Label.Text 每帧重设(哪怕同值)会触发布局
	/// 重算;ReconcileUi 每帧调 ShowClient→RefreshStatus 把状态行反复重刷 = UI 一闪一闪(实测) </summary>
	static void SetText( Label l, string text )
	{
		if ( !l.IsValid() || l.Text == text ) return;
		l.Text = text;
	}

	/// <summary> 掉线自动重连中(CircleroyaleGame.OnDisconnectRecoveryStarted):状态行提示 + 收掉 JOIN 按钮 </summary>
	public void SetConnectionLost()
	{
		_connectionLost = true;
		SetText( _status, "CONNECTION LOST — RECONNECTING ..." );
		if ( _join.IsValid() ) _join.Style.Display = DisplayMode.None;
	}

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

		BuildUi();

		// EnsureWorld 先于 OnStart 调过 ShowClient/ShowHost 且 Panel 未就绪 → 此刻补开
		if ( _retryOpen && !_shown )
		{
			Open();
			return;
		}

		if ( !_shown ) return;

		if ( Panel.IsValid() ) Panel.Style.Display = DisplayMode.Flex;
		Mouse.Visibility = MouseVisibility.Visible;   // 按钮/设置项要能点

		// host:玩家进出、设置变化本地就能看到,0.5s 轮刷;client 列表靠 LobbyState 推送
		if ( _isHost && _sinceListRefresh > 0.5f )
		{
			_sinceListRefresh = 0;
			RefreshPlayers();
			RefreshStatus();
		}
	}

	void RefreshAll()
	{
		RefreshSettings();
		RefreshPlayers();
		RefreshStatus();
		RefreshButtons();
	}

	void RefreshStatus()
	{
		if ( _isHost )
		{
			var names = CircleroyaleGame.Current?.LobbyPlayerNames();
			int n = names?.Count ?? 1;
			SetText( _status, $"ROOM OPEN — {n} PLAYER{( n > 1 ? "S" : "" )} IN LOBBY" );
		}
		else if ( _connectionLost )
		{
			SetText( _status, "CONNECTION LOST — RECONNECTING ..." );
		}
		else
		{
			SetText( _status, _matchRunning ? "MATCH IN PROGRESS — JOIN NOW" : "WAITING FOR HOST TO START ..." );
		}
	}

	void RefreshSettings()
	{
		if ( _settings is null || _settings[0] is null || !_settings[0].IsValid() ) return;

		byte mode;
		float duration;
		int players;

		if ( _isHost )
		{
			mode = (byte)MatchState.PendingMode;
			duration = MatchState.PendingDuration;
			players = MatchState.PendingPlayerTarget;
		}
		else
		{
			mode = _clientMode;
			duration = _clientDuration;
			players = _clientPlayers;
		}

		SetText( _settings[0], (MatchState.Mode)mode switch
		{
			MatchState.Mode.Ffa => "MODE — FREE FOR ALL",
			MatchState.Mode.Team2 => "MODE — TEAM (2 / TEAM)",
			MatchState.Mode.Team3 => "MODE — TEAM (3 / TEAM)",
			_ => "MODE — TEAM (4 / TEAM)",
		} );

		SetText( _settings[1], duration < 60f
			? $"TIME — {duration:0} SEC"
			: $"TIME — {duration / 60f:0} MIN" );

		SetText( _settings[2], $"PLAYERS — {players}" );
	}

	void RefreshPlayers()
	{
		if ( _playerRows is null || _playerRows[0] is null || !_playerRows[0].IsValid() ) return;

		List<string> names;
		if ( _isHost )
		{
			names = CircleroyaleGame.Current?.LobbyPlayerNames() ?? new List<string>();
		}
		else
		{
			names = _clientNames;
		}

		if ( names.Count == 0 ) names = new List<string> { "..." };

		for ( int i = 0; i < _playerRows.Length; i++ )
		{
			var row = _playerRows[i];
			if ( !row.IsValid() ) continue;

			if ( i >= names.Count )
			{
				SetText( row, "" );
				continue;
			}

			var tag = i == 0 ? "  (HOST)" : "";
			SetText( row, $"{i + 1}.  {names[i]}{tag}" );
			row.Style.FontColor = i == 0 ? new Color( 0.21f, 0.94f, 1f ) : new Color( 1f, 1f, 1f, 0.75f );
		}
	}

	void RefreshButtons()
	{
		// host 有 START;客户端有 JOIN GAME(对局进行中才亮)。LEAVE 两边都有
		if ( _start.IsValid() )
			_start.Style.Display = _isHost ? DisplayMode.Flex : DisplayMode.None;

		if ( _join.IsValid() )
			_join.Style.Display = !_isHost && _matchRunning ? DisplayMode.Flex : DisplayMode.None;
	}

	/// <summary> JOIN GAME 点击(实例方法:热重载可重映射) </summary>
	void OnJoinClicked() => CircleroyaleGame.Current?.ClientJoinMatch();

	/// <summary> START 点击 </summary>
	void OnStartClicked() => CircleroyaleGame.Current?.StartMatchFromLobby();

	/// <summary> LEAVE 点击 </summary>
	void OnLeaveClicked() => CircleroyaleGame.Current?.ResetToMenu();

	// ---- host 调参(与主菜单时代的循环逻辑一致)----

	void CycleSetting( int index )
	{
		if ( !_isHost ) return;

		switch ( index )
		{
			case 0:
				// 模式 4 档循环(v0.6.5.0):普通赛 → 2 人队 → 3 人队 → 4 人队
				MatchState.PendingMode = (MatchState.Mode) ( ( (int)MatchState.PendingMode + 1 ) % 4 );
				break;

			case 1:
				MatchState.PendingDuration = NextChoice( GameConfig.MatchDurationChoices, MatchState.PendingDuration );
				break;

			case 2:
				MatchState.PendingPlayerTarget = NextIntChoice( GameConfig.PlayerTargetChoices, MatchState.PendingPlayerTarget );
				break;
		}

		RefreshSettings();
		CircleroyaleGame.Current?.MarkLobbyDirty();   // 让等待中的客户端看到新参数
	}

	static float NextChoice( float[] choices, float current )
	{
		for ( int i = 0; i < choices.Length; i++ )
		{
			if ( MathF.Abs( choices[i] - current ) < 0.5f )
				return choices[( i + 1 ) % choices.Length];
		}
		return choices[0];
	}

	static int NextIntChoice( int[] choices, int current )
	{
		for ( int i = 0; i < choices.Length; i++ )
		{
			if ( choices[i] == current )
				return choices[( i + 1 ) % choices.Length];
		}
		return choices[0];
	}
}