UI/Player/Scoreboard.razor
@using Sandbox.UI
@inherits Panel

<style>
    Scoreboard {
        position: absolute;
        width: 100%;
        height: 100%;
        align-items: flex-start;
        justify-content: flex-start;
        padding-top: 40px;
        padding-left: 60px;
        opacity: 0;
        pointer-events: none;
        transition: opacity 0.15s ease;

        &.open {
            opacity: 1;
        }

        > .panel {
            flex-direction: column;
            min-width: 620px;
            background-color: rgba(30, 30, 30, 0.55);

            > .title-bar {
                flex-direction: row;
                align-items: center;
                justify-content: space-between;
                padding: 16px 26px;

                > .title {
                    font-family: "Wallpoet";
                    font-size: 36px;
                    color: rgba(39, 181, 238, 1);
                    text-shadow: 0 0 14px rgba(39, 181, 238, 0.5);
                }

                > .subtitle {
                    font-family: "AzeretMono-Medium";
                    font-size: 20px;
                    color: rgba(39, 181, 238, 0.4);
                }
            }

            > .col-headers {
                flex-direction: row;
                align-items: center;
                padding: 6px 26px;
                font-family: "AzeretMono-Medium";
                font-size: 15px;
                color: rgba(255, 255, 255, 0.3);

                > .h-rank  { width: 44px; }
                > .h-name  { flex-grow: 1; }
                > .h-pts   { width: 90px; text-align: right; }
                > .h-k     { width: 60px; text-align: right; }
                > .h-d     { width: 60px; text-align: right; }
                > .h-ping  { width: 76px; text-align: right; }
            }

            > .list {
                flex-direction: column;
                padding: 4px 0;

                > .entry {
                    flex-direction: row;
                    align-items: center;
                    padding: 12px 26px;
                    font-family: "AzeretMono-Medium";
                    font-size: 24px;
                    color: rgba(255, 255, 255, 0.7);

                    &.local {
                        color: rgba(255, 255, 255, 0.95);
                        border-left: 3px solid rgba(39, 181, 238, 0.7);
                        padding-left: 23px;
                    }

                    &.bot {
                        color: rgba(255, 255, 255, 0.35);
                    }

                    > .rank {
                        width: 44px;
                        font-size: 20px;
                        color: rgba(255, 255, 255, 0.25);

                        &.r1 { color: rgba(255, 215, 0, 0.9); font-size: 24px; }
                        &.r2 { color: rgba(192, 192, 192, 0.85); }
                        &.r3 { color: rgba(180, 105, 55, 0.85); }
                    }

                    > .name-col {
                        flex-grow: 1;
                        flex-direction: row;
                        align-items: center;

                        > .avatar {
                            width: 28px;
                            height: 28px;
                            border-radius: 100px;
                            margin-right: 12px;
                            opacity: 0.75;
                        }
                    }

                    > .pts {
                        width: 90px;
                        text-align: right;
                        color: rgba(39, 181, 238, 0.9);
                        font-size: 28px;
                        font-weight: 700;
                    }

                    > .kills {
                        width: 60px;
                        text-align: right;
                        color: rgba(110, 220, 110, 0.75);
                    }

                    > .deaths {
                        width: 60px;
                        text-align: right;
                        color: rgba(220, 90, 90, 0.65);
                    }

                    > .ping {
                        width: 76px;
                        text-align: right;
                        font-size: 19px;
                        color: rgba(255, 255, 255, 0.25);
                    }
                }

                > .divider {
                    height: 1px;
                    background-color: rgba(255, 255, 255, 0.08);
                    margin: 6px 26px;
                }
            }
        }
    }
</style>

