SlowMo.cs
using System;

namespace SquishyTerry;

/// <summary>
/// Game-wide slow motion for big moments. <see cref="Trigger"/> dips time to <see cref="MinScale"/>
/// almost instantly, holds it briefly, then eases back to normal speed.
/// <para>
/// Consumers read <see cref="Scale"/> (1 = normal): the face simulation runs that fraction of its
/// steps and the combo timer runs at that rate, so slow motion never costs the player their combo.
/// (The audio is a live synth stream, so it isn't pitched: that would drift its buffer and warble.)
/// </para>
/// </summary>
public static class SlowMo
{
	const float MinScale = 0.25f;
	const float DropTime = 0.06f;   // snap into slow motion
	const float HoldTime = 0.28f;   // stay there
	const float RecoverTime = 0.4f; // ease back to full speed

	static float _t = 999f;
	static float _lastUpdate = -1f;

	/// <summary>Current time scale, 1 = normal speed.</summary>
	public static float Scale { get; private set; } = 1f;

	/// <summary>0 at normal speed, 1 at full slow motion (for pitch and effects).</summary>
	public static float Amount => (1f - Scale) / (1f - MinScale);

	public static void Trigger() => _t = 0f;

	/// <summary>Advance by real time; safe to call from several components in the same frame.</summary>
	public static void Update( float realNow, float realDelta )
	{
		if ( realNow == _lastUpdate ) return;
		_lastUpdate = realNow;
		_t += realDelta;

		float slow;
		if ( _t < DropTime ) slow = _t / DropTime;
		else if ( _t < DropTime + HoldTime ) slow = 1f;
		else
		{
			float x = Math.Clamp( (_t - DropTime - HoldTime) / RecoverTime, 0f, 1f );
			slow = 1f - x * x * (3f - 2f * x);
		}
		Scale = 1f - (1f - MinScale) * slow;
	}
}