UI/AtmScreen.razor

Razor panel for the HZ BANK ATM. Moves the local camera onto the machine's screen, draws the touch interface onto the projected screen rectangle, and steps through card insertion, account choice, withdrawal, deposit, balance, ticket and card return, sending each operation to the host through RoleplayPlayer.RequestAtm.

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

@*
	L'écran du distributeur HZ BANK (Money/Atm.cs ; Julien, 30/10/2026). Touche E devant un
	distributeur : la vue glisse jusqu'à faire face à son écran (PreRender, appelé par
	RoleplayPlayer.OnPreRender) et l'interface se pose exactement sur le rectangle de l'écran du
	modèle (ses quatre coins projetés à chaque image) — on lit et on touche l'écran du prop.
	Déroulé d'un vrai distributeur américain, en français : lecture de la carte, choix du compte
	(un seul pour l'instant : les comptes d'entreprise viendront s'y ajouter), retrait, dépôt,
	solde, ticket, carte rendue. L'hôte revérifie tout (RoleplayPlayer.RequestAtm).
	Créé par le HUD sur chaque machine, comme CashMenu. Bruitages : vrais enregistrements d'ATM
	(CC0, outils/atm/sons_atm_reels.py) ; les écrans minutés durent ce que durent leurs sons.
*@

<root>
	@if ( IsOpen && Me is not null )
	{
		<div class="atm-frame" style="@FrameStyle">
			<div class="atm-surface" style="@SurfaceStyle">
				<div class="atm-top">
					<div class="atm-logo"><div class="atm-logo-mark">HZ</div><div class="atm-logo-text">BANK</div></div>
					<div class="atm-top-right">
						@if ( Account is not null && Step is not (Steps.Insertion or Steps.Lecture or Steps.Comptes) )
						{
							<div class="atm-account">@($"{Account.Label}  ·  {Atm.Masked( Account.Number )}")</div>
						}
						<div class="atm-clock">@GameClock.Text</div>
					</div>
				</div>

				<div class="atm-body">
					@if ( Step == Steps.Insertion || Step == Steps.Lecture )
					{
						<div class="atm-center">
							<div class="atm-card @(Step == Steps.Lecture ? "reading" : "")">
								<div class="atm-card-chip"></div>
								<div class="atm-card-brand">HZ BANK</div>
								<div class="atm-card-number">@Atm.Masked( Atm.AccountsOf( Me ).FirstOrDefault()?.Number )</div>
								<div class="atm-card-name">@((Me.RpName ?? "").ToUpperInvariant())</div>
							</div>
							<div class="atm-title">@(Step == Steps.Insertion ? "Bienvenue chez HZ BANK" : "Lecture de votre carte…")</div>
							<div class="atm-sub">@(Step == Steps.Insertion ? "Insertion de la carte" : "Veuillez patienter, ne retirez pas votre carte")</div>
							<div class="atm-progress"><div class="atm-progress-fill" style="width: @Pct( Progress );"></div></div>
						</div>
					}
					else if ( Step == Steps.Comptes )
					{
						<div class="atm-title left">Sélectionnez un compte</div>
						<div class="atm-sub left">@($"Carte HZ BANK  ·  {(Me.RpName ?? "").ToUpperInvariant()}")</div>
						<div class="atm-list">
							@foreach ( var account in Atm.AccountsOf( Me ) )
							{
								<div class="atm-key wide" onclick=@(() => ChooseAccount( account ))>
									<div class="atm-key-icon"><i>@(account.Kind == "entreprise" ? "business" : "account_balance_wallet")</i></div>
									<div class="atm-key-text">
										<div class="atm-key-label">@account.Label</div>
										<div class="atm-key-desc">@account.Number</div>
									</div>
									<div class="atm-key-arrow"><i>chevron_right</i></div>
								</div>
							}
						</div>
						<div class="atm-actions"><div class="atm-key small ghost" onclick=@(() => Finish())><i>eject</i> Annuler et rendre la carte</div></div>
					}
					else if ( Step == Steps.Menu )
					{
						<div class="atm-title left">Bonjour @FirstName,</div>
						<div class="atm-sub left">@(Done ? "Souhaitez-vous effectuer une autre opération ?" : "Que souhaitez-vous faire ?")</div>
						<div class="atm-grid">
							<div class="atm-key tile" onclick=@(() => Go( Steps.Retrait ))><div class="atm-key-icon"><i>payments</i></div><div class="atm-key-label">Retrait</div></div>
							<div class="atm-key tile" onclick=@(() => Go( Steps.Solde ))><div class="atm-key-icon"><i>account_balance</i></div><div class="atm-key-label">Consulter le solde</div></div>
							<div class="atm-key tile" onclick=@(() => Go( Steps.Depot ))><div class="atm-key-icon"><i>savings</i></div><div class="atm-key-label">Dépôt d'espèces</div></div>
							<div class="atm-key tile @(Me.Bank < 100 ? "off" : "")" onclick=@(() => Withdraw( 100 ))><div class="atm-key-icon"><i>bolt</i></div><div class="atm-key-label">Retrait rapide @Format.Money( 100 )</div></div>
							<div class="atm-key tile" onclick=@(() => Go( Steps.Comptes ))><div class="atm-key-icon"><i>swap_horiz</i></div><div class="atm-key-label">Changer de compte</div></div>
							<div class="atm-key tile ghost" onclick=@(() => Finish())><div class="atm-key-icon"><i>eject</i></div><div class="atm-key-label">Terminer</div></div>
						</div>
					}
					else if ( Step == Steps.Retrait || Step == Steps.Depot )
					{
						var depot = Step == Steps.Depot;
						<div class="atm-title left">@(depot ? "Dépôt d'espèces" : "Montant du retrait")</div>
						<div class="atm-sub left">@(depot ? $"Espèces sur vous : {Format.Money( Me.Wallet )}" : $"Solde disponible : {Format.Money( Me.Bank )}")</div>
						<div class="atm-grid amounts">
							@foreach ( var amount in depot ? DepositAmounts : WithdrawAmounts )
							{
								var possible = depot ? amount <= Me.Wallet : amount <= Me.Bank;
								<div class="atm-key amount @(possible ? "" : "off")" onclick=@(() => Pick( amount ))>@Format.Money( amount )</div>
							}
							@if ( depot )
							{
								<div class="atm-key amount text @(Me.Wallet > 0 ? "" : "off")" onclick=@(() => Pick( Math.Min( Me.Wallet, RoleplayPlayer.AtmLimit ) ))>Tout déposer</div>
							}
							<div class="atm-key amount text" onclick=@(() => Go( Steps.Saisie ))>Autre montant</div>
						</div>
						<div class="atm-actions"><div class="atm-key small ghost" onclick=@(() => Go( Steps.Menu ))><i>arrow_back</i> Retour</div></div>
					}
					else if ( Step == Steps.Saisie )
					{
						<div class="atm-title left">@(Operation == "depot" ? "Montant du dépôt" : "Montant du retrait")</div>
						<div class="atm-sub left">@($"Jusqu'à {Format.Money( RoleplayPlayer.AtmLimit )}, puis validez")</div>
						<div class="atm-entry">
							<div class="atm-field">@Format.Money( Typed )</div>
							<div class="atm-pad">
								@foreach ( var d in new[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "C", "0", "←" } )
								{
									<div class="atm-pad-key @(d is "C" or "←" ? "muted" : "")" onclick=@(() => Type( d ))>@d</div>
								}
							</div>
							<div class="atm-entry-actions">
								<div class="atm-key small ghost" onclick=@(() => Go( Operation == "depot" ? Steps.Depot : Steps.Retrait ))><i>arrow_back</i> Annuler</div>
								<div class="atm-key small ok @(Typed > 0 ? "" : "off")" onclick=@(() => Pick( Typed ))><i>check</i> Valider</div>
							</div>
						</div>
					}
					else if ( Step == Steps.InsertionBillets || Step == Steps.Attente || Step == Steps.Billets )
					{
						<div class="atm-center">
							<div class="atm-big-icon"><i>@(Step == Steps.Billets ? "payments" : Step == Steps.InsertionBillets ? "move_to_inbox" : "hourglass_top")</i></div>
							<div class="atm-title">@WaitTitle</div>
							<div class="atm-sub">@WaitText</div>
							<div class="atm-progress"><div class="atm-progress-fill" style="width: @Pct( Progress );"></div></div>
						</div>
					}
					else if ( Step == Steps.Fait )
					{
						<div class="atm-center">
							<div class="atm-big-icon ok"><i>check_circle</i></div>
							<div class="atm-title">@(Operation == "depot" ? "Dépôt effectué" : "Retrait effectué")</div>
							<div class="atm-sub">@(Operation == "depot" ? $"{Format.Money( LastAmount )} crédités sur votre compte" : $"{Format.Money( LastAmount )} remis en espèces")</div>
							<div class="atm-balance-line">Nouveau solde : <span>@Format.Money( Me.Bank )</span></div>
						</div>
						<div class="atm-actions">
							<div class="atm-key small" onclick=@(() => PrintTicket())><i>receipt_long</i> Imprimer un ticket</div>
							<div class="atm-key small" onclick=@(() => Go( Steps.Menu ))><i>replay</i> Autre opération</div>
							<div class="atm-key small ghost" onclick=@(() => Finish())><i>eject</i> Terminer</div>
						</div>
					}
					else if ( Step == Steps.Solde )
					{
						<div class="atm-title left">Consultation du solde</div>
						<div class="atm-sub left">@($"{Account?.Label}  ·  {Account?.Number}")</div>
						<div class="atm-balances">
							<div class="atm-balance"><div class="atm-balance-label">Solde disponible</div><div class="atm-balance-value">@Format.Money( Me.Bank )</div></div>
							<div class="atm-balance"><div class="atm-balance-label">Espèces sur vous</div><div class="atm-balance-value muted">@Format.Money( Me.Wallet )</div></div>
						</div>
						<div class="atm-actions">
							<div class="atm-key small" onclick=@(() => PrintTicket())><i>receipt_long</i> Imprimer un ticket</div>
							<div class="atm-key small ghost" onclick=@(() => Go( Steps.Menu ))><i>arrow_back</i> Retour</div>
						</div>
					}
					else if ( Step == Steps.Ticket )
					{
						<div class="atm-center">
							<div class="atm-ticket">
								<div class="atm-ticket-head">HZ BANK · UNION CITY</div>
								<div class="atm-ticket-line"><span>Date</span><span>@GameClock.Text</span></div>
								<div class="atm-ticket-line"><span>Compte</span><span>@Atm.Masked( Account?.Number )</span></div>
								@if ( LastAmount > 0 )
								{
									<div class="atm-ticket-line"><span>@(Operation == "depot" ? "Dépôt" : "Retrait")</span><span>@Format.Money( LastAmount )</span></div>
								}
								<div class="atm-ticket-line strong"><span>Solde</span><span>@Format.Money( Me.Bank )</span></div>
							</div>
							<div class="atm-sub">Prenez votre ticket</div>
						</div>
					}
					else if ( Step == Steps.Erreur )
					{
						<div class="atm-center">
							<div class="atm-big-icon bad"><i>error</i></div>
							<div class="atm-title">Opération refusée</div>
							<div class="atm-sub">@Error</div>
						</div>
						<div class="atm-actions">
							<div class="atm-key small" onclick=@(() => Go( Steps.Menu ))><i>arrow_back</i> Retour au menu</div>
							<div class="atm-key small ghost" onclick=@(() => Finish())><i>eject</i> Terminer</div>
						</div>
					}
					else if ( Step == Steps.Fin )
					{
						<div class="atm-center">
							<div class="atm-big-icon"><i>credit_card</i></div>
							<div class="atm-title">Merci d'avoir choisi HZ BANK</div>
							<div class="atm-sub">N'oubliez pas votre carte</div>
						</div>
					}
				</div>

				<div class="atm-foot">
					<div>Touchez l'écran pour choisir</div>
					<div><span class="atm-esc">Échap</span> Annuler</div>
				</div>
			</div>
		</div>
	}
</root>

@code
{
	static AtmScreen instance;

	public enum Steps { Insertion, Lecture, Comptes, Menu, Retrait, Depot, Saisie, InsertionBillets, Attente, Billets, Fait, Solde, Ticket, Erreur, Fin }

	static readonly int[] WithdrawAmounts = { 20, 40, 60, 100, 200, 500 };
	static readonly int[] DepositAmounts = { 20, 50, 100, 500 };

	const string Sons = "sounds/atm/";

	/// <summary>Le distributeur utilisé (null : écran fermé).</summary>
	public static GameObject Current => instance?.IsOpen == true ? instance.machine : null;

	public bool IsOpen { get; private set; }
	public Steps Step { get; private set; }

	GameObject machine;
	Atm.Account Account { get; set; }
	string Operation { get; set; } = "retrait";
	int Typed { get; set; }
	int LastAmount { get; set; }
	bool Done { get; set; }
	string Error { get; set; }
	bool waiting;
	float stepStart;
	float stepLength;
	Steps afterWait;

	RoleplayPlayer Me => RoleplayPlayer.Local;
	string FirstName => (Me?.RpName ?? "").Split( ' ' ).FirstOrDefault() ?? "";
	float Progress => stepLength <= 0 ? 0 : Math.Clamp( (RealTime.Now - stepStart) / stepLength, 0, 1 );

	// La vue : posée devant l'écran, champ resserré (celui du joueur est à 121° horizontaux), assez
	// loin pour que l'écran entier tienne dans l'image avec un peu d'air (ViewFill de la hauteur).
	// La distance était fixe (25 po) : sur l'écran portrait du distributeur de 1,92 m, haut de
	// 15 po, le haut de l'interface sortait de l'image (Julien, 30/10/2026).
	const float ViewFov = 50f;
	const float ViewFill = 0.86f;
	const float Blend = 0.7f;
	Vector3 fromPosition;
	Rotation fromRotation;
	float fromFov;
	float openedAt;
	float closingAt = -1;
	Rect screenRect;

	/// <summary>Le distributeur accordé par l'hôte (RoleplayPlayer.AtmGranted, après la touche E).</summary>
	public static void Open( GameObject atm ) => instance?.Show( atm );

	/// <summary>La réponse de l'hôte à une opération (RoleplayPlayer.AtmResult).</summary>
	public static void Answer( bool ok, string message, int bank, int wallet ) => instance?.OnAnswer( ok, message );

	protected override void OnStart() => instance = this;

	void Show( GameObject atm )
	{
		var camera = Scene.Camera;
		if ( Me is null || UiLock.IsLocked || !atm.IsValid() || !camera.IsValid() || IsOpen )
			return;
		instance = this;
		machine = atm;
		fromPosition = camera.WorldPosition;
		fromRotation = camera.WorldRotation;
		fromFov = camera.FieldOfView;
		openedAt = RealTime.Now;
		closingAt = -1;
		Account = null;
		Done = false;
		LastAmount = 0;
		IsOpen = true;
		UiLock.Set( "atm", true );
		Timed( Steps.Insertion, Blend + 0.25f, Steps.Lecture );
	}

	void Close()
	{
		IsOpen = false;
		waiting = false;
		machine = null;
		UiLock.Set( "atm", false );
		// Le distributeur est rendu aux autres (un seul joueur à la fois, Atm.TryTake).
		RoleplayPlayer.Local?.ReleaseAtm();
	}

	protected override void OnDisabled()
	{
		if ( IsOpen )
			Close();
	}

	protected override void OnUpdate()
	{
		instance = this;
		if ( !IsOpen )
			return;

		// Mort, menotté, éloigné ou distributeur retiré : la carte est rendue sur-le-champ.
		if ( !machine.IsValid() || Me is null || Me.OnTheGround || Me.Restrained || Me.WorldPosition.Distance( machine.WorldPosition ) > Atm.Reach * 1.6f )
		{
			Close();
			return;
		}

		if ( Input.EscapePressed )
		{
			Input.EscapePressed = false;
			if ( Step != Steps.Fin )
				Finish();
		}

		// L'hôte ne répond pas (coupure) : refus plutôt qu'un écran bloqué.
		if ( waiting && RealTime.Now - stepStart > 8f )
		{
			waiting = false;
			Error = "Pas de réponse de HZ BANK. Réessayez plus tard.";
			afterWait = Steps.Erreur;
		}

		// Les écrans minutés passent seuls à la suite (l'attente de l'hôte en plus pour « Attente »).
		if ( stepLength > 0 && RealTime.Now - stepStart >= stepLength && !waiting )
			Advance();
	}

	// Déroulé

	void Timed( Steps step, float seconds, Steps next )
	{
		Step = step;
		stepStart = RealTime.Now;
		stepLength = seconds;
		afterWait = next;
		switch ( step )
		{
			case Steps.Lecture: Play( "carte_entree" ); break;
			case Steps.InsertionBillets: Play( "depot" ); break;
			case Steps.Billets: Play( "billets" ); break;
			case Steps.Ticket: Play( "ticket" ); break;
			case Steps.Fin: Play( "carte_sortie" ); break;
		}
	}

	void Go( Steps step )
	{
		Play( "bip" );
		stepLength = 0;
		Step = step;
		if ( step == Steps.Retrait ) Operation = "retrait";
		if ( step == Steps.Depot ) Operation = "depot";
		if ( step == Steps.Saisie ) Typed = 0;
	}

	void Advance()
	{
		var step = Step;
		stepLength = 0;
		switch ( step )
		{
			case Steps.Insertion:
				Timed( Steps.Lecture, 2.6f, Steps.Comptes );
				break;
			case Steps.Lecture:
				Step = Steps.Comptes;
				break;
			case Steps.InsertionBillets:
				Send();
				break;
			case Steps.Attente:
				// Réponse reçue et durée minimale passée : billets à prendre, ou résultat.
				if ( afterWait == Steps.Billets )
					Timed( Steps.Billets, 4.3f, Steps.Fait );
				else
				{
					Play( afterWait == Steps.Erreur ? "refus" : "valide" );
					Step = afterWait;
				}
				break;
			case Steps.Billets:
				Play( "valide" );
				Step = Steps.Fait;
				Done = true;
				break;
			case Steps.Ticket:
				Step = Done ? Steps.Menu : Steps.Solde;
				break;
			case Steps.Fin:
				BeginClosing();
				break;
		}
	}

	void ChooseAccount( Atm.Account account )
	{
		Play( "bip" );
		Account = account;
		Step = Steps.Menu;
	}

	void Withdraw( int amount )
	{
		Operation = "retrait";
		Pick( amount );
	}

	void Pick( int amount )
	{
		Play( "bip" );
		if ( amount <= 0 )
			return;
		LastAmount = amount;
		if ( Operation == "depot" )
			Timed( Steps.InsertionBillets, 2.9f, Steps.Attente );
		else
			Send();
	}

	/// <summary>L'opération part à l'hôte ; l'écran d'attente dure au moins 1,6 s.</summary>
	void Send()
	{
		waiting = true;
		Timed( Steps.Attente, 1.6f, Steps.Attente );
		Me.RequestAtm( Operation, LastAmount, Account?.Id ?? Atm.PersonalId );
	}

	void OnAnswer( bool ok, string message )
	{
		if ( !IsOpen || Step != Steps.Attente )
			return;
		waiting = false;
		Error = message;
		afterWait = !ok ? Steps.Erreur : Operation == "retrait" ? Steps.Billets : Steps.Fait;
		if ( ok )
			Done = true;
	}

	void Type( string key )
	{
		Play( "bip" );
		if ( key == "C" ) Typed = 0;
		else if ( key == "←" ) Typed /= 10;
		else if ( Typed < 100000 ) Typed = Typed * 10 + int.Parse( key );
	}

	void PrintTicket()
	{
		Play( "bip" );
		Timed( Steps.Ticket, 3.0f, Steps.Menu );
	}

	void Finish()
	{
		Play( "bip" );
		waiting = false;
		Timed( Steps.Fin, 2.1f, Steps.Fin );
	}

	void BeginClosing()
	{
		closingAt = RealTime.Now;
		stepLength = 0;
	}

	string WaitTitle => Step switch
	{
		Steps.InsertionBillets => "Insérez vos billets",
		Steps.Billets => "Prenez vos billets",
		_ => "Opération en cours…",
	};

	string WaitText => Step switch
	{
		Steps.InsertionBillets => $"Comptage de {Format.Money( LastAmount )} en cours",
		Steps.Billets => $"{Format.Money( LastAmount )} · le volet va se refermer",
		_ => "Connexion à HZ BANK, veuillez patienter",
	};

	void Play( string name )
	{
		if ( machine.IsValid() )
			Sounds.PlayFileAt( $"{Sons}{name}.wav", Atm.Screen( machine ).Center, name == "bip" ? 0.5f : 0.75f );
	}

	// La vue et la place de l'interface

	/// <summary>Chez le joueur local, après les autres caméras (RoleplayPlayer.OnPreRender).</summary>
	public static void PreRender( CameraComponent camera ) => instance?.PlaceCamera( camera );

	void PlaceCamera( CameraComponent camera )
	{
		if ( machine is null || !camera.IsValid() )
			return;
		if ( !machine.IsValid() )
		{
			camera.FieldOfView = fromFov;
			Close();
			return;
		}

		var (center, normal, up) = Atm.Screen( machine );
		var targetPosition = center + normal * ViewDistance();
		var targetRotation = Rotation.LookAt( -normal, up );

		float k;
		if ( closingAt >= 0 )
		{
			k = 1 - Ease( (RealTime.Now - closingAt) / Blend );
			if ( k <= 0 )
			{
				camera.FieldOfView = fromFov;
				Close();
				return;
			}
		}
		else
			k = Ease( (RealTime.Now - openedAt) / Blend );

		// Au retour, on revient là où regarde le joueur maintenant (sa caméra a été posée avant nous).
		var basePosition = closingAt >= 0 ? camera.WorldPosition : fromPosition;
		var baseRotation = closingAt >= 0 ? camera.WorldRotation : fromRotation;
		camera.WorldPosition = Vector3.Lerp( basePosition, targetPosition, k );
		camera.WorldRotation = Rotation.Slerp( baseRotation, targetRotation, k );
		camera.FieldOfView = MathX.Lerp( fromFov, ViewFov, k );

		// Le rectangle de l'écran, en pixels de l'interface.
		var corners = Atm.ScreenCorners( machine ).Select( c => camera.PointToScreenPixels( c ) ).ToArray();
		var min = new Vector2( corners.Min( c => c.x ), corners.Min( c => c.y ) );
		var max = new Vector2( corners.Max( c => c.x ), corners.Max( c => c.y ) );
		var scale = Panel?.ScaleFromScreen ?? 1f;
		screenRect = new Rect( min * scale, (max - min) * scale );
	}

	/// <summary>La distance à laquelle l'écran (marge comprise) occupe ViewFill de l'image, en hauteur comme en largeur.</summary>
	static float ViewDistance()
	{
		var height = (Atm.ScreenTop - Atm.ScreenBottom).Length + 0.6f;
		var width = Atm.ScreenWidth;
		var aspect = Screen.Width > 1 && Screen.Height > 1 ? Screen.Width / Screen.Height : 16f / 9f;
		var halfH = MathF.Tan( ViewFov.DegreeToRadian() / 2 );
		var halfV = halfH / aspect;
		return MathF.Max( height / 2 / (halfV * ViewFill), width / 2 / (halfH * ViewFill) );
	}

	static float Ease( float t )
	{
		t = Math.Clamp( t, 0, 1 );
		return t * t * (3 - 2 * t);
	}

	// L'écran est dessiné sur 800 px de large, et sa hauteur suit la forme du rectangle projeté
	// (~990 sur l'écran portrait du distributeur de 1,92 m) : une seule échelle, rien d'écrasé.
	// Il était dessiné sur 1000 × 740, la forme de l'écran du premier modèle, puis étiré sur les
	// deux axes : sur l'écran portrait, tout sortait tassé en largeur (Julien, 30/10/2026).
	const float SurfaceWidth = 800f;

	string FrameStyle => FormattableString.Invariant(
		$"left: {screenRect.Left:0.0}px; top: {screenRect.Top:0.0}px; width: {screenRect.Width:0.0}px; height: {screenRect.Height:0.0}px;" );

	string SurfaceStyle
	{
		get
		{
			var scale = screenRect.Width > 1 ? screenRect.Width / SurfaceWidth : 1f;
			var height = screenRect.Width > 1 ? SurfaceWidth * screenRect.Height / screenRect.Width : 990f;
			return FormattableString.Invariant( $"width: {SurfaceWidth:0}px; height: {height:0.0}px; transform: scale( {scale:0.0000} );" );
		}
	}

	static string Pct( float v ) => FormattableString.Invariant( $"{v * 100:0.0}%" );

	protected override int BuildHash() => HashCode.Combine(
		IsOpen, Step, HashCode.Combine( Typed, LastAmount, Account, Error ),
		Me?.Bank, Me?.Wallet, (int)(Progress * 60), GameClock.Text,
		HashCode.Combine( (int)screenRect.Left, (int)screenRect.Top, (int)screenRect.Width, (int)screenRect.Height ) );
}