Partial Weapon class extension that implements the "Marked" mechanic (t3_marked). It tracks a per-weapon streak on a single zombie across trigger pulls, computes a damage multiplier based on a growth factor from TechEffects, and exposes a console command to print the current streak and target for the local weapon.
using Sandbox;
namespace SWB.Base;
/// <summary>
/// MARKED (`t3_marked`) — staying on one zombie makes every shot hit harder.
///
/// ⛔ THE STREAK LIVES ON THE WEAPON, NOT ON THE ZOMBIE, and that is what makes the rule
/// "if I shoot a different zombie it returns to base" cheap to honour. A status on the target
/// would have to be found, refreshed, expired and cleaned up on every corpse; a remembered
/// target and a count is two fields that die with the weapon.
///
/// ⚠️ AND IT IS PER WEAPON RATHER THAN PER PLAYER, because the node is bought per weapon. A
/// player carrying a marked rifle and an unmarked SMG keeps one streak, on the gun that paid
/// for it, and switching weapons cannot launder a stack from one onto the other.
///
/// ⚠️ NO STATIC STATE, SO IT IS MULTIPLAYER-SAFE BY CONSTRUCTION. `HitScan` runs on the
/// shooter, the fields are instance fields on that shooter's weapon, and two players shooting
/// the same zombie keep entirely separate streaks — which is the right answer: the reward is
/// for YOUR aim, not for the zombie having been shot a lot.
/// </summary>
public partial class Weapon
{
/// <summary>The zombie the streak is currently running on.</summary>
GameObject _markedTarget;
/// <summary>How many CONSECUTIVE shots have landed on it. 0 is the first hit.</summary>
int _markedStreak;
/// <summary>The trigger pull the streak last counted, so pellets cannot each advance it.</summary>
int _markedShot = -1;
/// <summary>
/// Trigger pulls, counted once each.
///
/// ⛔ `shotId` COULD NOT DO THIS JOB. It is a fresh Guid PER PELLET — its own comment says
/// so, "matching `shotId`'s scope exactly" beside the per-pellet penetration budget — so
/// counting on it would advance the streak eight times for one shotgun blast and make this
/// node eight times better on shotguns than on everything else. This increments once in
/// `Shoot`, whatever leaves the barrel.
/// </summary>
int _shotSeq;
/// <summary>Called once per trigger pull, before any pellet is traced.</summary>
public void BeginMarkedShot() => _shotSeq++;
/// <summary>
/// The damage multiplier this bullet earns, and the streak bookkeeping that produces it.
///
/// ⚠️ ADDITIVE ON THE BASE, NOT COMPOUNDING. Hit 1 is x1.00, hit 2 x1.10, hit 3 x1.20 —
/// `1 + growth * streak`. Compounding was the other reading of "+10% per shot" and it is a
/// different node entirely: with no cap, 800 rpm held on one zombie reaches x3.4 in one
/// second and x45 in three, where this reaches x2.3 and x4.6.
///
/// ⚠️ ONLY THE FIRST ZOMBIE OF THE TRIGGER PULL MOVES THE STREAK, which is the requested
/// rule and also what makes it survive penetration: a round through five bodies advances
/// the count once, by the body it touched first. Later pellets of the same shot read the
/// answer without changing it, so a shotgun blast is one step whether one pellet lands or
/// all of them.
///
/// ⚠️ A MISS CHANGES NOTHING. Only a zombie resets the streak, so hitting a wall, a barrel
/// or a team-mate between shots leaves it standing — the node rewards staying ON a target
/// rather than never missing.
///
/// ⚠️ THE WHOLE BULLET IS SCALED, not just the body that carries the mark. It is one
/// buffed round; splitting the bonus at the first body would make a penetrating shot weaker
/// the deeper it went, on top of `PenetrationDamageMult` already doing exactly that.
/// </summary>
public float MarkedFactor( GameObject hit )
{
var growth = NZombies.TechEffects.Factor( this, "t3_marked", 0f );
if ( growth <= 0f ) return 1f;
var body = NZombies.ZombieAI.RootOf( hit );
// ⚠️ Not a zombie: no mark, no reset, and the running streak still applies to this
// bullet — it is the same shot that would have hit one.
if ( !body.IsValid() )
return 1f + growth * _markedStreak;
// Already counted this trigger pull — answer, do not advance.
if ( _markedShot != _shotSeq )
{
_markedShot = _shotSeq;
if ( body == _markedTarget )
_markedStreak++;
else
{
_markedTarget = body;
_markedStreak = 0;
}
}
return 1f + growth * _markedStreak;
}
/// <summary>
/// `nz_marked` — what the streak is doing right now, on the weapon in your hands.
///
/// ⚠️ A READOUT RATHER THAN A SETTER. The magnitude belongs to the catalogue
/// (`WeaponTech.Tier3`), which `nz_tech` prints; a second place to set it is the drift this
/// project has already fixed twice.
/// </summary>
[ConCmd( "nz_marked" )]
public static void MarkedCmd()
{
var player = NZombies.NZPlayer.Local;
var wep = player?.Components.Get<Weapon>( FindMode.EverythingInSelfAndDescendants );
if ( !wep.IsValid() )
{
Log.Info( "[nz-marked] nothing in hand" );
return;
}
var growth = NZombies.TechEffects.Factor( wep, "t3_marked", 0f );
if ( growth <= 0f )
{
Log.Info( $"[nz-marked] '{wep.ClassName}' does not have Marked" );
return;
}
Log.Info( $"[nz-marked] +{growth * 100f:0.#}% per consecutive hit, no cap" );
Log.Info( $"[nz-marked] streak {_StreakOf( wep )} -> x{1f + growth * _StreakOf( wep ):0.00}"
+ $" target {(wep._markedTarget.IsValid() ? wep._markedTarget.Name : "none")}" );
}
static int _StreakOf( Weapon w ) => w._markedStreak;
}