Utility class for reading combined weapon tech stats for a player and weapon prefab. It provides methods to get multiplicative (Mul), additive (Add), flag, and set values by aggregating owned WeaponTech nodes' Extra entries, and helper overloads that resolve the weapon owner/prefab or use TechRef.
using Sandbox;
namespace NZombies;
/// <summary>
/// THE GENERIC STATS OF THE TIER-4/5 AUGMENTS (2026-10-04, Sbox nzombies/Docs/WEAPON_TECH_BY_CLASS.md): every owned
/// node's `s.*` and `f.*` magnitudes, combined, for one weapon.
///
/// ⚠️ TIERS 1–3 USE THEM TOO SINCE 2026-10-04 (WEAPON_TECH_TIERS_1_3.md): Floated Barrel, Quick Sling, Steel Core, Gunner's
/// Plate, Backup Sights… are made only of these. They brought one new key, `s.reserve+` (Side Pouch's flat +30 reserve),
/// read once at `NZPlayer`'s reserve site.
///
/// ⛔ ONE ACCESSOR PER STAT SITE, NOT ONE CALL PER NODE. The per-class redesign brought about seventy augments, most of
/// them made only of stats (×1.6 damage, half the range, +3 penetration…). Wiring each by id at every site would be
/// seventy names to remember in a dozen files; instead each stat site reads its stat once from here, and a node made
/// of stats needs no code at all. The catalogue is still the only source of every number (`WeaponTech`'s rule).
///
/// ⚠️ ONLY `s.*` AND `f.*` NAMES ARE READ HERE. The old nodes' magnitudes (`rpm`, `clip`, `walk`…) are wired by id at
/// their own sites; giving the new stats their own prefix is what keeps an old node from being counted twice.
///
/// ⚠️ THROUGH `NZPlayer.TechOrNull`, NOT `TechFor`: these are read several times a frame (`EffectiveFiringType`,
/// `GetRealSpread`), and `TechFor` allocates a new list for every gun without tech.
///
/// ⚠️ AMPLIFIED like every other magnitude (`TechEffects.AmpMag`): a multiplier obeys `nz_tech_amp`, a count or a
/// switch (`Multiplier: false`) does not.
/// </summary>
public static class TechStats
{
/// <summary>The product of every owned node's <paramref name="stat"/> multiplier on this prefab. 1 when none.</summary>
public static float Mul( NZPlayer player, string prefab, string stat )
{
if ( !player.IsValid() || string.IsNullOrEmpty( prefab ) ) return 1f;
var f = 1f;
var owned = player.TechOrNull( prefab );
if ( owned is null ) return 1f;
foreach ( var id in owned )
{
var extra = WeaponTech.Find( id )?.Extra;
if ( extra is null ) continue;
foreach ( var m in extra )
if ( m.Name == stat ) f *= TechEffects.AmpMag( m.Value, m.Multiplier );
}
return f;
}
/// <summary>The sum of every owned node's <paramref name="stat"/> addend on this prefab. 0 when none.</summary>
public static float Add( NZPlayer player, string prefab, string stat )
{
if ( !player.IsValid() || string.IsNullOrEmpty( prefab ) ) return 0f;
var sum = 0f;
var owned = player.TechOrNull( prefab );
if ( owned is null ) return 0f;
foreach ( var id in owned )
{
var extra = WeaponTech.Find( id )?.Extra;
if ( extra is null ) continue;
foreach ( var m in extra )
if ( m.Name == stat ) sum += m.Value;
}
return sum;
}
/// <summary>Does any owned node on this prefab switch <paramref name="flag"/> on.</summary>
public static bool Flag( NZPlayer player, string prefab, string flag )
{
if ( !player.IsValid() || string.IsNullOrEmpty( prefab ) ) return false;
var owned = player.TechOrNull( prefab );
if ( owned is null ) return false;
foreach ( var id in owned )
{
var extra = WeaponTech.Find( id )?.Extra;
if ( extra is null ) continue;
foreach ( var m in extra )
if ( m.Name == flag && m.Value != 0f ) return true;
}
return false;
}
/// <summary>A value an owned node SETS (`s.pellets=`), or <paramref name="ifAbsent"/>. The last owned one wins.</summary>
public static float Set( NZPlayer player, string prefab, string stat, float ifAbsent )
{
if ( !player.IsValid() || string.IsNullOrEmpty( prefab ) ) return ifAbsent;
var v = ifAbsent;
var owned = player.TechOrNull( prefab );
if ( owned is null ) return ifAbsent;
foreach ( var id in owned )
{
var extra = WeaponTech.Find( id )?.Extra;
if ( extra is null ) continue;
foreach ( var m in extra )
if ( m.Name == stat ) v = m.Value;
}
return v;
}
// ── the weapon's own reads (read-time sites) ────────────────────────────────────────
/// <summary>
/// The owner and prefab of a weapon, the way `TechEffects.Has` resolves them: from the WEAPON, never from whatever
/// the player is holding, so one slot's tech never leaks onto the other.
/// </summary>
static (NZPlayer player, string prefab) Of( Component weapon )
{
if ( !weapon.IsValid() ) return (null, null);
var player = weapon.Components.Get<NZPlayer>( FindMode.InAncestors | FindMode.Enabled );
var prefab = Rarity.PrefabOf( weapon as SWB.Base.Weapon );
return (player, prefab);
}
public static float Mul( Component weapon, string stat )
{
var (p, prefab) = Of( weapon );
return Mul( p, prefab, stat );
}
public static float Add( Component weapon, string stat )
{
var (p, prefab) = Of( weapon );
return Add( p, prefab, stat );
}
public static bool Flag( Component weapon, string flag )
{
var (p, prefab) = Of( weapon );
return Flag( p, prefab, flag );
}
// ── the victim's side (a relayed hit has no weapon on the host) ─────────────────────
public static float Mul( in TechEffects.TechRef tech, string stat )
=> tech.Valid ? Mul( tech.Player, tech.Prefab, stat ) : 1f;
public static bool Flag( in TechEffects.TechRef tech, string flag )
=> tech.Valid && Flag( tech.Player, tech.Prefab, flag );
}