The garage and dealership menu UI, opened from a Garage point in an RP game. Lists owned vehicles with spawn/store buttons, shows a 3D showroom of for-sale models with color picker, and sends buy and spawn requests to VehicleManager. Closes on Escape or when the player walks away from a garage.
@using Sandbox;
@using Sandbox.UI;
@namespace HorizonRP
@inherits PanelComponent
@*
Concession et garage, ouverts par la touche E sur un point « Garage » (F6).
Mes véhicules : sortir, ranger. Concession : choisir un modèle et une couleur,
payer depuis la banque. Chaque bouton est une demande à l'hôte (VehicleManager).
*@
<root>
@if ( IsOpen && Me is not null )
{
<div class="garage-backdrop">
<div class="garage">
<div class="garage-head">
<div class="garage-icon"><i>garage</i></div>
<div class="garage-title">GARAGE <span>HORIZON RP</span></div>
<div class="garage-tabs">
<div class="garage-tab @(Tab == "mes" ? "active" : "")" onclick=@(() => Tab = "mes")><i>directions_car</i> Mes véhicules <span class="count">@Owned.Count</span></div>
<div class="garage-tab @(Tab == "concession" ? "active" : "")" onclick=@(() => Tab = "concession")><i>storefront</i> Concession</div>
</div>
<div class="garage-bank">Banque <b>@Format.Money( Me.Bank )</b></div>
<div class="garage-close" onclick=@(() => Close())><i>close</i></div>
</div>
<div class="garage-body">
@if ( Tab == "mes" )
{
@if ( OutVehicle is { } outside )
{
<div class="out">
<i>near_me</i>
<div class="out-text">@($"{(outside.Info?.Name ?? "Véhicule")} · {(outside.Plate)} est dehors")</div>
<div class="btn" onclick=@(() => Manager.RequestStore())><i>garage</i> Ranger</div>
</div>
}
@if ( !Owned.Any() )
{
<div class="empty">Vous n'avez pas encore de véhicule. Passez par la concession.</div>
}
<div class="cards">
@foreach ( var car in Owned )
{
var model = VehicleCatalog.Get( car.ModelId );
<div class="card">
<div class="swatch" style="background-color: @car.Color"></div>
<div class="card-name">@(model?.Name ?? car.ModelId)</div>
<div class="plate">@car.Plate</div>
<div class="btn primary" onclick=@(() => Spawn( car.Id ))><i>key</i> Sortir</div>
</div>
}
</div>
}
else
{
<div class="showroom">
<ScenePanel @ref="Showroom" class="showroom-3d"></ScenePanel>
<div class="showroom-info">
<div class="showroom-name">@(ShownModel?.Name ?? "")</div>
<div class="showroom-price">@(ShownModel is null ? "" : Format.Money( ShownModel.Price ))</div>
</div>
</div>
<div class="cards">
@foreach ( var model in ForSale )
{
<div class="card @(Selected == model.Id ? "selected" : "")" onclick=@(() => Selected = model.Id)>
<div class="swatch" style="background-color: @Color"></div>
<div class="card-name">@model.Name</div>
<div class="card-desc">@model.Description</div>
<div class="specs">
<span><i>speed</i> @($"{(model.MaxSpeedKmh.CeilToInt())} km/h")</span>
<span><i>bolt</i> @model.Acceleration.CeilToInt()</span>
</div>
<div class="price">@Format.Money( model.Price )</div>
</div>
}
</div>
<div class="section-label">Couleur</div>
<div class="colors">
@foreach ( var c in VehicleCatalog.Colors )
{
<div class="color @(Color == c.Hex ? "on" : "")" style="background-color: @c.Hex" onclick=@(() => Color = c.Hex)></div>
}
</div>
<div class="buy @(SelectedModel is null || Me.Bank < SelectedModel.Price ? "disabled" : "")" onclick=@(() => Buy())>
<i>shopping_cart</i> @(SelectedModel is null ? "Choisissez un modèle" : $"Acheter {SelectedModel.Name} · {Format.Money( SelectedModel.Price )} (banque)")
</div>
}
</div>
</div>
</div>
}
</root>
@code
{
public bool IsOpen { get; private set; }
/// <summary>Le salon d'exposition : le modèle choisi, à sa couleur, en 3D.</summary>
ScenePanel Showroom { get; set; }
readonly Preview3D showroom = new() { TurnSpeed = 14f };
/// <summary>Ce que la concession vend : les fiches « en vente » (pas la flotte de police, par exemple).</summary>
static IEnumerable<VehicleModel> ForSale => VehicleCatalog.All.Where( x => x.Shop );
VehicleModel ShownModel => SelectedModel ?? ForSale.FirstOrDefault();
string Tab { get; set; } = "mes";
string Selected { get; set; }
string Color { get; set; } = VehicleCatalog.Colors[0].Hex;
RoleplayPlayer Me => RoleplayPlayer.Local;
VehicleManager Manager => VehicleManager.Current;
VehicleModel SelectedModel => VehicleCatalog.Get( Selected );
Vehicle OutVehicle => Me is null ? null : Vehicle.All.FirstOrDefault( x => x.OwnerId == Me.SteamId );
List<VehicleManager.OwnedVehicle> Owned
{
get
{
var json = Me?.GarageJson;
if ( json != parsedJson )
{
parsedJson = json;
owned = string.IsNullOrEmpty( json ) ? new() : Json.Deserialize<List<VehicleManager.OwnedVehicle>>( json ) ?? new();
}
return owned;
}
}
string parsedJson;
List<VehicleManager.OwnedVehicle> owned = new();
public void Open()
{
IsOpen = true;
Tab = Owned.Any() ? "mes" : "concession";
UiLock.Set( "garage", true );
}
void Close()
{
IsOpen = false;
UiLock.Set( "garage", false );
}
protected override void OnDisabled() => UiLock.Set( "garage", false );
protected override void OnUpdate()
{
// Échap referme ce menu (et ne laisse pas s'ouvrir le menu pause en même temps).
if ( IsOpen && Input.EscapePressed )
{
Input.EscapePressed = false;
Close();
return;
}
if ( IsOpen && Tab == "concession" && Showroom is not null && ShownModel is { } shown )
{
showroom.Attach( Showroom );
showroom.ShowModel( shown.Model, Vehicle.PaintTint( Color ) );
showroom.Tick();
}
// Loin de tout garage, le menu se referme : l'hôte refuserait de toute façon.
if ( IsOpen && (Me is null || !CityObject.All.Any( x => x.Kind == CityKinds.Garage && x.WorldPosition.Distance( Me.WorldPosition ) <= VehicleManager.GarageReach )) )
Close();
}
void Spawn( long id )
{
Manager?.RequestSpawn( id );
Close();
}
void Buy()
{
if ( SelectedModel is null )
return;
Manager?.RequestBuy( SelectedModel.Id, Color );
Tab = "mes";
}
protected override int BuildHash() => HashCode.Combine(
HashCode.Combine( IsOpen, Tab, Selected, Color ),
HashCode.Combine( Me?.Bank, Me?.GarageJson, OutVehicle?.Plate ) );
}