UI/PlayerHud/Components/Scoreboard.razor

A Blazor-style UI component that renders the in-game scoreboard. It builds a table per team showing player level, avatar, name, points (damage+heals), kills, deaths and ping, and toggles visibility while the Score input is pressed.

File Access
@using System;
@using Sandbox.UI;
@using KOTH.PlayerExp;

@namespace KOTH.UI
@inherits Panel

@attribute [StyleSheet]

@if (!GameMode.Instance.GameStats.IsValid())
{
	return;
}

<root class="hidden flex absolute inset-0 align-center justify-center">
@* 	<div class="title">
		@GameMode.Instance.Title
	</div> *@
	@RenderTableSection(PlayerState.Local.Team)
	@RenderTableSection(PlayerState.Local.Team.GetOpponents())
</root>

@code
{
	private RenderFragment RenderTableSection(Team Team) => Builder =>
	{
		var ColourString = "rgba(200, 200, 200, 0.25)";
		if (Team == Team.Terrorist)
		{
			ColourString = "rgba(0, 255, 0, 0.25)";
		}
		if (Team == Team.CounterTerrorist)
		{
			ColourString = "rgba(255, 0, 0, 0.25)";
		}

		Builder.OpenElement(0, "table");
		Builder.AddAttribute(1, "class", "with-padding-lg layout");
		Builder.AddAttribute(2, "style", $"background-color: {ColourString};");

		// Table Header Row
		Builder.OpenElement(3, "div");
		Builder.AddAttribute(4, "class", "row header with-px-lg with-py-sm");

		// Level Column
		Builder.OpenElement(5, "label");
		Builder.AddAttribute(6, "class", "col stat-little");
		Builder.AddContent(7, "◎");
		Builder.CloseElement();

		// Avatar Column
		Builder.OpenElement(8, "label");
		Builder.AddAttribute(9, "class", "col stat-avatar");
		Builder.CloseElement();

		// Team Name Column
		Builder.OpenElement(10, "label");
		Builder.AddAttribute(11, "class", "col stat-large");
		Builder.AddContent(12, $"{Team.GetName()}");
		Builder.CloseElement();

		// Stat Columns
		Builder.OpenElement(13, "label");
		Builder.AddAttribute(14, "class", "col stat");
		Builder.AddContent(15, "Points");
		Builder.CloseElement();

		Builder.OpenElement(16, "label");
		Builder.AddAttribute(17, "class", "col stat");
		Builder.AddContent(18, "Kills");
		Builder.CloseElement();

		Builder.OpenElement(19, "label");
		Builder.AddAttribute(20, "class", "col stat");
		Builder.AddContent(21, "Deaths");
		Builder.CloseElement();

		// Level Column
		Builder.OpenElement(22, "label");
		Builder.AddAttribute(23, "class", "col stat-little");
		Builder.CloseElement();

		// Close Header Row
		Builder.CloseElement();

		var OrderedPlayers = GameUtils.AllPlayers.OrderByDescending(PlayerState =>
		{
			FPlayerGameStat Stats = GameMode.Instance.GameStats.PlayerStateStats[PlayerState];
			return Stats.Damage;
		}).ToList();

		foreach (var PlayerState in OrderedPlayers)
		{
			if (PlayerState.IsValid() && PlayerState.Team == Team)
			{
				GeneratePlayerRow(PlayerState).Invoke(Builder);
			}
		}

		// Close the table element
		Builder.CloseElement();
	};

	private RenderFragment GeneratePlayerRow(PlayerState PlayerState) => Builder =>
	{
		if (!PlayerState.IsValid())
		{
			return;
		}

		FPlayerGameStat PlayerStats = GameMode.Instance.GameStats.PlayerStateStats[PlayerState];

		// Open the row div
		Builder.OpenElement(0, "div");
		Builder.AddAttribute(1, "class", "row header with-px-lg with-py-sm");
		// builder.AddAttribute(2, "onclick", EventCallback.Factory.Create(this, () => Log.Info("clicky")));

		// Level Column
		Builder.OpenElement(3, "label");
		Builder.AddAttribute(4, "class", "col stat-little");
		Builder.AddContent(5, ExpManager.GetLevelString(PlayerState.SteamId));
		Builder.CloseElement();

		// Avatar Column
		Builder.OpenElement(6, "label");
		Builder.AddAttribute(7, "class", "col stat-avatar");
		Builder.AddAttribute(8, "style", $"background-image: url(avatar:{PlayerState.SteamId});");
		if (!PlayerState.PlayerPawn.IsValid())
		{
			Builder.AddAttribute(26, "class", "avatar-overlay");
		}
		Builder.CloseElement();

		// Player Name Column
		Builder.OpenElement(9, "label");
		Builder.AddAttribute(10, "class", "col stat-large");
		Builder.AddContent(11, PlayerState.SteamName);
		Builder.CloseElement();

		// Stat Columns
		Builder.OpenElement(12, "label");
		Builder.AddAttribute(13, "class", "col stat");
		Builder.AddContent(14, ((int)PlayerStats.Damage + (int)PlayerStats.Heals));
		Builder.CloseElement();

		Builder.OpenElement(15, "label");
		Builder.AddAttribute(16, "class", "col stat");
		Builder.AddContent(17, PlayerStats.Kills);
		Builder.CloseElement();

		Builder.OpenElement(18, "label");
		Builder.AddAttribute(19, "class", "col stat");
		Builder.AddContent(20, PlayerStats.Deaths);
		Builder.CloseElement();

		// Ping Column
		Builder.OpenElement(21, "label");
		Builder.AddAttribute(22, "class", "col stat-little");
		Builder.AddContent(23, PlayerState.PingString);
		Builder.CloseElement();

		// Close the row div
		Builder.CloseElement();
	};

	public bool IsActive { get; set; }

	protected override int BuildHash()
	{
		return HashCode.Combine(Time.Now);
	}

	public override void Tick()
	{
		if (!PlayerState.Local.IsValid() || !GameMode.Instance.IsValid())
		{
			return;
		}

		if (Input.Pressed("Score"))
		{
			IsActive = Input.Pressed("Score");
			SetClass("visible", true);
		}

		if (Input.Released("Score"))
		{
			SetClass("visible", false);
			IsActive = false;
		}
	}
}