Player/DeadshotAugments.cs

Static class implementing the Deadshot perk augments for NZombies. Defines tunable values, checks player augment ownership, applies effects like Lucky Shot promotion, Deadeye/Focus headshot scaling, First Blood damage on fresh targets, Concussion stun, Recycler ammo refund, Cranial Detonation AOE, HUD/reporting and console commands to inspect and change values.

NetworkingFile Access
using Sandbox;
using System;
using System.Linq;

namespace NZombies;

/// <summary>
/// Deadshot Daiquiri's augments. Base perk: ADS time, ADS recoil and spread all ×0.5.
///
/// | id | effect | status |
/// |----|--------|--------|
/// | M1 Deadeye     | headshot damage ×1.5 | ⚠ was ×2 |
/// | M2 First Blood | ANY hit on a full-HP zombie deals ×3 | ⚠ no longer headshot-gated |
/// | M3 Cranial Detonation | a headshot KILL splashes 10% of the damage dealt | ⚠ redesigned |
/// | M4 Focus       | +15% headshot damage per headshot kill, up to ×5 | ⚠ was capped at +150% |
/// | m1 Lucky Shot  | 10% of hits count as headshots | ⚠ NEW — replaced Steady Hands |
/// | m2 Trophy      | +25 points per headshot kill | as the original |
/// | m3 Concussion  | 25% chance a headshot stuns 0.8–1.2s | as the original |
/// | m4 Hip Precision | hip-fire spread ×0.65, ADS untouched | as the original |
/// | m5 Recycler    | headshot kills refund 2 rounds | as the original |
///
/// ⛔ M3 WAS REDESIGNED BECAUSE HALF OF THE ORIGINAL WAS UNREACHABLE. Its first clause is
/// *"head-pop meter builds 2× faster"* — and that meter does not exist in this project.
/// `PERK_BASE_EFFECTS.md` lists Deadshot's base as carrying a 25%-capped headshot-explosion
/// chance (`nz.DeadshotChance`), but nothing in the code implements it, so there was nothing
/// to accelerate. The blast half is now unconditional and scales off the killing hit rather
/// than off a round-health curve, which also means it cannot outgrow the weapon that fired it.
///
/// ⛔ m1 WAS REPLACED BECAUSE IT DUPLICATED SPEED COLA. "Steady Hands" was faster ADS settle
/// — which Speed Cola's m3 Sleight of Hand already does, on the same multiplier. Two perks
/// selling one stat is a worse outcome than a gap. Its sway half was never wired in GMod
/// either (its own note: the cross-base system exposes no `SwayMult` key), and sway here
/// exists only as sniper-scope idle drift, so nothing was lost.
///
/// ⚠️ m1 "LUCKY SHOT" DELIBERATELY AMPLIFIES EVERY OTHER AUGMENT HERE. A proc'd hit is
/// marked as a real headshot at the one place headshots are decided, so Focus counts it,
/// Trophy pays for it, Recycler refunds on it and Cranial Detonation splashes from it. That
/// is the point of putting it on Deadshot rather than anywhere else.
/// </summary>
public static class DeadshotAugments
{
	const string Perk = "deadshot";

	// ── tuning ───────────────────────────────────────────────────────────────

	/// <summary>M1 Deadeye — headshot damage multiplier. ×1.5.</summary>
	public static float DeadeyeHeadshot { get; set; } = 1.5f;

	/// <summary>
	/// M2 First Blood — multiplier on a hit against an undamaged zombie. ×3.
	///
	/// ⛔ NOT HEADSHOT-GATED, unlike the original. Its test was
	/// `headshot AND health >= maxhealth`; this one drops the headshot half by request, so
	/// any first hit on a fresh zombie triples. That makes it a genuinely different major
	/// from Deadeye rather than a bigger version of it — Deadeye rewards aim, First Blood
	/// rewards opening on new targets.
	/// </summary>
	public static float FirstBloodMultiplier { get; set; } = 3f;

