Static helper for computing weapon reserve ammo rules. It defines hard cap and starting mags, maps a clip size to spare magazines via tiers, computes base reserve (mags × clip with loadout override), and provides console commands to inspect or set starting mags and print reserve info for the local player.
using System.Linq;
using Sandbox;
namespace NZombies;
/// <summary>
/// HOW BIG A WEAPON'S RESERVE IS — derived from its magazine, never authored.
///
/// Ported from `nzWeps:GetReserveMags` (`gamemode/weapons/sv_ammo.lua`). A tiered step function
/// returning MAGAZINES, which the caller multiplies by the clip:
///
/// clip >= 100 -> 4 mags
/// clip >= 50 -> 6 mags
/// clip >= 25 -> 8 mags
/// clip < 25 -> 10 mags
///
/// ⛔ THE OLD RULE WAS `clip * 10` CLAMPED TO 300, AND BOTH HALVES ARE GONE. Upstream replaced it
/// deliberately, and its own note gives the reason: a flat multiplier meant "a 100-round mag got only
/// 3 spare mags while an 8-round pistol got 10". Bigger magazines now get proportionally fewer spare
/// mags. The tiers ARE the cap — re-adding a 300 clamp on top would flatten the top two tiers back
/// out, which is exactly what upstream removed.
///
/// ⛔ PACK-A-PUNCH DOES NOT TOUCH RESERVE. Upstream's old PaP branch
/// (`clip = round(clip * 1.5 / 5) * 5` against a 500 cap) is commented out with "PaP is DAMAGE-ONLY
/// now", and our reserve block never had a PaP term — so this is already the behaviour, recorded here
/// so nobody adds one back thinking it was an omission.
///
/// ⚠️ DELIBERATELY NON-MONOTONIC AT THE BOUNDARIES, and upstream says so in as many words. A
/// 49-round mag yields 294 while a 50-round yields 300 — but a 99-round yields 594 against a
/// 100-round's 400. Two of our weapons feel this through Extended Mag, which raises the LIVE clip:
/// the G11 (48 -> 55) drops 384 -> 330 and the MPL (24 -> 27) drops 240 -> 216, because both sit
/// just under a threshold and cross it. If that matters the thresholds are the only thing to change.
///
/// ⚠️ THE AUTHORED `NZAmmo.MaxReserve` IS NO LONGER READ. It is left in the 31 prefabs because
/// `NZAmmo` still needs a starting value for the instant before a weapon is first equipped, but
/// `ApplyStoredUpgrades` overwrites it from this on every equip. `nz_reserve` prints both so any
/// drift is visible rather than merely present.
/// </summary>
public static class ReserveAmmo
{
/// <summary>
/// The absolute ceiling on reserve ammo. 600.
///
/// ⛔ ITS OLD COMMENT CLAIMED IT WAS "applied last on every path" AND IT WAS NOT. The only
/// place it was used is `BaseFor`, which clamps the BASE — and the base is then multiplied
/// by the `t1_reserve` tech factor and has Mule Kick's bonus magazines added on top
/// (`NZPlayer.ApplyStoredUpgrades`). A ceiling applied before two contributors is not a
/// ceiling; it was upstream's 999 and nothing ever reached it, so the claim was never tested.
///
/// ⚠️ REAL ENFORCEMENT IS IN `NZAmmo`'s SETTERS, which clamp to this. That is what makes it
/// outrank tech, perks, wall-buys, Max Ammo, Vulture Aid, the Fabricator and the trade table
/// by construction rather than by every one of them remembering to ask.
///
/// ⚠️ NO LONGER `const`. A const is baked into each call site at compile time, so
/// `nz_ammo_cap` could not move it — and a cap you cannot try at runtime is one nobody tunes.
/// </summary>
public static int HardCap { get; set; } = 600;
/// <summary>
/// Spare magazines for a magazine of this size.
///
/// ⛔ -1 AND 0 RETURN 0, NOT 10. `ClipSize == -1` is SWB's "no magazine, feeds straight from the
/// reserve" sentinel — `HasAmmo` and `StartReload` both branch on it — so treating it as a
/// one-round clip would hand a beltless weapon ten rounds of reserve and call it full. Upstream
/// guards the same way (`if not isnumber(clip) or clip <= 0 then return 0 end`).
/// </summary>
public static int MagsFor( int clip )
{
if ( clip <= 0 ) return 0;
if ( clip >= 100 ) return 4;
if ( clip >= 50 ) return 6;
if ( clip >= 25 ) return 8;
return 10;
}
/// <summary>
/// Spare magazines for the weapon you START the game with. 3.
///
/// ⛔ IT IGNORES THE TIERS ENTIRELY, WHICH IS THE POINT. The starting pistol has an 8-round
/// magazine, so `MagsFor` put it in the smallest-clip tier and handed it TEN spare mags — 80
/// rounds, the most generous reserve in the game, on the gun you are supposed to be desperate to
/// replace. The tiers are a sensible rule for a weapon you chose; they are the wrong rule for the
/// one you were given.
///
/// ⚠️ MAGAZINES, NOT ROUNDS, so it scales with whatever the loadout weapon actually is —
/// including Extended Mag raising the live clip — rather than pinning a number that only suits
/// one pistol.
/// </summary>
public static int StartingMags { get => _startMags ?? 3; set => _startMags = value; }
static int? _startMags;
/// <summary>
/// The reserve a weapon with this magazine carries, before perks, tech and augments.
///
/// ⚠️ THE BASE ONLY. `t1_reserve`'s multiplier and Mule Kick's flat magazines are applied by
/// `ApplyStoredUpgrades`, in that order — multiplier first so the flat mags are not amplified,
/// which is upstream's ordering too.
///
/// ⚠️ `loadout` SWAPS THE MAGAZINE COUNT AND NOTHING ELSE. It is still `mags × clip`, still
/// capped, and every multiplier downstream composes exactly as before — so Mule Kick's bandolier
/// and `t1_reserve` still pay out on a starting pistol, they just pay out on a smaller base.
/// </summary>
public static int BaseFor( int clip, bool loadout = false )
=> System.Math.Min(
clip * (loadout ? System.Math.Max( 0, StartingMags ) : MagsFor( clip )),
HardCap );
// ── console ──────────────────────────────────────────────────────────────────────────────
/// <summary>
/// `nz_reserve [clip]` — the rule, and what every carried weapon gets from it.
///
/// ⚠️ PRINTS THE AUTHORED VALUE BESIDE THE LIVE ONE. "The reserve is wrong" splits into two very
/// different faults — the rule computing the wrong number, or the rule never being applied and
/// the prefab's own figure surviving — and on the HUD they look identical.
/// </summary>
/// <summary>
/// `nz_starting_mags [n]` — how many spare magazines the loadout weapon carries.
///
/// ⚠️ RE-EQUIP TO SEE IT. `ApplyStoredUpgrades` writes the reserve on equip, so a weapon
/// already in hand keeps the old figure until it is drawn again.
/// </summary>
[ConCmd( "nz_starting_mags" )]
public static void StartingMagsCmd( int mags = -1 )
{
if ( mags >= 0 ) StartingMags = mags;
Log.Info( $"[nz] starting weapon carries {StartingMags} spare magazine(s)"
+ $" · an 8-round pistol → {8 * StartingMags} reserve"
+ $" (the tiered rule would give {8 * MagsFor( 8 )})"
+ " — re-equip the weapon to apply" );
}
[ConCmd( "nz_reserve" )]
public static void ReserveCmd( int clip = 0 )
{
if ( clip > 0 )
{
Log.Info( $"[nz-reserve] clip {clip} -> {MagsFor( clip )} mags -> {BaseFor( clip )}" );
return;
}
Log.Info( "[nz-reserve] >=100: 4 mags | >=50: 6 | >=25: 8 | <25: 10 (cap 999)"
+ $" · LOADOUT weapon overrides all of it with {StartingMags} mags (nz_starting_mags)" );
var player = PlayerCharacters.Local();
if ( !player.IsValid() ) { Log.Warning( "[nz-reserve] no player" ); return; }
var weapons = player.Components
.GetAll<SWB.Base.Weapon>( FindMode.EverythingInSelfAndDescendants );
int n = 0;
foreach ( var wep in weapons )
{
var ammo = wep.Components.Get<NZAmmo>( FindMode.EverythingInSelf );
int c = wep.Primary?.ClipSize ?? 0;
n++;
// ⚠️ THE RULE PRINTED HAS TO BE THE RULE APPLIED. This method exists to make drift
// between the derived figure and the live one VISIBLE — so printing the tiered number
// beside a loadout weapon holding three magazines would manufacture exactly the
// discrepancy it is here to detect, and send someone hunting a bug that is the design.
var loadout = !string.IsNullOrWhiteSpace( player.LoadoutWeapon )
&& string.Equals( Rarity.PrefabOf( wep ), player.LoadoutWeapon,
System.StringComparison.OrdinalIgnoreCase );
Log.Info( $"[nz-reserve] {wep.DisplayName,-22} clip {c,4}"
+ $" {(loadout ? StartingMags : MagsFor( c )),2} mags rule {BaseFor( c, loadout ),4}"
+ (loadout ? " [LOADOUT]" : " ")
+ (ammo.IsValid()
? $" live {ammo.MaxReserve,4} holding {ammo.Reserve,4}"
: " NO NZAmmo") );
}
if ( n == 0 ) Log.Info( "[nz-reserve] no weapons carried" );
}
}