Audio/SoundThrottle.cs

A small utility that enforces a minimum time gap between repeats of the same sound. It records the last-played time per sound key and decides whether a sound may play given the current time and a minimum gap, with a Clear method to reset state.

namespace Coilgarden;

/// <summary>
/// Enforces a minimum gap between repeats of the same sound.
/// <para>
/// Without this, any sound tied to a fast event machine-guns. In this game the turn sound is
/// the danger: a player rounding a tight corner can queue two turns inside a fifth of a second,
/// and two identical plucks that close together do not read as two events - they read as a
/// glitch.
/// </para>
/// <para>
/// Pure logic with no engine dependency: it is handed the current time rather than reading a
/// clock. That is what makes the gap rule testable headlessly, which matters because the bug it
/// prevents is one you can only hear, and only sometimes.
/// </para>
/// </summary>
public sealed class SoundThrottle
{
	private readonly Dictionary<string, float> lastPlayed = new();

	/// <summary>
	/// Whether this sound may play now, recording the time if it may.
	/// <para>
	/// A gap of zero always allows the sound, so an unthrottled event costs nothing and needs no
	/// special case at the call site.
	/// </para>
	/// </summary>
	public bool Allow( string sound, float now, float minimumGap )
	{
		if ( string.IsNullOrEmpty( sound ) ) return false;

		if ( minimumGap > 0f && lastPlayed.TryGetValue( sound, out var previous ) )
		{
			// Guard against time running backwards, which happens on a restart or a scene
			// reload. Treating that as "not yet" would mute the sound for as long as the clock
			// took to catch up.
			var elapsed = now - previous;

			if ( elapsed >= 0f && elapsed < minimumGap ) return false;
		}

		lastPlayed[sound] = now;
		return true;
	}

	/// <summary>Forgets every recorded time, so nothing is held back after a restart.</summary>
	public void Clear() => lastPlayed.Clear();
}