Static helper that manages and times an opening intro card HUD showing two lines (map title in caps and location). It computes typing progress, opacity over time, and provides Begin/End and a console command to show or hide the card.
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
namespace NZombies;
/// <summary>
/// THE OPENING CARD — the map's name typed out in the lower left as a game fades up, as Black Ops opened its maps: basalt's
/// *"IGNIS AETERNUS / Beneath the Basalt"* (2026-09-28, "absolutely want this"). `IntroCardHud` draws it.
///
/// ⚠️ TWO LINES, EACH FROM WHERE IT ALREADY LIVES: the map's name as its loading screen shows it (`MapLoading.Title` — the
/// config's `Title`, else the manifest's name), in capitals, and the config's `Location`, a line of place (blank: no line).
/// ⚠️ NO ROUND LINE. It had a third, the round starting ("Round I"), taken off the same day — *"remove the Round 1 part leave the
/// rest"*. The HUD's own round counter says it.
///
/// ⚠️ ON THE SPAWN-IN'S BEAT, ON EVERY MACHINE, AND NOTHING SENT. `ScreenFade.Tick` starts it a moment after the tremor, as the
/// screen comes up out of the black, so every player sees it at their own fade. Survival only, never Creative.
/// ⚠️ REAL TIME, as the fade: a slowed game cannot hold it on screen.
/// </summary>
public static class IntroCard
{
/// <summary>Letters a second as it types.</summary>
public const float CharsPerSecond = 26f;
/// <summary>The pause between one line finishing and the next beginning.</summary>
public const float LineGap = 0.35f;
/// <summary>How long the whole card holds once the last letter is down, before it fades.</summary>
public const float Hold = 3.2f;
/// <summary>How long it takes to fade.</summary>
public const float Fade = 1.4f;
static float _start;
static string[] _lines = Array.Empty<string>();
/// <summary>The card's lines, read once, when it began.</summary>
public static IReadOnlyList<string> Lines => _lines;
/// <summary>Seconds since it began, or negative while it waits for its delay.</summary>
public static float Elapsed => _start > 0f ? RealTime.Now - _start : -1f;
/// <summary>When each line starts typing, seconds from the card's start.</summary>
static float LineStart( int line )
{
var t = 0f;
for ( var i = 0; i < line && i < _lines.Length; i++ )
t += _lines[i].Length / CharsPerSecond + LineGap;
return t;
}
/// <summary>How long it takes to type every line.</summary>
public static float TypingSeconds => _lines.Length == 0 ? 0f : LineStart( _lines.Length - 1 ) + _lines[^1].Length / CharsPerSecond;
/// <summary>Its whole length, typed, held and faded.</summary>
public static float Length => TypingSeconds + Hold + Fade;
/// <summary>Is it on screen?</summary>
public static bool On => _lines.Length > 0 && Elapsed >= 0f && Elapsed < Length;
/// <summary>How many letters of a line are down by now.</summary>
public static int Typed( int line )
{
if ( line < 0 || line >= _lines.Length ) return 0;
var t = Elapsed - LineStart( line );
if ( t <= 0f ) return 0;
return Math.Min( _lines[line].Length, (int)(t * CharsPerSecond) );
}
/// <summary>The line being typed now, or -1 once all are down (for the cursor).</summary>
public static int TypingLine
{
get
{
for ( var i = 0; i < _lines.Length; i++ )
if ( Typed( i ) < _lines[i].Length ) return Elapsed >= LineStart( i ) ? i : -1;
return -1;
}
}
/// <summary>1 while it types and holds, easing to 0 over the fade.</summary>
public static float Opacity
{
get
{
var left = Length - Elapsed;
return left >= Fade ? 1f : Math.Clamp( left / Fade, 0f, 1f );
}
}
/// <summary>Put the card up after <paramref name="delay"/> seconds: the lines are read now.</summary>
public static void Begin( float delay = 0f )
{
var title = MapLoading.Title?.Trim() ?? "";
var place = ActiveConfig.Current?.Location?.Trim() ?? "";
_lines = new[] { title.ToUpperInvariant(), place }.Where( l => l.Length > 0 ).ToArray();
_start = RealTime.Now + MathF.Max( 0f, delay );
Log.Info( $"[nz-intro] the opening card: {string.Join( " / ", _lines )} · {Length:0.#}s" );
}
/// <summary>Take it down.</summary>
public static void End()
{
_start = 0f;
_lines = Array.Empty<string>();
}
/// <summary>`nz_intro` — the opening card now, to see it without starting a game. `nz_intro 0` takes it down.</summary>
[ConCmd( "nz_intro" )]
public static void Cmd( int on = 1 )
{
if ( on == 0 ) { End(); Log.Info( "[nz-intro] down" ); return; }
Begin();
if ( NZGame.Mode != GameMode.Survival || NZGame.IsCreative )
Log.Info( "[nz-intro] ⚠ it draws in a Survival game only, not in Creative or the lobby" );
}
}