DiamondPanel.Restart.cs
using System;
using Sandbox;

namespace Diamonds;

public sealed partial class DiamondPanel
{
	const float RestartFrameWidth = 400, RestartFrameHeight = 132, RestartBarWidth = 320, RestartBarHeight = 10;
	// Hotload leaves new fields at their defaults, so an idle, unlatched hold must be the default state.
	float restartHeld, restartShownProgress, restartOverlay, restartColorPhase;
	bool restartHolding, restartLatched;

	/// <summary>The restart key is held and counting toward its delay.</summary>
	public bool RestartHolding => restartHolding;
	/// <summary>Held fraction of the restart delay, from zero to one.</summary>
	public float RestartProgress => restartShownProgress;

	/// <summary>
	/// Count a held restart key toward <paramref name="delay"/> seconds. Releasing it discards the progress. Completing a hold
	/// fades out, restarts through <see cref="PlayRequested"/> and fades back in. Returns true when a zero delay asks the
	/// caller to restart instantly instead. Presses during the title, an overlay or a fade are latched until release,
	/// so a key still held afterwards never restarts by itself.
	/// </summary>
	public bool UpdateRestartHold( float delta, bool down, float delay )
	{
		delta = MathF.Max( 0, delta );
		restartColorPhase = (restartColorPhase + delta * 2.4f) % Palette.Length;
		bool allowed = !TitleScreen && !ModalOpen && !TransitionPending;
		bool instant = false;
		if ( !down ) restartLatched = false;
		restartHolding = false;
		if ( down && !restartLatched )
		{
			if ( !allowed ) restartLatched = true;
			else if ( delay <= 0 ) { restartLatched = true; instant = true; }
			else
			{
				restartHeld += delta;
				restartShownProgress = Math.Clamp( restartHeld / delay, 0, 1 );
				if ( restartHeld >= delay )
				{
					restartLatched = true;
					BeginTransition( true );
				}
				else restartHolding = true;
			}
		}
		if ( !restartHolding ) restartHeld = 0;
		if ( instant || !allowed && !TransitionPending ) restartOverlay = 0;
		// Appear quickly, and let a completed hold fade under the black rather than vanish.
		else restartOverlay = restartHolding ? MathF.Min( 1, restartOverlay + delta / 0.06f ) : MathF.Max( 0, restartOverlay - delta / 0.12f );
		return instant;
	}

	/// <summary>Drop the hold and its overlay at once, for a restart switch, a new game or a disabled panel.</summary>
	public void ClearRestartHold()
	{
		restartHeld = restartShownProgress = restartOverlay = 0;
		restartHolding = false;
	}

	float RestartOverlayFade => restartOverlay * restartOverlay * (3 - 2 * restartOverlay);

	/// <summary>A light screen-wide dimming with the RESTARTING bar above it; gameplay keeps running beneath.</summary>
	void DrawRestartHold( Painter p, Matrix screenTransform, Vector2 center, float canvasScale )
	{
		if ( restartOverlay <= 0 || TitleScreen ) return;
		float fade = RestartOverlayFade;
		using var scope = p.Scope();
		var canvasTransform = p.Transform;
		p.Transform = screenTransform;
		p.Stroke = Stroke.None;
		p.Fill = Color.Black.WithAlpha( 0.3f * fade );
		p.Rect( p.Bounds );
		p.Transform = canvasTransform;

		var frame = new Rect( center - new Vector2( RestartFrameWidth, RestartFrameHeight ) * 0.5f, new Vector2( RestartFrameWidth, RestartFrameHeight ) );
		p.Fill = Color.Parse( "#0d151e" ).Value.WithAlpha( 0.96f * fade );
		p.Rect( frame );
		var color = ScoreColor( restartColorPhase );
		DrawScoreNumber( p, "RESTARTING", center + new Vector2( 0, -22 ), 34, Ink.WithAlpha( fade ) );
		var track = new Rect( center + new Vector2( -RestartBarWidth * 0.5f, 30 - RestartBarHeight * 0.5f ), new Vector2( RestartBarWidth, RestartBarHeight ) );
		p.Stroke = Stroke.None;
		p.Fill = Ink.WithAlpha( 0.08f * fade );
		p.Rect( track );
		float fill = track.Width * restartShownProgress;
		if ( fill <= 0 ) return;
		p.Fill = color.WithAlpha( 0.14f * fade );
		p.Rect( new Rect( track.Left, track.Center.y - 9, fill, 18 ) );
		p.Fill = color.WithAlpha( 0.9f * fade );
		p.Rect( new Rect( track.Left, track.Top, fill, track.Height ) );
		// Keep the leading edge crisp at any canvas scale.
		p.Fill = Color.Lerp( color, Color.White, 0.6f ).WithAlpha( fade );
		p.Rect( new Rect( track.Left + fill - 2 / canvasScale, track.Top, 2 / canvasScale, track.Height ) );
	}
}