WalletFont.cs

Static utility that renders a built-in 5x7 LCD-style font to small textures for a cold-chip wallet UI, formats balance and rate lines, caches generated line textures and exposes cache clearing.

File Access
namespace NoChillquarium;

/// <summary>
/// GBA-style LCD text for the cold-chip wallet.
/// Renders full lines to textures from a built-in 5×7 pixel font (no atlas crop — readable in S&amp;box UI).
/// </summary>
public static class WalletFont
{
	// Output glyph cell size in the baked texture
	public const int CellW = 12;
	public const int CellH = 16;
	const int Dot = 2; // each font bit → 2×2 px block

	// Fixed LCD viewport (px). Must match .cold-chip-lcd-glass content area.
	public const float LcdGlassInnerW = 164f;
	public const float LcdBalanceMaxH = 28f;
	/// <summary>Rate line stays tiny under the balance (much smaller than primary digits).</summary>
	public const float LcdRateMaxH = 12f;
	public const int MaxBalanceChars = 8; // e.g. Ð999.9K
	public const int MaxRateChars = 10;   // e.g. +12.5K/S

	static readonly Dictionary<string, Texture> _lineCache = new();

	// 5×7 bitmaps (rows of 5 chars '0'/'1')
	static readonly Dictionary<char, string[]> Patterns = BuildPatterns();

	static Dictionary<char, string[]> BuildPatterns()
	{
		var d = new Dictionary<char, string[]>
		{
			['0'] = new[] { "01110", "10001", "10011", "10101", "11001", "10001", "01110" },
			['1'] = new[] { "00100", "01100", "00100", "00100", "00100", "00100", "01110" },
			['2'] = new[] { "01110", "10001", "00001", "00010", "00100", "01000", "11111" },
			['3'] = new[] { "01110", "10001", "00001", "00110", "00001", "10001", "01110" },
			['4'] = new[] { "00010", "00110", "01010", "10010", "11111", "00010", "00010" },
			['5'] = new[] { "11111", "10000", "11110", "00001", "00001", "10001", "01110" },
			['6'] = new[] { "00110", "01000", "10000", "11110", "10001", "10001", "01110" },
			['7'] = new[] { "11111", "00001", "00010", "00100", "01000", "01000", "01000" },
			['8'] = new[] { "01110", "10001", "10001", "01110", "10001", "10001", "01110" },
			['9'] = new[] { "01110", "10001", "10001", "01111", "00001", "00010", "01100" },
			['.'] = new[] { "00000", "00000", "00000", "00000", "00000", "01100", "01100" },
			['+'] = new[] { "00000", "00100", "00100", "11111", "00100", "00100", "00000" },
			['-'] = new[] { "00000", "00000", "00000", "11111", "00000", "00000", "00000" },
			['/'] = new[] { "00001", "00010", "00100", "01000", "10000", "00000", "00000" },
			[':'] = new[] { "00000", "01100", "01100", "00000", "01100", "01100", "00000" },
			[' '] = new[] { "00000", "00000", "00000", "00000", "00000", "00000", "00000" },
			['A'] = new[] { "01110", "10001", "10001", "11111", "10001", "10001", "10001" },
			// B also used for Billions suffix on LCD
			['B'] = new[] { "11110", "10001", "10001", "11110", "10001", "10001", "11110" },
			['C'] = new[] { "01110", "10001", "10000", "10000", "10000", "10001", "01110" },
			['D'] = new[] { "11110", "10001", "10001", "10001", "10001", "10001", "11110" },
			['E'] = new[] { "11111", "10000", "10000", "11110", "10000", "10000", "11111" },
			['F'] = new[] { "11111", "10000", "10000", "11110", "10000", "10000", "10000" },
			['H'] = new[] { "10001", "10001", "10001", "11111", "10001", "10001", "10001" },
			['K'] = new[] { "10001", "10010", "10100", "11000", "10100", "10010", "10001" },
			['L'] = new[] { "10000", "10000", "10000", "10000", "10000", "10000", "11111" },
			['M'] = new[] { "10001", "11011", "10101", "10001", "10001", "10001", "10001" },
			['N'] = new[] { "10001", "11001", "10101", "10011", "10001", "10001", "10001" },
			['S'] = new[] { "01111", "10000", "10000", "01110", "00001", "00001", "11110" },
			['T'] = new[] { "11111", "00100", "00100", "00100", "00100", "00100", "00100" },
			['U'] = new[] { "10001", "10001", "10001", "10001", "10001", "10001", "01110" },
			['W'] = new[] { "10001", "10001", "10001", "10001", "10101", "11011", "10001" },
			['Y'] = new[] { "10001", "10001", "01010", "00100", "00100", "00100", "00100" },
			// Ð — D with bar
			['\u00d0'] = new[] { "01111", "01001", "01001", "11101", "01001", "01001", "01111" },
			['Ð'] = new[] { "01111", "01001", "01001", "11101", "01001", "01001", "01111" },
		};
		return d;
	}

