Partial Vehicle class that adds a five-slot trunk inventory, synced from the host as JSON. Host RPCs handle moving items between a player's pockets and the trunk with weight and stack checks, plus a staff take/discard path, trunk open/close sounds and proximity checks for standing behind the car.
namespace HorizonRP;
/// <summary>
/// Le coffre : cinq emplacements, comme les poches. On l'ouvre par E derrière la voiture,
/// collé au pare-chocs, véhicule déverrouillé. L'hôte seul y écrit (<see cref="TrunkJson"/>) ;
/// le menu (VehicleMenu) ne fait que des demandes. Un véhicule acheté garde son coffre entre
/// deux sorties (enregistré avec ses dégâts) ; celui d'un véhicule donné par le staff disparaît
/// avec lui.
/// </summary>
public sealed partial class Vehicle
{
public const int TrunkSize = 5;
/// <summary>Ce que porte le coffre (kg), d'après la fiche.</summary>
public float TrunkCapacity => Info?.TrunkKg ?? 80f;
/// <summary>Le poids de ce qui est dans le coffre (kg).</summary>
public float TrunkWeight => InventoryRules.WeightOf( TrunkSlots );
/// <summary>Distance à plat entre les pieds du joueur et le milieu du pare-chocs arrière.</summary>
public const float TrunkReach = 44f;
[Sync( SyncFlags.FromHost )] public string TrunkJson { get; set; } = "[]";
string parsedTrunk;
List<InventorySlot> trunkSlots = new();
/// <summary>Le contenu du coffre (relu quand TrunkJson change).</summary>
public List<InventorySlot> TrunkSlots
{
get
{
if ( TrunkJson != parsedTrunk )
{
parsedTrunk = TrunkJson;
try { trunkSlots = Json.Deserialize<List<InventorySlot>>( TrunkJson ?? "[]" ) ?? new(); }
catch { trunkSlots = new(); }
}
return trunkSlots;
}
}
/// <summary>Le milieu du pare-chocs arrière, au sol, en coordonnées du monde.</summary>
public Vector3 TrunkPoint
{
get
{
var bb = Rig?.BodyBounds ?? new BBox( -Vector3.One * 80, Vector3.One * 80 );
var yaw = Rotation.Identity; // déjà dans le gréement
return WorldTransform.PointToWorld( yaw * new Vector3( bb.Mins.x, bb.Center.y, bb.Mins.z ) );
}
}
const string TrunkOpenSound = "sounds/vehicules/trunk_opening.mp3";
const string TrunkCloseSound = "sounds/vehicules/trunk_close.mp3";
/// <summary>Partout : le coffre qu'on ouvre ou qu'on referme (sons fournis par Julien).</summary>
[Rpc.Broadcast]
public void PlayTrunkSound( bool open )
{
if ( Application.IsDedicatedServer )
return;
Sounds.PlayFileAt( open ? TrunkOpenSound : TrunkCloseSound, TrunkPoint, 0.4f );
}
/// <summary>Le joueur est-il derrière la voiture, à toucher le coffre ?</summary>
public bool AtTrunk( GameObject who ) =>
Ready && who.IsValid() && who.GetComponentInParent<Vehicle>() != this
&& who.WorldPosition.WithZ( 0 ).Distance( TrunkPoint.WithZ( 0 ) ) <= TrunkReach;
/// <summary>Hôte : publie le coffre (et le fait enregistrer).</summary>
void PublishTrunk( List<InventorySlot> slots )
{
TrunkJson = Json.Serialize( slots.OrderBy( x => x.Slot ).ToList() );
RequestSaveState();
}
static RoleplayPlayer CallerPlayer() =>
RoleplayPlayer.All.FirstOrDefault( x => Rpc.Caller is not null && x.Network.OwnerId == Rpc.Caller.Id && x.ProfileLoaded );
/// <summary>Hôte : le joueur peut-il se servir du coffre maintenant ?</summary>
bool CanUseTrunk( RoleplayPlayer player )
{
if ( player is null || !AtTrunk( player.GameObject ) )
return false;
if ( Locked )
{
player.Notify( "Le véhicule est verrouillé.", "erreur" );
return false;
}
return true;
}
/// <summary>Une poche du joueur vers le coffre (toute la pile, sur la même pile si elle y est déjà).</summary>
[Rpc.Host]
public void RequestTrunkPut( int pocket )
{
var player = CallerPlayer();
if ( !CanUseTrunk( player ) )
return;
var slot = player.SlotAt( pocket );
var item = slot?.Definition;
if ( item is null )
return;
// Le poids : autant d'unités que le coffre en porte encore (une pile peut n'entrer qu'en partie).
var room = TrunkCapacity - TrunkWeight;
var fits = item.Weight <= 0 ? slot.Count : (int)MathF.Floor( (room + 0.0001f) / item.Weight );
if ( fits <= 0 )
{
player.Notify( $"Le coffre est plein : {Format.Kilos( TrunkWeight )} sur {Format.Kilos( TrunkCapacity )}.", "erreur" );
return;
}
var wanted = Math.Min( slot.Count, fits );
var slots = TrunkSlots.Select( Copy ).ToList();
var same = slot.Data is null ? slots.FirstOrDefault( x => x.Item == slot.Item && x.Data is null && x.Count < item.MaxStack ) : null;
int moved;
if ( same is not null )
{
moved = Math.Min( wanted, item.MaxStack - same.Count );
same.Count += moved;
}
else
{
var free = Enumerable.Range( 0, TrunkSize ).Where( i => slots.All( x => x.Slot != i ) ).Select( i => (int?)i ).FirstOrDefault();
if ( free is null )
{
player.Notify( "Le coffre est plein.", "erreur" );
return;
}
moved = wanted;
slots.Add( new InventorySlot { Slot = free.Value, Item = slot.Item, Count = moved, Data = slot.Data } );
}
if ( moved < slot.Count )
player.Notify( $"Le coffre ne prend que {moved} sur {slot.Count} : il est presque plein.", "info" );
WeaponSystem.Current?.HolsterIfActive( player, slot );
player.RemoveFrom( slot, moved );
PublishTrunk( slots );
HorizonGame.Current?.AddLog( "coffre", player.SteamId, $"Dépose {item.Name} ×{moved} dans {Plate}" );
}
/// <summary>Un emplacement du coffre vers les poches du joueur.</summary>
[Rpc.Host]
public void RequestTrunkTake( int trunkSlot )
{
var player = CallerPlayer();
if ( !CanUseTrunk( player ) )
return;
var slots = TrunkSlots.Select( Copy ).ToList();
var slot = slots.FirstOrDefault( x => x.Slot == trunkSlot );
var item = slot?.Definition;
if ( item is null )
return;
var taken = player.AddItem( item, slot.Count, slot.Data );
if ( taken == 0 )
{
player.Notify( "Plus de place dans vos poches.", "erreur" );
return;
}
slot.Count -= taken;
if ( slot.Count <= 0 )
slots.Remove( slot );
PublishTrunk( slots );
HorizonGame.Current?.AddLog( "coffre", player.SteamId, $"Prend {item.Name} ×{taken} dans {Plate}" );
}
/// <summary>
/// Hôte, pour le staff (menu en jeu, panneau web) : un emplacement du coffre pris dans les
/// poches de qui le demande (« prendre »), ou supprimé (« jeter »). Journalisé.
/// </summary>
public (bool, string) StaffTrunk( int trunkSlot, string action, string by, RoleplayPlayer taker = null )
{
var slots = TrunkSlots.Select( Copy ).ToList();
var slot = slots.FirstOrDefault( x => x.Slot == trunkSlot );
var item = slot?.Definition;
if ( item is null )
return (false, "Cet emplacement du coffre est vide");
var count = slot.Count;
if ( action == "prendre" )
{
if ( taker is null )
return (false, "Prendre se fait depuis le jeu");
count = taker.AddItem( item, slot.Count, slot.Data );
if ( count == 0 )
return (false, "Plus de place dans vos poches");
}
else if ( action != "jeter" )
return (false, $"Action inconnue : {action}");
slot.Count -= count;
if ( slot.Count <= 0 )
slots.Remove( slot );
PublishTrunk( slots );
var what = action == "prendre" ? "pris" : "supprimés";
HorizonGame.Current?.AddLog( "coffre", OwnerId, $"{by} : {item.Name} ×{count} {what} du coffre de {Plate}" );
return (true, $"{item.Name} ×{count} {what} du coffre de {Plate}");
}
static InventorySlot Copy( InventorySlot x ) => new() { Slot = x.Slot, Item = x.Item, Count = x.Count, Data = x.Data };
}