GameLogic/crowd/CrowdAudioController.cs
using Sandbox;

public sealed class CrowdAudioController : Component
{
	[Property, Group( "Ambiance Arène" )] public SoundEvent ArenaAmbientLoop { get; set; }
	[Property, Group( "Ambiance Arène" )] public float BaseAmbientVolume { get; set; } = 0.4f;

	[Property, Group( "Événements de Manche" ), Title( "Clameur Début de Manche" )] 
	public SoundEvent RoundStartCheerSound { get; set; }

	[Property, Group( "Réactions Bot" )] public SoundEvent BotKillReactionSound { get; set; }

	private SoundHandle _ambientHandle;
	private RealTimeSince _timeSinceLastReaction = 10f;
	private float _minReactionInterval = 1.3f; // Évite la cacophonie

	protected override void OnEnabled()
	{
		GameManager.OnPhaseChanged += HandlePhaseChanged;
	}

	protected override void OnDisabled()
	{
		GameManager.OnPhaseChanged -= HandlePhaseChanged;
	}

	protected override void OnStart()
	{
		if ( ArenaAmbientLoop != null )
		{
			_ambientHandle = Sound.Play( ArenaAmbientLoop );
			if ( _ambientHandle != null )
				_ambientHandle.Volume = BaseAmbientVolume;
		}
	}

	protected override void OnDestroy()
	{
		_ambientHandle?.Stop( 0.5f );
	}

	private void HandlePhaseChanged( GamePhase newPhase )
	{
		// Déclenchement à l'entrée
		if ( newPhase == GamePhase.RoundIntro )
		{
			PlayRoundStartReaction();
		}
	}

	public void PlayRoundStartReaction()
	{
		if ( RoundStartCheerSound == null ) return;

		// Force la lecture sans tenir compte du cooldown anti-spam
		_timeSinceLastReaction = 0f;
		Sound.Play( RoundStartCheerSound );
	}

	public void PlayReaction( SoundEvent sound, bool isPositive )
	{
		if ( sound == null ) return;

		if ( _timeSinceLastReaction < _minReactionInterval )
			return;

		_timeSinceLastReaction = 0f;
		Sound.Play( sound );
	}

	public void PlayBotHighlightReaction()
	{
		if ( BotKillReactionSound != null && _timeSinceLastReaction > 2.0f )
		{
			_timeSinceLastReaction = 0f;
			Sound.Play( BotKillReactionSound );
		}
	}
}