Static helper for the round progress UI. It reads RoundManager and input state to compute kills, left, total, percent fill, a human reason why the bar is hidden, and exposes a console command nz_roundbar to query or set visibility mode.
using Sandbox;
namespace NZombies;
/// <summary>
/// The round bar's numbers, its switch and its console command (`RoundBarHud`).
///
/// ⚠️ A STATIC CLASS IN A .cs FILE, as `PowerupBannerState` is: a razor component is a GENERATED type that plain code cannot
/// reference, so what the bar shows is worked out here — and the command prints the very numbers the panel draws.
/// </summary>
public static class RoundBarState
{
/// <summary>
/// When the bar is drawn: 0 never, 1 with the scoreboard — only while TAB is held, the default — 2 always, to look at it.
/// Asked for as *"make it only visible in the scoreboard — as in it only appears when im holding TAB"* (2026-09-27).
///
/// ⚠️ A NULLABLE BEHIND IT, so the default is 1 without leaning on an initializer a hotload may not run.
/// </summary>
public static int Mode
{
get => _mode ?? 1;
set => _mode = System.Math.Clamp( value, 0, 2 );
}
static int? _mode;
static string ModeText => Mode switch { 0 => "OFF", 2 => "always on", _ => "with the scoreboard (TAB)" };
static RoundManager Rm => RoundManager.Instance;
/// <summary>This round's zombies killed so far — the host's, on every screen (`RoundManager.RoundKills`).</summary>
public static int Kills => Rm.IsValid() ? Rm.RoundKills : 0;
/// <summary>This round's zombies not yet dead: still to spawn, and alive (`RoundManager.ZombiesLeft`).</summary>
public static int Left => Rm.IsValid() ? Rm.ZombiesLeft : 0;
/// <summary>
/// Of how many: killed and left. ⚠️ NOT THE WAVE'S SIZE — an ambient Shrieker holds a round open without being in its wave,
/// so the wave alone would read 41 of 40. This is full exactly when the round can end.
/// </summary>
public static int Total => Kills + Left;
/// <summary>The fill, in percent, to a tenth: full with nothing left.</summary>
public static float Percent => Total <= 0 ? 100f : System.MathF.Round( Kills * 1000f / Total ) / 10f;
/// <summary>
/// Why the bar is hidden now, or null while it shows. Shown from round 1, through the round and the break after it (full
/// then), while TAB is held; hidden before the first round, at a game over, and through basalt's boss fight from his arrival
/// to everyone home — the round stands frozen, his health bar sits in the same place, and the fight's zombies are not the
/// round's.
/// </summary>
public static string HiddenBecause
{
get
{
if ( Mode <= 0 ) return "switched off — nz_roundbar 1 shows it with the scoreboard";
if ( !Rm.IsValid() ) return "no round manager — no game running";
if ( Rm.Round < 1 || Rm.WaveTotal <= 0 ) return "before round 1";
if ( Rm.State is not (RoundState.Active or RoundState.Prep) ) return $"the game is {Rm.State}";
if ( HexPlatforms.FightShownOn ) return "basalt's boss fight is on";
// ⚠️ THE KEY LAST, so the console — where TAB is never held — still says what else would hide it. The scoreboard's
// own test (`Scoreboard.Showing`), asked here because a razor type is not something plain code can read.
if ( Mode == 1 && !Input.Down( "Score" ) ) return "the scoreboard is closed — it shows while TAB is held";
return null;
}
}
/// <summary>
/// `nz_roundbar [0|1|2]` — 0 hides the bar, 1 shows it with the scoreboard (while TAB is held), 2 always; and what it shows
/// now: this round's zombies killed, and left, of how many.
/// </summary>
[ConCmd( "nz_roundbar" )]
public static void Cmd( int mode = -1 )
{
if ( mode >= 0 ) Mode = mode;
var hidden = HiddenBecause;
var rm = Rm;
Log.Info( $"[nz-roundbar] {ModeText} · {(hidden is null ? "shown" : $"hidden: {hidden}")}"
+ (!rm.IsValid() ? "" : $" · round {rm.Round}, {rm.State} · {Kills} killed, {Left} left ({rm.Remaining} still to spawn)"
+ $" — reads {Kills} / {Total}, {Percent}% full · the wave was {rm.WaveTotal}") );
}
}