A game weapon class derived from BaseCombatWeapon that configures a simple hitscan firearm for the NZombies gamemode. It sets damage, pellets, range, spread, fire delay, clip and reserve sizes, ballistic configuration and ensures the magazine is filled on start.
using Sandbox;
namespace NZombies;
/// <summary>
/// WEAPON — a plain hitscan gun on top of s&box's BaseCombatWeapon.
///
/// Deliberately thin. The base already does everything a first gun needs:
/// tracing, damage, impact effects, muzzle presentation on every peer,
/// magazines, reserve ammo and the reload loop. Its default PrimaryAttack
/// "spends a round, shoots the volley and plays the effects" — so firing needs
/// no code at all, only configuration.
///
/// It also merges the struck hitbox's tags into the damage
/// (BaseCombatWeapon.MergeHitboxTags), which is how "head" reaches
/// Health.IsHeadshot without us tracing for it. That matters here: nZombies
/// pays 100 points for a headshot against 50 for a body shot.
///
/// Damage lands on anything with a Health component, because Health implements
/// Sandbox.Component.IDamageable and the engine's bullet path looks for that.
///
/// ⚠️ Values below are placeholders to make the loop playable, NOT the
/// original's balance. Real weapon stats come from the GMod weapon tables when
/// weapons get ported properly.
/// </summary>
public sealed class NZWeapon : BaseCombatWeapon
{
[Property] public float ShotDamage { get; set; } = 35f;
[Property] public int ShotPellets { get; set; } = 1;
[Property] public float ShotRange { get; set; } = 8192f;
[Property] public float ShotSpread { get; set; } = 0.6f;
/// <summary>Seconds between shots — GMod's Primary.Delay.</summary>
[Property] public float FireDelay { get; set; } = 0.12f;
[Property] public int ClipSize { get; set; } = 30;
[Property] public int ReserveAmmo { get; set; } = 240;
protected override void OnStart()
{
// ⚠️ EVERYTHING BELOW base.OnStart(), NOT after it. The base seeds the
// magazine during its own start, so configuring afterwards is too late:
// Clip1 comes out as -1 ("no magazine") no matter what UsesClips says,
// and the HUD shows -1 rounds. Set the authoring values first and the
// base initialises from them.
//
// Feed from a magazine, like every nZombies weapon: the reload is part
// of the tension, and Max Ammo as a powerup only means something if a
// reserve pool exists.
UsesAmmo = true;
UsesClips = true;
// ⚠️ ClipMaxSize / StartingAmmo are the AUTHORING properties.
// PrimaryClipSize and MaxReserveAmmo look like the obvious ones but are
// read-only — they're derived (PrimaryClipSize resolves to -1 when the
// weapon feeds straight from reserve, MaxReserveAmmo comes from the
// ammo type).
ClipMaxSize = ClipSize;
StartingAmmo = ReserveAmmo;
PrimaryDelay = FireDelay;
PrimaryAutomatic = true;
Ballistics = new BallisticConfig
{
Damage = ShotDamage,
Pellets = ShotPellets,
Range = ShotRange,
SpreadBase = new Vector2( ShotSpread, ShotSpread ),
};
base.OnStart();
// The base does not put rounds in the magazine — it sizes it. Clip1
// stays at -1 ("no magazine") until something loads it, so a weapon that
// is configured correctly still reads as having no clip. Load it here.
if ( UsesPrimaryClip && Clip1 < 0 )
Clip1 = ClipMaxSize;
Log.Info( $"[NZWeapon] clip {Clip1}/{ClipMaxSize} usesPrimaryClip={UsesPrimaryClip} "
+ $"usesClips={UsesClips} reserve={(HasInfiniteReserve ? "inf" : Ammo1.ToString())}" );
}
/// <summary>
/// Is the reserve pool bottomless?
///
/// PrimaryAmmoType null means exactly that — "the magazine still forces the
/// reload rhythm, it just never runs out". Ammo1 then reads int.MaxValue,
/// which is correct but unprintable, so the HUD shows ∞ instead.
///
/// Assigning a BaseAmmoResource is what makes the reserve finite. nZombies
/// wants finite (Max Ammo is a powerup and only means something against a
/// pool that empties), so this is a TODO, not the intended end state.
/// </summary>
public bool HasInfiniteReserve => PrimaryAmmoType is null;
}