A developer console command utility to tune visual recoil on weapons. It records each weapon ShootInfo baseline values once, enumerates live Weapon components (primary and secondary), and scales their VisualRecoilUp/Side/Roll/Punch fields by a factor or reports current and authored values.
using Sandbox;
using System.Collections.Generic;
using System.Linq;
using SWB.Base;
namespace NZombies;
/// <summary>
/// Dial how hard the MODEL kicks, live. `nz_vrecoil 0.4`.
///
/// ⛔ THE MODEL'S KICK, NOT THE CAMERA'S. `VisualRecoil*` moves the gun on screen and nothing else —
/// where the bullet goes is `RecoilUp`/`RecoilSide`, a separate set. Scaling the wrong one makes the
/// weapon look calm while still throwing your aim at the ceiling, and the two read identically in a
/// prefab diff.
///
/// ⚠️ RELATIVE TO THE AUTHORED VALUES, captured the first time a weapon is touched. Scaling the
/// CURRENT numbers would compound — `nz_vrecoil 0.5` twice would land on 0.25 while reporting 0.5 —
/// so every call is measured from the same baseline and calling it again with 1 restores the gun.
/// </summary>
public static class VisualRecoilTuner
{
/// <summary>The four magnitudes. Recovery is deliberately absent: it is timing, not size.</summary>
static readonly string[] Fields =
{ "VisualRecoilUp", "VisualRecoilSide", "VisualRecoilRoll", "VisualRecoilPunch" };
// ⚠️ Keyed on the ShootInfo instance, so Primary and Secondary are remembered separately and a
// respawned weapon simply gets a fresh baseline rather than inheriting the last one's.
static readonly Dictionary<ShootInfo, float[]> Authored = new();
static float[] BaselineOf( ShootInfo si )
{
if ( Authored.TryGetValue( si, out var v ) ) return v;
v = new[] { si.VisualRecoilUp, si.VisualRecoilSide, si.VisualRecoilRoll, si.VisualRecoilPunch };
Authored[si] = v;
return v;
}
static void Apply( ShootInfo si, float factor )
{
var b = BaselineOf( si );
si.VisualRecoilUp = b[0] * factor;
si.VisualRecoilSide = b[1] * factor;
si.VisualRecoilRoll = b[2] * factor;
si.VisualRecoilPunch = b[3] * factor;
}
static IEnumerable<(Weapon w, ShootInfo si, string slot)> Live()
{
var weapons = Game.ActiveScene?.GetAllComponents<Weapon>()
.Where( w => w.IsValid() && w.GameObject.Enabled ) ?? Enumerable.Empty<Weapon>();
foreach ( var w in weapons )
{
if ( w.Primary is not null ) yield return (w, w.Primary, "Primary");
if ( w.Secondary is not null ) yield return (w, w.Secondary, "Secondary");
}
}
/// <summary>
/// `nz_vrecoil [factor]` — scale the model kick on every weapon in hand. No argument reports.
/// </summary>
[ConCmd( "nz_vrecoil" )]
public static void Scale( float factor = -1f )
{
var live = Live().ToList();
if ( live.Count == 0 ) { Log.Info( "[vrecoil] no weapon in hand" ); return; }
foreach ( var (w, si, slot) in live )
{
var b = BaselineOf( si );
if ( factor >= 0 ) Apply( si, factor );
Log.Info( $"[vrecoil] {w.DisplayName} ({slot}) up {si.VisualRecoilUp:0.####}"
+ $" side {si.VisualRecoilSide:0.####} roll {si.VisualRecoilRoll:0.####}"
+ $" punch {si.VisualRecoilPunch:0.####}"
+ $" [authored {b[0]:0.####} {b[1]:0.####} {b[2]:0.####} {b[3]:0.####}]" );
}
if ( factor >= 0 )
Log.Info( $"[vrecoil] x{factor:0.###} of authored on {live.Count} shootinfo(s)."
+ " Say the word and it gets baked into all 73 Destiny prefabs." );
else
Log.Info( "[vrecoil] nz_vrecoil <factor> e.g. 0.4 for a much softer kick, 1 to restore" );
}
}