UI/HudTheme.cs

Static helper that manages the HUD theme configuration for the game. It reads the active map config's HudTheme, exposes flags and helper methods (Basalt theme detection, applying a CSS class to panels, Roman numeral formatting and controls), and a console command to change theme or toggle Roman numbering at runtime.

Reflection
using Sandbox;
using System.Text;

namespace NZombies;

/// <summary>
/// THE HUD'S THEME — per map config (`MapConfig.HudTheme`). Blank is the ordinary HUD; "basalt" is the carved one, asked for as
/// *"i want the hud on this config to have the same vibe as the map, like geometrical and ancient looking but with modern light
/// strips"*, then *"make it more ancient … ancient with accents basically"* and *"make sure everything is in the correct place"*
/// (2026-09-27): carved stone tablets, bone and ember, the round in Roman numerals, and a few thin white light strips as the only
/// modern thing — the way the map's own strips are the only manufactured thing in it.
///
/// ⛔ A LOOK, NEVER A LAYOUT. Each panel wears <see cref="Class"/> on its root and its stylesheet restyles its own elements
/// under it: colours, borders and the face. Nothing about what the HUD shows or does changes.
///
/// ⛔ THE STONE TABLETS ARE GONE (2026-09-27, later) — *"the background for the hud elements take too much space so i want
/// to remove them"*: the slabs behind the vitals, the score and the weapon, and the room name's plaque.
///
/// ⛔ AND THE FACE IS THE MAP'S, CINZEL, ON EVERY WORD (same day) — *"the ammo and weapon name and stats screen and hit
/// numbers and all text on the wunderfizz and arsenal and all hud messages that appear on screen"*. It is set on Inter's line
/// box (`line-height: 0.898`, a share of Cinzel's own line), so no row grows taller, but it is wider on mixed-case text, which it sets in small capitals, so a
/// long line ends further along or wraps. The panels that wear the class: the survival HUD (its use prompt with it), the room
/// name, the round bar, the Wunderfizz, the Arsenal, the weapon stats, the scoreboard, the hit numbers, the points pops, the
/// revive markers, the powerup banner and timers, the boss bar and the downed screen.
///
/// ⚠️ A NEW PANEL'S TEXT JOINS IN BY WEARING <see cref="Class"/> ON ITS ROOT — or <see cref="Wear"/> from `OnUpdate`, for a
/// tree built once — and naming, under `.theme-basalt`, each of its boxes that sets Inter itself.
/// </summary>
public static class HudTheme
{
	/// <summary>The theme the loaded config asks for, lower-cased — "" is the ordinary HUD.</summary>
	public static string Name => (ActiveConfig.Current?.HudTheme ?? "").Trim().ToLowerInvariant();

	/// <summary>Basalt's carved HUD?</summary>
	public static bool Basalt => Name == "basalt";

	/// <summary>The class itself, whichever theme is on: what <see cref="Wear"/> puts on and takes off.</summary>
	public const string BasaltClass = "theme-basalt";

	/// <summary>The class every themed panel's root wears — "theme-basalt", or "" for the ordinary HUD.</summary>
	public static string Class => Basalt ? BasaltClass : "";

	/// <summary>
	/// Put the theme's class on a panel from code — for a tree built ONCE, as the hit numbers', the points pops' and the revive
	/// markers' are (a constant `BuildHash`), where a class written in the razor would keep whatever the theme was when it was
	/// built. Their `OnUpdate` calls this every frame; a class the panel already has changes nothing.
	/// </summary>
	public static void Wear( Sandbox.UI.Panel panel ) => panel?.SetClass( BasaltClass, Basalt );

	/// <summary>
	/// Does the round counter read in Roman numerals? Basalt's own hex slots number theirs I to IV. Rounds 1 to
	/// <see cref="RomanUpTo"/> only, digits after. On by default; `nz_hud_theme roman 0` gives the tally and the digits back
	/// (for this session).
	/// </summary>
	public static bool RomanRounds
	{
		get => _roman ?? true;
		set => _roman = value;
	}

	static bool? _roman;

	/// <summary>The round counter in Roman numerals now — the basalt theme, with them on.</summary>
	public static bool Roman => Basalt && RomanRounds;

	/// <summary>
	/// ⛔ THE LAST ROUND IN ROMAN NUMERALS — *"Roman up to round 9 and digits from there on"* (2026-09-27): I to IX, then 10.
	/// </summary>
	public const int RomanUpTo = 9;

	/// <summary>Does round <paramref name="n"/> read in Roman numerals on this HUD — basalt's, from I to <see cref="RomanUpTo"/>?</summary>
	public static bool RomanFor( int n ) => Roman && n >= 1 && n <= RomanUpTo;

	/// <summary>
	/// A tier's number as the HUD writes it — Roman on basalt (the Arsenal's armor and tech tiers: "TIER 2" is "TIER II" there,
	/// to match the round counter, 2026-09-27), digits on every other map.
	/// </summary>
	public static string TierNumeral( int n ) => Basalt ? ToRoman( n ) : n.ToString();

	/// <summary>A number in Roman numerals, 1 to 3999; anything else as digits.</summary>
	public static string ToRoman( int n )
	{
		if ( n < 1 || n > 3999 ) return n.ToString();

		var sb = new StringBuilder();
		foreach ( var (value, glyph) in Numerals )
		{
			while ( n >= value )
			{
				sb.Append( glyph );
				n -= value;
			}
		}

		return sb.ToString();
	}

	/// <summary>⚠️ A PROPERTY THAT BUILDS THE TABLE, not a `static readonly` array — INSTRUCTIONS §1: an array held in a static
	/// survives a hotload with its old contents.</summary>
	static (int Value, string Glyph)[] Numerals => new[]
	{
		(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"),
		(50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
	};

	/// <summary>
	/// `nz_hud_theme [name | roman 0|1]` — the HUD's theme for the loaded config: `basalt`, or `-` for the ordinary HUD (the config
	/// in memory — `nz_save` keeps it); `roman 0|1` switches the Roman round counter (this session). Bare, what is on.
	/// </summary>
	[ConCmd( "nz_hud_theme" )]
	public static void Cmd( string name = "", int on = -1 )
	{
		var cfg = ActiveConfig.Current;
		var n = name.Trim().ToLowerInvariant();

		if ( n == "roman" )
		{
			if ( on >= 0 ) RomanRounds = on != 0;
		}
		else if ( n.Length > 0 && cfg is not null )
		{
			cfg.HudTheme = n is "-" or "none" or "default" ? "" : n;
			Log.Info( $"[nz-hud] theme {(cfg.HudTheme.Length > 0 ? $"'{cfg.HudTheme}'" : "the ordinary HUD")} — nz_save keeps it" );
		}

		Log.Info( $"[nz-hud] theme: {(Name.Length > 0 ? Name : "(ordinary)")} · class '{Class}' · face {(Basalt ? "Cinzel" : "Inter")}"
			+ $" · Roman round counter {(Roman ? "ON" : RomanRounds ? "on, but only with the basalt theme" : "off")}"
			+ $" · round 9 reads {(RomanFor( 9 ) ? ToRoman( 9 ) : "9")}, round 10 reads 10" );
	}
}