<root>
    <div class="panel">
        <div class="title-bar">
            <label class="title">SCOREBOARD</label>
            <label class="subtitle">@_playerCount PILOTS</label>
        </div>
        <div class="col-headers">
            <label class="h-rank">#</label>
            <label class="h-name">PILOT</label>
            <label class="h-pts">PTS</label>
            <label class="h-k">K</label>
            <label class="h-d">D</label>
            <label class="h-ping">PING</label>
        </div>
        <div class="list">
            @{
                var allEntries = _entries;
                bool addedBotDivider = false;
                int rank = 1;
            }
            @foreach ( var e in allEntries )
            {
                @if ( e.IsBot && !addedBotDivider && rank > 1 )
                {
                    addedBotDivider = true;
                    <div class="divider"></div>
                }
                var rankClass = rank == 1 ? "r1" : rank == 2 ? "r2" : rank == 3 ? "r3" : "";
                var isLocal = !e.IsBot && e.Conn == Connection.Local;
                <div class="entry @(e.IsBot ? "bot" : "") @(isLocal ? "local" : "")">
                    <label class="rank @rankClass">@rank</label>
                    <div class="name-col">
                        @if ( !e.IsBot )
                        {
                            <img class="avatar" src=@($"avatar:{e.Conn.SteamId}") />
                        }
                        <label>@e.Name</label>
                    </div>
                    <label class="pts">@e.Score</label>
                    <label class="kills">@e.Kills</label>
                    <label class="deaths">@e.Deaths</label>
                    <label class="ping">@e.PingText</label>
                </div>
                rank++;
            }
        </div>
    </div>
</root>

@code
{
    private class ScoreEntry
    {
        public string Name;
        public int Score, Kills, Deaths;
        public string PingText;
        public bool IsBot;
        public Connection Conn;
    }

    private List<ScoreEntry> _entries = new();
    private int _playerCount;

    public override void Tick()
    {
        SetClass( "open", Input.Down( "Score" ) );
        RebuildEntries();
    }

    protected override int BuildHash()
    {
        var hash = new HashCode();
        hash.Add( Connection.All.Count() );
        hash.Add( Input.Down( "Score" ) );

        var pawns = Game.ActiveScene?.GetAllComponents<PlayerPawn>() ?? Enumerable.Empty<PlayerPawn>();
        foreach ( var p in pawns.OrderBy( p => p.IsBot ).ThenBy( p => p.Network?.Owner?.DisplayName ?? p.BotName ) )
        {
            hash.Add( p.IsBot );
            hash.Add( p.Network?.Owner?.DisplayName ?? p.BotName );
            hash.Add( p.Score );
            hash.Add( p.Kills );
            hash.Add( p.Deaths );
            hash.Add( p.IsAlive );
        }

        return hash.ToHashCode();
    }

    private void RebuildEntries()
    {
        var list = new List<ScoreEntry>();
        var pawns = Game.ActiveScene?.GetAllComponents<PlayerPawn>() ?? Enumerable.Empty<PlayerPawn>();

        // Build player rows from pawns first (authoritative source for score/kills/deaths),
        // then fall back to bare connections that don't have a pawn yet.
        foreach ( var pawn in pawns.Where( p => p != null && !p.IsBot && p.Network?.Owner != null ) )
        {
            var conn = pawn.Network.Owner;
            list.Add( new ScoreEntry
            {
                Name     = conn.DisplayName,
                Score    = pawn.Score,
                Kills    = pawn.Kills,
                Deaths   = pawn.Deaths,
                PingText = $"{conn.Ping}ms",
                IsBot    = false,
                Conn     = conn
            } );
        }

        foreach ( var conn in Connection.All )
        {
            if ( list.Any( e => e.Conn == conn ) ) continue;

            list.Add( new ScoreEntry
            {
                Name     = conn.DisplayName,
                Score    = 0,
                Kills    = 0,
                Deaths   = 0,
                PingText = $"{conn.Ping}ms",
                IsBot    = false,
                Conn     = conn
            } );
        }

        var bots = pawns.Where( p => p.IsBot );
        foreach ( var bot in bots )
        {
            list.Add( new ScoreEntry
            {
                Name     = bot.BotName?.Length > 0 ? bot.BotName : "Bot",
                Score    = bot.Score,
                Kills    = bot.Kills,
                Deaths   = bot.Deaths,
                PingText = "BOT",
                IsBot    = true,
                Conn     = null
            } );
        }

        // Players sorted by score first, then bots sorted by score
        _entries = list
            .OrderBy( e => e.IsBot ? 1 : 0 )
            .ThenByDescending( e => e.Score )
            .ToList();

        _playerCount = list.Count;
    }
}