Console command module that probes the local players held weapon and prints its effective recoil numbers. It computes per-shot vertical and horizontal recoil (hip and ADS), one-second climb rates, and warns if prefab multipliers look unstamped.
using Sandbox;
using System;
using System.Linq;
namespace NZombies;
/// <summary>
/// `nz_recoil_weapon` — what the gun in your hands actually does, per shot, in degrees.
///
/// ⛔ IT EXISTS BECAUSE THE PREFAB NUMBER IS FOUR MULTIPLIES AWAY FROM THE FELT ONE.
/// `RecoilVerticalMult` is stamped per weapon, then `VerticalBase`, then `RecoilScale`, then
/// `RecoilAutoControl`, then the 0.4x aiming damp — and perks, tech, Double Tap, Overpressure
/// and Railgun all sit on top at fire time. Reading "1.45" off a prefab and calling it the
/// recoil has misled every recoil conversation this project has had; `nz_recoil` prints the
/// delivered figure for the BASE and this prints it for the WEAPON.
///
/// ⚠️ IT IS ALSO THE ONLY WAY TO CONFIRM THE STAMP REACHED THE GAME. `Tools/recoil_mults.py`
/// writes JSON into 496 prefab files; whether the running session has reloaded those assets is
/// a different question, and one no amount of re-reading the files can answer.
/// </summary>
public static class RecoilWeaponProbe
{
/// <summary>
/// The local player's active weapon.
///
/// ⚠️ `PlayerPresence.Find()`, NOT a FirstOrDefault over every player in the scene — this is
/// a console command run by one person about their own gun, and in a four-player game the
/// scene-wide version would answer about somebody else's. SurvivalHud uses the same pair of
/// lookups for the same reason.
/// </summary>
static SWB.Base.Weapon Held()
{
var go = PlayerPresence.Find();
if ( !go.IsValid() ) return null;
return go.Components.GetAll<SWB.Base.Weapon>( FindMode.EverythingInSelfAndDescendants )
.FirstOrDefault( w => w.IsValid() && w.GameObject.Enabled );
}
[ConCmd( "nz_recoil_weapon" )]
public static void Cmd()
{
var w = Held();
if ( !w.IsValid() || w.Primary is null )
{
Log.Info( "[nz-recoil] no weapon in hand — hold one and run this again." );
return;
}
var si = w.Primary;
var rpm = si.RPM > 0f ? si.RPM : 1f;
// ⚠️ THE SAME CHAIN `GetRecoilAngles` WALKS, IN THE SAME ORDER, and deliberately written
// out rather than factored into a shared helper. A helper would have to be called from
// the hot path too, and the point of this command is to be an INDEPENDENT reading — a
// probe that shares its arithmetic with the thing under test cannot disagree with it.
var control = 1f - Math.Clamp( si.RecoilAutoControl, 0f, 1f );
var useBase = GlobalHandling.UseRecoilBase;
var up = (useBase ? GlobalHandling.VerticalBase * si.RecoilVerticalMult : si.RecoilUp)
* GlobalHandling.RecoilScale * control;
var sideAmount = useBase
? GlobalHandling.HorizontalBase * si.RecoilHorizontalMult
: si.RecoilSide > 0f ? si.RecoilSide : si.RecoilRandomSide;
var side = sideAmount * GlobalHandling.RecoilScale * control;
Log.Info( $"[nz-recoil] {w.DisplayName} {rpm:0} rpm "
+ (useBase ? "BASE MODE" : "authored (base mode is OFF)") );
Log.Info( $"[nz-recoil] multipliers vertical x{si.RecoilVerticalMult:0.00}"
+ $" horizontal x{si.RecoilHorizontalMult:0.00}"
+ (useBase ? "" : " ⚠ unread — base mode is off") );
Log.Info( $"[nz-recoil] per shot up {up:0.000}° hip / {up * 0.4f:0.000}° ads"
+ $" · side ±{side:0.000}° hip / ±{side * 0.4f:0.000}° ads" );
// ⚠️ A SECOND OF HELD TRIGGER, because degrees-per-shot is not a unit anyone can picture
// and the whole point of the climb-hold change is what a SUSTAINED burst does. This is
// the figure to compare between two weapons.
var perSec = up * (rpm / 60f);
var sticks = 1f - GlobalHandling.RecoilRecoverFraction;
Log.Info( $"[nz-recoil] one second climbs {perSec:0.0}° hip / {perSec * 0.4f:0.0}° ads"
+ $" · {perSec * sticks:0.0}° of that is permanent" );
// ⛔ AND THE STAMP'S OWN FAILURE MODE, NAMED. `ShootInfo` defaults both multipliers to 1f,
// so a prefab the stamp never reached and a weapon deliberately left at the base read
// identically — except that the stamp gives no weapon exactly 1.00 unless its fire rate
// is the fleet median AND its class is medium. Two 1.00s is worth a second look.
if ( si.RecoilVerticalMult == 1f && si.RecoilHorizontalMult == 1f )
Log.Warning( "[nz-recoil] both multipliers are exactly 1.00 — this weapon may not"
+ " have been stamped, or the session has not reloaded the prefab."
+ " Re-run Tools/recoil_mults.py and restart play mode." );
}
}