DiamondScoreLayout.cs
using System.Collections.Generic;
using System.Globalization;
using Sandbox;

namespace Diamonds;

/// <summary>Shared score rows for Painter rendering and flying-number destinations.</summary>
public sealed class DiamondScoreLayout
{
	public const float FontSize = 84;
	public float MaxWidth { get; set; } = 288;
	// Enixe's visible capitals occupy 57% of the em. Leave a small gap between rows.
	public const float GlyphHeight = FontSize * 0.57f;
	public const float RowSpacing = GlyphHeight + 12;
	public readonly record struct Row( string Text, float Width );
	readonly List<Row> rows = new();
	long? lastValue;
	float lastWidth;
	float totalWidth;
	public IReadOnlyList<Row> Rows => rows;

	public void Update( long value )
	{
		if ( lastValue == value && lastWidth == MaxWidth ) return;
		lastValue = value;
		lastWidth = MaxWidth;
		rows.Clear();
		totalWidth = 0;
		string text = value.ToString( "N0", CultureInfo.InvariantCulture );
		string line = "";
		var groups = text.Split( ',' );
		for ( int i = 0; i < groups.Length; i++ )
		{
			string group = groups[i] + (i < groups.Length - 1 ? "," : "");
			if ( line.Length > 0 && Width( line + group ) > MaxWidth )
			{
				AddRow( line );
				line = "";
			}
			line += group;
		}
		AddRow( line );
	}

	void AddRow( string text )
	{
		float width = Width( text );
		rows.Add( new Row( text, width ) );
		totalWidth += width;
	}

	static float Width( string text ) => DiamondScoreEffects.MeasureNumber( text ).x * FontSize / DiamondScoreEffects.PickupFontSize;

	public Vector2 ArrivalOffset( Vector2 sample )
	{
		// Weight rows by width so short final rows don't attract disproportionate hits.
		// Reuse the same sample throughout a flight as the score grows and wraps.
		float distance = sample.x * totalWidth;
		for ( int i = 0; i < rows.Count; i++ )
		{
			var row = rows[i];
			if ( distance <= row.Width || i == rows.Count - 1 )
				return new Vector2( distance - row.Width * 0.5f, i * RowSpacing + (sample.y - 0.5f) * GlyphHeight );
			distance -= row.Width;
		}
		return Vector2.Zero;
	}
}