Weapons/MwRunBob.cs

Utility class that selectively disables SWB viewmodel run bob for weapons coming from MW base packs. It detects MW weapons by their viewmodel resource path, exposes console commands to toggle keeping SWB run bob and to set MW sprint loop playback rate, and provides helpers to query whether to skip SWB run/walk cycle and to get per-weapon loop rate.

File Access
using Sandbox;
using System.Collections.Generic;

namespace NZombies;

/// <summary>
/// SWB'S RUN BOB, OFF WHILE AN MW BASE GUN PLAYS ITS OWN SPRINT (2026-10-02, the user, of the MW guns: *"the run
/// animations are still a bit weird, lets understand what's diferent from the non mw base weapon"*).
///
/// ⛔ TWO RHYTHMS ON ONE GUN. `ViewModelHandler.HandleWalkAnimation` adds a procedural bob to every gun while it runs: at
/// the 340 u/s sprint, a ±2° pitch jitter at 2.9 Hz and a ±4° sway at 1.4 Hz. An MW gun's sprint clip already bounces
/// at MW's own cadence, 3.7 Hz in height and 1.9 Hz in pitch (MWII/MWIII; measured from the DMX), so the two fight. A BO1
/// gun's clip bounces at 1.6 / 0.8 Hz, calm enough to sit under it. The MW Base does what this does: its guns set
/// `SWEP.BobScale = 0` and `SwayScale = 0`, so in GMod their run is the clip's alone.
///
/// ⚠️ ONLY WHILE THE GUN SPRINTS (`Weapon.IsRunning`). Walking keeps the bob: the port has no walk layers, and there the
/// bob is the only motion. Mouse sway, breathing and the jump are untouched.
///
/// ⚠️ AN MW GUN IS ONE FROM AN MW BASE PACK'S FOLDER (`weapons/mw19_` … `weapons/mwc_`, as `Tools/mw_port.py` names them).
/// `nz_mw_runbob 1` puts the bob back, to compare.
/// </summary>
public static class MwRunBob
{
	static bool? _keep;

	/// <summary>True puts SWB's run bob back on MW guns, as before 2026-10-02. `nz_mw_runbob 1`.</summary>
	public static bool Keep { get => _keep ?? false; set => _keep = value; }

	static readonly string[] Folders =
	{
		"weapons/mw19_", "weapons/mw22_", "weapons/mw23_", "weapons/bocw_", "weapons/bo6_", "weapons/vg_", "weapons/bo7_",
		"weapons/mwc_",
	};

	static readonly Dictionary<string, bool> _isMw = new();

	/// <summary>Whether this gun came out of an MW Base pack, by its viewmodel's folder.</summary>
	public static bool IsMwBase( SWB.Base.Weapon w )
	{
		var path = w?.ViewModel?.ResourcePath;
		if ( string.IsNullOrEmpty( path ) ) return false;
		if ( _isMw.TryGetValue( path, out var hit ) ) return hit;

		var p = path.Replace( '\\', '/' );
		var yes = false;
		foreach ( var f in Folders )
			if ( p.StartsWith( f, System.StringComparison.OrdinalIgnoreCase ) ) { yes = true; break; }

		return _isMw[path] = yes;
	}

	/// <summary>Leave SWB's walk/run cycle out this frame: an MW gun playing its own sprint.</summary>
	public static bool Skip( SWB.Base.Weapon w ) => !Keep && w.IsValid() && w.IsRunning && IsMwBase( w );

	static float? _loopRate;

	/// <summary>
	/// HOW FAST AN MW GUN'S SPRINT LOOP PLAYS (2026-10-02, the user: *"starting with the bounce speed, mw weapons have
	/// double"*). MW's loop bounces 3.7 times a second in height and 1.9 in pitch; BO1's 1.6 and 0.8. At 0.5 they meet.
	/// `nz_mw_sprint_rate <x>`; 1 is MW's own. The way in and out (sprint_in / sprint_out) keep their speed.
	/// </summary>
	public static float SprintLoopRate { get => _loopRate ?? 0.5f; set => _loopRate = value; }

	/// <summary>The playback rate for this gun's sprint loop: <see cref="SprintLoopRate"/> on an MW gun, 1 on any other.</summary>
	public static float LoopRate( SWB.Base.Weapon w ) => IsMwBase( w ) ? System.Math.Clamp( SprintLoopRate, 0.05f, 4f ) : 1f;

	/// <summary>`nz_mw_sprint_rate [x]`: how fast MW guns' sprint loop plays (0.5 = half, the default; 1 = MW's own). The next loop takes it.</summary>
	[ConCmd( "nz_mw_sprint_rate" )]
	public static void RateCmd( float rate = -1f )
	{
		if ( rate > 0f ) SprintLoopRate = rate;
		Log.Info( $"[mw-runbob] MW guns' sprint loop plays at {SprintLoopRate:0.##}x (1 = MW's own; the next loop takes a change)" );
	}

	/// <summary>`nz_mw_runbob [0|1]`: 1 puts SWB's run bob back on MW guns while they sprint, 0 takes it off (the default).</summary>
	[ConCmd( "nz_mw_runbob" )]
	public static void Cmd( int keep = -1 )
	{
		if ( keep >= 0 ) Keep = keep != 0;
		Log.Info( $"[mw-runbob] SWB's run bob on MW guns while they sprint: {(Keep ? "ON (as before)" : "OFF (their own sprint only)")}" );
	}
}