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

<style>
    PilotInfo {
        position: absolute;
        width: 100%;
        height: 100%;
        left: 0;
        top: 0;
        pointer-events: none;
        justify-content: center;
        align-items: center;
        mix-blend-mode: lighten;

        .scanner-scene {
            position: absolute;
            bottom: 36px;
            left: 50%;
            width: 400px;
            height: 400px;
            margin-left: -200px;
            overflow: hidden;
            z-index: 4;
        }

        .scanner-frame {
            position: absolute;
            bottom: 36px;
            left: 50%;
            width: 400px;
            height: 400px;
            margin-left: -200px;
            border: 0;
            box-shadow: 0 0 14px rgba(0, 0, 0, 0.35);
            overflow: hidden;
            background-color: rgba(20, 9, 3, 0.2);
            z-index: 5;
        }

        .ring-meters {
            position: absolute;
            bottom: 36px;
            left: 50%;
            width: 400px;
            height: 400px;
            margin-left: -200px;
            z-index: 6;
        }

        .meter-seg {
            position: absolute;
            width: 7px;
            height: 20px;
            opacity: 0.25;
        }

        .meter-seg.speed {
            background-color: rgba(255, 255, 255, 0.85);
        }

        .meter-seg.boost {
            background-color: rgba(90, 255, 180, 0.85);
        }

        .meter-seg.active {
            opacity: 1;
            box-shadow: 0 0 8px rgba(120, 235, 255, 0.45);
        }

        .meter-label {
            position: absolute;
            font-family: "Wallpoet";
            font-size: 24px;
            color: rgba(210, 245, 255, 0.9);
            text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
        }

        .meter-label.speed {
            left: 22px;
            bottom: 22px;
        }

        .meter-label.boost {
            right: 8px;
            bottom: 22px;
        }
    }
</style>

<root>
    <div class="scanner-frame"> </div>
    <div class="ring-meters">
        @foreach ( var s in GetArc( true ) )
        {
            <div class="meter-seg speed @(s.Active ? "active" : "")"
                 style="left:@($"{s.X:F1}px"); top:@($"{s.Y:F1}px"); transform: rotate(@($"{s.Angle + 90f:F1}deg"));"></div>
        }
        @foreach ( var b in GetArc( false ) )
        {
            <div class="meter-seg boost @(b.Active ? "active" : "")"
                 style="left:@($"{b.X:F1}px"); top:@($"{b.Y:F1}px"); transform: rotate(@($"{b.Angle + 90f:F1}deg"));"></div>
        }
        <div class="meter-label speed">SPD @MathF.Round( Pawn?.Speed ?? 0f )</div>
        <div class="meter-label boost">BST @MathF.Round( BoostProgress * 100f )%</div>
    </div>
</root>

