Weapons/Reanimator.cs

Reanimator and ReanimatorHuman implement the "Re-Animator" ammo mod: when a zombie is killed it can spawn a temporary human that screams, runs toward a chosen nav point (or to downed players at upgrade V), lures nearby zombies, may deal no damage or burst/damage zombies depending on upgrades, and can revive players (host-side). It handles tuning values, upgrade interactions, network asks/broadcasts, pathfinding, timing, AI lure lists, and diagnostics console commands.

NetworkingFile AccessNative Interop
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

namespace NZombies;

/// <summary>
/// Re-Animator — a zombie you kill may get up as a screaming human and run off, and the zombies around go after it instead
/// of you.
///
/// | | value |
/// |---|---|
/// | proc | **8%** a kill, **30s** cooldown |
/// | human | the VR-11's CIA agent (`models/nz/vr11_human`), where the zombie died |
/// | run | a beat of `react`, then `run_b` at **200 u/s** toward a random spot **~900u** off |
/// | lure | every zombie within **1200u** of it chases it instead of the players |
/// | life | **6s**, then it bursts; **10s** at I Long Lure |
/// | damage | **none**; at III Last Stand the burst deals **1000%** of the killing gun's damage within **250u** |
/// | IV aura | at IV Live Bait, while it lives, every zombie within **200u** of it takes **100%** of the killing gun's damage **each second** |
/// | V rescue | at V Good Samaritan it runs to the nearest **downed** player instead, picks them up within **60u** over the revive's **3s** (its clock holds meanwhile), then the next, or off |
///
/// ⚠️ THE USER (2026-10-04): *"the killed zombie has a chance to turn into this human and it runs away in a random
/// direction, the zombies follow it, it lasts 6 seconds and the cooldown is 30 seconds"*. GMod's own is the TFA VR-11's
/// `nextbot_bo3_vrill_human` (workshop 2898919146): `react`, then `ACT_RUN` (`run_b`) at 200 toward a random nav spot, a
/// scream every 2-4 s, and a burst at the end — whose 160u kill this port leaves out (not asked for; see the doc).
///
/// ⛔ EVERY MACHINE RUNS ITS OWN HUMAN ALONG THE SAME PATH; ONLY THE HOST'S IS CHASED. The host picks the spot and tells
/// everyone where it starts and where it runs to (`NZNet.ReanimatorFx`); each machine builds its own and walks it along its
/// own navmesh path between the two — the same mesh, the same two points, the same route. Nothing is network-spawned and
/// nothing streams a position: the human moves as smoothly on a client as on the host. The host's copy is the lure the AI
/// chases (`ZombieAI.GetTargetables`).
///
/// ⚠️ THE KILLER'S MACHINE ROLLS — the chance and the cooldown are its own (`KillMods`) — and asks the host for the rest
/// (`NZNet.ReanimatorAsk`), which also takes the corpse away: the zombie BECAME the human.
///
/// ⚠️ THE UPGRADES (2026-10-05, `Sbox nzombies/Docs/AMMO_MODS.md` "Upgrades"; II's cooldown is the catalogue's, not here):
/// - I is the HOST'S to read, for the killer it is handed (`Start`), because the host picks the life it broadcasts.
/// - III is the KILLER'S MACHINE'S. The user: *"For III make it so the decoy explodes at the end, dealing 1000% of the
///   weapon's damage"*. The gun can only be read there, so that machine snapshots it at the kill (`ArmLastStand`), and its
///   own copy of the human takes it up when it rises: the one copy whose burst hurts. Below III every burst stays harmless
///   (the user's 2026-10-04 18:35 decision).
///
/// ⚠️ TIERS IV AND V (2026-10-06, `AMMO_MODS.md` "Tiers IV and V"). The user: *"IV: the human damages zombies nearby every second
/// for 100% weapon damage in a 200u radius / V: the human runs towards downed players, and revives them by being near them"*.
/// - IV is the KILLER'S MACHINE'S, as III is: the gun is snapshotted at the kill beside III's burst (`ArmLastStand`), and only that
///   machine's copy of the human hurts (`ReanimatorHuman.TickBait`). Every machine runs a human; the damage is dealt once.
/// - V is the HOST'S to decide, at the rise, and ⛔ IT SAYS SO: `NZNet.ReanimatorFx`'s `samaritan` (2026-10-07, the review). The
///   first build read V off the spot sent, so `PickFlee` had to keep EVERY plain human clear of the downed — a change at levels
///   0-IV, and a human with nowhere clear to run stood still. The spot the host sends is the downed player's own (`Start`), where
///   each machine finds that first patient (`ReanimatorHuman.Spawn`). From there each copy keeps the same rules on the same synced
///   facts — who is down, and where — and the host's copy does the one thing that counts: the revive, through the game's own
///   (`NZPlayer.Revive`).
/// - ⚠️ SO V IS FOR WHOEVER IS DOWN WHEN IT RISES. One risen with nobody down runs off as before and stays a plain human for its
///   whole life (the host sent it as one). One that rose for somebody keeps at it — the next downed player, even one who fell
///   meanwhile — until its time is up.
/// - ⚠️ CO-OP, MOSTLY: a solo player goes down only with a self-revive coming (`NZPlayer.CanBeRevived`, unchanged — the user hasn't
///   decided whether a human should count as a reviver), and then the human may pick them up before Quick Revive does.
/// </summary>
public static class Reanimator
{
	// ══ tuning ═══════════════════════════════════════════════════════════════
	//
	// ⛔ NULLABLE-BACKED GETTERS — INSTRUCTIONS.md §1.

	static float? _life;
	/// <summary>How long the human lasts. 6s — the user's.</summary>
	public static float Life { get => _life ?? 6f; set => _life = value; }

	static float? _speed;
	/// <summary>How fast it runs. 200 u/s — GMod's `SetDesiredSpeed( 200 )`.</summary>
	public static float Speed { get => _speed ?? 200f; set => _speed = value; }

	static float? _flee;
	/// <summary>
	/// How far off the spot it runs for is. 900u — a little more than six seconds can cover, so it never arrives early.
	/// ⚠️ AT I LONG LURE'S 10s IT ARRIVES WITH ABOUT 4s LEFT (2026-10-05) and stands in `react`, still the lure (`LongLure`).
	/// </summary>
	public static float Flee { get => _flee ?? 900f; set => _flee = value; }

	static float? _lure;
	/// <summary>How far away a zombie may be and still be drawn to it. 1200u.</summary>
	public static float LureRadius { get => _lure ?? 1200f; set => _lure = value; }

	static float? _react;
	/// <summary>How long it stands in `react` before it runs. 0.8s.</summary>
	public static float ReactSeconds { get => _react ?? 0.8f; set => _react = value; }

	static float? _runRate;
	/// <summary>The run clip's playback rate. 1 — the clip's own speed.</summary>
	public static float RunRate { get => _runRate ?? 1f; set => _runRate = value; }