	/// <summary>
	/// M3 Cranial Detonation — the fraction of the killing hit splashed to everything nearby.
	///
	/// ⚠️ A FRACTION OF THE DAMAGE DEALT, not a round-health curve. The original computed
	/// `roundHealth * 0.5 + 150`, which is independent of the weapon — a pistol headshot and
	/// a Pack-a-Punched LMG headshot produced the same blast. Scaling off the hit means the
	/// augment tracks whatever gun you are actually holding.
	/// </summary>
	public static float DetonationShare { get; set; } = 0.10f;

	/// <summary>M3 Cranial Detonation — blast radius. The original's 260.</summary>
	public static float DetonationRadius { get; set; } = 260f;

	/// <summary>M4 Focus — headshot damage gained per consecutive headshot kill. +15%.</summary>
	public static float FocusPerHeadshot { get; set; } = 0.15f;

	// ⛔ PER HEADSHOT, NOT PER HEADSHOT KILL, and the asymmetry is the design: it builds
	// on every head HIT and is only thrown away by a non-headshot KILL. Counting kills made
	// the bar crawl - 27 head kills to reach the cap - and made it invisible against anything
	// that takes more than one shot to drop, which by the late rounds is everything.

	/// <summary>
	/// M4 Focus — the ceiling on the headshot multiplier. ×5.
	///
	/// ⚠️ ADDITIVE STEPS AGAINST A MULTIPLICATIVE CAP, so the streak needed is
	/// `(5 - 1) / 0.15` ≈ **27 headshot kills**. That is a long build on purpose — a
	/// compounding 1.15^n would reach ×5 in twelve and make the cap almost incidental.
	/// </summary>
	public static float FocusMax { get; set; } = 5f;

	/// <summary>m1 Lucky Shot — chance a hit is treated as a headshot. 10%.</summary>
	public static float LuckyShotChance { get; set; } = 0.10f;

	/// <summary>m2 Trophy — bonus points per headshot kill.</summary>
	public static int TrophyPoints { get; set; } = 25;

	/// <summary>m3 Concussion — chance a headshot stuns. 25%.</summary>
	public static float ConcussionChance { get; set; } = 0.25f;

	/// <summary>m3 Concussion — stun duration bounds, seconds.</summary>
	public static float ConcussionMin { get; set; } = 0.8f;
	public static float ConcussionMax { get; set; } = 1.2f;

	/// <summary>m4 Hip Precision — hip-fire spread multiplier. 0.65.</summary>
	public static float HipPrecisionSpread { get; set; } = 0.65f;

	/// <summary>m5 Recycler — rounds refunded per headshot kill.</summary>
	public static int RecyclerRounds { get; set; } = 2;

	// ── helpers ──────────────────────────────────────────────────────────────

	static bool Has( NZPlayer p, string augId )
		=> p.IsValid() && p.HasPerk( Perk ) && PerkAugments.Has( p, Perk, augId );

	static NZPlayer PlayerOf( GameObject attacker )
		=> attacker.IsValid()
			? attacker.Components.Get<NZPlayer>( FindMode.EverythingInSelfAndAncestors )
			: null;

	// ── m1 Lucky Shot ────────────────────────────────────────────────────────

	/// <summary>
	/// Roll m1 Lucky Shot — should this non-headshot hit be promoted to one.
	///
	/// ⛔ CALLED AT THE ONE PLACE HEADSHOTS ARE DECIDED (`Health.OnDamage`'s
	/// `var headshot = ...`), which is what makes the promotion complete rather than
	/// cosmetic. Everything downstream — the head damage product, Focus's streak, Trophy's
	/// points, Recycler's refund, Cranial Detonation's blast and the 100-point kill award —
	/// reads that one local. Scaling the damage instead would have given the multiplier and
	/// none of the rest.
	///
	/// ⚠️ ONLY PROMOTES, NEVER DEMOTES. A real headshot is already a headshot; rolling on it
	/// too would mean a 90% chance of downgrading one, which is the opposite augment.
	/// </summary>
	public static bool RollLuckyShot( GameObject attacker )
		=> Has( PlayerOf( attacker ), "m1" )
			&& Game.Random.Float() < LuckyShotChance;

