Jump/JumpHud.cs
using System;
using Sandbox.UI;
// board

/// <summary>
/// 跳一跳 HUD:开始菜单(AI 背景演示)、计分、连击提示、操作提示、新手演示字幕、结束画面、难度角标。
/// 用纯 C# Panel 构建(编辑器热重载不会为新加的 .razor 重新编译,C# 面板可以即改即生效),
/// 样式在同名样式表 JumpHud.cs.scss 里(PanelComponent 会按"类名.cs.scss"自动加载)。
/// </summary>
public sealed class JumpHud : PanelComponent
{
	static readonly Color TextColor = new Color( 0.21f, 0.24f, 0.28f );
	static readonly Color SubTextColor = new Color( 0.45f, 0.49f, 0.54f );
	static readonly Color AccentColor = new Color( 0.91f, 0.35f, 0.05f );

	Label _score;
	Label _streak;
	Label _hint;
	Panel _tutorialWrap;
	Label _tutorialText;
	Panel _gameOver;
	Label _gameOverScore;
	Panel _menuWrap;
	Label _menuBest;
	Label _difficultyLabel;
	Panel _boardWrap;
	Panel[] _localRows;
	Label[] _localRank, _localAvatarChar, _localNameLabels, _localScore;
	Panel[] _localAvatar;
	Panel[] _cloudRows;
	Label[] _cloudRank, _cloudAvatarChar, _cloudName, _cloudScore;
	Panel[] _cloudAvatar;
	const int BoardRows = 5;
	static string _localName;

	/// <summary> 本地玩家名字(拿不到就显示"你") </summary>
	static string LocalName()
	{
		if ( _localName is not null ) return _localName;
		try { _localName = string.IsNullOrWhiteSpace( Connection.Local?.DisplayName ) ? "You" : Connection.Local.DisplayName; }
		catch { _localName = "You"; }
		return _localName;
	}

	static readonly Color[] AvatarColors =
	{
		new Color( 0.86f, 0.47f, 0.28f ),
		new Color( 0.36f, 0.62f, 0.86f ),
		new Color( 0.44f, 0.72f, 0.50f ),
		new Color( 0.72f, 0.50f, 0.86f ),
		new Color( 0.90f, 0.72f, 0.30f ),
	};
	bool _menuBoardRequested;
	bool _built;

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