	// ══ upgrades (2026-10-05) ════════════════════════════════════════════════
	//
	// ⚠️ EACH UPGRADED NUMBER IS ITS OWN TUNABLE BESIDE THE BASE: I through `LifeFor`, III through `ArmLastStand` (§3).

	static float? _longLure;
	/// <summary>
	/// I LONG LURE: how long the human lasts. 10s (`Life`, 6s).
	///
	/// ⚠️ THE RUN IS NOT LENGTHENED: its spot is still `Flee` off, so it gets there with seconds to spare and stands in `react`
	/// for the rest (`Step`), still the lure.
	/// </summary>
	public static float LongLure { get => _longLure ?? 10f; set => _longLure = value; }

	static float? _lastStandScale;
	/// <summary>III LAST STAND: the burst, as a multiple of the killing gun's damage (`AmmoMods.WeaponDamage`). 10 — 1000%.</summary>
	public static float LastStandScale { get => _lastStandScale ?? 10f; set => _lastStandScale = value; }

	static float? _lastStandRadius;
	/// <summary>III LAST STAND: the burst's reach. 250u, the pack chasing it (GMod's own burst reached 160u).</summary>
	public static float LastStandRadius { get => _lastStandRadius ?? 250f; set => _lastStandRadius = value; }

	// ── tiers IV and V (2026-10-06, `AMMO_MODS.md` "Tiers IV and V") ──
	//
	// ⚠️ EACH ITS OWN TUNABLE (§3), NULLABLE-BACKED (§1): IV through `LiveBaitFor`. V's revive time is the game's own
	// (`ReviveAugments.ReviveSeconds`, 3s, with no Quick Revive speed-up), not a number of this file's.

	static float? _liveBaitScale;
	/// <summary>IV LIVE BAIT: what each zombie near the human takes a tick, as a multiple of the killing gun's damage. 1 — 100%.</summary>
	public static float LiveBaitScale { get => _liveBaitScale ?? 1f; set => _liveBaitScale = value; }

	static float? _liveBaitRadius;
	/// <summary>IV LIVE BAIT: how near the human a zombie must be to take it. 200u — the user's.</summary>
	public static float LiveBaitRadius { get => _liveBaitRadius ?? 200f; set => _liveBaitRadius = value; }

	static float? _liveBaitEvery;
	/// <summary>IV LIVE BAIT: how often it hurts. 1s — the user's "every second": ten ticks in I's 10s.</summary>
	public static float LiveBaitEvery { get => _liveBaitEvery ?? 1f; set => _liveBaitEvery = value; }

	static float? _samaritanReach;
	/// <summary>V GOOD SAMARITAN: how near a downed player the human must be to pick them up. 60u (a player's is 90u, `ReviveAugments.ReviveReach`).</summary>
	public static float SamaritanReach { get => _samaritanReach ?? 60f; set => _samaritanReach = value; }

	/// <summary>
	/// V: how near the spot the host sent a downed player must lie for a Good Samaritan's human to take them as its first patient
	/// (`ReanimatorHuman.Spawn`). 96u: the host sends the patient's own navmesh point, and only one within half of this (`Start`),
	/// so a crawl still in flight to another machine cannot carry them out of it. Half of it is also how far off the navmesh a
	/// patient may lie and still be walked to (`NearestPatient`).
	///
	/// ⛔ A CONST, NOT A TUNABLE: every machine must give the same answer, and a console value is one machine's (§1's own rule — what
	/// must not drift is a `const`).
	/// </summary>
	internal const float SamaritanMatch = 96f;

	public const string Model = "models/nz/vr11_human/vr11_human.vmdl";

	// ══ the effect ═══════════════════════════════════════════════════════════

	/// <summary>
	/// The mod went off on a kill. THE KILLER'S MACHINE (`KillMods`): done here on the host, asked of it otherwise.
	/// </summary>
	/// <param name="killer">Whose mod it is, for its upgrades (2026-10-05). Null reads as level 0.</param>
	public static void Fire( GameObject victim, Vector3 at, NZPlayer killer = null )
	{
		// ⚠️ III IS ARMED FIRST, AND HERE: the gun can only be read on this machine, and the human rises from the broadcast. IV's
		// tick with it (2026-10-06).
		ArmLastStand( killer, at );

		if ( Networking.IsActive && !NZGame.IsHost )
		{
			// ⚠️ NO LEVEL TRAVELS WITH THE ASK: the host reads I off its caller's synced levels (§36).
			NZNet.ReanimatorAsk( at, victim.IsValid() ? victim.Id : Guid.Empty );
			return;
		}

		Start( at, victim, killer );
	}

	/// <summary>
	/// Raise the human where the zombie died, and send it running. THE HOST, or solo.
	/// </summary>
	/// <param name="killer">
	/// Whose Re-Animator it is: the host's own player, or the client that asked (`NZNet.ReanimatorAsk`'s caller). Its level I
	/// sets the life every machine is told, and its level V whether the human heads for the downed (2026-10-06). Null reads as
	/// level 0.
	/// </param>
	public static void Start( Vector3 at, GameObject corpse, NZPlayer killer = null )
	{
		var scene = Game.ActiveScene;
		if ( !scene.IsValid() ) return;

		var from = ZombieAI.NavGround( scene, at );

		// ⚠️ V GOOD SAMARITAN (2026-10-06): the nearest downed player it can reach, in place of a random spot — and the broadcast says
		// it is V (`samaritan`, 2026-10-07). Nobody down, or nobody it can reach: off as before, a plain human.
		var patient = AmmoModUpgrades.Has( killer, "reanimator", 5 ) ? NearestPatient( scene, from, from ) : null;
		var spot = patient.IsValid() ? ZombieAI.NavGround( scene, patient.WorldPosition ) : from;

		// ⛔ ONLY A SPOT EVERY MACHINE WILL FIND THE PATIENT AT: within half of `SamaritanMatch` of them — a margin no crawl in flight
		// can eat — and somewhere it actually runs to. A body further off the mesh (on a crate), or lying where the zombie fell, could
		// be found by some machine and not another; it runs off as a plain human instead.
		if ( patient.IsValid() && (spot.Distance( patient.WorldPosition ) > SamaritanMatch * 0.5f || spot.Distance( from ) <= 1f) )
			patient = null;

		var to = patient.IsValid() ? spot : PickFlee( scene, from );

		// ⚠️ THE ZOMBIE BECAME THE HUMAN, so its body goes — the host's, which every machine's copy follows.
		if ( corpse.IsValid() && corpse.Components.Get<ZombieAI>( FindMode.EverythingInSelfAndAncestors ) is { } ai )
			SWB.Shared.GameObjectExtensions.DestroyAsync( ai.GameObject, 0.05f );

		var life = MathF.Max( 1f, LifeFor( killer ) );

		// ⚠️ EVERYBODY BUILDS ONE, THIS MACHINE INCLUDED — a broadcast runs here too, and solo it simply runs. V said outright.
		NZNet.ReanimatorFx( from, to, life, patient.IsValid() );

		Log.Info( $"[nz-ammo] RE-ANIMATOR — a human at {from} runs for {to} ({from.Distance( to ):0}u) · lures {LureRadius:0}u for {life:0.#}s"
			+ (patient.IsValid() ? $" · V GOOD SAMARITAN, for {patient.GameObject.Name}, who is down" : "") );
	}

