UI/SpawnTremor.cs

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.

NetworkingFile Access
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();
	}
}