UI/VehicleMenu.razor

Vehicle UI panel built in Razor. Shows either the trunk transfer view (trunk slots and player pockets, click moves a stack) or a staff management view (repair, flip, lock, engine off, key, remove) and sends each action as a request to the host via VehicleManager. Opens with E at the trunk or G for staff and closes on Escape, distance or lock.

@using Sandbox;
@using Sandbox.UI;
@namespace HorizonRP
@inherits PanelComponent

@*
	Les deux menus d'un véhicule, à droite de l'écran (on garde la voiture en vue) :
	- le coffre (E derrière la voiture, déverrouillée) : les poches d'un côté, les cinq
	  emplacements du coffre de l'autre ; un clic fait passer une pile de l'un à l'autre ;
	- la gestion du staff (G, au volant ou en regardant un véhicule) : réparer, remettre sur
	  ses roues, verrouiller, couper le moteur, ranger.
	Rien n'est décidé ici : chaque bouton est une demande à l'hôte.
	Ajouté au HUD par VehicleManager à l'exécution (pas dans la scène).
*@

<root>
	@if ( Open is { } car && car.IsValid() )
	{
		<div class="vm-backdrop">
			<div class="vm-menu @(Mode == "staff" ? "staff" : "")">
				<div class="vm-head">
					<div class="vm-icon"><i>@(Mode == "staff" ? "shield" : "inventory_2")</i></div>
					<div class="vm-title">
						<div class="vm-name">@(Mode == "staff" ? "Gestion du véhicule" : "Coffre")</div>
						<div class="vm-sub">@($"{(car.Info?.Name ?? "Véhicule")} · {(car.Plate)}")</div>
					</div>
					<div class="vm-close" onclick=@(() => Close())><i>close</i></div>
				</div>

				@if ( Mode == "coffre" )
				{
					<div class="vm-body">
						<div class="vm-label">Coffre <span>@($"{(car.TrunkSlots.Count)} / {(Vehicle.TrunkSize)} · {(Format.Kilos( car.TrunkWeight ))} sur {(Format.Kilos( car.TrunkCapacity ))} · clic : reprendre")</span></div>
						<div class="vm-weight"><div class="vm-weight-fill @(car.TrunkWeight > car.TrunkCapacity * 0.85f ? "heavy" : "")" style="width: @(MathF.Min( 100, car.TrunkWeight / MathF.Max( 0.1f, car.TrunkCapacity ) * 100 ).ToString( "0.#", System.Globalization.CultureInfo.InvariantCulture ))%"></div></div>
						<div class="vm-grid">
							@for ( var i = 0; i < Vehicle.TrunkSize; i++ )
							{
								var index = i;
								var slot = car.TrunkSlots.FirstOrDefault( x => x.Slot == index );
								<div class="vm-slot @(slot is null ? "empty" : "")" onclick=@(() => { if ( slot is not null ) car.RequestTrunkTake( index ); })>
									@if ( slot?.Definition is { } item )
									{
										@SlotContent( item, slot.Count )
									}
								</div>
							}
						</div>

						<div class="vm-label">Vos poches <span>clic : déposer</span></div>
						<div class="vm-grid">
							@for ( var i = 0; i < InventoryRules.Slots; i++ )
							{
								var index = i;
								var slot = Pockets.FirstOrDefault( x => x.Slot == index );
								<div class="vm-slot @(slot is null ? "empty" : "")" onclick=@(() => { if ( slot is not null ) car.RequestTrunkPut( index ); })>
									@if ( slot?.Definition is { } item )
									{
										@SlotContent( item, slot.Count )
									}
								</div>
							}
						</div>
					</div>
				}
				else
				{
					<div class="vm-body">
						<div class="vm-stats">
							<div class="vm-stat"><div class="vm-stat-label">Propriétaire</div><div class="vm-stat-value">@(string.IsNullOrEmpty( car.OwnerName ) ? "—" : car.OwnerName)</div></div>
							<div class="vm-stat @Tone( car.EngineHealth )"><div class="vm-stat-label">Moteur</div><div class="vm-stat-value">@Pct( car.EngineHealth )</div></div>
							<div class="vm-stat @Tone( car.BodyHealth )"><div class="vm-stat-label">Carrosserie</div><div class="vm-stat-value">@Pct( car.BodyHealth )</div></div>
							<div class="vm-stat"><div class="vm-stat-label">Verrou</div><div class="vm-stat-value">@(car.Locked ? "Verrouillé" : "Ouvert")</div></div>
						</div>

						<div class="vm-actions">
							<div class="vm-action primary" onclick=@(() => Act( "reparer" ))><i>build</i> Réparer</div>
							<div class="vm-action" onclick=@(() => Act( "retourner" ))><i>flip</i> Remettre sur ses roues</div>
							<div class="vm-action" onclick=@(() => Act( car.Locked ? "ouvrir" : "verrouiller" ))><i>@(car.Locked ? "lock_open" : "lock")</i> @(car.Locked ? "Déverrouiller" : "Verrouiller")</div>
							<div class="vm-action" onclick=@(() => Act( "moteur" ))><i>power_settings_new</i> Couper le moteur</div>
							<div class="vm-action" onclick=@(() => Act( "cle" ))><i>key</i> Me donner un double</div>
							<div class="vm-action danger" onclick=@(() => Act( "ranger" ))><i>delete</i> Ranger (retirer de la ville)</div>
						</div>

						<div class="vm-label">Coffre <span>@($"{(car.TrunkSlots.Count)} / {(Vehicle.TrunkSize)} · {(Format.Kilos( car.TrunkWeight ))} sur {(Format.Kilos( car.TrunkCapacity ))}")</span></div>
						<div class="vm-list">
							@if ( !car.TrunkSlots.Any() )
							{
								<div class="vm-muted">Vide.</div>
							}
							@foreach ( var slot in car.TrunkSlots.OrderBy( x => x.Slot ) )
							{
								<div class="vm-line">@($"{(slot.Definition?.Name ?? slot.Item)} ×{(slot.Count)}")</div>
							}
						</div>
					</div>
				}
			</div>
		</div>
	}
