CashDisplay.cs
using Sandbox;
using Sandbox.UI;
using Sandbox.UI.Construct;
using SWB.Player;

// Always-visible cash total, so you can tell what a job earned you without
// walking back to the vendor to check.
//
// Put this on the player prefab - it then follows the player into every
// scene (hideout and missions alike) with no per-scene wiring. Creates its
// own ScreenPanel.
public sealed class CashDisplay : PanelComponent
{
	// How long the "+$1000" flash stays up after a payout.
	[Property]
	public float ChangeFlashDuration { get; set; } = 2.5f;

	PlayerLoadout _loadout;
	Label _cashLabel;
	Label _changeLabel;

	int _lastSeenCash;
	bool _hasLastSeen = false;
	TimeSince _timeSinceChange;

	protected override void OnStart()
	{
		if ( IsProxy )
		{
			Enabled = false;
			return;
		}

		_loadout = Components.Get<PlayerLoadout>();

		if ( _loadout is null )
		{
			Log.Warning( $"{GameObject.Name}: CashDisplay needs a PlayerLoadout on the same GameObject." );
			Enabled = false;
			return;
		}

		Components.GetOrCreate<ScreenPanel>();

		Panel.StyleSheet.Load( "/CashDisplay.cs.scss" );

		var wrapper = Panel.Add.Panel( "wrapper" );
		_cashLabel = wrapper.Add.Label( "", "cash" );
		_changeLabel = wrapper.Add.Label( "", "change" );
	}

	protected override void OnUpdate()
	{
		if ( _loadout is null )
			return;

		var cash = _loadout.Cash;
		_cashLabel.Text = $"${cash}";

		// Flash the delta whenever the balance moves, so a payout or a
		// purchase actually registers instead of a number quietly changing
		// in the corner.
		if ( _hasLastSeen && cash != _lastSeenCash )
		{
			var delta = cash - _lastSeenCash;
			_changeLabel.Text = delta > 0 ? $"+${delta}" : $"-${-delta}";
			_changeLabel.SetClass( "gain", delta > 0 );
			_changeLabel.SetClass( "loss", delta < 0 );
			_timeSinceChange = 0;
		}

		_lastSeenCash = cash;
		_hasLastSeen = true;

		_changeLabel.SetClass( "show", _timeSinceChange < ChangeFlashDuration );
	}
}