	/// <summary>How long this killer's human lasts: I's, or the base. THE HOST, which broadcasts it to everyone.</summary>
	static float LifeFor( NZPlayer killer ) => AmmoModUpgrades.Has( killer, "reanimator", 1 ) ? LongLure : Life;

	/// <summary>
	/// Where to run: a random direction, `Flee` away, onto the navmesh, with a complete path to it. Up to eight tries, then
	/// the reachable point the first one got closest to; with nothing at all, where it stands.
	///
	/// ⚠️ UNCHANGED BY V (2026-10-07): the first build kept every plain human clear of the downed, because V was read off the spot;
	/// V is sent outright now (`NZNet.ReanimatorFx`'s `samaritan`), so this is the 2026-10-04 picker again.
	/// </summary>
	static Vector3 PickFlee( Scene scene, Vector3 from )
	{
		var nav = scene.NavMesh;
		if ( nav is null ) return from;

		Vector3? partial = null;

		for ( var i = 0; i < 8; i++ )
		{
			var yaw = Game.Random.Float( 0f, 360f );
			var aim = from + Rotation.FromYaw( yaw ).Forward * MathF.Max( 64f, Flee );

			var spot = nav.GetRandomPoint( aim, 200f ) ?? nav.GetClosestPoint( aim );
			if ( spot is not Vector3 to ) continue;

			var path = nav.CalculatePath( new Sandbox.Navigation.CalculatePathRequest { Start = from, Target = to } );
			if ( !path.IsValid || path.Points.Count < 2 ) continue;

			if ( path.Status == Sandbox.Navigation.NavMeshPathStatus.Complete ) return to;

			partial ??= path.Points[path.Points.Count - 1].Position;
		}

		return partial ?? from;
	}

	// ══ V GOOD SAMARITAN (2026-10-06) ════════════════════════════════════════
	//
	// ⛔ EVERY MACHINE ASKS THESE OF ITS OWN COPY, ON THE SAME SYNCED FACTS (`NZPlayer.IsDown`, mirrored from each owner, and the
	// bodies' positions), so the copies choose alike without a message. The host's choice of the FIRST patient is the one that is
	// sent (`Start`); after that each copy asks for itself, from where it stands — the same spot on every machine, near enough.

	/// <summary>
	/// V: a player the human may pick up — down, not out of the round, alive. `ReviveAugments.TargetFor`'s patient test, less what
	/// is a rescuer's own (the reach, the one just picked up): the human keeps its own of those.
	/// </summary>
	internal static bool Revivable( NZPlayer p )
		=> p.IsValid() && p.IsDown && !p.IsOutOfRound && p.Hp.IsValid() && !p.Hp.IsDead;

	/// <summary>
	/// V: the downed player nearest <paramref name="near"/>, within <paramref name="within"/> of it, that the human can walk to
	/// from <paramref name="here"/> (a whole navmesh path), and not <paramref name="except"/>. Null for nobody.
	///
	/// ⚠️ A PLAYER IT CANNOT WALK TO IS SKIPPED, NOT CHASED (one on a ledge): a partial path would park the human at its end for the
	/// rest of its life. The same mesh everywhere gives every machine the same verdict.
	/// </summary>
	internal static NZPlayer NearestPatient( Scene scene, Vector3 here, Vector3 near, float within = float.MaxValue,
		NZPlayer except = null )
	{
		var nav = scene?.NavMesh;
		if ( nav is null ) return null;

		NZPlayer best = null;
		var bestDist = float.MaxValue;

		foreach ( var p in scene.GetAllComponents<NZPlayer>() )
		{
			if ( !Revivable( p ) || p == except ) continue;

			var dist = near.Distance( p.WorldPosition );
			if ( dist > within || dist >= bestDist ) continue;

			// ⚠️ THE PATH ONLY FOR ONE THAT WOULD WIN, and none at all for one already in reach.
			if ( here.Distance( p.WorldPosition ) > MathF.Max( 0f, SamaritanReach ) )
			{
				var spot = ZombieAI.NavGround( scene, p.WorldPosition );

				// ⛔ NOT ONE LYING OFF THE MESH (2026-10-07, the review): on a crate or a car, the walkable point under them can be far
				// from the body, and the human would stop at its path's end out of reach and stand there the rest of its life. Every
				// leg asks this, not only the first (`Start`'s own test is the same margin).
				if ( spot.Distance( p.WorldPosition ) > SamaritanMatch * 0.5f ) continue;

				var path = nav.CalculatePath( new Sandbox.Navigation.CalculatePathRequest { Start = here, Target = spot } );
				if ( !path.IsValid || path.Status != Sandbox.Navigation.NavMeshPathStatus.Complete ) continue;
			}

			best = p;
			bestDist = dist;
		}

		return best;
	}

	// ══ III LAST STAND (2026-10-05) AND IV LIVE BAIT (2026-10-06) ═══════════════
	//
	// ⛔ THE DAMAGE IS DEALT ONCE, BY THE KILLER'S MACHINE, AND NOTHING NEW CROSSES THE WIRE FOR IT. Every machine runs its own
	// human, and only the killer's can read the gun. So that machine arms the burst at the kill, and its own copy, when the
	// broadcast raises it there, takes it up by where it rises: the host raises it at `ZombieAI.NavGround` of the very point
	// this machine sends, which is where this machine armed it. Every other copy finds nothing and bursts harmlessly.
	//
	// ⚠️ IV RIDES ON THE SAME ARMING (2026-10-06): Live Bait's tick is a share of the same gun, read at the same kill, for the same
	// human — `Bait` beside `Damage` — and the same copy deals it (`ReanimatorHuman.TickBait`), so it too is dealt once. The host's
	// copy is the lure the zombies bunch round; the killer's walks the same path, so it stands among the same zombies.

	/// <summary>
	/// One kill's arming on THIS machine, waiting for its human: where it will rise, when, III's burst and IV's tick (each 0 below
	/// its level), whose kill, and the gun.
	/// </summary>
	internal sealed record LastStand( Vector3 From, float Armed, float Damage, NZPlayer Killer, GameObject Weapon, float Bait );

	/// <summary>
	/// This machine's armed Last Stands. ⛔ A PROPERTY OVER A LAZY FIELD, NOT A `static readonly` INITIALISER (§1, the
	/// `BulletDecals.Holes` shape): runtime state, emptied as each one is taken up or goes stale.
	/// </summary>
	static List<LastStand> _lastStands;
	static List<LastStand> LastStands => _lastStands ??= new();

	/// <summary>How long an armed Last Stand waits for its human: a client's ask goes to the host and back. Room to spare.</summary>
	const float LastStandWait = 3f;

	/// <summary>How near the point it was armed for a human must rise to take it. The same point either side, so normally 0.</summary>
	const float LastStandMatch = 128f;