	// ── M1 · M4 · the head damage product ────────────────────────────────────

	/// <summary>
	/// Multiplier on the HEADSHOT product from M1 Deadeye and M4 Focus.
	///
	/// ⚠️ IT RIDES THE EXISTING PRODUCT rather than replacing it, exactly as Death
	/// Perception, Precision Rounds and Deadeye already do on that line — so a Deadshot
	/// player still gets `HeadshotDamageScale` and every node they own.
	/// </summary>
	public static float HeadshotScale( GameObject attacker )
	{
		var p = PlayerOf( attacker );
		if ( !p.IsValid() || !p.HasPerk( Perk ) ) return 1f;

		var mult = 1f;

		if ( Has( p, "M1" ) ) mult *= DeadeyeHeadshot;
		if ( Has( p, "M4" ) ) mult *= FocusMultiplier( p );

		return mult;
	}

	/// <summary>
	/// M4 Focus's current multiplier for a player. 1 at no streak.
	///
	/// ⚠️ CLAMPED AT BOTH ENDS. The floor of 1 matters because `FocusMax` is settable and a
	/// console value below 1 would otherwise make a streak REDUCE headshot damage.
	/// </summary>
	public static float FocusMultiplier( NZPlayer player )
	{
		if ( !player.IsValid() ) return 1f;

		return MathX.Clamp( 1f + player.DeadshotFocus * FocusPerHeadshot,
			1f, MathF.Max( 1f, FocusMax ) );
	}

	/// <summary>How far along Focus is, 0-1. For the HUD bar.</summary>
	public static float FocusProgress( NZPlayer player )
	{
		var span = MathF.Max( 0.01f, MathF.Max( 1f, FocusMax ) - 1f );
		return MathX.Clamp( (FocusMultiplier( player ) - 1f) / span, 0f, 1f );
	}

	/// <summary>Headshot kills needed to reach the cap from nothing.</summary>
	public static int FocusHitsToCap
		=> FocusPerHeadshot <= 0f
			? 0
			: (int)MathF.Ceiling( (MathF.Max( 1f, FocusMax ) - 1f) / FocusPerHeadshot );

	// ── M2 First Blood ───────────────────────────────────────────────────────

	/// <summary>
	/// Multiplier on a hit against a zombie at full health.
	///
	/// ⚠️ `>=`, NOT `==`. Health is a float and a zombie topped up by a round-scaling
	/// `Reset( max )` can land a hair above its own maximum; an equality test would make
	/// the augment fire on some spawns and not others with nothing to distinguish them.
	///
	/// ⚠️ TAKES THE VICTIM'S NUMBERS rather than looking them up, because the one caller is
	/// inside the victim's own Health component and already has them.
	/// </summary>
	public static float FirstBloodScale( GameObject attacker, float current, float max )
	{
		// ⚠️ THE VICTIM TEST STAYS WHEREVER THIS IS CALLED, and it is only meaningful on the
		// HOST — a client's copy of a zombie is never damaged locally, so `current` there is
		// permanently `max`. This method is deliberately no longer called from the client's
		// pre-multiply; see `Health.AttackerScale`.
		if ( max <= 0f || current < max ) return 1f;

		return ScaleFor( PlayerOf( attacker ), FirstBloodScaleLocal, p => p.FirstBloodLuck );
	}