@code
{
    private PlayerPawn Pawn => LocalPlayer.Pawn;

    private ScenePanel _scenePanel;
    private ScannerSceneController _sceneController;
    private readonly List<ScannerSceneController.BlipData> _blips = new();
    private RealTimeUntil _nextScanUpdate;

    private const float ScannerRange = 5200f;
    private const float WorldRadius = 44f;
    private const int ArcSegments = 14;
    private const float ArcStartInset = 0.08f;
    private const float ArcEndInset = 0.08f;

    private float SpeedProgress => Pawn != null ? Pawn.Speed.LerpInverse( 4f, Pawn.BoostSpeed ) : 0f;
    private float BoostProgress => Pawn != null ? Pawn.BoostCoolDown.LerpInverse( 0f, Pawn.BoostAmount ) : 0f;

    private readonly struct ArcSeg
    {
        public ArcSeg( float x, float y, float angle, bool active )
        {
            X = x;
            Y = y;
            Angle = angle;
            Active = active;
        }

        public float X { get; }
        public float Y { get; }
        public float Angle { get; }
        public bool Active { get; }
    }

    private IEnumerable<ArcSeg> GetArc( bool speed )
    {
        var progress = speed ? SpeedProgress : BoostProgress;
        const float baseStart = 150f;
        const float baseEnd = 235f;
        const float radiusX = 208f;
        const float radiusY = 136f;
        const float cx = 200f;
        const float cy = 266f;

        for ( int i = 0; i < ArcSegments; i++ )
        {
            var rawT = ArcSegments <= 1 ? 1f : i / (float)(ArcSegments - 1);
            var t = ArcStartInset + rawT * (1f - ArcStartInset - ArcEndInset);
            var baseAngle = baseStart + (baseEnd - baseStart) * t;
            var angle = speed ? baseAngle : (180f - baseAngle);
            var rad = angle.DegreeToRadian();
            var x = cx + MathF.Cos( rad ) * radiusX;
            var y = cy + MathF.Sin( rad ) * radiusY;
            var active = t <= progress;
            yield return new ArcSeg( x, y, angle, active );
        }
    }

    protected override void OnAfterTreeRender( bool firstTime )
    {
        base.OnAfterTreeRender( firstTime );
        if ( !firstTime ) return;

        _scenePanel = new ScenePanel();
        _scenePanel.AddClass( "scanner-scene" );
        _scenePanel.Style.Position = PositionMode.Absolute;
        _scenePanel.Style.Left = Length.Percent( 50 );
        _scenePanel.Style.Bottom = 36;
        _scenePanel.Style.Width = 400;
        _scenePanel.Style.Height = 400;
        _scenePanel.Style.MarginLeft = -200;
        AddChild( _scenePanel );

        var renderScene = _scenePanel.RenderScene;
        if ( renderScene == null )
            return;

        _sceneController = renderScene.GetAllComponents<ScannerSceneController>().FirstOrDefault();
        if ( _sceneController == null )
        {
            var controllerGo = renderScene.CreateObject();
            _sceneController = controllerGo.Components.Create<ScannerSceneController>();
        }

        var cam = renderScene.GetAllComponents<CameraComponent>().FirstOrDefault();
        if ( cam == null )
        {
            var camGo = renderScene.CreateObject();
            cam = camGo.Components.Create<CameraComponent>();
            cam.BackgroundColor = Color.Transparent;
            cam.ZNear = 1f;
            cam.ZFar = 1000f;
        }

        cam.Orthographic = false;
        cam.GameObject.WorldPosition = new Vector3( -88f, 0f, 50f );
        cam.GameObject.WorldRotation = Rotation.From(25, 0, 0);
    }

    public override void Tick()
    {
        base.Tick();
        if ( _sceneController == null && _scenePanel?.RenderScene != null )
            _sceneController = _scenePanel.RenderScene.GetAllComponents<ScannerSceneController>().FirstOrDefault();

        if ( _sceneController == null || Pawn == null || Game.ActiveScene == null )
            return;

        if ( _nextScanUpdate > 0f )
            return;

        _nextScanUpdate = 0.05f;
        _blips.Clear();
        int count = 0;
        foreach ( var target in Game.ActiveScene.GetAllComponents<PlayerPawn>() )
        {
            if ( count >= 24 ) break;
            if ( target == null || target == Pawn || !target.IsAlive ) continue;

            var to = target.WorldPosition - Pawn.WorldPosition;
            if ( to.Length < 2f || to.Length > ScannerRange ) continue;

            // Axis mapping: x = forward/back, y = left/right, z = up/down
            var local = Pawn.WorldRotation.Inverse * to;
            var planar = new Vector2( local.x, local.y );
            if ( planar.Length < 0.001f ) continue;

            var distance01 = Math.Clamp( planar.Length / ScannerRange, 0f, 1f );
            var position = planar.Normal * (distance01 * WorldRadius);
            // Player-centered altitude: explicitly relative to player position (player's z is always scanner z=0)
            var altitudeDelta = to.z;
            var altitudeNorm = Math.Clamp( altitudeDelta / 1400f, -1f, 1f );
            var altitude = altitudeNorm * 18f;

            _blips.Add( new ScannerSceneController.BlipData
            {
                Position = new Vector3( position.x, position.y, altitude ),
                Above = altitudeDelta > 0f,
                IsBot = target.IsBot
            } );
            count++;
        }

        _sceneController.SetBlips( _blips );
    }

    public override void OnDeleted()
    {
        base.OnDeleted();
        _scenePanel?.Delete( true );
    }

    protected override int BuildHash()
        => HashCode.Combine( Pawn?.WorldPosition, Pawn?.WorldRotation, Pawn?.Speed, Pawn?.BoostCoolDown, Pawn?.BoostAmount );
}