DiamondPanel.Score.cs
using System;
using Sandbox;
namespace Diamonds;
public sealed partial class DiamondPanel
{
readonly DiamondScoreLayout fallbackScoreLayout = new();
static Color ScoreColor( float phase, int step = 1 )
{
int index = ((int)phase * step) % Palette.Length;
float blend = phase - MathF.Floor( phase );
blend = blend * blend * (3 - 2 * blend);
return Color.Lerp( Palette[index], Palette[(index + step) % Palette.Length], blend );
}
void DrawScore( Painter p, float center )
{
var effects = ScoreEffects;
var origin = new Vector2( Left, Top + boardOffsetY );
var target = new Vector2( center, NextPreviewCenterY );
if ( effects is not null ) effects.Target = target - origin;
var color = ScoreColor( effects?.ColorPhase ?? 0 );
var layout = effects?.Layout ?? fallbackScoreLayout;
// Use the actual letterbox, including extra space on wide viewports.
// Keep 16 units clear on each side; never shrink the digits to fit.
layout.MaxWidth = (Left - center) * 2 - 32;
layout.Update( effects?.DisplayedScore ?? Board.Score );
float scoreScale = effects?.Scale ?? 1;
for ( int i = 0; i < layout.Rows.Count; i++ )
DrawScoreNumber( p, layout.Rows[i].Text, target + new Vector2( 0, i * DiamondScoreLayout.RowSpacing * scoreScale ),
DiamondScoreLayout.FontSize * scoreScale, color );
if ( effects is null ) return;
foreach ( var pickup in effects.Pickups )
{
var position = origin + pickup.Position;
var tint = Palette[pickup.ColorIndex % Palette.Length];
float launch = Math.Clamp( pickup.Age / 0.12f, 0, 1 );
float pop = 1 + MathF.Sin( launch * MathF.PI ) * 0.28f;
float scale = MathF.Min( 2, DiamondScoreEffects.ValueScale( pickup.Points ) * pop );
float pickupSize = DiamondScoreEffects.PickupFontSize * scale * (1 - pickup.PullProgress * 0.45f);
if ( pickup.PullProgress > 0 )
{
var tail = pickup.Velocity.Normal * MathF.Min( 45, pickup.Velocity.Length * 0.018f );
p.Stroke = Stroke.Solid( tint.WithAlpha( 0.24f ), 2 );
p.Line( position - tail, position );
}
DrawScoreNumber( p, $"+{pickup.Points}", position, pickupSize, tint );
}
foreach ( var spark in effects.Sparks )
{
float fade = 1 - spark.Age / spark.Lifetime;
var position = origin + spark.Position;
var tint = Color.Lerp( Palette[spark.ColorIndex % Palette.Length], Color.White, 0.55f );
p.Stroke = Stroke.Solid( tint.WithAlpha( fade * fade ), 1.8f * fade );
p.Line( position - spark.Velocity * 0.024f, position );
}
}
/// <summary>Draw at <paramref name="center"/>; <paramref name="align"/> 0 places the text's left edge there instead.</summary>
static void DrawScoreNumber( Painter p, string value, Vector2 center, float fontSize, Color color, float align = 0.5f, bool outline = true )
{
using var scope = p.Scope();
p.Translate( center );
p.Scale( fontSize / DiamondScoreEffects.PickupFontSize );
p.TextStyle = new TextStyle( DiamondScoreEffects.ScoreFont, DiamondScoreEffects.PickupFontSize, color )
{
Alignment = TextFlag.Center,
Outline = new TextRendering.Outline { Enabled = outline, Color = Color.Black.WithAlpha( 0.8f ), Size = 1.5f }
};
// Keep layout at a fixed size; Painter transforms animate the cached glyphs.
var bounds = p.MeasureText( value ) + new Vector2( 4 );
p.Text( value, new Rect( new Vector2( -bounds.x * align, -bounds.y * 0.5f ), bounds ) );
}
// Advances at PickupFontSize, measured on first use.
static float[] timeAdvances;
/// <summary>
/// A character's cell width in a time. Enixe's numerals are proportional only because 1 is narrow,
/// so every digit takes the widest numeral's cell and a ticking time keeps its width and positions.
/// </summary>
static float TimeAdvance( char c )
{
if ( c >= 128 ) return DiamondScoreEffects.MeasureNumber( c.ToString() ).x;
if ( timeAdvances is null )
{
float digit = 0;
for ( char d = '0'; d <= '9'; d++ ) digit = MathF.Max( digit, DiamondScoreEffects.MeasureNumber( d.ToString() ).x );
// Don't keep widths measured before the font is available.
if ( digit <= 0 ) return 0;
timeAdvances = new float[128];
for ( char d = '0'; d <= '9'; d++ ) timeAdvances[d] = digit;
}
if ( timeAdvances[c] <= 0 ) timeAdvances[c] = DiamondScoreEffects.MeasureNumber( c.ToString() ).x;
return timeAdvances[c];
}
static float MeasureTimeNumber( string value, float fontSize )
{
float width = 0;
foreach ( char c in value ) width += TimeAdvance( c );
return width * fontSize / DiamondScoreEffects.PickupFontSize;
}
/// <summary>Draw a time like <see cref="DrawScoreNumber"/>, but with each glyph centered in a fixed-width cell.</summary>
static void DrawTimeNumber( Painter p, string value, Vector2 center, float fontSize, Color color, float align = 0.5f, bool outline = true )
{
float scale = fontSize / DiamondScoreEffects.PickupFontSize;
float x = center.x - MeasureTimeNumber( value, fontSize ) * align;
foreach ( char c in value )
{
float advance = TimeAdvance( c ) * scale;
DrawScoreNumber( p, c.ToString(), new Vector2( x + advance * 0.5f, center.y ), fontSize, color, outline: outline );
x += advance;
}
}
}