A Razor UI panel for displaying a player profile and stats. It renders avatar, display name, difficulty panel and a list of computed stats sourced from Sandbox.Services.Stats.PlayerStats, and refreshes data when the shown Steam ID changes.
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@using System.Text.Json;
@inherits Panel
@attribute [StyleSheet("PlayerProfilePanel.razor.scss")]
<root>
<div class="hide_button" onclick=@(() => HideProfile())></div>
@if(PlayerStats is null)
{
@* <div class="loading">
<label>@($"Loading...")</label>
</div> *@
return;
}
<div class="name_container">
<div class="avatar" style="background-image: url( avatar:@Manager.Instance.PlayerProfileToShow.steamId )"></div>
@{
var displayName = Manager.Instance.PlayerProfileToShow.displayName;
}
<div class="displayName">@displayName</div>
</div>
<div class="main-content">
@* @if( _isLoadingPerks)
{
<div class="loading_perks">@($"Loading perks...")</div>
}
else
{
if(_perkPickPercents.Count > 0)
{
<div class="perks_container">
<div class="perks_title">@($"Favorite Perks:")</div>
<div class="perks">
@{
foreach(var pair in _perkPickPercents.OrderByDescending(x => x.Value).Take(10))
{
var perkType = pair.Key;
<PerkIconStatic style="height: 100%;" PerkType=@perkType Level=@(1) HideLevel=@true [email protected] />
}
}
</div>
</div>
}
} *@
@* <div class="stats_title">
Stats
</div> *@
<div class="stats">
<DifficultyPanel DontChangeGameDifficulty=@true />
@{
var numRuns = GetStatSum(StatType.NumRuns);
var numWins = GetStatSum(StatType.NumVictory);
// var numLosses = GetStatSum(StatType.NumDefeat);
// var numResets = Math.Max(numRuns - (numWins + numLosses), 0);
var winPercent = numRuns > 0 ? (numWins / (float)numRuns) * 100f : 0f;
string winRateString;
if(winPercent >= 1f) winRateString = $"{winPercent:0.#}%";
else if(winPercent >= 0.1f) winRateString = $"{winPercent:0.##}%";
else if(winPercent >= 0.01f) winRateString = $"{winPercent:0.###}%";
else if(winPercent > 0f) winRateString = $"{winPercent:0.####}%";
else winRateString = "0%";
var numKills = GetStatSum(StatType.NumKills);
var numMinibossKills = GetStatSum(StatType.NumMinibossKills);
var fastestWinScore = GetStatMax(StatType.LeaderboardRun);
var hasFastestWin = fastestWinScore > Manager.VICTORY_OFFSET / 2f;
var fastestWinTime = hasFastestWin ? Manager.VICTORY_OFFSET - fastestWinScore : 0f;
var fastestWinString = hasFastestWin ? Utils.FormatTime(fastestWinTime) : "...";
var i = 0;
PlayerStatsToShow.Clear();
PlayerStatsToShow.Add(new PlayerProfileStatData("Victories", numWins.ToString("N0"), new Color(1f, 1f, 0.5f), "win", GetFontSizeForNumber(numWins)));
PlayerStatsToShow.Add(new PlayerProfileStatData("Fastest Victory", fastestWinString, new Color(0.5f, 1f, 0.5f), "clock"));
PlayerStatsToShow.Add(new PlayerProfileStatData("Attempts", numRuns.ToString("N0"), new Color(0.8f), "num_runs", GetFontSizeForNumber(numRuns)));
PlayerStatsToShow.Add(new PlayerProfileStatData("Winrate", winRateString, new Color(0.6f, 0.6f, 0.9f, 0.7f), "win_rate"));
PlayerStatsToShow.Add(new PlayerProfileStatData("Enemy Kills", numKills.ToString("N0"), new Color(0.8f, 0.2f, 0.2f), "kill", GetFontSizeForNumber(numKills)));
PlayerStatsToShow.Add(new PlayerProfileStatData("Miniboss Kills", numMinibossKills.ToString("N0"), new Color(0.9f, 0.4f, 0.1f), "kill_miniboss", GetFontSizeForNumber(numMinibossKills)));
}
<div class="list_container">
@foreach(var data in PlayerStatsToShow)
{
<div class="flat list" style="background-color: @((i % 2 == 0 ? new Color(0, 0, 0, 0.4f) : new Color(0, 0, 0, 0.7f)).Rgba);">
<div>
<div class="icon_container">
<label class="icon" style="background-color:@(data.color.Rgba); mask-image:@($"url(/textures/ui/stats/{data.icon}.png)")"></label>
</div>
<label class="bold stat_name" style="font-size:@(data.title.Length > 15 ? Math.Round(Utils.Map(data.title.Length, 15, 36, 14f, 10f, EasingType.SineIn)) : 14)px; color:@(Color.Lerp(data.color, Color.White, (i % 2 == 0 ? 0.5f : 0.4f)).Rgba);">@(data.title)</label>
</div>
<div class="values">
@{
var color = (data.text == "0" || data.text == "0%" || data.text == "...")
? Color.Lerp(data.color, new Color(0.5f), 0.75f).WithAlpha(0.5f).Rgba
: data.color.Rgba;
}
<label class="bold stat_value" style="color:@(color); font-size:@(data.fontSize)px;">@(data.text)</label>
</div>
</div>
@{
i++;
}
}
</div>
</div>
</div>
</root>
@code
{
public struct PlayerProfileStatData
{
public string title;
public string text;
public Color color;
public string icon;
public float fontSize;
public PlayerProfileStatData(string _title, string _text, Color _color, string _icon, float _fontSize = 16f)
{
title = _title;
text = _text;
color = _color;
icon = _icon;
fontSize = _fontSize;
}
}
public List<PlayerProfileStatData> PlayerStatsToShow { get; set; } = new();
private bool _isLoadingPerks;
private long _loadedSteamId;
public Sandbox.Services.Stats.PlayerStats PlayerStats { get; set; }
private Dictionary<TypeDescription, float> _perkPickPercents = new();
protected override void OnAfterTreeRender(bool firstTime)
{
base.OnAfterTreeRender(firstTime);
var currentSteamId = Manager.Instance.PlayerProfileToShow?.steamId ?? 0;
if (firstTime || currentSteamId != _loadedSteamId)
{
_loadedSteamId = currentSteamId;
Refresh();
}
}
async void Refresh()
{
PlayerStats = null;
StateHasChanged();
_isLoadingPerks = true;
PlayerStats = Sandbox.Services.Stats.GetPlayerStats("facepunch.ss2", Manager.Instance.PlayerProfileToShow.steamId);
await PlayerStats.Refresh();
_isLoadingPerks = false;
_perkPickPercents.Clear();
/*
foreach(var type in TypeLibrary.GetTypes<Perk>())
{
var attrib = type.GetAttribute<PerkAttribute>();
if(attrib == null)
continue;
if(attrib.Disabled)
continue;
if(attrib.Curse)
continue;
var timesChosen = (int)PlayerStats.Get(Manager.GetPerkStatString(StatType.PerkChosen, type)).Sum;
var timesIgnored = (int)PlayerStats.Get(Manager.GetPerkStatString(StatType.PerkIgnored, type)).Sum;
// if( timesChosen + timesIgnored > 0 && (timesChosen > 2 || timesIgnored > 3) )
if( timesChosen >= 2 )
{
var percent = MathX.Clamp(timesChosen / (float)(timesChosen + timesIgnored), 0f, 1f);
// Log.Info($"{type}: chosen {timesChosen}, ignored {timesIgnored} - percent: {percent}");
_perkPickPercents[type] = percent;
}
}
*/
// Log.Info($"Loaded {_perkPickPercents.Count} perks for player profile.");
// var totalWinTime = 0f;
// for(int difficulty = 1; difficulty <= Manager.MaxDifficulty; difficulty++)
// {
// }
StateHasChanged();
// var zombies = stats.Get("zombies_killed");
// Log.Info($"Garry has killed {zombies.Sum} zombies!");
// Log.Info($"Refreshed leaderboard with {Leaderboard.Entries.Count()} entries.");
}
protected override int BuildHash()
{
return HashCode.Combine(
DifficultyPanel.DifficultyToDisplay,
Manager.Instance.PlayerProfileToShow
);
}
public void HideProfile()
{
Manager.Instance.PlayerProfileToShow = null;
}
public int GetStatSum( StatType statType )
{
if( DifficultyPanel.DifficultyToDisplay == -1 )
{
int total = 0;
for(int diff = Manager.MinDifficulty; diff <= Manager.MaxDifficulty; diff++)
{
total += (int)PlayerStats.Get(Manager.GetStatString(statType, numPlayers: 1, diff)).Sum;
}
return total;
}
else
{
return (int)PlayerStats.Get(Manager.GetStatString(statType, numPlayers: 1, DifficultyPanel.DifficultyToDisplay)).Sum;
}
}
public float GetStatMax( StatType statType )
{
if( DifficultyPanel.DifficultyToDisplay == -1 )
{
float max = 0f;
for(int diff = Manager.MinDifficulty; diff <= Manager.MaxDifficulty; diff++)
{
var val = (float)PlayerStats.Get(Manager.GetStatString(statType, numPlayers: 1, diff)).Max;
if(val > max)
max = val;
}
return max;
}
else
{
return (float)PlayerStats.Get(Manager.GetStatString(statType, numPlayers: 1, DifficultyPanel.DifficultyToDisplay)).Max;
}
}
public static float GetFontSizeForNumber( float number )
{
if(number >= 1000000000f) return 10f;
if(number >= 100000000f) return 11f;
if(number >= 10000000f) return 12f;
if(number >= 1000000f) return 14f;
return 16f;
}
}