	/// <summary>
	/// A multiplier owned by a player, from wherever the truth for that player is.
	///
	/// ⚠️ THE SAME SHAPE AS `DeathAugments.ScaleFor`, duplicated rather than shared because the
	/// two read different synced fields and a shared helper would need the field passed in anyway.
	/// </summary>
	static float ScaleFor( NZPlayer player, Func<NZPlayer, float> local,
		Func<NZPlayer, float> published )
	{
		if ( !player.IsValid() ) return 1f;

		return Networking.IsActive && PlayerPresence.Theirs( player.GameObject )
			? MathF.Max( 0f, published( player ) )
			: local( player );
	}

	/// <summary>M2's multiplier read from the REAL loadout. Only meaningful on the owner.</summary>
	public static float FirstBloodScaleLocal( NZPlayer player )
		=> Has( player, "M2" ) ? MathF.Max( 0f, FirstBloodMultiplier ) : 1f;

	// ── m4 Hip Precision ─────────────────────────────────────────────────────

	/// <summary>Hip-fire spread multiplier from a weapon. Read by GetRealSpread.</summary>
	public static float HipSpreadFor( Component weapon )
	{
		var p = weapon.IsValid()
			? weapon.Components.Get<NZPlayer>( FindMode.InAncestors | FindMode.Enabled )
			: null;

		return Has( p, "m4" ) ? HipPrecisionSpread : 1f;
	}

	// ── m3 Concussion ────────────────────────────────────────────────────────

	/// <summary>
	/// Roll m3 Concussion on a headshot that did NOT kill.
	///
	/// ⚠️ THE DURATION IS RANDOMISED PER APPLICATION, which `StatusEffects.Apply` can only
	/// do because Juggernog's m5 needed a `seconds` override. Without that this would have
	/// had to either accept the rule's flat 1s or add a second stun rule.
	/// </summary>
	public static void TryConcuss( GameObject attacker, GameObject victim )
	{
		if ( !Has( PlayerOf( attacker ), "m3" ) ) return;
		if ( !victim.IsValid() ) return;
		if ( Game.Random.Float() >= ConcussionChance ) return;

		var zombie = victim.Components.Get<ZombieAI>( FindMode.EverythingInSelfAndAncestors );
		if ( !zombie.IsValid() ) return;

		StatusEffects.Apply( zombie.GameObject, "stun", attacker,
			seconds: Game.Random.Float( ConcussionMin, ConcussionMax ) );
	}

	// ── M3 · M4 · m2 · m5 · on a kill ────────────────────────────────────────

	/// <summary>
	/// Everything that happens on a zombie death.
	///
	/// ⛔ THE STREAK IS UPDATED FOR EVERY KILL, NOT ONLY HEADSHOT ONES, because a
	/// non-headshot kill is what RESETS it. Gating the whole method on `headshot` would have
	/// made Focus a permanent buff after the first head kill — the augment's entire cost is
	/// that a sloppy kill throws it away.
	/// </summary>
	/// <summary>
	/// A headshot LANDED - build M4 Focus. Called from `Health` on every head hit, killing or
	/// not.
	///
	/// ⚠ CAPPED WHERE IT IS COUNTED, not only where it is read. An uncapped counter would
	/// climb for the rest of the game, so a player who reached the ceiling and then made one
	/// body kill would still be at the cap and the reset would be decorative.
	///
	/// ⚠ IT DOES NOT BOOST THE SHOT THAT CAUSED IT. `Health` computes the headshot bonus
	/// well above the point it calls this, so the increment lands for the NEXT shot. A shot
	/// that paid its own bonus forward would make the first headshot of a streak worth more
	/// than the second, which is backwards.
	/// </summary>
	public static void OnHeadshotHit( GameObject attacker )
	{
		if ( !attacker.IsValid() ) return;

		var player = attacker.Components.Get<NZPlayer>( FindMode.EverythingInSelfAndAncestors );
		if ( !player.IsValid() || !player.HasPerk( Perk ) ) return;
		if ( !Has( player, "M4" ) ) return;

		player.DeadshotFocus = Math.Min( player.DeadshotFocus + 1, FocusHitsToCap );
	}

