Money/RoleplayPlayer.Atm.cs

Partial of the roleplay player that implements ATM interactions: host RPCs to claim an ATM, open the screen on the owner, release it, and process a withdrawal or deposit after re-validating distance, ownership of the machine, account access, amount limits and balances. The result is sent back to the requesting player only.

Networking
namespace HorizonRP;

/// <summary>
/// Les opérations du distributeur (UI/AtmScreen.razor) : l'hôte revérifie tout — un
/// distributeur à portée, le compte (le sien), le montant (entier, positif, plafonné, couvert) —
/// puis répond au seul joueur par <see cref="AtmResult"/>, que l'écran attend pour afficher
/// « Prenez vos billets » ou le refus.
/// </summary>
public sealed partial class RoleplayPlayer
{
	/// <summary>Le plus gros retrait (ou dépôt) d'une seule opération, comme le plafond d'une carte.</summary>
	public const int AtmLimit = 5000;

	/// <summary>
	/// Touche E sur un distributeur : l'hôte le donne s'il est libre (Atm.TryTake), et seulement
	/// alors l'écran s'ouvre chez le joueur (<see cref="AtmGranted"/>). Un seul joueur à la fois
	/// (Julien, 31/10/2026) ; sinon un message, rien ne s'ouvre.
	/// </summary>
	[Rpc.Host]
	public void RequestAtmOpen( Guid atmId )
	{
		if ( !CallerIsOwner() || !ProfileLoaded )
			return;
		var atm = Atm.All( Scene ).FirstOrDefault( x => x.Id == atmId );
		if ( atm is null || WorldPosition.WithZ( 0 ).Distance( atm.WorldPosition.WithZ( 0 ) ) > Atm.Reach )
			return;
		if ( !Atm.TryTake( atm, this ) )
		{
			Notify( "Ce distributeur est déjà utilisé : patientez.", "erreur" );
			return;
		}
		AtmGranted( atmId );
	}

	/// <summary>Chez le joueur : le distributeur est à lui, l'écran s'ouvre.</summary>
	[Rpc.Owner]
	public void AtmGranted( Guid atmId )
	{
		var atm = Atm.All( Scene ).FirstOrDefault( x => x.Id == atmId );
		if ( atm is not null )
			AtmScreen.Open( atm );
	}

	/// <summary>L'écran se ferme (carte rendue, éloigné, mort…) : le distributeur est libéré.</summary>
	[Rpc.Host]
	public void ReleaseAtm()
	{
		if ( CallerIsOwner() )
			Atm.Release( this );
	}

	[Rpc.Host]
	public void RequestAtm( string operation, int amount, string accountId )
	{
		if ( !CallerIsOwner() )
			return;
		var game = HorizonGame.Current;
		string refus = null;
		if ( game is null || !ProfileLoaded )
			refus = "Service momentanément indisponible.";
		else if ( !Atm.IsNear( this ) )
			refus = "Vous êtes trop loin du distributeur.";
		else if ( !Atm.Holds( this ) )
			refus = "Ce distributeur est déjà utilisé.";
		else if ( Atm.AccountsOf( this ).All( x => x.Id != accountId ) )
			refus = "Ce compte n'est pas accessible avec cette carte.";
		else if ( amount <= 0 || amount > AtmLimit )
			refus = $"Montant non autorisé (de 1 à {Format.Money( AtmLimit )}).";
		else if ( operation == "retrait" && amount > Atm.BalanceOf( this, accountId ) )
			refus = "Solde insuffisant.";
		else if ( operation == "depot" && amount > Wallet )
			refus = "Vous n'avez pas autant d'espèces sur vous.";
		else if ( operation is not ("retrait" or "depot") )
			refus = "Opération inconnue.";

		if ( refus is not null )
		{
			AtmResult( false, refus, Bank, Wallet );
			return;
		}

		game.TryTransfer( this, amount, toBank: operation == "depot", notify: false );
		AtmResult( true, "", Bank, Wallet );
	}

	/// <summary>Chez le joueur : la réponse de l'hôte, avec les soldes après l'opération.</summary>
	[Rpc.Owner]
	public void AtmResult( bool ok, string message, int bank, int wallet )
	{
		AtmScreen.Answer( ok, message, bank, wallet );
	}
}