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();
}