	public static void OnZombieKilled( NZPlayer player, bool headshot, Vector3 position,
		float damage, GameObject victim )
	{
		if ( !player.IsValid() || !player.HasPerk( Perk ) ) return;

		// M4 Focus — the streak.
		// ⛔ THE KILL PATH ONLY RESETS NOW. Building the streak moved to `OnHeadshotHit`,
		// which `Health` calls on every head hit - so a headshot that also KILLS is counted
		// there, once, and must not be counted again here. A second increment on the kill
		// would make head-killing worth double head-wounding, quietly reintroducing the
		// per-kill design this replaced.
		//
		// ⚠ THE RESET STAYS ON THE KILL, which is why this method still runs for every
		// death rather than only headshot ones. A body SHOT costs nothing; a body KILL is the
		// mistake the augment charges you for.
		if ( Has( player, "M4" ) && !headshot )
		{
			if ( player.DeadshotFocus > 0 )
				Log.Info( $"[nz-aug] deadshot M4 Focus - streak of {player.DeadshotFocus}"
					+ " lost to a body kill" );

			player.DeadshotFocus = 0;
		}

		if ( !headshot ) return;

		// m2 Trophy — ⚠️ nothing while basalt's altar defense runs, when a kill pays only its 10 (`HexPlatforms.DefensePoints`)
		if ( Has( player, "m2" ) && !HexPlatforms.DefensePoints ) player.AddPoints( TrophyPoints );

		// m5 Recycler
		if ( Has( player, "m5" ) )
		{
			var wep = player.Components.Get<SWB.Base.Weapon>( FindMode.EverythingInDescendants );
			var si = wep.IsValid() ? wep.Primary : null;

			// ⚠️ Clamped to the CLIP, so a refund cannot overfill a magazine — and skipped
			// entirely on a weapon with no magazine (melee reports ClipSize <= 0).
			if ( si is not null && si.ClipSize > 0 && si.Ammo < si.ClipSize )
				si.Ammo = Math.Min( si.ClipSize, si.Ammo + RecyclerRounds );
		}

		// M3 Cranial Detonation
		if ( Has( player, "M3" ) ) Detonate( player, position, damage, victim );
	}

	/// <summary>
	/// M3's splash.
	///
	/// ⛔ THE CORPSE IS EXCLUDED BY OBJECT, not by health. The zombie that just died still
	/// has a `Health` for its whole corpse linger and `IsDead` only becomes true once the
	/// death handler has run — so a health test would sometimes splash the thing that
	/// caused the splash and sometimes not, depending on ordering.
	///
	/// ⚠️ NO RE-ENTRY GUARD IS NEEDED HERE, unlike the original's `deadshotAoE` flag. Its
	/// blast ran through the same `EntityTakeDamage` hook as its damage augments, so it had
	/// to mask itself out. This calls `Health.OnDamage` directly with an empty TagSet, so
	/// it carries no bullet tag — Vigor Rush, the head product and First Blood all test for
	/// one and none of them can see it.
	///
	/// ⚠️ AND THAT ALSO MEANS THE SPLASH CANNOT CHAIN. A blast kill does not produce another
	/// blast, because a splash hit is not a headshot. One detonation per headshot kill, full
	/// stop — which is what keeps a dense horde from becoming one screen-clearing cascade.
	/// </summary>
	static void Detonate( NZPlayer player, Vector3 position, float damage, GameObject victim )
	{
		// ⚠ NAPALM NECTAR M2 (area damage x3) / m4 (radius +20%). One of the six sites listed in
		// `FireAugments.AreaDamageScale` — there is no explosive damage TYPE here, so "all area
		// damage" is a register of call sites. A new AoE that is not wrapped is not covered.
		var splash = damage * MathF.Max( 0f, DetonationShare )
			* FireAugments.AreaDamageScale( player?.GameObject );
		if ( splash <= 0f ) return;

		var hit = 0;

		foreach ( var z in ZombieAI.All )
		{
			if ( !z.IsValid() || !z.GameObject.IsValid() ) continue;
			if ( victim.IsValid() && z.GameObject == victim ) continue;

			var target = z.WorldPosition + Vector3.Up * 32f;
			if ( position.Distance( target )
				> DetonationRadius * FireAugments.AreaRadiusScale( player?.GameObject ) ) continue;

			var hp = z.Components.Get<Health>( FindMode.EverythingInSelfAndAncestors );
			if ( !hp.IsValid() || hp.IsDead ) continue;

			hp.OnDamage( new DamageInfo
			{
				Damage = splash,
				Attacker = player.GameObject,
				Position = target,
				Tags = new TagSet(),
			} );

			hit++;
		}

		if ( hit > 0 )
			Log.Info( $"[nz-aug] deadshot M3 Cranial Detonation — {splash:0.#} to {hit} zombie(s)" );
	}