	/// <summary>
	/// III and IV: snapshot the killing gun's damage for the human about to rise, as Radioactive Decay snapshots its dose. THE
	/// KILLER'S MACHINE, the only one that can read the gun (`AmmoMods.WeaponDamage`). Below III nothing is armed (IV has III).
	/// </summary>
	static void ArmLastStand( NZPlayer killer, Vector3 at )
	{
		if ( !AmmoModUpgrades.Has( killer, "reanimator", 3 ) ) return;

		// ⚠️ ONE READ OF THE GUN FOR BOTH (2026-10-06): III's burst and IV's tick are shares of the same snapshot.
		var gun = AmmoMods.WeaponDamage( killer );
		var damage = gun * MathF.Max( 0f, LastStandScale );
		var bait = LiveBaitFor( killer, gun );
		var scene = Game.ActiveScene;
		if ( (damage <= 0f && bait <= 0f) || !scene.IsValid() ) return;

		PruneLastStands();
		LastStands.Add( new LastStand( ZombieAI.NavGround( scene, at ), Time.Now, damage, killer,
			VultureAugments.HeldWeapon( killer )?.GameObject, bait ) );
	}

	/// <summary>
	/// IV LIVE BAIT: what each tick deals for this killer, out of the gun's damage <paramref name="gun"/> — 0 below IV, where the
	/// human hurts nobody. THE KILLER'S MACHINE (`ArmLastStand`); the report asks it too, for you.
	/// </summary>
	static float LiveBaitFor( NZPlayer killer, float gun )
		=> AmmoModUpgrades.Has( killer, "reanimator", 4 ) ? gun * MathF.Max( 0f, LiveBaitScale ) : 0f;

	/// <summary>
	/// The Last Stand this machine armed for a human rising at <paramref name="from"/>, taken off the list; null for every other
	/// copy and every human below III. `ReanimatorHuman.Spawn`.
	/// </summary>
	internal static LastStand ClaimLastStand( Vector3 from )
	{
		if ( _lastStands is null || _lastStands.Count == 0 ) return null;

		PruneLastStands();

		var stand = _lastStands
			.Where( s => s.From.Distance( from ) <= LastStandMatch )
			.OrderBy( s => s.From.Distance( from ) )
			.FirstOrDefault();

		if ( stand is not null ) _lastStands.Remove( stand );
		return stand;
	}

	/// <summary>Drop what no human rose for in time, and anything stamped by an earlier scene's clock (`Time.Now` restarts at 0).</summary>
	static void PruneLastStands()
		=> _lastStands?.RemoveAll( s => s.Armed > Time.Now || Time.Now - s.Armed > LastStandWait );

	/// <summary>
	/// III: the burst, for real, on the killer's machine, as the kill mods' splashes are dealt (`TechBlast.ModBlast`: zombies
	/// only, line of sight, a blast and not a bullet, so what it kills sets off no mod). Its explosion is announced at its reach
	/// (`BlastEffect`), so every machine sees how far it went.
	/// </summary>
	internal static void LastStandBurst( LastStand stand, Vector3 at )
	{
		// ⚠️ AND NOTHING FOR AN ARMING WITH NO BURST IN IT (2026-10-06): IV's tick alone can arm one, with III tuned to 0.
		if ( stand is null || stand.Damage <= 0f || !stand.Killer.IsValid() ) return;

		var weapon = stand.Weapon.IsValid() ? stand.Weapon : VultureAugments.HeldWeapon( stand.Killer )?.GameObject;
		var hitAt = new List<Vector3>();

		// ⚠️ ITS HITS ROLL NO MOD (2026-10-05): a human's whole life after the kill (III owns I's 10s), the hand may hold a hit-mod
		// gun off its cooldown, and a mod's splash must not set one off (`AmmoMods.WithoutProcs`).
		AmmoMods.WithoutProcs( stand.Killer, () =>
			TechBlast.ModBlast( at, stand.Damage, MathF.Max( 0f, LastStandRadius ), stand.Killer.GameObject, weapon, hitAt ) );

		Log.Info( $"[nz-ammo] RE-ANIMATOR LAST STAND — {stand.Damage:0} to {hitAt.Count} zombie(s) within {LastStandRadius:0}u" );
	}

	// ══ diagnostics ══════════════════════════════════════════════════════════

	/// <summary>`nz_reanimator` — the resolved numbers, and whether the model is there.</summary>
	[ConCmd( "nz_reanimator" )]
	public static void Report()
	{
		var mod = AmmoMods.Find( "reanimator" );
		var model = Sandbox.Model.Load( Model );

		// ⚠️ YOUR COOLDOWN, II's in it (2026-10-05, `AmmoMods.BaseCooldown`): the catalogue's at level 0.
		Log.Info( $"[nz-ammo] RE-ANIMATOR · {(mod?.Chance ?? 0f) * 100f:0.#}% a kill,"
			+ $" {AmmoMods.BaseCooldown( NZPlayer.Local, mod ):0.#}s cooldown"
			+ $" · {Life:0.#}s · runs {Speed:0} u/s for a spot {Flee:0}u off · lures {LureRadius:0}u"
			+ $" · model {(model is null || model.IsError ? "MISSING (has the editor compiled it?)" : "ok")}"
			+ $" · {ReanimatorHuman.Lures.Count} human(s) luring on this machine" );

		Log.Info( $"[nz-ammo]   upgrades: I lasts {LongLure:0.#}s · II {AmmoMods.QuickReturnCooldown:0.#}s cooldown"
			+ $" · III bursts for {LastStandScale * 100f:0}% of the gun within"
			+ $" {LastStandRadius:0}u · you: level {AmmoModUpgrades.Level( NZPlayer.Local, "reanimator" )}"
			+ $" · {_lastStands?.Count ?? 0} Last Stand(s) armed here" );

		// ⚠️ IV AND V (2026-10-06). Your tick is `LiveBaitFor` on the gun in your hand NOW; the real one is snapshotted at the kill.
		var me = NZPlayer.Local;
		var humans = Game.ActiveScene?.GetAllComponents<ReanimatorHuman>().Where( h => h.IsValid() ).ToList()
			?? new List<ReanimatorHuman>();

		Log.Info( $"[nz-ammo]   IV hurts {LiveBaitScale * 100f:0}% of the gun within {LiveBaitRadius:0}u every {LiveBaitEvery:0.#}s"
			+ $" ({LiveBaitFor( me, AmmoMods.WeaponDamage( me ) ):0} a tick for you now)"
			+ $" · V heads for the downed and picks them up within {SamaritanReach:0}u in {ReviveAugments.ReviveSeconds:0.#}s"
			+ $" · {humans.Count} human(s) on this machine, {humans.Count( h => h.IsSamaritan )} of them Good Samaritans" );
	}

