A Razor UI panel for the game's leaderboard. It renders dropdowns and a scrollable list of leaderboard entries, formats entry visuals (rank colors, avatars, values), handles input scrolling, refreshes leaderboard data from Sandbox.Services.Leaderboards, and opens run details for a selected entry.
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@using System.Text.Json;
@inherits Panel
@attribute [StyleSheet("LeaderboardPanel.razor.scss")]
<root>
<div class="header_container">
@* <div class="@(GetTitleClass(Manager.Instance.Difficulty))"></div> *@
<div class="dropdown_container">
<DropDown class="leaderboard_mode_dropdown" Value:bind=@LeaderboardModeValue Options=@LeaderboardModeOptions></DropDown>
<DropDown class="date_range_dropdown" Value:bind=@DateRangeValue Options=@DateRangeOptions></DropDown>
<DropDown class="friends_only_dropdown" Value:bind=@ShowFriendsValue Options=@ShowFriendsOptions></DropDown>
</div>
<div class="button_container">
@if(LeaderboardModeValue == LeaderboardMode.FastestWin || LeaderboardModeValue == LeaderboardMode.LongestSurvival)
{
<button class="toggle_button players_button @(GetNumPlayersButtonClass())" onclick=@(() => ClickNumPlayersButton())></button>
}
</div>
</div>
<div class="entries_container" style="height: @((IsOnMainMenu ? 900 : 780))px;">
<div class="entries">
@{
if(Leaderboard is null || IsLoading)
{
<div class="loading">Loading...</div>
return;
}
if(_cachedEntries.Count <= 0)
{
<div class="loading">No entries.</div>
return;
}
var entries = _cachedEntries;
var entryHeight = 42;
var index = 0;
}
@for(int i = StartIndex; i < Math.Min(StartIndex + MaxVisible, entries.Count); i++)
{
var entry = entries[i];
@{
var isVictory = entry.Value > Manager.VICTORY_OFFSET / 2f;
var isBossDefeat = LeaderboardModeValue == LeaderboardMode.FastestWin && !isVictory && entry.Value >= Manager.BOSS_DEFEAT_OFFSET;
var rankT = Utils.Map(i + 1, 0, 40, 0f, 1f, EasingType.QuadOut);
var rank = i + 1;
Color nameColor;
if ( rank == 1 ) nameColor = new Color(1f, 0.84f, 0f);
else if ( rank == 2 ) nameColor = new Color(0.75f, 0.75f, 0.85f);
else if ( rank == 3 ) nameColor = new Color(0.8f, 0.5f, 0.2f);
else nameColor = Color.Lerp(new Color(0.95f, 0.95f, 0.95f), new Color(0.7f, 0.7f, 0.7f), rankT);
var valueColor = isVictory
? Color.White.Rgba
: (isBossDefeat ? new Color(0.8f, 0.47f, 0.27f).Rgba : new Color(0.55f, 0.25f, 0.25f).Rgba);
var bgColorStr = index % 2 == 0 ? "background-color:#00000077;" : "";
}
@* <div class="entry" style="height: @(entryHeight)px; color:@(color.Rgba); @(bgColorStr)" onclick=@(() => ShowRunData(entry))> *@
<div class="entry" style="height: @(entryHeight)px; color:@(nameColor.Rgba); @(bgColorStr)">
<label class="rank">##@(rank)</label>
@if(entry.SteamId != 0)
{
LeaderboardPlayerIcon.LeaderboardIconType iconType = LeaderboardPlayerIcon.LeaderboardIconType.None;
if( LeaderboardModeValue == LeaderboardMode.FastestWin )
iconType = isVictory? LeaderboardPlayerIcon.LeaderboardIconType.Victory : LeaderboardPlayerIcon.LeaderboardIconType.Defeat;
var playerLevel = entry.Data != null ? Utils.GetIntFromDictionary(entry.Data, "player_level", -1) : -1;
int numPlayers = entry.Data != null ? Utils.GetIntFromDictionary(entry.Data, "num_players", 1) : 1;
<div class="avatar_container">
<LeaderboardPlayerIcon PlayerInfo=@(new PlayerInfo(entry.DisplayName, entry.SteamId, playerLevel)) ShowTooltip=@(numPlayers > 1) Icon=@iconType />
@if(numPlayers > 1 && LeaderboardModeValue == LeaderboardMode.FastestWin)
{
long otherPlayerId0 = Utils.GetLongFromDictionary(entry.Data, "other_player_id_0", 0);
long otherPlayerId1 = Utils.GetLongFromDictionary(entry.Data, "other_player_id_1", 0);
@if(otherPlayerId0 > 0)
{
string otherPlayerName0 = Utils.GetStringFromDictionary(entry.Data, "other_player_name_0", "");
int otherPlayerLevel0 = Utils.GetIntFromDictionary(entry.Data, "other_player_level_0", -1);
<LeaderboardPlayerIcon PlayerInfo=@(new PlayerInfo( otherPlayerName0, otherPlayerId0, otherPlayerLevel0 )) IsSmall=@(true) ShowTooltip=@true />
}
@if(otherPlayerId1 > 0)
{
string otherPlayerName1 = Utils.GetStringFromDictionary(entry.Data, "other_player_name_1", "");
int otherPlayerLevel1 = Utils.GetIntFromDictionary(entry.Data, "other_player_level_1", -1);
<LeaderboardPlayerIcon PlayerInfo=@(new PlayerInfo( otherPlayerName1, otherPlayerId1, otherPlayerLevel1 )) IsSmall=@(true) ShowTooltip=@true />
}
}
</div>
}
<label class="name">@entry.DisplayName</label>
@if(false && entry.Data != null) // disabled for now
{
@if(!isVictory && entry.Data.ContainsKey("boss_life_removed_pct"))
{
var bossLifePct = Utils.GetFloatFromDictionary(entry.Data, "boss_life_removed_pct");
<label class="value">Boss: @(bossLifePct.ToString("0"))%</label>
}
else
{
<label class="value">@Utils.FormatTime(Utils.GetFloatFromDictionary(entry.Data, "run_time"))</label>
}
@* LogData(entry.Data, (float)entry.Value, entry.DisplayName); *@
if(entry.Data.TryGetValue("important_perks", out var importantPerksObj))
{
Dictionary<int, int> importantPerks = Utils.JsonObjectToIntDictionary(importantPerksObj);
@* Log.Info($"importantPerks: {importantPerks.Count}"); *@
<div class="perks" style="width: @(entryHeight * 3)px;">
@foreach(var pair in importantPerks)
{
var perkType = PerkManager.IdentityToType(pair.Key);
var perkLevel = pair.Value;
<PerkIconStatic style="height: 100%;" PerkType=@perkType Level=@perkLevel />
}
</div>
@* <div class="view_button" onclick=@(() => ShowRunData(entry))>visibility</div> *@
}
}
else
{
if(LeaderboardModeValue == LeaderboardMode.FastestWin)
{
if ( isBossDefeat )
{
var bossLifePct = ((float)entry.Value - Manager.BOSS_DEFEAT_OFFSET) / 100f;
<label class="value" style="color:@valueColor;">Boss: @(bossLifePct.ToString("0"))%</label>
}
else
{
var runTime = isVictory
? Manager.VICTORY_OFFSET - entry.Value
: entry.Value;
bool showMs = (i > 0 && Utils.HasSameMinutesAndSeconds(entry.Value, entries[i - 1].Value)) || (i < entries.Count() - 1 && Utils.HasSameMinutesAndSeconds(entry.Value, entries[i + 1].Value));
<div class="value_container">
<label class="value" style="color:@valueColor;">
@Utils.FormatTime(runTime)
</label>
@if(showMs)
{
<span class="milliseconds">@Utils.GetMillisecondsString(runTime)</span>
}
</div>
}
}
else if(LeaderboardModeValue == LeaderboardMode.LongestSurvival)
{
<label class="value">@Utils.FormatTime((float)entry.Value)</label>
}
else
{
<label class="value">@entry.Value.ToString("N0")</label>
}
}
</div>
@{
index++;
}
}
</div>
@{
if(_cachedEntries.Count > MaxVisible)
{
var scrollT = StartIndex / (float)(_cachedEntries.Count - MaxVisible);
var containerH = IsOnMainMenu ? 900f : 780f;
var barH = (MaxVisible / (float)_cachedEntries.Count) * containerH;
var barTop = scrollT * (containerH - barH);
<div class="scrollbar" style="top:@(barTop)px; height:@(barH)px;"></div>
}
}
</div>
</root>
@code
{
public int Difficulty { get; set; }
private int _lastDifficulty;
public bool IsSurvival { get; set; }
Sandbox.Services.Leaderboards.Board2 Leaderboard;
public bool IsLoading { get; set; }
public string LoadingText { get; set; } = "";
public bool IsOnMainMenu { get; set; }
List<Sandbox.Services.Leaderboards.Board2.Entry> _cachedEntries = new();
static int StartIndex = 0;
static float _scrollPos = 0f;
static float _scrollVelocity = 0f;
int MaxVisible => IsOnMainMenu ? 21 : 18;
public override void OnMouseWheel( Vector2 value )
{
base.OnMouseWheel( value.y );
_scrollVelocity = Math.Clamp( _scrollVelocity + value.y * 1.5f, -15f, 15f );
}
public override void Tick()
{
base.Tick();
if ( MathF.Abs( _scrollVelocity ) < 0.05f )
{
_scrollVelocity = 0;
return;
}
var maxPos = Math.Max( 0, _cachedEntries.Count - MaxVisible );
_scrollPos = Math.Clamp( _scrollPos + _scrollVelocity, 0, maxPos );
_scrollVelocity *= 0.85f;
var newIndex = (int)_scrollPos;
if ( newIndex != StartIndex )
{
StartIndex = newIndex;
StateHasChanged();
}
}
protected override void OnAfterTreeRender(bool firstTime)
{
base.OnAfterTreeRender(firstTime);
if(firstTime || Difficulty != _lastDifficulty)
{
Refresh();
_lastDifficulty = Difficulty;
// _lastLeaderboardName = LeaderboardName;
}
}
async void Refresh()
{
var statType = GetStatTypeForLeaderboardMode(LeaderboardModeValue);
var statName = Manager.GetStatString(statType, numPlayers: Manager.Instance.NumPlayersLeaderboardToShow, difficulty: Difficulty);
Leaderboard = Sandbox.Services.Leaderboards.GetFromStat(statName);
if(LeaderboardModeValue == LeaderboardMode.FastestWin)
{
Leaderboard.SetAggregationMax();
Leaderboard.SetSortDescending();
}
else if(LeaderboardModeValue == LeaderboardMode.LongestSurvival)
{
Leaderboard.SetAggregationMax();
Leaderboard.SetSortDescending();
}
else if(LeaderboardModeValue == LeaderboardMode.MostEnemyKills || LeaderboardModeValue == LeaderboardMode.MostMinibossKills)
{
Leaderboard.SetAggregationSum();
Leaderboard.SetSortDescending();
}
// Leaderboard.CenterOnMe();
// Leaderboard.Offset = 0;
switch(Manager.Instance.DateRangeToShow)
{
case DateRangeMode.All: Leaderboard.FilterByNone(); break;
case DateRangeMode.Day: Leaderboard.FilterByDay(); break;
case DateRangeMode.Week: Leaderboard.FilterByWeek(); break;
case DateRangeMode.Month: Leaderboard.FilterByMonth(); break;
}
IsLoading = true;
StartIndex = 0;
_scrollPos = 0;
_scrollVelocity = 0;
Leaderboard.SetFriendsOnly(Manager.Instance.LeaderboardShowFriends);
Leaderboard.MaxEntries = 1000;
await Leaderboard.Refresh();
_cachedEntries = Leaderboard.Entries
.Where( e => !Manager.HiddenLeaderboardSteamIds.Contains( e.SteamId ) )
.Where( e => !Manager.HiddenLeaderboardEntries.Any( h =>
h.SteamId == e.SteamId && h.Difficulty == Difficulty &&
e.Value >= h.ValueMin && e.Value <= h.ValueMax ) )
.ToList();
StateHasChanged();
IsLoading = false;
// Log.Info($"Refreshed leaderboard with {Leaderboard.Entries.Count()} entries.");
}
protected override int BuildHash()
{
return System.HashCode.Combine(
Manager.Instance.NumPlayersLeaderboardToShow,
Manager.Instance.DateRangeToShow,
Manager.Instance.LeaderboardModeToShow,
Manager.Instance.LeaderboardShowFriends,
StartIndex
);
}
public void ShowRunData( Sandbox.Services.Leaderboards.Board2.Entry entry )
{
if( entry.Data == null)
{
Manager.Instance.PlaySfxUI("error2", pitch: 0.5f, volume: 0.95f);
return;
}
var localTime = entry.Timestamp.ToLocalTime();
string dateString = $"{localTime.Year}/{localTime.Month}/{localTime.Day}";
Manager.Instance.RunEntryToShow = new RunInfo(entry.DisplayName, entry.SteamId, (float)entry.Value, dateString, entry.Data);
}
private void LogData(Dictionary<string, object> data, float value, string name)
{
int version = -1;
if( data.TryGetValue("data_version", out var dataVersionObj) )
version = ((JsonElement)dataVersionObj).GetInt32();
var sb = new System.Text.StringBuilder();
sb.AppendLine($"---- entry ---- time: {value} name: {name} version: {version} data({data.Count}):");
if(data.TryGetValue("important_perks", out var importantPerksObj))
{
Dictionary<int, int> importantPerks = Utils.JsonObjectToIntDictionary(importantPerksObj);
sb.AppendLine($"important_perks ({importantPerks.Count}):");
foreach(var pair in importantPerks)
{
var perkType = PerkManager.IdentityToType(pair.Key);
var perkLevel = pair.Value;
sb.AppendLine($" {perkType.Name} (id:{pair.Key}): level {perkLevel}");
}
}
if(data.TryGetValue("perks", out var perksObj))
{
Dictionary<int, int> perks = Utils.JsonObjectToIntDictionary(perksObj);
sb.AppendLine($"perks ({perks.Count}):");
foreach(var pair in perks)
{
var perkType = PerkManager.IdentityToType(pair.Key);
var perkLevel = pair.Value;
sb.AppendLine($" {perkType.Name} (id:{pair.Key}): level {perkLevel}");
}
}
Log.Info(sb.ToString());
}
string GetTitleClass( int difficulty )
{
switch(difficulty)
{
case 0: default: return "title_normal";
case 1: return "title_expert";
case 2: return "title_cursed";
}
}
void ClickNumPlayersButton()
{
Manager.Instance.NumPlayersLeaderboardToShow++;
if(Manager.Instance.NumPlayersLeaderboardToShow > Manager.MAX_PLAYERS)
Manager.Instance.NumPlayersLeaderboardToShow = 1;
Refresh();
Manager.Instance.SkipShowingLeaderboardFrames = 1;
}
string GetNumPlayersButtonClass()
{
switch(Manager.Instance.NumPlayersLeaderboardToShow)
{
case 1: default: return "num_players_one";
case 2: return "num_players_two";
case 3: return "num_players_three";
}
}
List<Option> LeaderboardModeOptions = new List<Option>()
{
new Option("Fastest Victory", LeaderboardMode.FastestWin),
new Option("Longest Survival", LeaderboardMode.LongestSurvival),
new Option("Total Victories", LeaderboardMode.MostWins),
new Option("Total Attempts", LeaderboardMode.MostRuns),
new Option("Total Enemy Kills", LeaderboardMode.MostEnemyKills),
new Option("Total Miniboss Kills", LeaderboardMode.MostMinibossKills),
};
StatType GetStatTypeForLeaderboardMode( LeaderboardMode mode )
{
switch(mode)
{
case LeaderboardMode.FastestWin: default: return StatType.LeaderboardRun;
case LeaderboardMode.MostWins: return StatType.NumVictory;
case LeaderboardMode.MostRuns: return StatType.NumRuns;
case LeaderboardMode.MostEnemyKills: return StatType.NumKills;
case LeaderboardMode.MostMinibossKills: return StatType.NumMinibossKills;
case LeaderboardMode.LongestSurvival: return StatType.SurvivalTime;
}
}
LeaderboardMode LeaderboardModeValue
{
get => Manager.Instance.LeaderboardModeToShow;
set
{
if (Manager.Instance.LeaderboardModeToShow != value)
{
Manager.Instance.LeaderboardModeToShow = value;
Refresh();
Manager.Instance.SkipShowingLeaderboardFrames = 1;
}
}
}
List<Option> DateRangeOptions = new List<Option>()
{
new Option("All Time", DateRangeMode.All),
new Option("Today", DateRangeMode.Day),
new Option("This Week", DateRangeMode.Week),
new Option("This Month", DateRangeMode.Month),
};
DateRangeMode DateRangeValue
{
get => Manager.Instance.DateRangeToShow;
set
{
if (Manager.Instance.DateRangeToShow != value)
{
Manager.Instance.DateRangeToShow = value;
Refresh();
Manager.Instance.SkipShowingLeaderboardFrames = 1;
}
}
}
List<Option> ShowFriendsOptions = new List<Option>()
{
new Option("Global", false),
new Option("Friends", true),
};
bool ShowFriendsValue
{
get => Manager.Instance.LeaderboardShowFriends;
set
{
if (Manager.Instance.LeaderboardShowFriends != value)
{
Manager.Instance.LeaderboardShowFriends = value;
GameSettingsSystem.Save();
Refresh();
Manager.Instance.SkipShowingLeaderboardFrames = 1;
}
}
}
}