Static utility that triggers a spawn-in tremor effect locally: it shakes the camera (rumble), plays a UI rumble sound, and triggers ceiling dust. Exposes tunable static properties (Level, Seconds, Lead) with nullable-backed defaults, an OnSpawnIn check that respects map config, a Play method to execute the effect, and a console command nz_tremor to play and optionally retune values.
using Sandbox;
namespace NZombies;
/// <summary>
/// THE SPAWN-IN'S TREMOR — as the game fades up out of the black, the ground shakes and rock moves: *"i want when the game
/// starts, as the game fades in the screen is shaking and we hear a tremmor sound and rocks moving"* (2026-09-27). Fired by the
/// fade itself (`ScreenFade.Tick`), so it lands on the same beat on every machine and nothing is sent.
///
/// ⚠️ THE SHAKE IS A HELD RUMBLE (`CameraShake.Rumble`), NOT A PUNCH. A punch decays in a third of a second; this has to last
/// the whole fade-up.
///
/// ⚠️ THE TUNABLES ARE NULLABLE-BACKED, as `AshParticles`' are: a static's value survives a hotload and its initializer does not
/// run again, so `Seconds` went on reading 3.6 after its default became 4.6 (INSTRUCTIONS §1).
/// </summary>
public static class SpawnTremor
{
/// <summary>The sound: a rumble with rock moving in it, 2D — `sounds/nz/nz.spawn.tremor.sound`.</summary>
public const string Cue = "nz.spawn.tremor";
/// <summary>How hard the view shakes at the tremor's height, as trauma (0-1): 0.08 is about ±2° of pitch and ±3° of roll.</summary>
public static float Level
{
get => _level ?? 0.08f;
set => _level = value;
}
static float? _level;
/// <summary>
/// How long the shaking lasts — the fade-up and past it, to the end of the sound's rumble (its first 5 s) — easing in, then out
/// over its last half.
/// </summary>
public static float Seconds
{
get => _seconds ?? 4.6f;
set => _seconds = value;
}
static float? _seconds;
/// <summary>How long before the fade-up the tremor begins, so the rumble is heard in the dark first.</summary>
public static float Lead
{
get => _lead ?? 0.4f;
set => _lead = value;
}
static float? _lead;
/// <summary>
/// The spawn-in's, from `ScreenFade.Tick`: only on a map whose config asks for it (`Gameplay.SpawnInTremor`, basalt's).
///
/// ⛔ IT HAD NO SUCH CHECK and shook every map's start (2026-10-01). `nz_tremor` still plays it anywhere, to feel it.
/// </summary>
public static void OnSpawnIn()
{
if ( ActiveConfig.Current?.Gameplay?.SpawnInTremor != true ) return;
Play();
}
/// <summary>The tremor, now, on this machine: the view shaking, and the sound.</summary>
public static void Play()
{
CameraShake.Rumble( Level, Seconds );
NZSound.PlayUi( Cue );
// ⚠️ AND THE CEILING SHEDS ITS DUST — *"also add like dust falling from the cieling"* (`CeilingDust`)
CeilingDust.Shed( Seconds );
Log.Info( $"[nz-fade] the tremor — shaking at {Level:0.00} for {Seconds:0.#}s"
+ (NZSound.Exists( Cue ) ? "" : $" · ⚠ no sound yet, sounds/nz/{Cue}.sound") );
}
/// <summary>
/// `nz_tremor [level] [seconds]` — the tremor now, without the fade, to feel it; numbers retune it first (for this session —
/// say the ones you like and they become the defaults).
/// </summary>
[ConCmd( "nz_tremor" )]
public static void Cmd( float level = -1f, float seconds = -1f )
{
if ( level >= 0f ) Level = level;
if ( seconds > 0f ) Seconds = seconds;
Play();
}
}