	/// <summary>
	/// `nz_reanimator_test [down]` — raise a human in front of you, no kill needed. Your upgrades count, as on a kill.
	///
	/// ⚠️ V NEEDS SOMEBODY DOWN WHEN IT RISES (2026-10-06). Solo that can only be you, with Quick Revive (`nz_quickrevive 1`) —
	/// whose self-revive comes 5s after the fall (`NZPlayer.SelfReviveTime`), too soon to type a second command. So `down` puts you
	/// down first and raises the human in the same breath: 200u off, it has you up in about 4.5s, just ahead of the perk. With
	/// `nz_ammomod_level reanimator 5`; solo or on the host (a client's own fall can reach the host after its ask does).
	/// </summary>
	[ConCmd( "nz_reanimator_test" )]
	public static void TestCmd( string mode = "" )
	{
		var p = NZPlayer.Local;
		if ( !p.IsValid() ) return;

		// ⚠️ DOWN THROUGH THE REVIVE TEST'S OWN COMMAND (`nz_revive_down`). It needs somebody who could revive you — Quick Revive
		// solo, or a teammate up (`NZPlayer.CanBeRevived`, unchanged) — or that hit KILLS, and the human has nobody to come for.
		if ( mode.Equals( "down", StringComparison.OrdinalIgnoreCase ) && !p.IsDown ) ReviveAugments.DownCmd();

		var at = p.WorldPosition + p.EyeAngles.Forward.WithZ( 0f ).Normal * 200f;

		// ⚠️ THROUGH `Fire`, the kill's own path (2026-10-05): the host raises it or is asked to, and III and IV are armed at your
		// level — V's patient is the host's to find, as on a kill.
		Fire( null, at, p );
	}

	/// <summary>`nz_reanimator_set &lt;key&gt; &lt;value&gt;` — retune one number live.</summary>
	[ConCmd( "nz_reanimator_set" )]
	public static void SetCmd( string key = "", float value = 0f )
	{
		switch ( key.ToLowerInvariant() )
		{
			case "life": Life = value; break;
			case "speed": Speed = value; break;
			case "flee": Flee = value; break;
			case "lure": LureRadius = value; break;
			case "react": ReactSeconds = value; break;
			case "runrate": RunRate = value; break;
			case "longlure": LongLure = value; break;
			case "laststand": LastStandScale = value; break;
			case "laststandradius": LastStandRadius = value; break;
			case "livebait": LiveBaitScale = value; break;
			case "livebaitradius": LiveBaitRadius = value; break;
			case "livebaitevery": LiveBaitEvery = value; break;
			case "samaritanreach": SamaritanReach = value; break;

			default:
				Log.Info( "[nz-ammo] nz_reanimator_set <life|speed|flee|lure|react|runrate|longlure|laststand|laststandradius"
					+ "|livebait|livebaitradius|livebaitevery|samaritanreach> <value>" );
				return;
		}

		Report();
	}
}

/// <summary>
/// One re-animated human, on THIS machine: the VR-11's agent, standing in `react`, then running `run_b` along the navmesh
/// to the spot the host picked, screaming, until it bursts. The host's copy is also a lure (<see cref="Lures"/>).
///
/// ⚠️ AT V A GOOD SAMARITAN'S (2026-10-06) runs to the downed instead, stands over each in `react` for the revive's length — its
/// clock held — and runs off when nobody is left (`TickSamaritan`). Only the host's copy stands anybody up.
/// </summary>
public sealed class ReanimatorHuman : Component
{
	/// <summary>The humans the zombies may chase — the HOST'S copies only.</summary>
	public static readonly List<ReanimatorHuman> Lures = new();

	public bool Lure { get; set; }
	public float Life { get; set; } = 6f;

	/// <summary>The spot the host picked: a random one, or at V the downed player's own (`Reanimator.Start`).</summary>
	Vector3 _to;
	List<Vector3> _path = new();
	int _leg;
	float _born;
	bool _running;
	SkinnedModelRenderer _r;
	TimeUntil _nextScream;
	TimeUntil _nextPull;

	/// <summary>III and IV: the arming this copy carries to its burst. Set on the killer's machine only; null on every other.</summary>
	Reanimator.LastStand _stand;

	/// <summary>IV: Live Bait's ticks paid, counted from the rise, and the hits they made (the burst's log line).</summary>
	int _baitTicks, _baitHits;

	// ── V GOOD SAMARITAN (2026-10-06) ──
	//
	// ⚠️ PLAIN FIELDS ON A COMPONENT: one built before a hotload gets their defaults (a plain human, nobody to go to) and carries on.

	/// <summary>Where it rose — with `_to`, the heading it runs off along once nobody is left to pick up (`OffSpot`).</summary>
	Vector3 _from;

	/// <summary>V: read at the rise (`Spawn`), the same way on every machine. Never set afterwards.</summary>
	bool _samaritan;

	/// <summary>The downed player it is going to, or picking up. Null while it runs off.</summary>
	NZPlayer _patient;

	/// <summary>How long it has been picking `_patient` up, by this copy's own clock. 0 when it isn't.</summary>
	float _reviving;

	/// <summary>How long pick-ups have held its clock, all of them together (`HoldCap` at most).</summary>
	float _held;

	/// <summary>The last one it picked up, left alone for a moment (`ReviveAugments.JustRevivedGrace`), and since when.</summary>
	NZPlayer _justRevived;
	TimeSince _sinceRevived;

	/// <summary>Where its path was last asked to end, and when it may ask again: a patient crawls.</summary>
	Vector3 _pathFor;
	TimeUntil _nextRepath;

	/// <summary>When it next looks for somebody down while it runs off.</summary>
	TimeUntil _nextLook;

	/// <summary>Where it runs off to once nobody is left to pick up. Picked once (`OffSpot`).</summary>
	Vector3? _off;

	/// <summary>
	/// V: the most pick-ups can hold its clock, all together. 30s — ten revives, more than any crew has downed at once. A bound, so
	/// no copy can outlive its life by more, and the safety destroy (`Spawn`) knows how long to wait.
	/// </summary>
	const float HoldCap = 30f;

	/// <summary>V: how often a copy running off looks for somebody newly down. Every machine looks at about the same moment.</summary>
	const float LookEvery = 0.25f;

	/// <summary>V: how often the path to a crawling patient may be asked again, and how far they must have crawled off its end.</summary>
	const float RepathEvery = 0.5f;
	const float RepathMove = 32f;

	/// <summary>V: is this a Good Samaritan's human — `nz_reanimator` counts them.</summary>
	public bool IsSamaritan => _samaritan;

