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.
@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" );
}
}