Component that creates and manages decorative heat-haze slabs over lava damage walls. It builds per-pool model slabs using a material, places them above each lava wall from the current map config, and exposes console commands to toggle haze and switch lava material between living and classic looks.
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
namespace NZombies;
/// <summary>
/// HEAT HAZE OVER THE LAVA — the air above every pool wavering, what lies beyond it swimming (2026-09-28, the living lava: *"i love
/// the living lava, we already have some form of that but you could make it realistic and professional looking"*). A map asks for
/// it with `Gameplay.LavaHaze` (basalt's 1; 0, the default, is none).
///
/// ⛔ A SLAB OVER EACH POOL, AS THE OLD LAVA FOG WAS: every damage wall drawn in game in a lava material gets a prism extruded from
/// its own footprint (`DebrisMesh`, the builder that draws the lava), from the lava's surface up <see cref="Height"/> units, in
/// `materials/nz/heathaze.vmat` (`nz_heathaze.shader`), which draws the frame behind it again, bent.
/// ⚠️ ITS SURFACE AND HEIGHT ARE THE RENDERER'S, not the material's (`SceneObject.Attributes`), as the fog sheets' are: one material
/// serves pools at different heights.
/// ⚠️ LOCAL, BUILT BESIDE THE WALLS AS A CONFIG IS SHOWN (`NZGame.ShowConfig`), and nothing sent.
///
/// ⚠️ AND `nz_lava_look` CHANGES THE LAVA ITSELF: basalt's walls on the living shader (`materials/nz/lava_living.vmat`) or back on
/// the classic (`materials/nz/lava.vmat`), in the config in memory — `nz_save` keeps it.
/// </summary>
public sealed class LavaHaze : Component
{
public static LavaHaze Instance { get; private set; }
protected override void OnAwake() => Instance = this;
protected override void OnDestroy() { if ( Instance == this ) Instance = null; }
public static LavaHaze Ensure( Scene scene = null )
{
if ( Instance.IsValid() ) return Instance;
scene ??= Game.ActiveScene;
if ( !scene.IsValid() ) return null;
var go = scene.CreateObject();
go.Name = "Lava Haze";
go.Flags |= GameObjectFlags.NotSaved;
return go.Components.Create<LavaHaze>();
}
public const string MaterialPath = "materials/nz/heathaze.vmat";
public const string LivingLava = "materials/nz/lava_living.vmat";
public const string ClassicLava = "materials/nz/lava.vmat";
/// <summary>How high above the lava the air wavers, in units.</summary>
public static float Height
{
get => _height ?? 140f;
set => _height = value;
}
static float? _height;
/// <summary>`nz_lava_haze 0` takes it away (this session).</summary>
public static bool On
{
get => _on ?? true;
set => _on = value;
}
static bool? _on;
readonly List<GameObject> _slabs = new();
/// <summary>How many slabs stand.</summary>
public int Slabs => _slabs.Count( s => s.IsValid() );
static bool IsLava( DamageWall w ) => w is not null && w.VisibleInGame
&& (w.Material ?? "").Contains( "lava", StringComparison.OrdinalIgnoreCase );
/// <summary>A slab over every lava wall drawn in game, where the map asks for haze; none otherwise.</summary>
public void Rebuild()
{
foreach ( var s in _slabs ) if ( s.IsValid() ) s.Destroy();
_slabs.Clear();
var cfg = ActiveConfig.Current;
if ( !On || (cfg?.Gameplay?.LavaHaze ?? 0f) <= 0f || cfg.DamageWalls is null ) return;
var material = Material.Load( MaterialPath );
if ( material is null ) { Log.Warning( $"[nz-lava] {MaterialPath} did not load — no heat haze" ); return; }
foreach ( var w in cfg.DamageWalls.Where( IsLava ) )
{
var footprint = w.HasFootprint
? w.Footprint
: new List<Vector2>
{
new( -w.Size.x * 0.5f, -w.Size.y * 0.5f ), new( w.Size.x * 0.5f, -w.Size.y * 0.5f ),
new( w.Size.x * 0.5f, w.Size.y * 0.5f ), new( -w.Size.x * 0.5f, w.Size.y * 0.5f ),
};
var model = DebrisMesh.Build( footprint, Height, material );
if ( model is null ) continue;
var top = w.Position.z + w.Size.z * 0.5f;
var go = Scene.CreateObject();
go.Name = "Lava haze";
go.Flags |= GameObjectFlags.NotSaved;
go.NetworkMode = NetworkMode.Never; // ⛔ THIS MACHINE'S OWN — out of a joiner's snapshot, where it would stand frozen (NZNetListener)
go.SetParent( GameObject );
go.WorldPosition = w.Position.WithZ( top );
go.WorldRotation = Rotation.FromYaw( w.Yaw );
var r = go.Components.Create<ModelRenderer>();
r.Model = model;
r.RenderType = ModelRenderer.ShadowRenderType.Off;
if ( r.SceneObject.IsValid() )
{
r.SceneObject.Attributes.Set( "g_flHazeBase", top );
r.SceneObject.Attributes.Set( "g_flHazeHeight", Height );
}
_slabs.Add( go );
}
if ( _slabs.Count > 0 ) Log.Info( $"[nz-lava] heat haze over {_slabs.Count} lava pool(s), {Height:0}u deep" );
}
/// <summary>
/// `nz_lava_haze [0|1]` — the heat haze over the lava: `0` takes it away and `1` brings it back (this session). Bare: how many
/// pools it stands over.
/// </summary>
[ConCmd( "nz_lava_haze" )]
public static void HazeCmd( string what = "" )
{
if ( what == "0" ) On = false;
if ( what == "1" ) On = true;
var m = Ensure();
if ( !m.IsValid() ) { Log.Warning( "[nz-lava] no scene" ); return; }
m.Rebuild();
Log.Info( $"[nz-lava] heat haze {(On ? "on" : "OFF")} · over {m.Slabs} pool(s) · the map's scale"
+ $" {ActiveConfig.Current?.Gameplay?.LavaHaze ?? 0f:0.##} (Settings → Gameplay → Lava haze)" );
}
/// <summary>
/// `nz_lava_look [living|classic]` — the lava's own look: every lava wall drawn in game on the living lava
/// (`lava_living.vmat`: plates, flow, heat, hot spots) or the classic (`lava.vmat`), in the config in memory — `nz_save` keeps
/// it. The walls are built again at once; the rising lava takes it the next time it rises. Bare: what each wall wears.
/// </summary>
[ConCmd( "nz_lava_look" )]
public static void LookCmd( string look = "" )
{
var walls = ActiveConfig.Current?.DamageWalls;
if ( walls is null ) { Log.Warning( "[nz-lava] no config" ); return; }
var want = look.Trim().ToLowerInvariant() switch
{
"living" or "new" => LivingLava,
"classic" or "old" => ClassicLava,
_ => null,
};
var n = 0;
for ( var i = 0; i < walls.Count; i++ )
{
var w = walls[i];
if ( !IsLava( w ) ) continue;
if ( want is not null && w.Material != want )
{
w.Material = want;
n++;
}
Log.Info( $"[nz-lava] damage wall #{i}: {w.Material}" );
}
if ( want is null )
{
Log.Info( "[nz-lava] nz_lava_look living | classic" );
return;
}
if ( n > 0 )
{
ActiveConfig.NotifyChanged();
DamageWallManager.Ensure( Game.ActiveScene )?.Rebuild();
Ensure()?.Rebuild();
}
Log.Info( $"[nz-lava] the lava is {(want == LivingLava ? "LIVING" : "CLASSIC")} now ({n} wall(s) changed) · `nz_save` keeps it" );
}
}