	void BuildUi()
	{
		var root = Panel;
		if ( root is null ) return;

		// 已建好且所有引用有效 → 不重复建。
		// 热重载可能保留旧实例(老字段有效但新增字段为 null),或构建中途被打断
		// (数组半满),任何一项失效都整体清空重建
		var healthy = _built && _score.IsValid() && _menuBest.IsValid() && _menuWrap.IsValid()
			&& _tutorialWrap.IsValid()
			&& ( _localAvatarChar?.All( x => x is not null ) ?? false )
			&& ( _localNameLabels?.All( x => x is not null ) ?? false )
			&& ( _cloudName?.All( x => x is not null ) ?? false )
			&& root.ChildrenCount > 0;
		if ( healthy ) 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.PointerEvents = PointerEvents.None;

		_score = new Label() { Classes = "score" };

		_streak = new Label() { Classes = "streak" };
		_streak.Style.Display = DisplayMode.None;

		_hint = new Label() { Classes = "hint" };
		_hint.Text = "Hold Left Mouse to charge  ·  release to jump  ·  Q — Menu";
		_hint.Style.Display = DisplayMode.None;

		_tutorialWrap = new Panel() { Classes = "tutorial" };
		_tutorialText = new Label() { Classes = "tutorial-text" };
		_tutorialWrap.AddChild( _tutorialText );
		_tutorialWrap.Style.Display = DisplayMode.None;

		_gameOver = new Panel() { Classes = "gameover" };
		_gameOver.Style.Display = DisplayMode.None;

		var title = new Label() { Classes = "title" };
		title.Text = "GAME OVER";
		_gameOver.AddChild( title );

		_gameOverScore = new Label() { Classes = "finalscore" };
		_gameOver.AddChild( _gameOverScore );

		_boardWrap = new Panel() { Classes = "board-wrap" };

		var localCol = new Panel() { Classes = "board-col" };
		var localTitle = new Label() { Classes = "board-title" };
		localTitle.Text = "Local Best";
		localCol.AddChild( localTitle );
		_localRows = new Panel[BoardRows];
		_localRank = new Label[BoardRows];
		_localAvatar = new Panel[BoardRows];
		_localAvatarChar = new Label[BoardRows];
		_localNameLabels = new Label[BoardRows];
		_localScore = new Label[BoardRows];
		for ( int i = 0; i < BoardRows; i++ )
		{
			( _localRows[i], _localRank[i], _localAvatar[i], _localAvatarChar[i], _localNameLabels[i], _localScore[i] ) = MakeBoardRow( localCol );
		}
		_boardWrap.AddChild( localCol );

		var cloudCol = new Panel() { Classes = "board-col" };
		var cloudTitle = new Label() { Classes = "board-title" };
		cloudTitle.Text = "Global Best";
		cloudCol.AddChild( cloudTitle );
		_cloudRows = new Panel[BoardRows];
		_cloudRank = new Label[BoardRows];
		_cloudAvatar = new Panel[BoardRows];
		_cloudAvatarChar = new Label[BoardRows];
		_cloudName = new Label[BoardRows];
		_cloudScore = new Label[BoardRows];
		for ( int i = 0; i < BoardRows; i++ )
		{
			( _cloudRows[i], _cloudRank[i], _cloudAvatar[i], _cloudAvatarChar[i], _cloudName[i], _cloudScore[i] ) = MakeBoardRow( cloudCol );
		}
		_boardWrap.AddChild( cloudCol );

		_gameOver.AddChild( _boardWrap );

		var goHint = new Label() { Classes = "sub" };
		goHint.Text = "Click to restart  ·  Q — Menu";
		_gameOver.AddChild( goHint );

		_menuWrap = new Panel() { Classes = "menu" };

		var menuTitle = new Label() { Classes = "menu-title" };
		menuTitle.Text = "Jump Jump";
		_menuWrap.AddChild( menuTitle );

		var menuSub = new Label() { Classes = "menu-sub" };
		menuSub.Text = "Select Difficulty  ·  press 1 / 2 / 3";
		_menuWrap.AddChild( menuSub );

		var menuBtnRow = new Panel() { Classes = "menu-btn-row" };
		AddMenuButton( menuBtnRow, "Easy", JumpDifficulty.Easy );
		AddMenuButton( menuBtnRow, "Normal", JumpDifficulty.Normal );
		AddMenuButton( menuBtnRow, "Hard", JumpDifficulty.Hard );
		_menuWrap.AddChild( menuBtnRow );

		_menuBest = new Label() { Classes = "menu-best" };
		_menuWrap.AddChild( _menuBest );

		_difficultyLabel = new Label() { Classes = "diff" };

		root.AddChild( _score );
		root.AddChild( _streak );
		root.AddChild( _hint );
		root.AddChild( _tutorialWrap );
		root.AddChild( _gameOver );
		root.AddChild( _difficultyLabel );
		root.AddChild( _menuWrap );

		_built = true;
	}

	void AddMenuButton( Panel parent, string text, JumpDifficulty difficulty )
	{
		var btn = new Label() { Classes = "menu-btn" };
		btn.Text = text;
		btn.AddEventListener( "onclick", () =>
		{
			Sound.Play( "sounds/kenney/ui/ui.button.press.sound" );
			JumpGame.Current?.StartGame( difficulty );
		} );
		parent.AddChild( btn );
	}

	static (Panel row, Label rank, Panel avatar, Label avatarChar, Label name, Label score) MakeBoardRow( Panel parent )
	{
		var row = new Panel() { Classes = "board-row" };

		var rank = new Label() { Classes = "cell-rank" };
		row.AddChild( rank );

		var avatar = new Panel() { Classes = "avatar" };
		var avatarChar = new Label() { Classes = "avatar-char" };
		avatar.AddChild( avatarChar );
		row.AddChild( avatar );

		var name = new Label() { Classes = "cell-name" };
		row.AddChild( name );

		var score = new Label() { Classes = "cell-score" };
		row.AddChild( score );

		parent.AddChild( row );
		return ( row, rank, avatar, avatarChar, name, score );
	}

