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.
@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 );
}