Component attached to a player that manages looping music for active powerups. It checks ActivePowerups each update, starts the appropriate looped sound when a powerup is active, re-starts if the played clip naturally ends, and stops the sound when the powerup ends or the component is destroyed/disabled.
using Sandbox;
namespace NZombies;
/// <summary>
/// The loop a powerup plays for as long as it is running.
///
/// ⛔ A COMPONENT, BECAUSE SOMETHING HAS TO TICK. `ActivePowerups` is a static with
/// no update of its own — it can say what is running, but nothing there can notice
/// the moment a powerup ENDS and stop a sound. This sits on the player and watches.
///
/// ⚠️ THE HANDLE IS KEPT AND STOPPED BY HAND, the same rule the powerup floor-hum and
/// the mystery box jingle both needed: a looping cue whose handle is dropped plays
/// until the map unloads, long after the thing that started it is gone.
/// </summary>
public sealed class PowerupMusic : Component
{
/// <summary>
/// The looping music for a powerup, or empty for silence.
///
/// ⚠️ FIRE SALE ONLY, for now. Insta-Kill and Double Points have `*_loop_zhd`
/// files in the pack that belong here too — they are not wired because nobody has
/// heard them yet and three overlapping loops is a decision to make with ears,
/// not in advance.
/// </summary>
public static string LoopFor( PowerupKind kind ) => kind switch
{
PowerupKind.FireSale => NZSound.PowerupFireSale,
_ => "",
};
PowerupKind? _playing;
SoundHandle _handle;
protected override void OnUpdate()
{
// ⛔ STOP FIRST, then consider starting. Checking "should something be
// playing" before "should this one stop" lets a powerup that just expired
// keep its loop for a frame while its replacement starts — briefly two.
if ( _playing.HasValue && !ActivePowerups.IsActive( _playing.Value ) )
Stop();
if ( _playing.HasValue )
{
// ⚠️ Re-established when the clip ends, like the powerup floor-hum: these
// are finite files, so starting once gives one play and then silence for
// the rest of the 30 seconds.
if ( !_handle.IsValid() )
_handle = Sound.Play( LoopFor( _playing.Value ) );
return;
}
foreach ( var (kind, _) in ActivePowerups.All() )
{
if ( string.IsNullOrEmpty( LoopFor( kind ) ) ) continue;
_playing = kind;
_handle = Sound.Play( LoopFor( kind ) );
Log.Info( $"[nz] {kind} music started" );
return;
}
}
void Stop()
{
_handle?.Stop();
_handle = null;
if ( _playing.HasValue ) Log.Info( $"[nz] {_playing.Value} music stopped" );
_playing = null;
}
/// <summary>⚠️ Both exits — the component going away must not orphan the loop.</summary>
protected override void OnDestroy() => Stop();
protected override void OnDisabled() => Stop();
}