Host-side money system for the roleplay gamemode. Holds the wallet hold state, and handles host RPCs to give cash to another player, drop a bundle on the ground, pick up a bundle, and search/loot a downed player's cash and inventory slots, with logging and saves.
namespace HorizonRP;
/// <summary>
/// L'argent liquide qui change de main. Il n'est pas un objet d'inventaire : c'est le compteur
/// du portefeuille (<see cref="RoleplayPlayer.Wallet"/>). On le tient en main par la case
/// « Billets » de la barre (après les cinq poches, à la molette), comme une clé : la liasse dans
/// la main (VehicleKeySystem ; rien à 0 $), et le clic droit ouvre <see cref="CashMenu"/> : donner
/// au joueur qu'on regarde, ou poser au sol (<see cref="CashDrop"/>), après quoi on revient aux
/// mains. Ce qu'on ramasse retourne droit au portefeuille.
///
/// Sur un corps : un inconscient garde son argent, on le prend en le fouillant (E maintenu,
/// RoleplayPlayer.Death.cs) ; à sa mort, ce qu'il avait sur lui reste par terre en liasse.
///
/// L'hôte relit tout : le client ne désigne qu'un montant et une cible, jamais un solde.
/// Chaque mouvement va au journal.
/// </summary>
public static class Cash
{
/// <summary>La valeur de <see cref="RoleplayPlayer.HeldKey"/> quand on tient ses billets.</summary>
public const string HoldId = "argent:liasse";
public const string ModelPath = "models/horizon/billets/liasse.vmdl";
/// <summary>Portée pour donner ou ramasser (~2,5 m), la même que pour les objets.</summary>
public const float Reach = InventoryActions.Reach;
public static bool IsHeld( RoleplayPlayer player ) => player?.HeldKey == HoldId;
static RoleplayPlayer Caller() =>
RoleplayPlayer.All.FirstOrDefault( x => Rpc.Caller is not null && x.Network.OwnerId == Rpc.Caller.Id && x.ProfileLoaded );
static bool Busy( RoleplayPlayer p ) => p.OnTheGround || p.NeedsCharacter || p.Restrained;
/// <summary>Prend ses billets en main (l'arme et la clé tenues sont rangées), ou les range.</summary>
[Rpc.Host]
public static void RequestHold( bool hold )
{
var player = Caller();
if ( player is null || Busy( player ) )
return;
if ( !hold )
{
if ( IsHeld( player ) )
player.HeldKey = null;
return;
}
if ( player.GameObject.GetComponentInParent<Vehicle>() is not null )
return;
WeaponSystem.Current?.Holster( player );
player.HeldKey = HoldId;
}
/// <summary>Donne une somme au joueur visé, à portée.</summary>
[Rpc.Host]
public static void RequestGive( Guid targetId, int amount )
{
var player = Caller();
var target = Game.ActiveScene.Directory.FindByGuid( targetId )?.GetComponent<RoleplayPlayer>();
if ( player is null || target is null || target == player || Busy( player ) || !target.ProfileLoaded )
return;
if ( target.WorldPosition.Distance( player.WorldPosition ) > Reach * 1.5f )
{
player.Notify( $"{target.RpName} est trop loin.", "erreur" );
return;
}
amount = Math.Clamp( amount, 0, player.Wallet );
if ( amount <= 0 )
{
player.Notify( "Vous n'avez pas cette somme sur vous.", "erreur" );
return;
}
player.Wallet -= amount;
target.Wallet += amount;
// L'argent donné, on revient aux mains (la barre suit chez lui : CashMenu).
player.HeldKey = null;
player.Notify( $"Vous donnez {Format.Money( amount )} à {target.RpName}.", "argent" );
target.Notify( $"{player.RpName} vous donne {Format.Money( amount )}.", "argent" );
HorizonGame.Current?.AddLog( "argent_donne", player.SteamId, $"{player.RpName} donne {Format.Money( amount )} en liquide à {target.RpName} ({target.SteamId})" );
HorizonGame.Current?.SaveNow( player );
HorizonGame.Current?.SaveNow( target );
}
/// <summary>Pose une somme au sol, devant soi.</summary>
[Rpc.Host]
public static void RequestDrop( int amount )
{
var player = Caller();
if ( player is null || Busy( player ) || player.GameObject.GetComponentInParent<Vehicle>() is not null )
return;
amount = Math.Clamp( amount, 0, player.Wallet );
if ( amount <= 0 )
{
player.Notify( "Vous n'avez pas cette somme sur vous.", "erreur" );
return;
}
player.Wallet -= amount;
player.HeldKey = null;
var at = player.WorldPosition + player.WorldRotation.Forward.WithZ( 0 ).Normal * 26f;
CashDrop.Spawn( amount, at, $"posé par {player.RpName} ({player.SteamId})" );
player.Notify( $"Vous posez {Format.Money( amount )} au sol.", "argent" );
HorizonGame.Current?.AddLog( "argent_pose", player.SteamId, $"{player.RpName} pose {Format.Money( amount )} en liquide au sol" );
HorizonGame.Current?.SaveNow( player );
}
/// <summary>Ramasse une liasse : tout va au portefeuille.</summary>
[Rpc.Host]
public static void RequestPickup( Guid dropId )
{
var player = Caller();
var drop = Game.ActiveScene.Directory.FindByGuid( dropId )?.GetComponent<CashDrop>();
if ( player is null || drop is null || Busy( player ) || drop.Amount <= 0 )
return;
if ( drop.WorldPosition.Distance( player.WorldPosition ) > Reach * 1.5f )
return;
var amount = drop.Amount;
drop.Amount = 0;
drop.GameObject.Destroy();
player.Wallet += amount;
player.Notify( $"Vous ramassez {Format.Money( amount )}.", "argent" );
HorizonGame.Current?.AddLog( "argent_ramasse", player.SteamId, $"{player.RpName} ramasse {Format.Money( amount )} au sol ({drop.Origin})" );
HorizonGame.Current?.SaveNow( player );
}
/// <summary>Fouille un inconscient : on prend l'argent qu'il a sur lui.</summary>
[Rpc.Host]
public static void RequestSearch( Guid targetId )
{
var player = Caller();
var target = Game.ActiveScene.Directory.FindByGuid( targetId )?.GetComponent<RoleplayPlayer>();
if ( player is null || target is null || target == player || Busy( player ) || !target.Downed )
return;
if ( target.BodyPosition.Distance( player.WorldPosition ) > Reach * 1.5f )
return;
var amount = target.Wallet;
if ( amount <= 0 )
{
player.Notify( $"{target.RpName} n'a pas d'argent sur lui.", "info" );
HorizonGame.Current?.AddLog( "fouille", player.SteamId, $"{player.RpName} fouille {target.RpName} ({target.SteamId}) inconscient : rien" );
return;
}
target.Wallet = 0;
player.Wallet += amount;
player.Notify( $"Vous prenez {Format.Money( amount )} sur {target.RpName}.", "argent" );
HorizonGame.Current?.AddLog( "fouille", player.SteamId, $"{player.RpName} prend {Format.Money( amount )} sur {target.RpName} ({target.SteamId}) inconscient" );
HorizonGame.Current?.SaveNow( player );
HorizonGame.Current?.SaveNow( target );
}
/// <summary>L'inconscient qu'on fouille, s'il l'est encore et qu'on est à côté.</summary>
static (RoleplayPlayer Player, RoleplayPlayer Target) SearchPair( Guid targetId )
{
var player = Caller();
var target = Game.ActiveScene.Directory.FindByGuid( targetId )?.GetComponent<RoleplayPlayer>();
if ( player is null || target is null || target == player || Busy( player ) || !target.Downed )
return (null, null);
return target.BodyPosition.Distance( player.WorldPosition ) > Reach * 1.5f ? (null, null) : (player, target);
}
/// <summary>Prend une poche d'un inconscient (SearchMenu) : ce qui tient dans ses propres poches.</summary>
[Rpc.Host]
public static void RequestTakeItem( Guid targetId, int slotIndex )
{
var (player, target) = SearchPair( targetId );
if ( player is null || TakeSlot( player, target, slotIndex ) is not { } taken )
return;
player.Notify( $"Vous prenez {taken} sur {target.RpName}.", "info" );
HorizonGame.Current?.AddLog( "fouille", player.SteamId, $"{player.RpName} prend {taken} sur {target.RpName} ({target.SteamId}) inconscient" );
HorizonGame.Current?.SaveNow( player );
HorizonGame.Current?.SaveNow( target );
}
/// <summary>Tout prendre : l'argent, puis chaque poche, tant qu'il y a de la place.</summary>
[Rpc.Host]
public static void RequestTakeAll( Guid targetId )
{
var (player, target) = SearchPair( targetId );
if ( player is null )
return;
var taken = new List<string>();
if ( target.Wallet > 0 )
{
taken.Add( Format.Money( target.Wallet ) );
player.Wallet += target.Wallet;
target.Wallet = 0;
}
foreach ( var slot in target.Inventory.OrderBy( x => x.Slot ).Select( x => x.Slot ).ToList() )
if ( TakeSlot( player, target, slot ) is { } t )
taken.Add( t );
var left = target.Inventory.Count;
if ( taken.Count == 0 )
{
player.Notify( left > 0 ? "Plus de place dans vos poches." : $"{target.RpName} n'a rien sur lui.", "info" );
return;
}
player.Notify( $"Vous prenez sur {target.RpName} : {string.Join( ", ", taken )}." + (left > 0 ? " Le reste ne tient pas dans vos poches." : ""), "info" );
HorizonGame.Current?.AddLog( "fouille", player.SteamId, $"{player.RpName} vide {target.RpName} ({target.SteamId}) inconscient : {string.Join( ", ", taken )}" );
HorizonGame.Current?.SaveNow( player );
HorizonGame.Current?.SaveNow( target );
}
/// <summary>Une poche passe d'un corps à l'autre (avec ses données : une arme garde sa série). Rend ce qui a été pris.</summary>
static string TakeSlot( RoleplayPlayer player, RoleplayPlayer target, int slotIndex )
{
var slot = target.SlotAt( slotIndex );
var item = slot?.Definition;
if ( item is null )
return null;
if ( target.HeldKey == slot.Item )
target.HeldKey = null;
WeaponSystem.Current?.HolsterIfActive( target, slot );
var count = player.AddItem( item, slot.Count, slot.Data );
if ( count <= 0 )
{
player.Notify( $"Pas de place pour {item.Name}.", "erreur" );
return null;
}
target.RemoveFrom( slot, count );
return count > 1 ? $"{item.Name} ×{count}" : item.Name;
}
}