DiamondPanel.Transition.cs
using System;
using Sandbox;

namespace Diamonds;

public sealed partial class DiamondPanel
{
	const float TransitionFadeOut = 0.1f, TransitionFadeIn = 0.25f;
	// Hotload leaves new fields at their defaults, so idle must be the default state.
	bool transitionActive, transitionSwitched, transitionToGame;
	float transitionTime;
	/// <summary>A title/game switch is fading the old screen out or the new one in.</summary>
	public bool Transitioning => transitionActive;
	/// <summary>The old screen is still fading out; the switch has not happened yet.</summary>
	public bool TransitionPending => transitionActive && !transitionSwitched;
	/// <summary>The pending or current switch leads into a game rather than the title.</summary>
	public bool TransitionToGame => transitionToGame;

	/// <summary>Fade to black, then switch between the title and a new game and fade back in.</summary>
	void BeginTransition( bool toGame )
	{
		if ( TransitionPending ) return;
		// Interrupting a fade-in continues from its current darkness rather than popping.
		transitionTime = transitionActive ? TransitionDarkness * TransitionFadeOut : 0;
		transitionActive = true;
		transitionSwitched = false;
		transitionToGame = toGame;
	}

	public void CancelTransition() => transitionActive = false;

	public void UpdateTransition( float delta )
	{
		if ( !transitionActive ) return;
		delta = MathF.Max( 0, delta );
		if ( !transitionSwitched )
		{
			transitionTime += delta;
			if ( transitionTime < TransitionFadeOut ) return;
			// Start the fade-in fully black; the switch frame's own cost must not skip it.
			transitionTime = TransitionFadeOut;
			transitionSwitched = true;
			if ( transitionToGame )
			{
				// A completed restart hold fades out under the black; the new game starts without it.
				ClearRestartHold();
				TitleScreen = false;
				PlayRequested?.Invoke();
			}
			else LeaveRequested?.Invoke();
			return;
		}
		// Cap hitch frames (such as the first gameplay frame) so the fade-in stays visible.
		transitionTime += MathF.Min( delta, 1f / 30 );
		if ( transitionTime >= TransitionFadeOut + TransitionFadeIn ) transitionActive = false;
	}

	float TransitionDarkness => !transitionActive ? 0
		: !transitionSwitched ? Math.Clamp( transitionTime / TransitionFadeOut, 0, 1 )
		: Math.Clamp( 1 - (transitionTime - TransitionFadeOut) / TransitionFadeIn, 0, 1 );

	void DrawTransition( Painter p )
	{
		float darkness = TransitionDarkness;
		if ( darkness <= 0 ) return;
		p.Fill = Color.Black.WithAlpha( darkness );
		p.Stroke = Stroke.None;
		p.Rect( p.Bounds );
	}
}