</root>

@code
{
	/// <summary>Le menu ouvert, sur ce véhicule ; null sinon.</summary>
	public static Vehicle Open { get; private set; }

	/// <summary>« coffre » ou « staff ».</summary>
	public static string Mode { get; private set; }

	RoleplayPlayer Me => RoleplayPlayer.Local;

	List<InventorySlot> Pockets => Me?.PublishedInventory ?? new();

	/// <summary>Ouvre le coffre (E derrière la voiture) ou la gestion du staff (G).</summary>
	public static void Show( Vehicle car, string mode )
	{
		if ( mode == "coffre" && (Open != car || Mode != "coffre") && car.IsValid() )
			car.PlayTrunkSound( true );
		Open = car;
		Mode = mode;
		UiLock.Set( "vehicule", true );
	}

	public static void Close()
	{
		if ( Mode == "coffre" && Open.IsValid() )
			Open.PlayTrunkSound( false );
		Open = null;
		UiLock.Set( "vehicule", false );
	}

	RenderFragment SlotContent( ItemDefinition item, int count ) => @<div class="vm-slot-in">
		@if ( item.Picture is { } picture )
		{
			<div class="vm-pic" style="background-image: url( @picture )"></div>
		}
		else
		{
			<i>@item.Icon</i>
		}
		<div class="vm-slot-name">@item.Name</div>
		<div class="vm-slot-count">@($"×{count}")</div>
	</div>;

	static string Pct( float health ) => $"{MathF.Round( health / Vehicle.MaxHealth * 100 )} %";

	static string Tone( float health ) => health < 250 ? "bad" : health < 600 ? "warn" : "good";

	void Act( string action )
	{
		if ( Open is { } car && car.IsValid() )
			VehicleManager.Current?.RequestStaffAction( car.GameObject.Id, action );
		if ( action == "ranger" )
			Close();
	}

	protected override void OnUpdate()
	{
		var me = Me;
		if ( Open is not null )
		{
			var controller = me?.GetComponent<PlayerController>();
			// Le coffre se ferme si l'on s'éloigne ou si la voiture est verrouillée.
			var gone = !Open.IsValid() || controller is null
				|| Mode == "coffre" && (Open.Locked || !Open.AtTrunk( controller.GameObject ));
			if ( gone || Input.EscapePressed )
			{
				Input.EscapePressed = false;
				Close();
			}
			return;
		}

		// G, staff seulement : le véhicule où l'on est assis, ou celui qu'on vise (la carte du
		// HUD le propose). Ailleurs, rien.
		if ( me?.IsStaff == true && Input.Pressed( "VehicleAdmin" ) && !UiLock.IsLocked && me.GetComponent<PlayerController>() is { } pc )
		{
			var car = me.GameObject.GetComponentInParent<Vehicle>() ?? (pc.Hovered is Vehicle aimed && aimed.NearBody( me.GameObject ) ? aimed : null);
			if ( car is not null && car.IsValid() )
				Show( car, "staff" );
		}
	}

	protected override void OnDisabled() => UiLock.Set( "vehicule", false );

	protected override int BuildHash()
	{
		var car = Open;
		if ( car is null || !car.IsValid() )
			return 0;
		return HashCode.Combine( Mode, car.TrunkJson, Me?.InventoryJson, car.Locked, (int)car.EngineHealth, (int)car.BodyHealth, car.OwnerName );
	}
}