UI/NapalmOverlay.razor

A UI PanelComponent Razor file that renders a napalm proximity overlay. It shows two layered panels, a wash and an animated flame, and updates their opacity each frame based on NapalmZombie.Heat with smoothing, decay, and convars for toggling, max strength, and fade rate. Includes a console command to pin a test strength.

NetworkingFile Access
@using Sandbox;
@using Sandbox.UI;
@using System;
@using NZombies;
@inherits PanelComponent

@*
    NAPALM OVERLAY — the screen glows and licks orange when a napalm zombie is on you.

    Built on `AshOverlay`'s shape, for the same reasons written up there: a separate
    PanelComponent, a constant BuildHash, the movement left to CSS keyframes and only the
    STRENGTH written to Style from code.

    ⛔ IT IS A PROXIMITY WARNING, NOT A DAMAGE EFFECT, AND THE DISTINCTION IS THE WHOLE POINT.
    `DamageOverlay` tells you that you have been hurt; this tells you that you are ABOUT to be.
    A napalm zombie kills from outside melee range after a two-second fuse, so the only fair
    way to field one is to make its approach impossible to miss — the glow is the tell, and it
    doubles the moment the fuse is lit.

    ⛔ THE FLAME IS THE GAME'S OWN TEXTURE, NOT SOMETHING I DREW. `nz_moo/overlay/fullscreen_fire`
    — 16 frames of 256x128 shipped as a 1024x512 sheet, 30fps, additive, straight out of the pack.
    The first version of this panel was a pair of CSS radial gradients pretending to be fire, which
    is exactly the "built a worse version of something that already existed" mistake INSTRUCTIONS
    records. The gradient survives only as a slow swell UNDER the real frames.

    ⚠️ THE MOD ITSELF ONLY EVER DRAWS THAT TEXTURE 40x40, as a status icon on the player card. Using
    it fullscreen is ours — but a 16-frame 30fps animation is not authored for forty pixels.

    # MAPPORT: napalm proximity overlay
*@

<root class="napalm">
    <div @ref="Wash" class="layer wash"></div>
    <div @ref="Flame" class="layer flame"></div>
</root>

@code
{
    /// <summary>⛔ CONSTANT, like `AshOverlay`'s and `DamageOverlay`'s — the strength is written to
    /// Style every frame, and a hash that tracked it would rebuild the tree constantly and restart
    /// the flicker from zero every time you moved.</summary>
    protected override int BuildHash() => 0;

    Panel Wash;
    Panel Flame;

    /// <summary>
    /// Master switch: `nz_napalm_overlay 0`.
    ///
    /// ⚠️ NOT called `Enabled`. `PanelComponent` inherits `Component.Enabled` and a static of that
    /// name SHADOWS it, so the convar and the component system end up as two switches with one
    /// name. This project has hit that collision at least five times — see `AshOverlay.ShowOverlay`
    /// and the four `new` keywords added to the effect components on 2026-09-17.
    /// </summary>
    [ConVar( "nz_napalm_overlay" )] public static bool ShowOverlay { get; set; } = true;

    /// <summary>
    /// Ceiling on how strong it gets when one is right on top of you.
    ///
    /// ⚠️ WELL UNDER 1, FOR THE REASON `AshOverlay` GIVES. At full opacity it stops being heat in
    /// the air and becomes a colour filter over the game — and you still have to be able to SHOOT
    /// the thing that is causing it.
    /// </summary>
    [ConVar( "nz_napalm_overlay_max" )] public static float MaxStrength { get; set; } = 0.55f;

    /// <summary>
    /// How fast the glow fades once nothing is near.
    ///
    /// ⛔ THE DECAY LIVES HERE, NOT IN `NapalmZombie`. Several of them can be alive and each writes
    /// `Heat` only when its own value is HIGHER, so nobody is in a position to lower it — the last
    /// one to tick would otherwise win and standing between two would flicker instead of burn.
    /// Raise-at-the-writer, decay-at-the-reader is the same split the fog weight uses.
    /// </summary>
    [ConVar( "nz_napalm_overlay_fade" )] public static float FadePerSecond { get; set; } = 2.5f;

    float _shown;

    protected override void OnUpdate()
    {
        var want = ShowOverlay ? NapalmZombie.Heat : 0f;

        // ⚠️ CONSUMED, NOT JUST READ. `NapalmZombie` only ever raises `Heat`; if the reader did not
        // pull it back down every frame it would latch at whatever the closest zombie ever reached
        // and the screen would stay orange after the thing was dead.
        NapalmZombie.CoolTo( MathF.Max( 0f, NapalmZombie.Heat - FadePerSecond * Time.Delta ) );

        _shown = _shown.LerpTo( want, Time.Delta * 8f );

        var a = _shown.Clamp( 0f, 1f ) * MaxStrength;

        // ⚠️ THE WASH LEADS AND THE FLAMES ARRIVE LATER. The swell is the "something is coming"
        // half and the real frames are the "it is here" half; both at one opacity reads as a single
        // sheet of orange and loses the approach entirely.
        if ( Wash is not null ) Wash.Style.Opacity = a * 0.7f;

        if ( Flame is not null )
            Flame.Style.Opacity = MathF.Max( 0f, ( _shown - 0.30f ) / 0.70f ).Clamp( 0f, 1f ) * a;
    }

    /// <summary>
    /// `nz_napalm_overlay_test [0..1] [seconds]` — pin it at a strength so it can be looked at.
    ///
    /// ⚠️ IT HOLDS, IT DOES NOT JUST SET. `Heat` decays at `FadePerSecond`, so a command that only
    /// wrote the value lost the race with its own decay — by the time you looked, or a screenshot
    /// landed, it was already zero and the overlay read as broken while working perfectly.
    /// </summary>
    [ConCmd( "nz_napalm_overlay_test" )]
    public static void Test( float strength = 1f, float seconds = 8f )
    {
        NapalmZombie.HoldHeat( strength, seconds );
        Log.Info( $"[nz-napalm] overlay pinned at {strength:0.##} for {seconds:0.#}s" );
    }
}