Diagnostics/ShootAnimToggle.cs

Static utility that toggles whether weapon fire animation clips play. Exposes a ConCmd "nz_shootanim" to flip or set the boolean, logs current held weapon and its shoot clip names, and exposes a nullable-backed Enabled property to survive hotload behavior.

Reflection
using Sandbox;
using System.Linq;

namespace NZombies;

/// <summary>
/// Turn the weapon's fire ANIMATION off and on. OFF by default since 2026-09-14; `nz_shootanim 1`
/// puts the authored clips back.
///
/// ⛔ THE ONE THING NO OTHER SETTING CAN REACH. Three separate systems move the viewmodel when you
/// shoot -- `VisualRecoil*` kicks the model, `ApplyEyeAnglesOffset` kicks the camera and the sway
/// system swings the gun in response, and the model plays its own authored `fire` clip. The first
/// two are numbers in a prefab; the third is baked into the .vmdl at compile time and has no
/// runtime amplitude at all. Turning it off is therefore the only way to tell it apart from the
/// other two, and the only way to confirm a fix has to happen at the SMD.
///
/// ⚠️ THE WEAPON STILL FIRES NORMALLY. The bullet, muzzle flash, sound, ammo and recoil are all
/// applied elsewhere in the shoot path -- only the clip is withheld. So if the gun stops lurching
/// with this off, the lurch is in the clip.
/// </summary>
public static class ShootAnimToggle
{
	static bool? _enabled;

	/// <summary>
	/// Whether weapons play their fire clip. OFF — requested 2026-09-14: *"i want to disable the
	/// animations and make our own recoil better, because some of them ruin the weapons"*.
	///
	/// ⛔ THE CLIP IS THE ONE RECOIL CHANNEL WITH NO AMPLITUDE. `RecoilScale`, the bases, the
	/// jitter and the stability settle are all numbers that can be dialled; the authored `fire`
	/// sequence is baked into the .vmdl at compile time and plays at exactly the size its animator
	/// chose, on 446 of the 496 weapons. Once the other three channels are tuned to a single base,
	/// a per-weapon lurch nobody can scale is the only thing left that differs between two guns
	/// that are supposed to feel the same.
	///
	/// ⚠️ THE WEAPON STILL FIRES NORMALLY — bullet, flash, sound, ammo, recoil and the visual
	/// kick are all applied elsewhere in the shoot path. Only the clip is withheld, so what is lost
	/// is the lurch and not the feedback.
	///
	/// ⚠️ NULLABLE-BACKED, NOT `= false`. A static auto-property's initialiser DOES NOT RE-RUN
	/// ON HOTLOAD, so editing the literal would leave the running session on whatever it already
	/// had while the source said otherwise — a trap this project has now paid for three times
	/// (PhdAugments, PapMuzzleFlash.Palettes, BulletDecals.GlowEnabled). A getter is code, and code
	/// is swapped.
	/// </summary>
	public static bool Enabled
	{
		get => _enabled ?? false;
		set => _enabled = value;
	}

	/// <summary>
	/// `nz_shootanim` toggles, `nz_shootanim 0` / `nz_shootanim 1` set it explicitly.
	///
	/// ⚠️ A BARE CALL TOGGLES rather than defaulting to on, because the whole point is flipping it
	/// back and forth while firing — needing to remember which way it currently is defeats that.
	/// </summary>
	[ConCmd( "nz_shootanim" )]
	public static void Set( int on = -1 )
	{
		Enabled = on < 0 ? !Enabled : on != 0;

		var held = Game.ActiveScene?.GetAllComponents<SWB.Base.Weapon>()
			.FirstOrDefault( w => w.IsValid() && w.GameObject.Enabled );

		Log.Info( $"[shootanim] fire animations {(Enabled ? "ON (as authored)" : "OFF")}"
			+ (held.IsValid() ? $"   holding {held.DisplayName}" : "") );

		// ⛔ NAME THE CLIPS THIS WEAPON WOULD HAVE PLAYED. "It still moves with animations off" and
		// "this weapon has no fire clip to begin with" look identical on screen, and only one of
		// them means the movement is coming from somewhere else.
		if ( held.IsValid() && held.Primary is not null )
		{
			var p = held.Primary;
			Log.Info( $"[shootanim]   ShootAnim '{Empty( p.ShootAnim )}'"
				+ $"  ShootAimedAnim '{Empty( p.ShootAimedAnim )}'"
				+ $"  ShootEmptyAnim '{Empty( p.ShootEmptyAnim )}'" );

			var vm = held.ViewModelRenderer;
			if ( vm.IsValid() && vm.Model is not null )
				Log.Info( $"[shootanim]   current sequence '{vm.Sequence.Name}'" );
		}
	}

	static string Empty( string s ) => string.IsNullOrEmpty( s ) ? "-none-" : s;
}