	/// <summary>
	/// Balance for LCD — compact abbreviations so digit count stays small and glyphs stay large.
	/// Shell size is fixed; we never grow the device for bigger wallets.
	/// </summary>
	public static string BalanceLine
	{
		get
		{
			// Use shown balance (calm ticker), compact LCD formatting
			var body = Economy.FormatDogeLcd( Economy.ShownBalance );
			var line = "\u00d0" + body;
			if ( line.Length > MaxBalanceChars )
				line = line[..MaxBalanceChars];
			return line;
		}
	}

	/// <summary>
	/// Rate for LCD — always short. Boost = trailing F (not the long word FED).
	/// </summary>
	public static string RateLine
	{
		get
		{
			var rate = Economy.IncomePerSecond;
			var s = "+" + Economy.FormatDogeLcd( rate ) + "/S";
			if ( FeedSystem.IsBoosted )
				s += " F";
			if ( s.Length > MaxRateChars )
				s = s[..MaxRateChars];
			return s.ToUpperInvariant();
		}
	}

	public static Texture GetLineTexture( string text, int maxChars = 14 )
	{
		text ??= "";
		if ( text.Length > maxChars )
			text = text[..maxChars];
		if ( text.Length == 0 )
			text = " ";

		if ( _lineCache.TryGetValue( text, out var cached ) && cached is not null )
			return cached;

		var n = text.Length;
		var w = Math.Max( 1, n * CellW );
		var h = CellH;
		var pixels = new Color32[w * h];

		var bg = new Color32( 10, 40, 10, 255 );
		var ink = new Color32( 200, 236, 48, 255 );
		var inkDim = new Color32( 80, 130, 30, 255 );

		for ( var i = 0; i < pixels.Length; i++ )
			pixels[i] = bg;

		for ( var gi = 0; gi < n; gi++ )
		{
			var ch = text[gi];
			if ( !Patterns.TryGetValue( ch, out var pat ) )
			{
				var u = char.ToUpperInvariant( ch );
				if ( !Patterns.TryGetValue( u, out pat ) )
					pat = Patterns[' '];
			}

			var baseX = gi * CellW + 1;
			var baseY = 1;

			for ( var ry = 0; ry < pat.Length; ry++ )
			{
				var row = pat[ry];
				for ( var rx = 0; rx < row.Length; rx++ )
				{
					if ( row[rx] != '1' )
						continue;

					var px0 = baseX + rx * Dot;
					var py0 = baseY + ry * Dot;
					for ( var dy = 0; dy < Dot; dy++ )
					for ( var dx = 0; dx < Dot; dx++ )
					{
						var x = px0 + dx;
						var y = py0 + dy;
						if ( (uint)x >= (uint)w || (uint)y >= (uint)h )
							continue;
						// slight edge darkening for contrast
						var c = (dx == 0 || dy == 0) ? inkDim : ink;
						pixels[y * w + x] = c;
						if ( dx > 0 && dy > 0 )
							pixels[y * w + x] = ink;
					}
				}
			}
		}

		try
		{
			var tex = Texture.Create( w, h )
				.WithName( $"nochill_lcd_{text.GetHashCode():x8}" )
				.Finish();
			if ( tex is null )
				return null;

			tex.Update( pixels );
			_lineCache[text] = tex;

			if ( _lineCache.Count > 64 )
			{
				var drop = _lineCache.Keys.FirstOrDefault();
				if ( drop is not null )
					_lineCache.Remove( drop );
			}

			return tex;
		}
		catch
		{
			try
			{
				var tex = Texture.Create( w, h ).Finish();
				if ( tex is null )
					return null;
				tex.Update( pixels, 0, 0, w, h );
				_lineCache[text] = tex;
				return tex;
			}
			catch ( Exception e )
			{
				Log.Warning( $"[NO-CHILLquarium] LCD bake failed: {e.Message}" );
				return null;
			}
		}
	}

	public static void ClearLineCache() => _lineCache.Clear();
}