UI/GarageMenu.razor

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