UI/CustomizePage.razor

Razor UI panel for the vehicle customization page. Renders a carousel of available CarResource entries, shows stats, thumbnails, lets the player preview, select and lock in a preferred car, and handles keyboard/controller navigation and persistence to the player preference.

File AccessNetworking
@using Sandbox;
@using Sandbox.UI;
@using Sandbox.UI.Navigation;
@using Machines.Components;
@using Machines.Resources;
@using Machines.Systems;

@namespace Machines.UI
@inherits Panel
@attribute [Route( "/customize" )]

@{
    EnsureInit();
    var car = Current;
    (string Label, int Value, string Css)[] rows = car is null
        ? System.Array.Empty<(string, int, string)>()
        : BuildRows( car.Stats.GetDisplayRatings() );
}

<root class="customize-page @(Input.UsingController ? "controller" : "mouse")">
    <div class="eyebrow">PICK YOUR RIDE</div>
    <div class="page-title">What are we driving?</div>

    <div class="card-stage">
        <div class="cycle cycle-left" onclick=@(() => Select( -1 ))>
            <div class="chevron">‹</div>
            <InputHint Action="MenuLeft" Dark=@true />
        </div>

        <div class="car-card @(_lastDir >= 0 ? "from-right" : "from-left")" @key=@(car?.ResourcePath ?? "none")>
            @if ( car is not null && IsCurrentActive )
            {
                <div class="picked-badge">SELECTED</div>
            }
            @if ( car is not null )
            {
                <div class="car-photo">
                    <Image class="car-photo-img" Texture=@(car.Thumb) />

                    @if ( car.Thumb is null )
                    {
                        <div class="photo-empty">@car.Name.ToUpper()</div>
                    }
                </div>

                <div class="car-info">
                    <div class="car-name">@car.Name</div>
                    <div class="car-desc">@car.Description</div>

                    <div class="stat-grid">
                        @foreach ( var row in rows )
                        {
                            <div class="stat">
                                <div class="stat-head">
                                    <span class="stat-label">@row.Label</span>
                                    <span class="stat-num">@row.Value</span>
                                </div>
                                <div class="stat-bar">
                                    @for ( int s = 0; s < 5; s++ )
                                    {
                                        <div class="seg @(s < row.Value ? "on " + row.Css : "")"></div>
                                    }
                                </div>
                            </div>
                        }
                    </div>
                </div>
            }
            else
            {
                <div class="photo-empty">NO CARS</div>
            }
        </div>

        <div class="cycle cycle-right" onclick=@(() => Select( 1 ))>
            <InputHint Action="MenuRight" Dark=@true />
            <div class="chevron">›</div>
        </div>
    </div>

    <div class="thumb-strip">
        @for ( int i = 0; i < Cars.Count; i++ )
        {
            var ci = i;
            <div class="thumb @(ci == _index ? "active" : "") @(ci == _activeIndex ? "picked" : "")" onclick=@(() => SelectAt( ci ))>
                <div class="thumb-name">@Cars[ci].Name.ToUpper()</div>
                @if ( ci == _activeIndex )
                {
                    <div class="thumb-check">✓</div>
                }
            </div>
        }
    </div>

    <div class="footer-row">
        <div class="back-btn" onclick=@GoBack>
            <InputHint Action="MenuBack" Dark=@true class="back-glyph" />
            <span class="back-label">BACK</span>
        </div>

        <div class="lock-btn @(IsCurrentActive ? "is-active" : "")" onclick=@LockIn>
            <span class="lock-label">@(IsCurrentActive ? "SELECTED" : "LOCK IT IN")</span>
            @if ( !IsCurrentActive )
            {
                <InputHint Action="MenuSelect" class="lock-glyph" />
            }
        </div>
    </div>
</root>

@code
{
    private List<CarResource> _cars;
    private int _index;
    private bool _initialized;

    // Last switch direction for the card slide-in animation.
    private int _lastDir = 1;

    // Locked-in car index (-1 = none); _index is just the preview cursor.
    private int _activeIndex = -1;

    private List<CarResource> Cars => _cars ??= ResourceLibrary.GetAll<CarResource>()
        .OrderBy( c => c.ResourcePath )
        .ToList();

    private CarResource Current => Cars.Count > 0
        ? Cars[System.Math.Clamp( _index, 0, Cars.Count - 1 )]
        : null;

    // True when the preview cursor is on the locked-in car.
    private bool IsCurrentActive => Cars.Count > 0 && _index == _activeIndex;

    private static (string, int, string)[] BuildRows( CarStatValues.DisplayRatings r ) =>
    [
        ("TOP SPEED", r.TopSpeed, "stat-red"),
        ("ACCEL", r.Accel, "stat-yellow"),
        ("HANDLING", r.Handling, "stat-blue"),
        ("BRAKING", r.Braking, "stat-green"),
    ];

    private void EnsureInit()
    {
        if ( _initialized || Cars.Count == 0 )
            return;

        var saved = Machines.Player.Car.PreferredCar;
        if ( !string.IsNullOrEmpty( saved ) )
        {
            var i = Cars.FindIndex( c => c.ResourcePath == saved );
            if ( i >= 0 )
                _index = _activeIndex = i;
        }

        _initialized = true;
    }

    private void Select( int dir )
    {
        if ( Cars.Count == 0 )
            return;

        _lastDir = dir >= 0 ? 1 : -1;
        _index = (_index + dir + Cars.Count) % Cars.Count;
    }

    private void SelectAt( int index )
    {
        if ( index < 0 || index >= Cars.Count )
            return;

        _lastDir = index >= _index ? 1 : -1;
        _index = index;
    }

    /// <summary>
    /// Locks the previewed car in; persists via convar and replicates to the host.
    /// </summary>
    private void LockIn()
    {
        var car = Current;
        if ( car is null || IsCurrentActive )
            return;

        Machines.Player.Car.PreferredCar = car.ResourcePath;

        _activeIndex = _index;
        Sound.Play( "button_accept" );
    }

    private void GoBack()
    {
        this.GetNavigator()?.GoBack();
    }

    public override void Tick()
    {
        base.Tick();
        EnsureInit();

        if ( Input.Pressed( "MenuLeft" ) )
            Select( -1 );
        if ( Input.Pressed( "MenuRight" ) )
            Select( 1 );
        if ( Input.Pressed( "MenuSelect" ) )
            LockIn();
        if ( Input.Pressed( "MenuBack" ) )
            GoBack();
    }

    protected override int BuildHash() => HashCode.Combine( _index, _activeIndex, Cars.Count, Input.UsingController );
}