	/// <summary>Build this machine's human at <paramref name="from"/>, running for <paramref name="to"/>. `NZNet.ReanimatorFx`.</summary>
	/// <param name="samaritan">V Good Samaritan's, as the host decided at the rise (2026-10-07).</param>
	public static void Spawn( Vector3 from, Vector3 to, float life, bool lure, bool samaritan = false )
	{
		var scene = Game.ActiveScene;
		if ( !scene.IsValid() ) return;

		var model = Sandbox.Model.Load( Reanimator.Model );
		if ( model is null || model.IsError )
		{
			Log.Warning( $"[nz-ammo] re-animator: '{Reanimator.Model}' is missing or not compiled — no human" );
			return;
		}

		var go = scene.CreateObject();
		go.Name = "nz_reanimated_human";
		go.Flags |= GameObjectFlags.NotSaved;
		go.NetworkMode = NetworkMode.Never;   // ⛔ THIS MACHINE'S OWN — every machine builds one (§39)
		go.WorldPosition = from;
		go.WorldRotation = Rotation.LookAt( (to - from).WithZ( 0f ).IsNearlyZero() ? Vector3.Forward : (to - from).WithZ( 0f ) );

		var r = go.Components.Create<SkinnedModelRenderer>();
		r.Model = model;

		// ⚠️ NO ANIMGRAPH: the clips are played by name, and a graph would own the skeleton (INSTRUCTIONS, Step 4b's table).
		r.UseAnimGraph = false;
		r.Sequence.Name = "react";

		var h = go.Components.Create<ReanimatorHuman>();
		h.Lure = lure;
		h.Life = life;
		h._to = to;
		h._from = from;
		h._r = r;

		// ⚠️ III (2026-10-05): the Last Stand this machine armed for a human rising here, if this is the killer's machine.
		h._stand = Reanimator.ClaimLastStand( from );

		// ⛔ V IS SAID OUTRIGHT (2026-10-07, the review): the host sends `samaritan`, so no plain human is ever taken for one and
		// `Reanimator.PickFlee` no longer has to keep plain humans clear of the downed. Its first patient is whoever lies where the
		// host sent it, the one the host chose; found by nobody here (a crawl carried them off), the first leg looks again (`NextLeg`).
		h._samaritan = samaritan;
		h._patient = samaritan && from.Distance( to ) > 1f
			? Reanimator.NearestPatient( scene, from, to, Reanimator.SamaritanMatch )
			: null;

		// ⚠️ THE SAFETY NET WAITS OUT A HELD CLOCK TOO: a Good Samaritan's can stand still for `HoldCap` more.
		SWB.Shared.GameObjectExtensions.DestroyAsync( go, life + (h._samaritan ? HoldCap : 0f) + 0.5f );
	}

	protected override void OnStart()
	{
		_born = Time.Now;
		_nextScream = 0.2f;

		if ( Lure )
		{
			Lures.Add( this );

			// ⚠️ THE ONES IN REACH TURN NOW, not on their next acquire 3-15 seconds away (`BananaStand.PullNearby`).
			PullNearby();
		}

		// ⚠️ THE VR-11'S HIT, AS THE ZOMBIE TURNS.
		Sound.Play( "nz.vr11.impact", WorldPosition + Vector3.Up * 40f );
	}

	protected override void OnDestroy()
	{
		// ⚠️ V: GONE IN THE MIDDLE OF A PICK-UP (its hold's cap, a new game) — the patient's bar goes with it, not on to nothing.
		LetGo();

		if ( !Lures.Remove( this ) ) return;

		// ⚠️ AND WHEN IT GOES, EVERY ZOMBIE LOOKS AGAIN — the ones that were after it would otherwise walk on to where it was.
		ZombieAI.ForceRetargetAll();
	}

	protected override void OnUpdate()
	{
		// ⚠️ V: ITS CLOCK IS THE TIME SINCE IT ROSE LESS WHAT PICK-UPS HELD (2026-10-06) — a revive stops it, and it stays till done.
		var age = Time.Now - _born - _held;

		// ⚠️ IV BEFORE THE END, so a tick due on the burst's own frame is paid (ten in I's 10s).
		TickBait();

		if ( age >= Life )
		{
			Burst();
			return;
		}

		// ── the beat of `react`, then the run ──
		if ( !_running && age >= MathF.Max( 0f, Reanimator.ReactSeconds ) )
		{
			_running = true;

			// ⚠️ V: TO ITS PATIENT, wherever they have crawled since it rose — or, if somebody got to them first, the next, or off.
			if ( !_samaritan ) Go( _to );
			else if ( Reanimator.Revivable( _patient ) ) Go( PatientSpot( _patient ) );
			else Go( NextLeg() );

			Sound.Play( "nz.vr11.timer", WorldPosition + Vector3.Up * 40f );
		}

		// ⚠️ V: PICKING SOMEBODY UP HOLDS IT WHERE IT STANDS.
		var holding = _running && _samaritan && TickSamaritan();

		if ( _running && !holding ) Step();

		// ⚠️ A SCREAM EVERY TWO TO FOUR SECONDS, GMod's own cadence (`math.Rand( 2, 4 )`).
		if ( _nextScream )
		{
			_nextScream = Game.Random.Float( 2f, 4f );
			Sound.Play( "nz.vr11.scream", WorldPosition + Vector3.Up * 60f );
		}

		// ⚠️ THE HOST'S COPY KEEPS PULLING: zombies that come into reach mid-run turn as they do (the chase tick's own check
		// covers most; this covers the ones whose retarget is still seconds off). GMod's MonkeyBomb ran each scream.
		if ( Lure && _nextPull )
		{
			_nextPull = 0.5f;
			PullNearby();
		}
	}

	/// <summary>One frame of the run along the path: `Speed`, facing the way it goes. At the end of the path it stands in `react`.</summary>
	void Step()
	{
		if ( _leg >= _path.Count )
		{
			if ( _r.IsValid() && _r.Sequence.Name != "react" )
			{
				_r.Sequence.Name = "react";
				_r.PlaybackRate = 1f;
			}

			return;
		}

		var here = WorldPosition;
		var next = _path[_leg];
		var step = MathF.Max( 0f, Reanimator.Speed ) * Time.Delta;
		var to = next - here;
		var flat = to.WithZ( 0f );

		if ( to.Length <= step )
		{
			WorldPosition = next;
			_leg++;
		}
		else
		{
			WorldPosition = here + to.Normal * step;
		}

		Face( flat );
	}

	/// <summary>Turn toward <paramref name="flat"/>, a heading along the ground: the run's, or (V) the patient's.</summary>
	void Face( Vector3 flat )
	{
		if ( flat.Length > 1f )
			WorldRotation = Rotation.Slerp( WorldRotation, Rotation.LookAt( flat.Normal ), MathF.Min( 1f, Time.Delta * 12f ) );
	}

	/// <summary>
	/// Run for <paramref name="target"/> along the navmesh, from here, in `run_b`: a plain human's one run, and each of a Good
	/// Samaritan's (2026-10-06). ⚠️ A RUN ALREADY UNDER WAY KEEPS ITS CLIP, so a path asked again does not restart the stride.
	/// </summary>
	void Go( Vector3 target )
	{
		_path = PathTo( WorldPosition, target );
		_leg = 0;
		_pathFor = target;
		_nextRepath = RepathEvery;

		if ( _r.IsValid() && _r.Sequence.Name != "run_b" )
		{
			_r.Sequence.Name = "run_b";
			_r.PlaybackRate = MathF.Max( 0.05f, Reanimator.RunRate );
		}
	}

