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