UI/RoomNameHud.razor

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.

File Access
@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 );
}