ui/PlayerProfilePanel.razor

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.

NetworkingHttp Calls
@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;
	}
}