	static void SetVisible( Panel panel, bool visible )
	{
		panel.Style.Display = visible ? DisplayMode.Flex : DisplayMode.None;
	}

	protected override void OnUpdate()
	{
		BuildUi();

		var game = JumpGame.Current;
		if ( game is null || !_built || _score is null || _tutorialText is null || _localAvatarChar is null ) return;

		_score.Text = game.Score.ToString();
		_streak.Text = $"PERFECT ×{game.Streak}";
		_gameOverScore.Text = game.Score.ToString();
		_tutorialText.Text = game.TutorialText ?? "";

		var inMenu = game.InMenu;

		_difficultyLabel.Text = game.Difficulty switch
		{
			JumpDifficulty.Easy => "Easy",
			JumpDifficulty.Normal => "Normal",
			_ => "Hard"
		} + "  ·  Q — Menu";

		// 回到菜单时拉一次云端榜(异步、尽力而为)
		if ( inMenu && !_menuBoardRequested )
		{
			_menuBoardRequested = true;
			_ = JumpScores.RefreshCloud();
		}
		else if ( !inMenu )
		{
			_menuBoardRequested = false;
		}

		_menuBest.Text = JumpScores.Best > 0 ? $"Best {JumpScores.Best}" : "Set your first record!";

		// 开始菜单盖住游戏 HUD,只保留背景演示
		SetVisible( _menuWrap, inMenu );
		SetVisible( _score, !game.IsGameOver && !inMenu );
		SetVisible( _streak, !game.IsGameOver && game.Streak >= 2 && !inMenu );
		SetVisible( _hint, !game.HasJumped && !game.IsGameOver && !game.IsTutorial && !inMenu );
		SetVisible( _tutorialWrap, !inMenu && !string.IsNullOrEmpty( game.TutorialText ) );
		SetVisible( _gameOver, game.IsGameOver && !inMenu );
		SetVisible( _difficultyLabel, !inMenu );

		// 结算画面显示排行榜:左本地 / 右平台
		// 只显示当前难度的本地排行(头像块 = 本地玩家)
		try
		{
			var top = JumpScores.Top( BoardRows, game.Difficulty );
			for ( int i = 0; i < BoardRows; i++ )
			{
				var row = _localRows[i];
				if ( i < top.Count )
				{
					var e = top[i];
					var name = LocalName();
					row.Style.Display = DisplayMode.Flex;
					_localRank[i].Text = $"{i + 1}.";
					_localAvatarChar[i].Text = name.Substring( 0, 1 );
					_localAvatar[i].Style.BackgroundColor = AvatarColors[Math.Abs( name.GetHashCode() ) % AvatarColors.Length];
					_localNameLabels[i].Text = name;
					_localScore[i].Text = e.Score.ToString();
				}
				else
				{
					row.Style.Display = DisplayMode.None;
				}
			}
		}
		catch ( Exception )
		{
			_built = false;   // 结构异常 → 下帧整体重建
		}

		// 平台排行:本地包没有云端身份时显示占位
		if ( JumpScores.Cloud.Count == 0 )
		{
			_cloudRows[0].Style.Display = DisplayMode.Flex;
			_cloudRank[0].Text = "";
			_cloudAvatar[0].Style.Display = DisplayMode.None;
			_cloudName[0].Text = "No records yet";
			_cloudScore[0].Text = "";
			for ( int i = 1; i < BoardRows; i++ ) _cloudRows[i].Style.Display = DisplayMode.None;
		}
		else
		{
			for ( int i = 0; i < BoardRows; i++ )
			{
				var row = _cloudRows[i];
				if ( i < JumpScores.Cloud.Count )
				{
					var (rank, name, score) = JumpScores.Cloud[i];
					row.Style.Display = DisplayMode.Flex;
					_cloudRank[i].Text = $"{rank}.";
					_cloudAvatarChar[i].Text = string.IsNullOrEmpty( name ) ? "?" : name.Substring( 0, 1 );
					_cloudAvatar[i].Style.BackgroundColor = AvatarColors[Math.Abs( name.GetHashCode() ) % AvatarColors.Length];
					_cloudName[i].Text = name;
					_cloudScore[i].Text = score.ToString();
				}
				else
				{
					row.Style.Display = DisplayMode.None;
				}
			}
		}
	}
}