	/// <summary>The navmesh route, as points to walk. A straight line when there is no mesh or no route.</summary>
	static List<Vector3> PathTo( Vector3 from, Vector3 to )
	{
		var nav = Game.ActiveScene?.NavMesh;
		if ( nav is not null )
		{
			var path = nav.CalculatePath( new Sandbox.Navigation.CalculatePathRequest { Start = from, Target = to } );
			if ( path.IsValid && path.Points.Count > 1 )
				return path.Points.Skip( 1 ).Select( p => p.Position ).ToList();
		}

		return new List<Vector3> { to };
	}

	/// <summary>Turn every zombie in reach that is not after it yet. HOST.</summary>
	void PullNearby()
	{
		var reach = MathF.Max( 0f, Reanimator.LureRadius );
		var at = WorldPosition;

		foreach ( var z in ZombieAI.All.ToList() )
		{
			if ( !z.IsValid() || z.State == ZombieState.Dead ) continue;
			if ( z.Target == GameObject ) continue;
			if ( at.Distance( z.WorldPosition ) > reach ) continue;

			z.ForceRetarget();
		}
	}

	// ══ V GOOD SAMARITAN (2026-10-06) ════════════════════════════════════════

	/// <summary>
	/// V, one frame: after its patient, picking them up once in reach, then the next — or off. True while it is picking somebody
	/// up: it stands, and its clock holds (`_held`).
	///
	/// ⛔ EVERY COPY RUNS THIS ALIKE, ON THE SAME SYNCED FACTS, SO THEY GO THE SAME WAY — AND ONLY THE HOST'S COPY STANDS ANYBODY UP,
	/// through `NZPlayer.Revive`: the funnel every revive takes, a teammate's included (`ReviveAugments.Complete`), which relays to
	/// the patient's own machine. The others run their own pick-up clock alongside and go on when it ends, as the host's does.
	///
	/// ⚠️ THE REVIVE'S OWN LENGTH, `ReviveAugments.ReviveSeconds` (3s): a player's bare speed, as the human holds no Quick Revive.
	/// </summary>
	bool TickSamaritan()
	{
		// ⚠️ GONE — stood up by somebody else, bled out, or left the game: let go, and the next.
		if ( _patient is not null && !Reanimator.Revivable( _patient ) )
		{
			LetGo();
			Go( NextLeg() );
		}

		// ── running off: somebody going down turns it round ──
		if ( _patient is null )
		{
			if ( !_nextLook ) return false;
			_nextLook = LookEvery;

			var next = NextPatient();
			if ( !next.IsValid() ) return false;

			_patient = next;
			Go( PatientSpot( next ) );
			return false;
		}

		var at = _patient.WorldPosition;

		// ── not there yet: after them ──
		if ( WorldPosition.Distance( at ) > MathF.Max( 0f, Reanimator.SamaritanReach ) )
		{
			// ⚠️ A PICK-UP THEY CRAWLED OUT OF STARTS OVER, as a player's does when they step away (`ReviveAugments.Tick`).
			if ( _reviving > 0f ) LetGo( keep: true );

			// ⚠️ AND THE PATH FOLLOWS THEM, asked again once they have crawled off its end.
			if ( _nextRepath )
			{
				_nextRepath = RepathEvery;

				var spot = PatientSpot( _patient );
				if ( spot.Distance( _pathFor ) > RepathMove ) Go( spot );
			}

			return false;
		}

		// ── in reach: picking them up ──
		if ( _reviving <= 0f ) BeginRevive();

		_reviving += Time.Delta;
		_held = MathF.Min( HoldCap, _held + Time.Delta );
		Face( (at - WorldPosition).WithZ( 0f ) );

		if ( _reviving < MathF.Max( 0.05f, ReviveAugments.ReviveSeconds ) ) return true;

		// ⚠️ DONE. The host's copy stands them up; every copy leaves them be for a moment and goes on.
		var patient = _patient;
		_patient = null;
		_reviving = 0f;
		_justRevived = patient;
		_sinceRevived = 0f;

		if ( Lure )
		{
			patient.Revive();

			Log.Info( $"[nz-ammo] RE-ANIMATOR GOOD SAMARITAN — {patient.GameObject.Name} back up after {ReviveAugments.ReviveSeconds:0.#}s"
				+ $" · {MathF.Max( 0f, Life - (Time.Now - _born - _held) ):0.#}s left in it, {_held:0.#}s held in all" );
		}

		Go( NextLeg() );
		return false;
	}

	/// <summary>
	/// A pick-up starts: it stands in `react`, and the patient is shown the bar (`NZPlayer.BeingRevived`).
	///
	/// ⚠️ THE BAR COMES FROM THE PATIENT'S OWN MACHINE'S COPY — the local half of `ReviveAugments.Announce`. Every machine runs a
	/// human, and theirs reaches them when the host's does, so nothing has to be sent to tell them help is here.
	/// </summary>
	void BeginRevive()
	{
		if ( _r.IsValid() && _r.Sequence.Name != "react" )
		{
			_r.Sequence.Name = "react";
			_r.PlaybackRate = 1f;
		}

		if ( Mine( _patient ) ) _patient.BeingRevivedBy( MathF.Max( 0.05f, ReviveAugments.ReviveSeconds ) );
	}

	/// <summary>Stop picking the patient up — their bar stops with it — and let go of them too unless <paramref name="keep"/>.</summary>
	void LetGo( bool keep = false )
	{
		if ( _reviving > 0f && Mine( _patient ) ) _patient.BeingRevivedBy( 0f );

		_reviving = 0f;
		if ( !keep ) _patient = null;
	}

	/// <summary>V: where now — the nearest downed player it can reach (not the one just picked up, for a moment), or off.</summary>
	Vector3 NextLeg()
	{
		_patient = NextPatient();
		_nextLook = LookEvery;

		return _patient.IsValid() ? PatientSpot( _patient ) : OffSpot();
	}

	/// <summary>
	/// V: the nearest downed player it can reach from where it stands. ⚠️ THE ONE IT JUST PICKED UP IS LEFT ALONE FOR
	/// `ReviveAugments.JustRevivedGrace`, as a player rescuer leaves theirs (`ReviveAugments.TargetFor`): until their own machine's
	/// stand-up arrives, every copy here still reads them as down.
	/// </summary>
	NZPlayer NextPatient()
		=> Reanimator.NearestPatient( Scene, WorldPosition, WorldPosition,
			except: _sinceRevived < ReviveAugments.JustRevivedGrace ? _justRevived : null );

	/// <summary>Where a patient lies on the navmesh: what a path is asked for.</summary>
	static Vector3 PatientSpot( NZPlayer p ) => ZombieAI.NavGround( Game.ActiveScene, p.WorldPosition );