	// ── diagnostics ──────────────────────────────────────────────────────────

	public static void Report( NZPlayer player )
	{
		if ( !player.IsValid() ) { Log.Warning( "[nz-aug] no player" ); return; }

		var has = player.HasPerk( Perk );
		var equipped = PerkAugments.EquippedOn( player, Perk );
		var hp = player.Components.Get<Health>( FindMode.EverythingInSelf );

		Log.Info( $"[nz-aug] DEADSHOT {(has ? "owned" : "NOT OWNED — every line below is inert")}"
			+ $" · equipped [{(equipped.Length == 0 ? "none" : string.Join( "+", equipped ))}]"
			+ $" · base handling x{PerkEffects.DeadshotFactor:0.##}" );

		// ⚠️ PRINTS THE WHOLE HEAD PRODUCT, not just this perk's share. Five things
		// multiply into it — the base 2.5, Death Perception, Precision Rounds, Deadeye and
		// these two augments — and "x1.5" tells you nothing about what a headshot deals.
		var baseHead = hp.IsValid() ? hp.HeadshotDamageScale : 2.5f;
		var augHead = HeadshotScale( player.GameObject );

		Log.Info( $"[nz-aug]  M1 Deadeye      x{(Has( player, "M1" ) ? DeadeyeHeadshot : 1f):0.##}"
			+ $"   head product {baseHead:0.##} → {baseHead * augHead:0.##}"
			+ " (before Death Perception and tech)" );

		Log.Info( $"[nz-aug]  M2 First Blood  {(Has( player, "M2" ) ? $"x{FirstBloodMultiplier:0.##} on any hit against an undamaged zombie" : "-")}" );

		Log.Info( $"[nz-aug]  M3 Cranial Det  {(Has( player, "M3" ) ? $"{DetonationShare * 100f:0.#}% of the killing hit within {DetonationRadius:0}u" : "-")}" );

		Log.Info( $"[nz-aug]  M4 Focus        streak {player.DeadshotFocus}/{FocusHitsToCap}"
			+ $" → x{FocusMultiplier( player ):0.##} of x{FocusMax:0.##}"
			+ $"   ({FocusProgress( player ) * 100f:0}% charged, +{FocusPerHeadshot * 100f:0}% per HEADSHOT, reset by a body KILL)" );

		Log.Info( $"[nz-aug]  m1 Lucky Shot   {(Has( player, "m1" ) ? $"{LuckyShotChance * 100f:0}% of hits count as headshots" : "-")}" );

		Log.Info( $"[nz-aug]  m2 Trophy       {(Has( player, "m2" ) ? $"+{TrophyPoints} points per headshot kill" : "-")}"
			+ $"   m5 Recycler {(Has( player, "m5" ) ? $"+{RecyclerRounds} rounds" : "-")}" );

		Log.Info( $"[nz-aug]  m3 Concussion   {(Has( player, "m3" ) ? $"{ConcussionChance * 100f:0}% for {ConcussionMin:0.##}-{ConcussionMax:0.##}s" : "-")}"
			+ $"   m4 Hip Precision spread x{(Has( player, "m4" ) ? HipPrecisionSpread : 1f):0.##}" );

		// ⚠️ THE ×5 HEADSHOT DATA BUG IS NAMED HERE because Focus multiplies straight into
		// it. AWM, G3, SVD and WA2000 author `HeadMultiplier: 2.0` on top of the 2.5, so
		// those four already deal ×5 headshots — a capped Focus on an AWM is ×25 before any
		// tech. Health.cs records the fault; this is where it becomes alarming.
		if ( Has( player, "M4" ) )
			Log.Info( "[nz-aug]  ⚠ Focus multiplies the WHOLE head product. On the four"
				+ " prefabs that author HeadMultiplier 2.0 (AWM, G3, SVD, WA2000) the head"
				+ " product is already doubled — see the note in Health.cs." );
	}

	// ── commands ─────────────────────────────────────────────────────────────

	static NZPlayer Me()
		=> NZPlayer.Local;

	/// <summary>`nz_aug_deadshot` — the report.</summary>
	[ConCmd( "nz_aug_deadshot" )]
	public static void DeadshotCmd() => Report( Me() );

	/// <summary>
	/// `nz_aug_deadshot_set [deadeye] [firstBlood] [detShare] [detRadius]` — the majors,
	/// Focus excepted.
	/// </summary>
	[ConCmd( "nz_aug_deadshot_set" )]
	public static void SetCmd( float deadeye = -1f, float firstBlood = -1f,
		float detShare = -1f, float detRadius = -1f )
	{
		if ( deadeye >= 0f ) DeadeyeHeadshot = deadeye;
		if ( firstBlood >= 0f ) FirstBloodMultiplier = firstBlood;
		if ( detShare >= 0f ) DetonationShare = detShare;
		if ( detRadius >= 0f ) DetonationRadius = detRadius;

		Report( Me() );
	}

	/// <summary>
	/// `nz_aug_deadshot_focus [perKill] [max] [streak]` — Focus, including its live streak.
	///
	/// ⛔ THE STREAK ARGUMENT IS THE ONLY WAY TO SEE THE MIDDLE OF THE BAR. Reaching the cap
	/// legitimately is 27 consecutive headshot kills; nobody is doing that to check a HUD
	/// element renders at 40%.
	/// </summary>
	[ConCmd( "nz_aug_deadshot_focus" )]
	public static void FocusCmd( float perKill = -1f, float max = -1f, int streak = -1 )
	{
		if ( perKill >= 0f ) FocusPerHeadshot = perKill;
		if ( max >= 0f ) FocusMax = max;

		var p = Me();
		if ( streak >= 0 && p.IsValid() ) p.DeadshotFocus = Math.Min( streak, FocusHitsToCap );

		Report( p );
	}

	/// <summary>
	/// `nz_aug_deadshot_minor [luckyChance] [trophy] [concussChance] [hipSpread] [recycler]`.
	/// </summary>
	[ConCmd( "nz_aug_deadshot_minor" )]
	public static void MinorCmd( float luckyChance = -1f, int trophy = -1,
		float concussChance = -1f, float hipSpread = -1f, int recycler = -1 )
	{
		if ( luckyChance >= 0f ) LuckyShotChance = MathX.Clamp( luckyChance, 0f, 1f );
		if ( trophy >= 0 ) TrophyPoints = trophy;
		if ( concussChance >= 0f ) ConcussionChance = MathX.Clamp( concussChance, 0f, 1f );
		if ( hipSpread >= 0f ) HipPrecisionSpread = hipSpread;
		if ( recycler >= 0 ) RecyclerRounds = recycler;

		Report( Me() );
	}
}