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.
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;
}