A Scene Component that implements the special round fog effect. It creates or reuses a GradientFog component, linearly fades its alpha over time, and enables/disables it based on a target set by SetSpecial or the console command nz_fog.
using Sandbox;
using System;
using System.Linq;
namespace NZombies;
/// <summary>
/// SPECIAL ROUND FOG — the mist that rolls in for a dog round.
///
/// Ported from the original's `round/cl_fog.lua`, which is small enough to be
/// quoted in full by its numbers. With no `edit_color` entity on the map it uses
/// these defaults, and the ONLY difference between a normal round and a special
/// one is the density:
///
/// normal start 50 end 1000 density 0.0 colour (0.4, 0.7, 0.8)
/// special start 50 end 1000 density 0.9 colour (0.4, 0.7, 0.8)
///
/// ⚠️ SO THE FOG IS NOT "BLUER ON A DOG ROUND", IT IS THE SAME COLOUR TURNED UP
/// FROM NOTHING. Normal rounds have no fog at all — the colour is only ever seen
/// on a special round. Anyone tuning this should move the density, not the hue.
///
/// ⚠️ `fadetime = 5` — a five second LINEAR fade, via
/// `math.Approach(fade, 1, FrameTime()/fadetime)`. Not an ease. The mist visibly
/// rolling in is the tell that a dog round has begun, and it is meant to land at
/// about the same time as the first hound.
/// </summary>
public sealed class SpecialFog : Component
{
/// <summary>Seconds to fade fully in or out. The original's `fadetime`.</summary>
public static float FadeTime { get; set; } = 5f;
/// <summary>Density at full strength — the original's special `fogdensity`.
/// Carried as the fog COLOUR'S ALPHA in s&box, which has no separate density
/// field on GradientFog.</summary>
public static float Density { get; set; } = 0.9f;
/// <summary>`fogcolor` — Vector(0.4, 0.7, 0.8), a pale blue-grey.</summary>
public static Color Tint { get; set; } = new( 0.4f, 0.7f, 0.8f );
public static float StartDistance { get; set; } = 50f;
public static float EndDistance { get; set; } = 1000f;
/// <summary>How far up the fog reaches.
///
/// ⚠️ NOT FROM THE ORIGINAL — Source's fog is pure distance fog with no
/// vertical term, while s&box's GradientFog always has one. Set high enough
/// that the vertical falloff never becomes the visible edge, which is what
/// makes it behave like the flat fog being ported.</summary>
public static float Height { get; set; } = 4096f;
/// <summary>Where the fade is going: 1 while a special round is on.</summary>
float _target;
/// <summary>Where it is now. Drives the alpha.</summary>
float _current;
GradientFog _fog;
/// <summary>Roll the fog in or out. Idempotent — calling it with the value it
/// already has does nothing, so it is safe to call every round.</summary>
public void SetSpecial( bool on ) => _target = on ? 1f : 0f;
/// <summary>True once the fog has fully arrived.</summary>
public bool FullyIn => _current >= 0.999f;
protected override void OnUpdate()
{
// ⛔ GetOrCreate FROM OnUpdate, NOT OnStart. A component created in
// OnStart is destroyed by a hotload and never comes back, so the fog
// would silently stop working after any code edit — the exact failure
// PowerupMusic had. This is idempotent and self-healing.
_fog ??= Components.GetOrCreate<GradientFog>();
if ( !_fog.IsValid() ) return;
// Linear, matching math.Approach — see the note at the top.
_current = _current.Approach( _target, Time.Delta / MathF.Max( 0.01f, FadeTime ) );
// ⚠️ DISABLED AT ZERO rather than left running at alpha 0. A fog that is
// merely transparent still costs a full-screen pass every frame, and a
// map with its own atmosphere should get its look back untouched between
// special rounds rather than sharing the field with ours.
if ( _current <= 0.001f )
{
_fog.Enabled = false;
return;
}
_fog.Enabled = true;
_fog.StartDistance = StartDistance;
_fog.EndDistance = EndDistance;
_fog.Height = Height;
_fog.Color = Tint.WithAlpha( Density * _current );
}
/// <summary>Report and drive the fog: `nz_fog [0|1]`.</summary>
[ConCmd( "nz_fog" )]
public static void Cmd( string on = "" )
{
var fog = Game.ActiveScene?.GetAllComponents<SpecialFog>().FirstOrDefault();
if ( !fog.IsValid() )
{
Log.Warning( "[nz-fog] no SpecialFog — it is created by the round manager "
+ "(nz_round_start)" );
return;
}
if ( !string.IsNullOrWhiteSpace( on ) )
fog.SetSpecial( on is "1" or "true" or "on" );
Log.Info( $"[nz-fog] {fog._current:0.00} -> {fog._target:0} · "
+ $"density {Density:0.##} tint {Tint} {StartDistance:0}-{EndDistance:0} "
+ $"fade {FadeTime:0.#}s renderer {(fog._fog.IsValid() && fog._fog.Enabled ? "on" : "off")}" );
}
}