	/// <summary>
	/// V: where it runs off once nobody is left to pick up — onward, along the heading from where it rose (`_from`) to its first
	/// patient (`_to`), `Flee` on. Picked once.
	///
	/// ⛔ NO RANDOM HERE: every machine must pick the same spot, and only the host could roll one (`Reanimator.PickFlee`) — the one
	/// spot it sent was the patient's. So the heading comes from the two points every machine was sent, the spot is the navmesh's
	/// nearest to the aim, and the turns are tried in one fixed order until one has a whole path. Nowhere at all: it stays, the lure.
	/// </summary>
	Vector3 OffSpot()
	{
		if ( _off is Vector3 picked ) return picked;

		var here = WorldPosition;
		var heading = (_to - _from).WithZ( 0f );
		var yaw = heading.Length >= 64f ? Rotation.LookAt( heading ).Yaw() : 0f;
		var far = MathF.Max( 64f, Reanimator.Flee );
		var nav = Scene?.NavMesh;

		_off = here;
		if ( nav is null ) return here;

		for ( var i = 0; i < 8; i++ )
		{
			// ⚠️ 0, +45, −45, +90, −90, +135, −135, 180: straight on first, then ever wider.
			var turn = (i + 1) / 2 * 45f * (i % 2 == 1 ? 1f : -1f);
			var aim = _to + Rotation.FromYaw( yaw + turn ).Forward * far;

			if ( nav.GetClosestPoint( aim ) is not Vector3 spot ) continue;

			var path = nav.CalculatePath( new Sandbox.Navigation.CalculatePathRequest { Start = here, Target = spot } );
			if ( !path.IsValid || path.Points.Count < 2 || path.Status != Sandbox.Navigation.NavMeshPathStatus.Complete ) continue;

			_off = spot;
			break;
		}

		return _off.Value;
	}

	/// <summary>Is this player this machine's own body — the one whose bar this machine shows. Solo, the only one.</summary>
	static bool Mine( NZPlayer p ) => p.IsValid() && (!Networking.IsActive || PlayerPresence.Mine( p.GameObject ));

	// ══ IV LIVE BAIT (2026-10-06) ════════════════════════════════════════════

	/// <summary>
	/// IV, one frame: every `LiveBaitEvery` of its life, each zombie within `LiveBaitRadius` of it takes the killer's snapshot
	/// (`Reanimator.LastStand.Bait`), credited to the killer. THE KILLER'S MACHINE'S COPY ONLY — the one that claimed the arming;
	/// every other copy holds none, so it is dealt once.
	///
	/// ⚠️ COUNTED FROM THE RISE, NOT A TIMER RE-ARMED AS IT FIRES (Gravity Well's shreds): a late frame pays the tick it owes. And by
	/// the time it has LIVED, held pick-ups included — the crowd round a Good Samaritan's patient keeps taking it.
	///
	/// ⚠️ NO LINE OF SIGHT, unlike III's burst: an aura round the human, as the knot's shreds are, and its zombies are the ones on
	/// its heels.
	/// </summary>
	void TickBait()
	{
		var stand = _stand;
		if ( stand is null || stand.Bait <= 0f || !stand.Killer.IsValid() ) return;

		var every = MathF.Max( 0.1f, Reanimator.LiveBaitEvery );
		var owed = (int)MathF.Floor( (Time.Now - _born) / every + 0.001f );
		var rounds = owed - _baitTicks;
		if ( rounds <= 0 ) return;

		// ⚠️ CLAIMED BEFORE THE DAMAGE, as a cooldown is stamped before its effect: a death inside cannot have them paid twice.
		_baitTicks = owed;

		var at = WorldPosition;
		var reach = MathF.Max( 0f, Reanimator.LiveBaitRadius );
		var hits = 0;

		// ⛔ ITS HITS ROLL NO MOD (`AmmoMods.WithoutProcs`): ten seconds and more of hits after the kill, with any gun in the hand.
		AmmoMods.WithoutProcs( stand.Killer, () =>
		{
			for ( var n = 0; n < rounds; n++ )
			{
				// ⚠️ DOWNWARD, WITH A BOUNDS CHECK (`TechBlast.Detonate`): a kill can take a zombie out of `All` mid-walk.
				for ( var i = ZombieAI.All.Count - 1; i >= 0; i-- )
				{
					if ( i >= ZombieAI.All.Count ) continue;

					var z = ZombieAI.All[i];
					if ( !z.IsValid() || z.State == ZombieState.Dead ) continue;
					if ( at.Distance( z.WorldPosition ) > reach ) continue;

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

					// ⚠️ NO WEAPON, as Gravity Well's shreds have none: a mod's damage must not pick up the gun's tech tree
					// (`TechEffects.Of`). A blast, not a bullet: it pays no hit points (`Health.Pays`), and what it kills carries no
					// mod (`AmmoMods.HitModOf`), so no kill mod goes off on it — another human least of all.
					hp.OnDamage( new SWB.Shared.DamageInfo
					{
						Attacker = stand.Killer.GameObject,
						Damage = stand.Bait,
						Position = z.WorldPosition + Vector3.Up * (z.BodyHeight * 0.5f),
						Origin = at,
						Tags = [TechBlast.BlastTag],
					} );

					hits++;
				}
			}
		} );

		_baitHits += hits;
	}

	/// <summary>Its end: the VR-11's burst, on this machine, and gone. At III, on the killer's machine, it hurts.</summary>
	void Burst()
	{
		var at = WorldPosition + Vector3.Up * 36f;

		Sound.Play( "nz.vr11.explode", at );
		BlastEffect.Spawn( at, 90f, announce: false );

		// ⚠️ IV's TALLY (2026-10-06), on the killer's machine: the one copy that hurt.
		if ( _stand is { Bait: > 0f } bait )
			Log.Info( $"[nz-ammo] RE-ANIMATOR LIVE BAIT — {_baitTicks} tick(s) of {bait.Bait:0} within {Reanimator.LiveBaitRadius:0}u,"
				+ $" {_baitHits} hit(s) in all" );

		// ⚠️ TAKEN BEFORE IT IS DEALT, so it can only go off once.
		var stand = _stand;
		_stand = null;
		Reanimator.LastStandBurst( stand, at );

		GameObject.Destroy();
	}

	// ══ the AI's questions ═══════════════════════════════════════════════════

	/// <summary>Add the humans luring <paramref name="from"/> to a zombie's candidate list (`ZombieAI.GetTargetables`).</summary>
	public static void AddTargetsFor( Vector3 from, List<GameObject> into )
	{
		var reach = MathF.Max( 0f, Reanimator.LureRadius );

		foreach ( var h in Lures )
			if ( h.IsValid() && h.GameObject.IsValid() && from.Distance( h.WorldPosition ) <= reach )
				into.Add( h.GameObject );
	}

	/// <summary>Is a point inside any human's lure — the chase tick's cheap test (`BananaStand.Luring`'s twin).</summary>
	public static bool Luring( Vector3 at )
	{
		var reach = MathF.Max( 0f, Reanimator.LureRadius );

		foreach ( var h in Lures )
			if ( h.IsValid() && at.Distance( h.WorldPosition ) <= reach ) return true;

		return false;
	}

	/// <summary>Is this a re-animated human — a swing at it lands on nothing (`ZombieAI`'s attack).</summary>
	public static bool IsHuman( GameObject go )
		=> go.IsValid() && go.Components.Get<ReanimatorHuman>( FindMode.EverythingInSelf ) is not null;
}