Utility/static class implementing the Adrenaline Rounds tech node. Tracks per-player stacking temporary buffs earned when bullets hit zombies, exposes methods to increment stacks (Rush), compute current bonus (Bonus), and provide damage/ADS multipliers for weapons that have the tech node. Also includes a console command to print current settings and local player state.
using Sandbox;
using System;
namespace NZombies;
/// <summary>
/// ADRENALINE ROUNDS (`t5_adrenaline`) — REDEFINED 2026-10-04 as the SMG's stacking reward (the user: *"each hit gives 1%
/// damage increase and 1% ads strafe speed, for 1s capped at 50% each, it stacks untill the player does not hit a zombie
/// for 1 second"*). Every zombie a bullet touches — each pellet, each body it passes through — adds one stack; a second
/// with no hit drops them all.
///
/// ⛔ IT REPLACES THE 2026-09 NODE, which was +20% damage, +25% walk speed for a second per hit and no slowdown while
/// aiming. That damage was written into `si.Damage` at spawn as the node's `Factor`; the new `Factor` is the 1% PER
/// STACK, so `NZPlayer.ApplyStoredUpgrades` no longer reads it — left there, it would have cut the gun to a hundredth.
///
/// ⚠️ SET ON THE SHOOTER, from `HitScan`'s zombie contact, the same placement and the same argument as the old node:
/// the stacks are the shooter's own state, so a client's own shots build them without asking the host.
///
/// ⚠️ ON THE PLAYER, NOT THE WEAPON. The stacks are earned by this gun and only this gun spends them (`DamageScale`,
/// `AdsScale` ask whether the gun owns the node), but they belong to the body, so a quick swap and back keeps them while
/// the second lasts.
/// </summary>
public static class AdrenalineRounds
{
/// <summary>How long one hit keeps the stacks alive, in seconds.</summary>
public static float Seconds => WeaponTech.MagOf( "t5_adrenaline", "seconds", 1f );
/// <summary>What one stack is worth, as a fraction (0.01 = +1%).</summary>
public static float Per => WeaponTech.MagOf( "t5_adrenaline", "per", 0f );
/// <summary>The cap on the bonus, as a fraction (0.5 = +50%).</summary>
public static float Cap => WeaponTech.MagOf( "t5_adrenaline", "cap", 0f );
/// <summary>
/// A round from an adrenaline gun just touched a zombie: one more stack, and the clock restarts.
///
/// ⚠️ THE STACKS STOP GROWING AT THE CAP rather than counting on unseen, so a long streak cannot bank stacks that
/// would outlast a later change to the cap.
/// </summary>
public static void Rush( SWB.Shared.IPlayerBase owner )
{
var player = owner?.GameObject?.Components
.Get<NZPlayer>( FindMode.EverythingInSelfAndAncestors );
if ( !player.IsValid() ) return;
if ( player.AdrenalineUntil <= Time.Now ) player.AdrenalineStacks = 0;
var most = Per > 0f ? (int)MathF.Ceiling( Cap / Per ) : 0;
player.AdrenalineStacks = Math.Min( most, player.AdrenalineStacks + 1 );
player.AdrenalineUntil = Time.Now + Seconds;
}
/// <summary>The bonus the stacks are worth right now, as a fraction. 0 once the second has run out.</summary>
public static float Bonus( NZPlayer player )
{
if ( !player.IsValid() || player.AdrenalineUntil <= Time.Now ) return 0f;
return MathF.Min( Cap, player.AdrenalineStacks * Per );
}
/// <summary>This gun's damage multiplier from the stacks: 1 + the bonus, on a gun that owns the node.</summary>
public static float DamageScale( SWB.Base.Weapon weapon )
{
if ( !weapon.IsValid() || !TechEffects.Has( weapon, "t5_adrenaline" ) ) return 1f;
var player = weapon.Components.Get<NZPlayer>( FindMode.InAncestors | FindMode.Enabled );
return 1f + Bonus( player );
}
/// <summary>
/// The aiming walk speed multiplier with this gun in hand: 1 + the bonus. `NZPlayer.TickAdsSpeed` multiplies the
/// fraction of walk speed you keep while aiming by it, under the same ceiling of 1.
/// </summary>
public static float AdsScale( NZPlayer player, SWB.Base.Weapon weapon )
=> weapon.IsValid() && TechEffects.Has( weapon, "t5_adrenaline" ) ? 1f + Bonus( player ) : 1f;
/// <summary>
/// The old node's walk rush, RETIRED with it. Always 1; kept because `TickAdsSpeed` multiplies it in, and a speed
/// term that is gone should read as neutral rather than as a missing method.
/// </summary>
public static float SpeedScale( NZPlayer player ) => 1f;
/// <summary>
/// `nz_adrenaline` — what the node is worth and the stacks right now.
/// </summary>
[ConCmd( "nz_adrenaline" )]
public static void Cmd()
{
var player = NZPlayer.Local;
Log.Info( $"[nz-adrenaline] +{Per * 100f:0.#}% damage and aiming walk speed per zombie hit,"
+ $" up to +{Cap * 100f:0}%; {Seconds:0.##}s without a hit drops them" );
if ( !player.IsValid() )
{
Log.Info( "[nz-adrenaline] no local player" );
return;
}
var left = player.AdrenalineUntil - Time.Now;
Log.Info( left > 0f
? $"[nz-adrenaline] {player.AdrenalineStacks} stacks = +{Bonus( player ) * 100f:0}%, {left:0.00}s left"
: "[nz-adrenaline] no stacks" );
}
}