A UI Razor component for the room-name HUD shown at top-left. It reads state from RoomNames and HudTheme, animates fade/slide of the name and decorative ornaments, shows a one-time subtitle on first visit, and calls RoomNames.Tick() each frame.
@using Sandbox;
@using Sandbox.UI;
@using NZombies;
@inherits PanelComponent
@*
ROOM NAME — top left: the name of the part of the map this player is in, which is the name of the flag whose doorway they
last walked through (`RoomNames`). Asked for as *"each flag has a name — when i go trough a place where a debris used to
be, it changes the name on screen to that — the name appears on hud at the top left of the screen — it only changes when
i go trough another flag"* (2026-09-27).
⚠️ IT ALSO FOLLOWS THE PLAYER: `OnUpdate` runs `RoomNames.Tick` every frame, testing each step against the opened
barriers — on every machine, for its own player. Hidden (`nz_room_hud 0`), it keeps following, so turning it back on
shows the right name.
⚠️ ITS OWN PanelComponent, mounted as the round bar is (`SurvivalHud.OnUpdate`): no scene edit. It slides in from the
left and fades up as the name changes, then holds.
*@
<root class="@HudTheme.Class">
@if ( Shown )
{
<div class="room" style="opacity: @(Css( Fade )); left: @(Css( Left ))px">
@* ⚠️ BASALT'S DIAMOND EITHER SIDE OF THE NAME (`HudTheme`), both drawn out of the flow, so the name sits exactly where
the ordinary HUD's does; the ordinary HUD has the name alone. ⛔ ITS CARVED PLAQUE IS GONE (2026-09-27): "the
background for the hud elements take too much space" *@
@if ( HudTheme.Basalt )
{
<div class="orn a" style="left: @(Css( OrnA ))px"></div>
<div class="orn b" style="right: @(Css( OrnB ))px"></div>
}
@RoomNames.CurrentName
</div>
@* ⚠️ THE FIRST VISIT'S SUBTITLE — its own box under the name, sliding with it, so the name's box does not grow *@
@if ( SubtitleShown )
{
<div class="subtitle" style="opacity: @(Css( SubFade * Fade )); left: @(Css( Left ))px">@Subtitle</div>
}
}
</root>
@code
{
/// <summary>How long the entrance takes.</summary>
const float EnterSeconds = 0.35f;
static bool Shown => RoomNames.HiddenBecause is null;
/// <summary>0 to 1 over the entrance, then 1.</summary>
static float Fade => System.Math.Clamp( (float)RoomNames.SinceChanged / EnterSeconds, 0f, 1f );
/// <summary>Slid in from 18px short of the HUD's own side margin (40px, as `.hud .left`), easing out.</summary>
static float Left => 40f - 18f * (1f - Fade) * (1f - Fade);
/// <summary>⚠️ A POINT FOR THE DECIMAL, whatever the machine's language — this goes into a style.</summary>
static string Css( float v ) => v.ToString( "0.###", System.Globalization.CultureInfo.InvariantCulture );
// ── the first visit — *"only the first time each player enters a room"* (2026-09-27) ───────────────────────────────────
// The diamonds slide out from the name, and the room's subtitle fades in under it, holds and goes (`RoomNames.NoteShown`).
// A later visit has the name alone, as before.
/// <summary>Is the name on screen the room this player is seeing for the first time this game?</summary>
static bool FirstVisit => RoomNames.FirstVisit is { } v && string.Equals( v, RoomNames.CurrentName, System.StringComparison.Ordinal );
/// <summary>Seconds into the first visit — or long past it, when this is not one.</summary>
static float T => FirstVisit ? (float)RoomNames.SinceFirstVisit : 99f;
/// <summary>How long the diamonds take to slide out, and how far in they start.</summary>
const float SlideSeconds = 0.6f;
const float TuckPixels = 24f;
/// <summary>1 tucked in against the name, easing out to 0 at rest.</summary>
static float Tuck
{
get
{
var p = System.Math.Clamp( T / SlideSeconds, 0f, 1f );
return (1f - p) * (1f - p);
}
}
/// <summary>The diamonds' insets — at rest the stylesheet's own (`.orn.a` -4px, `.orn.b` -11px).</summary>
static float OrnA => -4f + TuckPixels * Tuck;
static float OrnB => -11f + TuckPixels * Tuck;
/// <summary>The subtitle: in from 0.3 s to 0.9 s, held to 5 s, gone by 6 s.</summary>
static float SubFade
{
get
{
var t = T;
if ( t < 0.3f ) return 0f;
if ( t < 0.9f ) return (t - 0.3f) / 0.6f;
if ( t < 5f ) return 1f;
return System.Math.Clamp( 6f - t, 0f, 1f );
}
}
static string Subtitle => RoomNames.SubtitleOf( RoomNames.CurrentName );
static bool SubtitleShown => SubFade > 0f && Subtitle.Length > 0;
protected override void OnUpdate()
{
RoomNames.Tick();
// ⚠️ ONLY WHILE THE NAME IS ON SCREEN, so the lobby and the score screen use up no first visits
if ( Shown ) RoomNames.NoteShown( RoomNames.CurrentName );
}
/// <summary>⚠️ REBUILDS ON THE NAME, AND THROUGH THE ENTRANCE IN 20 STEPS — not every frame once it has settled.</summary>
protected override int BuildHash() => System.HashCode.Combine( Shown, RoomNames.CurrentName, (int)(Fade * 20f), HudTheme.Class,
(int)(Tuck * 20f), (int)(SubFade * 20f), SubtitleShown, Subtitle );
}