Music manager for the Easter egg altar and boss fight. It chooses which music cue to play each frame (preview, boss, defense), starts/stops looping SoundHandles, fades volume in/out, schedules seamless loop copies, and provides a console command to preview or mute tracks.
using Sandbox;
using System;
using System.Collections.Generic;
namespace NZombies;
/// <summary>
/// BASALT'S EASTER EGG MUSIC — the altar defense's challenge and the boss fight: *"i think we should add a boss music to the boss
/// fight, and also some challenge music to the step where we defend 1 minute against an endless horde"* (2026-09-28). A map names
/// its tracks (`Gameplay.DefenseMusic`, `Gameplay.BossMusic`) and a sting for the kill (`Gameplay.BossWinSound`). Basalt's are
/// Gorod Krovi's Pavlov's defend and its dragon fight, made into seamless loops by `Tools/basalt_music.py`, and Ancient Evil's
/// round end for the kill.
///
/// ⛔ EACH MACHINE, FROM THE MIRROR, NOTHING SENT. The defense and the fight are host state mirrored to everyone
/// (`NZNet.AltarDefenseState`, `NZNet.BossFightState`), and a joiner is caught up on both. So every machine asks what they are every
/// frame (`LobbyMenu.OnUpdate`, beside `MapAmbience`) and plays for itself — a joiner arriving mid-fight hears the fight.
///
/// ⚠️ THE ARC. The challenge's track comes up as the defense begins and goes as it ends, held or lost. The boss's comes up with him,
/// out of the lava — not in the quiet before — drops low through each phase change's breather and swells back as he returns; at
/// his death it goes, and the sting marks the kill. ⚠️ ONLY A KILL HEARD GETS THE STING: a joiner arriving after it hears nothing.
///
/// ⚠️ ITS OWN CHANNEL, like the ambience bed — `NZMusic` is the lobby's. A track loops by starting its next copy the moment the last
/// reaches its length (`SoundHandle.Time` against it, as `SoundSpotManager` loops); the files are seamless loops. Every fade is by
/// volume, from the event's own.
/// ⚠️ THE SWITCH IS NULLABLE-BACKED (INSTRUCTIONS §1).
/// </summary>
public static class EggMusic
{
/// <summary>How long a track takes to come up.</summary>
public const float FadeIn = 2f;
/// <summary>How long it takes to go, when its moment ends.</summary>
public const float FadeOut = 3.5f;
/// <summary>Where the boss's track sits through a phase change's breather — the dive, the arena changing, the wait.</summary>
public const float BreatherLevel = 0.35f;
/// <summary>How long before a copy runs out its successor starts: the files are seamless, so only a frame's worth.</summary>
const float LoopLead = 0.05f;
sealed class Voice
{
public SoundHandle Handle;
public string Cue;
public float EventVolume;
public float Level;
public bool Leaving;
public RealTimeSince Age;
public float Length;
}
static readonly List<Voice> _voices = new();
static string _cue;
static bool _wasFighting;
/// <summary>A track played by hand (`nz_egg_music`), to hear it without the fight; null when none.</summary>
static string _preview;
/// <summary>`nz_egg_music mute` — none of it, for this session.</summary>
public static bool Muted
{
get => _muted ?? false;
set => _muted = value;
}
static bool? _muted;
/// <summary>What plays now, for `nz_egg_music`.</summary>
public static string Playing => _voices.Exists( v => !v.Leaving ) ? _cue : null;
/// <summary>Every frame (`LobbyMenu.OnUpdate`): what this moment wants, faded in, looped, faded out.</summary>
public static void Tick()
{
var (want, level) = Wanted();
var dt = RealTime.Delta;
// ⚠️ ANOTHER TRACK, OR NONE: whatever plays leaves, fading, while the new one comes up
if ( want != _cue )
{
foreach ( var v in _voices ) v.Leaving = true;
_cue = want;
}
if ( _cue is not null && NZSound.Enabled && NZSound.Exists( _cue ) && !_voices.Exists( v => !v.Leaving ) )
Start( _cue, 0f );
for ( var i = _voices.Count - 1; i >= 0; i-- )
{
var v = _voices[i];
if ( !v.Handle.IsValid() ) { _voices.RemoveAt( i ); continue; }
var target = v.Leaving ? 0f : level;
v.Level = target > v.Level
? MathF.Min( target, v.Level + dt / FadeIn )
: MathF.Max( target, v.Level - dt / FadeOut );
if ( v.Leaving && v.Level <= 0f )
{
v.Handle.Stop();
_voices.RemoveAt( i );
continue;
}
v.Handle.Volume = v.EventVolume * NZSound.VolumeOf( v.Cue ) * v.Level;
if ( v.Leaving ) continue;
// ⚠️ THE LOOP: the next copy the moment this one reaches its length, at this one's level. ⚠️ `Time`, THE HANDLE'S OWN CLOCK —
// not `TimeUntilFinished`, which is a reverb tail and not what is left. ⚠️ NOT IN ITS FIRST SECOND: a handle not yet under
// way reads as stopped, and every frame would start another. One that stopped anyway, or whose length is unknown, is
// replaced the frame it is found — a gap of a frame, where the scheduled start has none.
var due = v.Length > 1f && v.Handle.Time >= v.Length - LoopLead - dt;
if ( v.Age > 1f && (due || v.Handle.Finished || !v.Handle.IsPlaying) )
{
v.Leaving = true;
Start( v.Cue, v.Level );
}
}
}
/// <summary>
/// The track this moment asks for, and its level: a preview first; then the fight, from his coming up to his death; then the
/// defense while it runs. Nothing in the lobby, in Creative, or muted.
/// </summary>
static (string Cue, float Level) Wanted()
{
if ( !string.IsNullOrEmpty( _preview ) ) return (_preview, 1f);
if ( Muted || NZGame.Mode is not (GameMode.Survival or GameMode.Spectator) ) return (null, 0f);
var g = ActiveConfig.Current?.Gameplay;
var fight = HexPlatforms.FightMusic;
// ⚠️ THE KILL: his track goes, and the sting — for a fight this machine was hearing, not a joiner told of it afterwards
if ( fight.Won && _wasFighting )
{
_wasFighting = false;
var sting = Cue( g?.BossWinSound );
if ( sting is not null ) NZSound.PlayUi( sting );
}
if ( fight.On )
{
_wasFighting = true;
return (Cue( g?.BossMusic ), fight.Breather ? BreatherLevel : 1f);
}
if ( !fight.Won ) _wasFighting = false;
return HexPlatforms.DefenseMusic ? (Cue( g?.DefenseMusic ), 1f) : (null, 0f);
}
static string Cue( string configured ) => string.IsNullOrWhiteSpace( configured ) ? null : configured.Trim();
/// <summary>
/// How long a track runs: its sound's own length when the file says, else the length `Tools/basalt_music.py` made it. ⚠️ THE LOOP
/// NEEDS IT: a handle says where it is (`Time`), not how long it is.
/// </summary>
static float LengthOf( string cue )
{
if ( ResourceLibrary.TryGet<SoundEvent>( $"sounds/nz/{cue}.sound", out var ev ) && ev.Sounds is { Count: > 0 } sounds )
{
var longest = 0f;
foreach ( var s in sounds )
if ( s is not null && s.Duration > longest ) longest = s.Duration;
if ( longest > 1f ) return longest;
}
return Made.TryGetValue( cue, out var made ) ? made : 0f;
}
/// <summary>The loops as `Tools/basalt_music.py` made them (ffprobe). ⚠️ A PROPERTY THAT BUILDS THE TABLE (INSTRUCTIONS §1).</summary>
static Dictionary<string, float> Made => new()
{
[NZSound.BasaltMusicBoss] = 144.010f,
[NZSound.BasaltMusicBossTakeo] = 128.000f,
[NZSound.BasaltMusicBossDarkTech] = 114.869f,
[NZSound.BasaltMusicDefend] = 114.474f,
[NZSound.BasaltMusicDefendTrial] = 123.043f,
};
static void Start( string cue, float level )
{
var h = Sound.Play( cue );
if ( !h.IsValid() ) return;
var v = new Voice { Handle = h, Cue = cue, EventVolume = h.Volume, Level = level, Age = 0f, Length = LengthOf( cue ) };
h.Volume = v.EventVolume * NZSound.VolumeOf( cue ) * level;
_voices.Add( v );
}
/// <summary>Silent, now — every copy.</summary>
public static void Stop()
{
foreach ( var v in _voices )
if ( v.Handle.IsValid() ) v.Handle.Stop();
_voices.Clear();
_cue = null;
}
/// <summary>
/// `nz_egg_music [boss|defend|off|mute|unmute] [cue]` — the Easter egg's music by hand, on this machine. `boss` or `defend` plays
/// this map's track to hear it; a cue after it tries another there first (the config in memory — `nz_save` keeps it). `off` stops
/// it; `mute` silences it all for the session. Bare: what plays, and what each moment names. Basalt's alternatives: boss
/// `nz.basalt.music.boss.takeo`, `nz.basalt.music.boss.darktech`; defend `nz.basalt.music.defend.trial`.
/// </summary>
[ConCmd( "nz_egg_music" )]
public static void Cmd( string what = "", string cue = "" )
{
var g = ActiveConfig.Current?.Gameplay;
switch ( what.Trim().ToLowerInvariant() )
{
case "off":
_preview = null;
break;
case "boss":
if ( cue.Length > 0 && g is not null ) g.BossMusic = cue.Trim();
_preview = Cue( g?.BossMusic );
break;
case "defend":
if ( cue.Length > 0 && g is not null ) g.DefenseMusic = cue.Trim();
_preview = Cue( g?.DefenseMusic );
break;
case "mute":
Muted = true;
_preview = null;
break;
case "unmute":
Muted = false;
break;
case "":
break;
default:
Log.Warning( "[nz-audio] nz_egg_music [boss|defend|off|mute|unmute] [cue]" );
return;
}
if ( _preview is not null && !NZSound.Exists( _preview ) )
Log.Warning( $"[nz-audio] no sound event '{_preview}' — Assets/sounds/nz/{_preview}.sound" );
Log.Info( $"[nz-audio] the Easter egg's music{(Muted ? " — MUTED" : "")} · playing {Playing ?? "nothing"}"
+ $"{(_preview is not null ? " (by hand — nz_egg_music off)" : "")}"
+ $" · defense '{g?.DefenseMusic}', boss '{g?.BossMusic}', his death '{g?.BossWinSound}' · nz_save keeps a change" );
}
}