DiamondPanel.GameOver.cs
using System;
using Sandbox;

namespace Diamonds;

public sealed partial class DiamondPanel
{
	const float GameOverFontSize = 116, GameOverWordGap = 34, GameOverRestartFontSize = 46;
	// Hotload leaves new fields at their defaults, so a fresh game-over clock must be the default state.
	float gameOverColorTime;

	Vector2 GameOverCenter => new( Left + DiamondBoard.BoardWidth * 0.5f, Top + boardOffsetY + DiamondBoard.BoardHeight * 0.5f );
	Rect GameOverRestartButton => new( GameOverCenter + new Vector2( -150, 46 ), new Vector2( 300, 84 ) );

	/// <summary>Advance the GAME OVER word colors; each game over starts from the same phase.</summary>
	public void UpdateGameOver( float delta )
	{
		gameOverColorTime = Board is { GameOver: true } ? (gameOverColorTime + MathF.Max( 0, delta )) % 100 : 0;
	}

	/// <summary>A click on the game-over RESTART button fades into a new game, like a completed restart hold.</summary>
	public bool HandleGameOverClick( Vector2 mouse, Vector2 size )
	{
		if ( TitleScreen || ModalOpen || TransitionPending || Board is not { GameOver: true } ) return false;
		float scale = MathF.Min( size.x / CanvasWidth, size.y / CanvasHeight );
		if ( scale <= 0 ) return false;
		var point = (mouse - (size - new Vector2( CanvasWidth, CanvasHeight ) * scale) * 0.5f) / scale;
		// Holding R fades RESTART out beneath its RESTARTING frame; a faded-out button isn't there to click (see HoveredButton).
		if ( RestartOverlayFade >= 1 || !GameOverRestartButton.IsInside( point ) ) return false;
		PlayClickSound();
		BeginTransition( toGame: true );
		return true;
	}

	void DrawGameOver( Painter p, float scale )
	{
		var center = GameOverCenter;
		// Opposing color cycles, matching the two title words.
		var title = center + new Vector2( 0, -44 );
		DrawScoreNumber( p, "GAME", title - new Vector2( GameOverWordGap * 0.5f, 0 ), GameOverFontSize, TitleWordColor( 0, gameOverColorTime ), align: 1 );
		DrawScoreNumber( p, "OVER", title + new Vector2( GameOverWordGap * 0.5f, 0 ), GameOverFontSize, TitleWordColor( 1, gameOverColorTime ), align: 0 );

		var size = Box.Rect.Size;
		var mouse = (MousePosition - (size - new Vector2( CanvasWidth, CanvasHeight ) * scale) * 0.5f) / scale;
		var button = GameOverRestartButton;
		// A held restart's RESTARTING frame replaces the button, so fade it out underneath.
		float shown = 1 - RestartOverlayFade;
		if ( shown <= 0 ) return;
		bool hover = !ModalOpen && !TransitionPending && button.IsInside( mouse );
		if ( hover )
		{
			p.Fill = Ink.WithAlpha( (HasActive ? 0.2f : 0.12f) * shown );
			p.Stroke = Stroke.None;
			p.Rect( button );
		}
		// Split the word so the hotkey letter cycles colors on its own, underlined beneath its glyph.
		float r = MeasureScoreText( p, "R", GameOverRestartFontSize ).x;
		float rest = MeasureScoreText( p, "ESTART", GameOverRestartFontSize ).x;
		float left = button.Center.x - (r + rest) * 0.5f;
		var key = ScoreColor( restartColorPhase ).WithAlpha( shown );
		DrawScoreNumber( p, "R", new Vector2( left, button.Center.y ), GameOverRestartFontSize, key, align: 0 );
		DrawScoreNumber( p, "ESTART", new Vector2( left + r, button.Center.y ), GameOverRestartFontSize, Color.White.WithAlpha( shown ), align: 0 );
		p.Stroke = Stroke.None;
		p.Fill = Color.Black.WithAlpha( 0.8f * shown );
		p.Rect( new Rect( left + 2 - 1.5f, button.Center.y + GameOverRestartFontSize * 0.42f - 1.5f, r + 3, 7 ) );
		p.Fill = key;
		p.Rect( new Rect( left + 2, button.Center.y + GameOverRestartFontSize * 0.42f, r, 4 ) );
	}

	/// <summary>The ink size of <paramref name="value"/> as <see cref="DrawScoreNumber"/> lays it out, excluding its padding.</summary>
	static Vector2 MeasureScoreText( Painter p, string value, float fontSize )
	{
		using var scope = p.Scope();
		p.TextStyle = new TextStyle( DiamondScoreEffects.ScoreFont, DiamondScoreEffects.PickupFontSize, Color.White ) { Alignment = TextFlag.Center };
		return p.MeasureText( value ) * (fontSize / DiamondScoreEffects.PickupFontSize);
	}
}