A UI component (Razor) that renders a blood damage overlay vignette. It computes an intensity from the local player's health, applies a threshold and pulsing sine factor, and writes opacity to three child panels each frame. It also exposes console commands to toggle and tune threshold and pulse rate.
@using Sandbox;
@using Sandbox.UI;
@using System;
@using System.Linq;
@using NZombies;
@inherits PanelComponent
@*
DAMAGE OVERLAY — the blood vignette that closes in as you lose health.
Ported from `DrawDamageOverlay()` in the GMod addon
(gamemode/revive_system/cl_view.lua), the `nz_bloodoverlay` block. Its
arithmetic, exactly:
threshold = MaxHealth * 0.8 -- 20% grace before anything shows
if health < threshold:
fade = 1 - clamp(health / threshold, 0, 1)
draw highlights @ fade
draw blood @ fade
draw blood AGAIN @ fade * abs(sin(t * 4)) -- the throb
downed -> fade = 1
⚠️ THE THIRD LAYER IS THE SAME TEXTURE AS THE SECOND, drawn twice. That is
not a mistake in the original — the pulse ADDS to a solid base rather than
modulating it, so the vignette never disappears between beats. Replacing the
two with one pulsing layer would make it strobe instead of throb.
⚠️ The 20% grace is why chip damage shows nothing at all. Below 80% health it
ramps from nothing to full — it is NOT a linear map of the whole bar.
⛔ BuildHash IS CONSTANT. Opacity is written to Style every frame; a hash that
tracked health would rebuild the tree on every point of damage and restart
the pulse from zero each time.
*@
<root class="blood">
<div @ref="Highlights" class="layer highlights"></div>
<div @ref="Base" class="layer damage"></div>
<div @ref="Pulse" class="layer damage"></div>
</root>
@code
{
/// <summary>⛔ CONSTANT ON PURPOSE — see the header.</summary>
protected override int BuildHash() => 0;
Panel Highlights;
Panel Base;
Panel Pulse;
/// <summary>
/// Health fraction at which the overlay first appears. The original derives
/// it as `max - max*0.2`, i.e. 80%.
/// </summary>
public static float Threshold { get; set; } = 0.8f;
/// <summary>Throb rate in radians/sec. The original uses `sin(CurTime()*4)`.</summary>
public static float PulseRate { get; set; } = 4f;
/// <summary>
/// Master switch — the original's `nz_bloodoverlay` cvar.
///
/// ⚠️ NOT called `Enabled`. PanelComponent inherits Component.Enabled, and a
/// static of that name SHADOWS it: the compiler warned, and the two would
/// then be separate switches with the same name — `nz_blood 0` setting one
/// while the component system reads the other.
/// </summary>
public static bool ShowOverlay { get; set; } = true;
/// <summary>Last computed intensity, for `nz_blood_status`.</summary>
public static float Fade { get; private set; }
protected override void OnUpdate()
{
if ( Highlights is null || Base is null || Pulse is null ) return;
Fade = Intensity();
Highlights.Style.Opacity = Fade;
Base.Style.Opacity = Fade;
// ⚠️ MathF.Abs(sin), not (sin+1)/2 — the original uses abs, which gives a
// double-rate throb that touches zero rather than a slow sine. That
// difference is most of the feel of it.
Pulse.Style.Opacity = Fade > 0f
? Fade * MathF.Abs( MathF.Sin( Time.Now * PulseRate ) )
: 0f;
}
/// <summary>0 = healthy and nothing drawn, 1 = downed or empty.</summary>
static float Intensity()
{
if ( !ShowOverlay ) return 0f;
var player = NZPlayer.Local;
if ( !player.IsValid() ) return 0f;
// ⚠️ NOTHING ONCE BLED OUT (2026-10-05): the body is gone and the screen shows another player (`SpectateOthers`), so a
// full-strength blood overlay would only paint their view red.
if ( player.IsOutOfRound ) return 0f;
// Downed pins it at full, whatever the health number says.
if ( player.IsDown ) return 1f;
var hp = player.Hp;
if ( !hp.IsValid() || hp.Max <= 0f ) return 0f;
var threshold = hp.Max * Threshold;
if ( hp.Current >= threshold ) return 0f;
return 1f - (hp.Current / threshold).Clamp( 0f, 1f );
}
// ── console ─────────────────────────────────────────────────────────────
/// <summary>`nz_blood` toggles the overlay.</summary>
[ConCmd( "nz_blood" )]
public static void CmdEnable( int state = -1 )
{
ShowOverlay = state < 0 ? !ShowOverlay : state > 0;
Log.Info( $"[blood] overlay {(ShowOverlay ? "on" : "off")}" );
}
/// <summary>`nz_blood_threshold 0.8` — health fraction it starts at.</summary>
[ConCmd( "nz_blood_threshold" )]
public static void CmdThreshold( float fraction )
{
Threshold = fraction.Clamp( 0.05f, 1f );
Log.Info( $"[blood] shows below {Threshold * 100:0}% health" );
}
/// <summary>`nz_blood_pulse 4` — throb rate.</summary>
[ConCmd( "nz_blood_pulse" )]
public static void CmdPulse( float rate )
{
PulseRate = rate.Clamp( 0f, 30f );
Log.Info( $"[blood] pulse {PulseRate:0.#}" );
}
/// <summary>
/// `nz_blood_status` — what it is drawing and why, without needing to get hurt.
/// </summary>
[ConCmd( "nz_blood_status" )]
public static void CmdStatus()
{
var player = NZPlayer.Local;
if ( !player.IsValid() ) { Log.Info( "[blood] no player" ); return; }
var hp = player.Hp;
Log.Info( $"[blood] enabled={ShowOverlay} down={player.IsDown} "
+ $"hp={(hp.IsValid() ? $"{hp.Current:0}/{hp.Max:0}" : "—")} "
+ $"threshold={Threshold * 100:0}% -> fade {Fade:0.00}" );
}
}