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;
}
}
}
}
}