UI/NameTags/NameTags.razor
@using Sandbox.UI
@inherits PanelComponent

<root>
    @foreach ( var tag in _tags )
    {
        <div class="nametag" style="left: @(tag.X * 100)%; top: @(tag.Y * 100)%; opacity: @tag.Alpha.ToString("F2");">
            <img class="target" src="ui/crosshair/target.png" />
            <label class="name">@tag.Name</label>
        </div>
    }
</root>

@code
{
    private record struct NameTagData( string Name, float X, float Y, float Alpha );
    private readonly List<NameTagData> _tags = new();

    protected override void OnUpdate()
    {
        _tags.Clear();

        var cam = Game.ActiveScene?.Camera;
        if ( cam == null ) return;

        var localPawn = LocalPlayer.Pawn;

        foreach ( var pawn in Game.ActiveScene?.GetAllComponents<PlayerPawn>() ?? Enumerable.Empty<PlayerPawn>() )
        {
            if ( pawn == localPawn ) continue;
            if ( !pawn.IsAlive ) continue;

            var worldPos = pawn.WorldPosition + Vector3.Up * 50f;

            var toTarget = worldPos - cam.WorldPosition;
            if ( Vector3.Dot( toTarget.Normal, cam.WorldRotation.Forward ) < 0.1f ) continue;

            var dist = toTarget.Length;
            if ( dist > 5000f ) continue;

            var screenPos = cam.PointToScreenNormal( worldPos );
            var alpha     = MathX.Remap( dist, 500f, 5000f, 1f, 0.2f );
            var name      = pawn.IsBot ? (pawn.BotName.Length > 0 ? pawn.BotName : "Bot") : (pawn.Network.Owner?.DisplayName ?? pawn.GameObject.Name);

            _tags.Add( new NameTagData( name, screenPos.x, screenPos.y, alpha ) );
        }
    }

    protected override int BuildHash() => HashCode.Combine( _tags.Count, Time.Now );
}