Health.cs

Health component used by players and zombies. Implements damage handling, hit description, multiplier logic (headshots, body parts, perks, tech effects), relaying client hits to host, mirroring health, and firing OnDamaged/OnKilled events.

NetworkingFile AccessReflection
using Sandbox;
using System;

namespace NZombies;

/// <summary>
/// HEALTH — one component, used by zombies and players alike.
///
/// Deliberately dumb: a number in, events out. It does NOT know about flinch
/// animations, ragdolls, points or downed states — zombies and players want
/// completely different reactions to being hit, and merging that in is what
/// makes a health component painful to live with later. Reactions belong in
/// ZombieAI and the player code, driven off the events here.
///
/// Implements Sandbox.Component.IDamageable so the engine's own bullet path
/// reaches it: BaseCombatWeapon traces, builds a DamageInfo and delivers it
/// here. We get hitboxes, damage tags and networking for free instead of
/// re-implementing tracing.
///
/// ⚠️ DamageInfo appears ONLY at that engine boundary. Everything of ours goes
/// through Apply(float, bool) — so we never have to construct a DamageInfo,
/// whose exact shape is engine-owned and changes between versions.
/// </summary>
public sealed class Health : Component, Component.IDamageable
{
	[Property] public float Max { get; set; } = 100f;

	/// <summary>Current health, clamped to [0, Max].</summary>
	[Property, ReadOnly] public float Current { get; private set; }

	/// <summary>
	/// Ignore incoming damage for this long after a hit lands.
	///
	/// The original gives a victim 0.5s of immunity after being hit, and the
	/// reference doc (§9.4) is explicit that it is not optional: it is what
	/// makes being surrounded survivable rather than instant death, because
	/// six zombies in contact would otherwise all land in the same tick.
	/// Zero disables it — zombies themselves don't get one.
	/// </summary>
	[Property] public float ImmunityAfterHit { get; set; } = 0f;

	/// <summary>
	/// Take no damage at all. Off.
	///
	/// ⛔ ADDED FOR THE DEV MENU'S GODMODE BUTTON, WHICH COULD NOT SIMPLY BE WIRED BECAUSE NOTHING
	/// IN THE PROJECT HAD A GODMODE. It was one of four disabled placeholders in the Dev tab; the
	/// other three had existing calls behind them and this one had no mechanic at all.
	///
	/// ⚠️ THE GATE IS AT THE VERY TOP OF `Apply`, WHICH IS NORMALLY THE WRONG PLACE. §4 warns that
	/// an early-out up there silently skips everything below it - and here that is exactly the
	/// intent: no reduction order, no armor, no augments, no events. "Takes nothing" has to mean
	/// nothing, and anything subtler would be a damage multiplier rather than immunity.
	///
	/// ⚠️ ON THE SHARED COMPONENT, so a zombie could be given it too. That is useful (an
	/// unkillable target to shoot at) and is what `nz_dev_tank` does with a million health instead -
	/// deliberately, because a tank that still TAKES damage lets you read the numbers.
	/// </summary>
	[Property, Group( "Damage" )] public bool Invulnerable { get; set; }

	public bool IsDead => Current <= 0f;
	public float Fraction => Max > 0f ? Current / Max : 0f;

	/// <summary>Damage that was actually applied — never fires for a blocked,
	/// zero or post-death hit. (amount, wasHeadshot)</summary>
	public Action<float, bool> OnDamaged { get; set; }

	/// <summary>Headshot damage multiplier. 2.5 matches the original gamemode.</summary>
	[Property, Group( "Damage" )] public float HeadshotDamageScale { get; set; } = 2.5f;

	// ── per-body-part multipliers ────────────────────────────────────────────
	//
	// ⚠️ BEYOND THE ORIGINAL. nZombies scales the head only; every other hitgroup
	// there drives gibbing/stun, not damage. These default to 1.0 so the game
	// behaves exactly like the original until a value is changed.
	//
	// ⚠️ Head is NOT in this table — it stays on HeadshotDamageScale so the
	// headshot flag, the points award (100 vs 50) and the damage bonus can never
	// disagree about what counts as a headshot.

	[Property, Group( "Damage" )] public float TorsoMultiplier { get; set; } = 1f;
	[Property, Group( "Damage" )] public float ArmMultiplier { get; set; } = 0.75f;
	[Property, Group( "Damage" )] public float LegMultiplier { get; set; } = 0.75f;

	/// <summary>Hands and feet — the extremities, softest of all.</summary>
	[Property, Group( "Damage" )] public float ExtremityMultiplier { get; set; } = 0.5f;

	/// <summary>
	/// The weapon that fired this hit, for the tech nodes that are owned by the SHOOTER but
	/// applied here on the VICTIM — as a <see cref="TechEffects.TechRef"/>, which is the form
	/// that survives a relay.
	///
	/// ⛔ THIS USED TO RETURN THE WEAPON COMPONENT AND THAT WORKED ON EXACTLY ONE MACHINE.
	/// `damage.Weapon` is null for every hit a client sends, because weapon prefabs are
	/// `NetworkMode.Never`, so eight nodes silently read as unowned here. `TechEffects.Of` falls
	/// back to the prefab path the relay stamps into `DamageInfo.Extra`; see `TechRef` for the
	/// whole story and for why the prefab path is the right handle rather than a workaround.
	///
	/// ⚠️ THE WEAPON, NOT JUST THE PLAYER, because tech is per weapon PREFAB — a player
	/// carrying two guns has two independent trees. Reading the attacker's ACTIVE weapon instead
	/// would apply one gun's tech to the other, the trap TechEffects.Has records as having cost a
	/// free Pack-a-Punch level three times. `TechRef` carries the prefab for precisely this.
	///
	/// ⚠️ NOTHING RESOLVES FOR A NON-BULLET SOURCE, and that is the right answer rather than
	/// a gap: the knife, grenades, traps, StatusEffects and the test commands build their
	/// DamageInfo by hand and set neither a weapon nor a stamp, so they cannot pick up a rifle's
	/// tree. Every `TechEffects` accessor returns its `ifAbsent` for an invalid ref.
	///
	/// ⚠️ KEPT AS A NAMED METHOD PURELY TO BE MEASURABLE. A `using var` needs a block, and
	/// this cannot be timed at the call site either: the result is assigned to a `var`, and a
	/// using-BLOCK around a declaration scopes the variable out of the rest of OnDamage.
	/// </summary>
	static TechEffects.TechRef FiredBy( in DamageInfo damage )
	{
		using var _cpu = CpuScope.Measure( "dmg.firedby" );

		return TechEffects.Of( damage );
	}

	/// <summary>
	/// Which body part these tags describe, as a damage multiplier.
	///
	/// ⛔ TAG NAMES ARE NOT ASSUMED. Verified on walker_honorguard_dmx (46
	/// hitboxes) with `nz_hitbox_tags`; the vocabulary is exactly:
	///
	///     head    j_head, j_neck
	///     chest   j_spinelower, j_spineupper, j_spine4
	///     arm     clavicle -> shoulder -> elbow -> wrist -> fingers  (+ left/right)
	///     leg     knee, ankle, ball                                  (+ left/right)
	///
	/// There is no stomach/pelvis/hand/foot tag — the extra names below are
	/// tolerated in case another walker variant uses them. A tag this does not
	/// recognise falls through to 1.0: a silent no-op, never a wrong number.
	///
	/// ⚠️ `j_neck` is tagged HEAD, so neck shots pay and scale as headshots.
	/// ⚠️ Every limb also carries a `left`/`right` tag — per-limb tracking (for
	/// gibbing, the original's real use of hitgroups) can key off that later.
	///
	/// ⚠️ `weapon` IS PASSED IN, not resolved here, so OnDamage pays for exactly one
	/// <see cref="FiredBy"/> lookup per hit — and every pellet of a 16-pellet shotgun
	/// blast is its own DamageInfo arriving through this path.
	/// </summary>
	float PartMultiplier( in DamageInfo damage, in TechEffects.TechRef tech )
	{
		// ⚠️ Same reason as dmg.firedby -- the call site is `amount *= PartMultiplier( ... )` in an
		// else-if, so it is measured here. It therefore NESTS inside dmg.headscale.
		using var _cpu = CpuScope.Measure( "dmg.part" );
		// ── HOLLOW POINTS (`t2_limbs`) ────────────────────────────────────────
		//
		// ⛔ A FLOOR PER PART, NOT A MULTIPLY. The node states two TARGET values —
		// limbs to 1.0, extremities to 0.75 — and a multiply cannot reach a target
		// without already knowing what it is multiplying. `MathF.Max` can, and it
		// carries the property the whole roster of zombie variants needs: these are
		// `[Property]` fields, so a boss may legitimately author arms at 1.2, and a
		// floor is incapable of lowering that. Scaling the deficit toward 1 was the
		// other candidate and was rejected for the same reason Long Barrel is a
		// floor: it has no fixed point, so a value already ABOVE the target gets
		// dragged back down toward it.
		//
		// ⛔ AND THE CATALOGUE ONLY HOLDS ONE OF THE TWO TARGETS. `t2_limbs` stores
		// 1.0, which is the LIMB target; the extremity target of 0.75 is not in
		// `WeaponTech.cs` at all. Hardcoding 0.75 here would be a second source for
		// a magnitude the catalogue is supposed to own, and `nz_tech` would print a
		// table that disagrees with the code — the exact failure TechEffects.Factor
		// exists to prevent.
		//
		// ⛔ BOTH PARTS NOW FLOOR AT THE SAME TARGET, AND THE DERIVATION IS GONE. The node
		// used to promote each part one rung up the body ladder — extremities scored as limbs,
		// limbs scored as the catalogue target — which on stock values was 0.5 -> 0.75 and
		// 0.75 -> 1.0. Requested instead: *"hollow point could make the minimum damage 1x"*. One
		// target for both, so the card can say "limbs and extremities take full damage" and be
		// literally true, and the `Min( Arm, Leg )` reasoning below it is no longer needed.
		//
		// ⚠️ IT IS STILL A FLOOR, WHICH IS WHAT KEEPS A BOSS COHERENT. A zombie variant may
		// legitimately author an arm at 1.2, and `MathF.Max` cannot lower that — a multiply or an
		// assignment would drag it back down to the target, which is the trap the block above
		// records for Body Shot and Deadeye.
		//
		// ⚠️ `ifAbsent: 0f` IS LOAD-BEARING. Factor defaults to 1 for the multiply
		// sites; a silent 1 here would floor every limb on every weapon in the game
		// to 1.0 and delete the limb penalty outright, node or no node.
		var limbFloor = MathF.Max( TechEffects.Factor( tech, "t2_limbs", 0f ),
			// ⚠️ HEAVY MATCH AMMO (sniper tier 4, 2026-10-04): the same full-damage floor, as a switch.
			TechStats.Flag( tech, "f.limbsfull" ) ? 1f : 0f );
		var extremityFloor = limbFloor;

		// ── BODY SHOT (`t4_bodyshot`) and DEADEYE (`t5_deadeye`) ──────────────
		//
		// ⛔ A TARGET FROM BELOW AND A TARGET FROM ABOVE, NOT MULTIPLIES, for the reason
		// Hollow Points records above: a scale has no fixed point. Body Shot written as
		// `x1.5` would drag a boss that legitimately authors a 2.0 torso to 3.0, and
		// Deadeye written as `x0.5` would take stock hands and feet from 0.5 to 0.25 —
		// twice the penalty the node advertises, on the one part a player cannot choose
		// to avoid. A floor and a ceiling both land ON the stated number, and both are
		// idempotent, which matters on a method that runs once per pellet.
		//
		// ⚠️ `ifAbsent: 0f` IS LOAD-BEARING HERE TOO, exactly as it is for limbFloor —
		// Factor's default of 1 would floor every part on every weapon in the game.
		//
		// ⛔ BODY SHOT NO LONGER TOUCHES THIS TABLE AT ALL. It used to floor torso and
		// limbs at 1.5 from here; it is now a flat x1.5 on `ShootInfo.Damage`, applied
		// once at spawn in NZPlayer beside every other damage node. Two consequences
		// worth stating because both are improvements rather than side effects:
		//   • the limb and extremity penalties SURVIVE underneath it (limb 1.125,
		//     extremity 0.75) instead of being flattened to 1.5, so Hollow Points still
		//     has something to fix and the two nodes stop overlapping;
		//   • nothing here reads a per-zombie part multiplier a variant may have
		//     authored, so a boss with a 2.0 torso is no longer a special case.
		var bodyFloor = 0f;

		// ⛔ DEADEYE'S BODY NUMBER IS ITS SECOND ONE, SO `Factor` IS THE WRONG ACCESSOR.
		// The catalogue's 2.5 is the HEAD scale this node adds in OnDamage; reading it
		// here would make a body shot two and a half times BETTER instead of half as
		// good — a penalty wired as its own inverse, which nothing in the numbers would
		// flag. `Bound` is the field that exists for a node's second number.
		//
		// ⚠️ THE 0.5 BELOW IS A FALLBACK, NOT THE MAGNITUDE. `t5_deadeye` declares no
		// `Bound` yet, so today the fallback is what runs — the same state Boat Tail was
		// in before its 1.25 moved into the catalogue. Declaring `Bound = 0.5f` on the
		// node makes this literal unreachable and puts the number in the table `nz_tech`
		// prints; until then it is a magnitude the printed catalogue cannot see.
		//
		// ⚠️ UNLIKE `Factor`, `Bound` IS NOT AMPLIFIED by `nz_tech_amp`, so under
		// amplification Deadeye's head bonus grows and its body penalty does not. Boat
		// Tail's ceiling behaves the same way; it is a property of Bound, not of here.
		var bodyCeiling = TechEffects.Has( tech, "t5_deadeye" )
			? WeaponTech.BoundOf( "t5_deadeye", 0.5f )
			: 0f;

		// ⛔ HANDS AND FEET HAVE NO TAG OF THEIR OWN. Verified on the model: every
		// finger, wrist and wristtwist is tagged plain `arm`, and ankle/ball are
		// plain `leg`. Splitting the extremities out therefore has to key on the
		// BONE, which the tags alone cannot tell you — a tags-only version would
		// silently score hands at the full arm value and look like it worked.
		var bone = damage.Hitbox?.Bone.Name;
		if ( bone is not null && IsExtremity( bone ) )
			return Shaped( ExtremityMultiplier, extremityFloor, bodyCeiling );

		var tags = damage.Tags;
		if ( tags is null ) return 1f;

		foreach ( var t in tags.TryGetAll() )
		{
			switch ( t )
			{
				// ⚠️ TORSO TAKES NO HOLLOW POINTS FLOOR. That node's own description
				// names limbs and extremities only; flooring the torso as well would
				// widen it into a flat damage node and duplicate Vigor Rush. Body Shot
				// DOES name the torso, which is why it is a second, separate floor
				// rather than a bigger value for the first one.
				case "chest" or "torso" or "stomach" or "spine" or "pelvis" or "body":
					return Shaped( TorsoMultiplier, bodyFloor, bodyCeiling );
				case "arm" or "leftarm" or "rightarm" or "upperarm" or "forearm" or "hand":
					return Shaped( ArmMultiplier,
						MathF.Max( limbFloor, bodyFloor ), bodyCeiling );
				case "leg" or "leftleg" or "rightleg" or "thigh" or "calf" or "foot":
					return Shaped( LegMultiplier,
						MathF.Max( limbFloor, bodyFloor ), bodyCeiling );
			}
		}

		// ⚠️ AN UNRECOGNISED PART IS STILL A NO-OP, AND DELIBERATELY NOT CEILINGED. It
		// is the one branch that does not know what it hit, so Deadeye's penalty is not
		// charged to it — which does mean an unknown tag scores above a Deadeye torso on
		// some future walker variant. `nz_hitbox_tags` is the tool that finds those; a
		// guessed penalty here would hide them instead.
		return 1f;
	}

	/// <summary>
	/// One authored part multiplier, after the tier-4 archetypes.
	///
	/// ⚠️ 0 MEANS "ABSENT" FOR BOTH BOUNDS — the convention limbFloor in
	/// PartMultiplier already uses, not a sentinel invented here.
	///
	/// ⛔ THE CEILING IS APPLIED LAST, SO DEADEYE BEATS BODY SHOT when a weapon somehow
	/// holds both. Tier 4 is pick-one (`WeaponTech.Tiers`: Picks = 1) so a real game
	/// cannot reach that pair — but `WeaponTech.Unlimited` lifts the pick cap in
	/// creative, where its own comment says every one of the 41 nodes can be bought on
	/// one weapon, so this is reachable and must not be undefined. Letting the PENALTY
	/// win is the safe direction: two contradictory nodes must never read as a buff.
	/// </summary>
	static float Shaped( float part, float floor, float ceiling )
	{
		if ( floor > 0f ) part = MathF.Max( part, floor );
		if ( ceiling > 0f ) part = MathF.Min( part, ceiling );

		return part;
	}

	/// <summary>
	/// Hand or foot, by bone name.
	///
	/// Hands: j_index/mid/pinky/ring/thumb/wrist/wristtwist_*
	/// Feet:  j_ankle_*, j_ball_*
	///
	/// ⚠️ `j_wrist` matches `j_wristtwist` too, which is intended — both are hand.
	/// ⚠️ Deliberately does NOT match j_elbow or j_knee: those are mid-limb and
	/// should stay on the arm/leg value.
	/// </summary>
	static bool IsExtremity( string bone )
	{
		return bone.Contains( "index" ) || bone.Contains( "mid_" ) || bone.Contains( "pinky" )
			|| bone.Contains( "ring" ) || bone.Contains( "thumb" ) || bone.Contains( "wrist" )
			|| bone.Contains( "ankle" ) || bone.Contains( "ball" );
	}

	/// <summary>
	/// Opt out of the headshot bonus. The original exempts walker_necromorph,
	/// special_bot and walker_xeno — set this on those enemy types.
	/// </summary>
	[Property, Group( "Damage" )] public bool ImmuneToHeadshotBonus { get; set; }

	/// <summary>Fired once, on the hit that took it to zero. (wasHeadshot)</summary>
	public Action<bool> OnKilled { get; set; }

	private TimeUntil _immuneUntil;

	/// <summary>Napalm Nectar hits landed on THIS body, toward its ignite.</summary>
	// ⛔ `_napalmHits` REMOVED with the block above. A field nobody reads is worse than no
	// field (§12), and the cooldown that replaced it is per PLAYER and lives on `NZPlayer`.

	protected override void OnAwake() => Current = Max;

	/// <summary>
	/// Was the most recent damage melee?
	///
	/// ⚠️ Read it in OnDamaged/OnKilled — it is set immediately before those
	/// fire. A separate flag rather than a parameter because OnDamaged and
	/// OnKilled are already public API with several subscribers, and widening
	/// their signature for one weapon type would break every one of them.
	///
	/// The melee tag comes from the weapon's DamageInfo, the same route "head"
	/// takes via BaseCombatWeapon.MergeHitboxTags.
	/// </summary>
	public bool LastHitWasMelee { get; private set; }

	/// <summary>
	/// May the hit that just landed pay points.
	///
	/// ⛔ FALSE ONLY WHEN THE SHOOTER SAID SO. Penetration and pellet count multiplied the
	/// per-hit award into thousands of points a trigger pull — see `ShotPoints`, which decides
	/// this on the machine that fired, because only that machine knows which pellet of which shot
	/// a given hit belongs to.
	///
	/// ⚠️ LATCHED, NOT ASKED LATER, for exactly the reason `LastHitWasMelee` is: `OnKilled`
	/// fires straight after `OnDamaged`, and this is the last moment the hit is knowable.
	///
	/// ⚠️ DEFAULTS TO TRUE FOR EVERYTHING THAT IS NOT A BULLET. A knife, a grenade, a trap or a
	/// Nuke carries no `nopay` tag and must go on paying normally — none of them can be fanned out
	/// across twelve pellets.
	/// </summary>
	public bool LastHitPays { get; private set; } = true;

	/// <summary>
	/// Does this damage pay the per-hit points drip.
	///
	/// ⚠️ TWO REFUSALS, ONE ANSWER. `nopay` is `ShotPoints`' verdict on a bullet; `blast` is the
	/// Tech blast, which has been stamping that tag and waiting for a reader since it was written.
	/// Its own header names this as the missing half — *"the fix is two lines in those files"* —
	/// and describes precisely this latch. They are the same requirement from two directions: one
	/// trigger pull must not pay for every body it reaches.
	/// </summary>
	public static bool Pays( in DamageInfo damage )
		=> damage.Tags is not { } tags
			|| (!tags.Has( "nopay" ) && !tags.Has( NZombies.TechBlast.BlastTag ));

	/// <summary>
	/// Who dealt the last hit. Null for the world, a trap, or anything anonymous.
	///
	/// ⚠️ SAME CONVENTION AS LastHitWasMelee — set immediately before OnDamaged
	/// fires, so a handler reading it during OnDamaged or OnKilled sees the hit that
	/// caused it. ZombieAI latches it there for exactly that reason.
	///
	/// ⚠️ SET BY WHICHEVER PATH ACTUALLY KNOWS. OnDamage reads damage.Attacker;
	/// Apply takes `from`, and only writes it when `from` is valid — because OnDamage
	/// calls Apply with a DELIBERATE null, and passing the attacker through there
	/// would switch Victorious Tortoise on for grenades and bullets, which its own
	/// comment says must not happen. The guard is what lets both paths write this
	/// without one erasing the other.
	///
	/// ⚠️ This does NOT fix FIX_LIST entry 5 (kill points pay the first player
	/// found, not the killer). It is the missing ingredient for that fix, but
	/// AwardPoints still resolves its own player and changing it is its own job.
	/// </summary>
	public GameObject LastAttacker { get; private set; }

	/// <summary>
	/// WHAT DEALT THE LAST HIT, in words, for the player's hit line (`NZPlayer.OnHurt`): the kind (bullet, knife, blast, fall,
	/// tick, cost), who, and which machine sent it when it came over the network. Set in `Apply` before `OnDamaged` fires.
	///
	/// ⛔ ADDED FOR TWO DOWNS NOBODY COULD NAME (2026-10-04): a 301 and a 291 in a three-player game, logged as nothing more
	/// than "hit for 301". A zombie's claw has its own line; these had none, and the log could not say what they were.
	/// </summary>
	public string LastHitSource { get; private set; } = "";

	/// <summary>A hit that reached `Apply` directly — a claw, a wall, a tick — described from what `Apply` was given.</summary>
	public string DescribeHit( GameObject from, bool blast = false, bool tick = false, bool cost = false )
	{
		var kind = cost ? "cost" : tick ? "tick" : blast ? "blast" : !from.IsValid() ? "hit"
			: from.Components.Get<ZombieAI>( FindMode.EverythingInSelfAndAncestors ).IsValid() ? "claw" : "hit";

		return $"{kind} from {WhoIs( from )}";
	}

	/// <summary>A hit that came through `OnDamage` — a bullet, the knife, a blast — described from its `DamageInfo`.</summary>
	string DescribeDamage( in DamageInfo damage )
	{
		var tags = damage.Tags;
		var kind = IsMelee( damage ) ? "knife"
			: tags is not null && tags.Has( ExplosionTag ) ? "explosion"
			: tags is not null && tags.Has( "fall" ) ? "fall"
			: tags is not null && tags.Has( SWB.Shared.TagsHelper.Bullet ) ? "bullet"
			: "hit";

		// ⚠️ THE PREFAB'S FILE NAME BY HAND, not `System.IO.Path`: s&box's whitelist refuses file APIs in the editor's compile
		// only (INSTRUCTIONS.md, "A clean dotnet build does not mean s&box will load it").
		var gun = FiredBy( damage ).Prefab ?? "";
		var slash = gun.LastIndexOf( '/' );
		if ( slash >= 0 ) gun = gun[(slash + 1)..];
		var dot = gun.LastIndexOf( '.' );
		if ( dot > 0 ) gun = gun[..dot];

		return $"{kind}{(gun.Length > 0 ? $" ({gun})" : "")} from {WhoIs( damage.Attacker )}";
	}

	/// <summary>"yourself", "zombie …", "player …", an object's name, or "nothing" for an anonymous hit.</summary>
	string WhoIs( GameObject from )
	{
		if ( !from.IsValid() ) return "nothing";
		if ( from == GameObject ) return "yourself";

		if ( from.Components.Get<ZombieAI>( FindMode.EverythingInSelfAndAncestors ).IsValid() ) return $"zombie '{from.Name}'";
		if ( from.Components.Get<NZPlayer>( FindMode.EverythingInSelfAndAncestors ).IsValid() ) return $"player '{from.Name}'";

		return $"'{from.Name}'";
	}

	/// <summary>
	/// The weapon GameObject that landed the most recent hit, or null.
	///
	/// ⚠️ Same convention as LastAttacker — set immediately before OnDamaged fires, so
	/// a handler reading it during OnDamaged or OnKilled sees the hit that caused it.
	///
	/// ⚠️ NULL FOR EVERYTHING THAT IS NOT A BULLET. Only DamageInfo.FromBullet sets
	/// the field, so the knife, grenades, traps and status effects leave it null — which
	/// is what makes it safe to hand straight to TechEffects, since tech is per weapon
	/// prefab and a grenade must not read a rifle's tree.
	/// </summary>
	public GameObject LastWeapon { get; private set; }

	/// <summary>
	/// The same weapon as <see cref="LastWeapon"/>, in the form that survives a relay.
	///
	/// ⛔ `LastWeapon` IS A GameObject AND A CLIENT'S WEAPON HAS NO GameObject HERE. Weapon
	/// prefabs are `NetworkMode.Never`, so on the host that field is null for every hit a client
	/// sent — which made `ZombieAI`'s Bounty award pay nothing for a client's kills. This carries
	/// the prefab path instead, which is what tech is keyed by anyway.
	///
	/// ⚠️ SET BESIDE `LastWeapon` AND CLEARED BY NOTHING, matching it exactly: the last hit
	/// is the one that killed, and a kill is scored in the same call stack.
	/// </summary>
	public TechEffects.TechRef LastTech { get; private set; }

	/// <summary>
	/// THE AMMO MOD OF THE GUN WHOSE BULLET HIT LAST (2026-10-04), or "" — Bleeder's, Midas's and the kill mods' answer to "was
	/// this that mod's ammo" (`Bleeder`, `KillMods`). Set beside <see cref="LastTech"/> on every hit through `OnDamage`: the
	/// host's own gun, or the mod a client's hit was sent with (`AmmoMods.HitModOf`). "" for anything that is not a bullet.
	/// </summary>
	public string LastMod { get; private set; } = "";

	/// <summary>
	/// Was the last hit's damage a STAND-IN — Insta-Kill's or Executioner's `Max × 10`, the idiom that kills through the
	/// normal damage path — rather than what the shot dealt (2026-10-04). See <see cref="LastHitDamage"/>.
	/// </summary>
	public bool LastHitForced { get; private set; }

	/// <summary>The health the last hit actually took, overheal included — never more than was left.</summary>
	public float LastLost { get; private set; }

	/// <summary>
	/// THE KILLING HIT AS THE KILL EFFECTS SIZE FROM IT (2026-10-04, the review): `LastDamage`, overkill and all — unless that
	/// was a stand-in (<see cref="LastHitForced"/>), and then the health it really took. Sized off a stand-in, Shatter Blast
	/// and Headhunter dealt fifty and ten times a zombie's whole health to the room on every Executioner kill.
	/// </summary>
	public float LastHitDamage => LastHitForced ? LastLost : LastDamage;

	/// <summary>
	/// Where the most recent damage landed, in world space.
	///
	/// ⚠️ Same convention as LastHitWasMelee — set immediately before OnDamaged
	/// fires, for the same reason: widening a public callback's signature for one
	/// consumer would break every existing subscriber.
	///
	/// ⚠️ Zero when the damage carries no position (scripted or area damage).
	/// Read it through <see cref="HitPositionOr"/> rather than directly.
	/// </summary>
	public Vector3 LastHitPosition { get; private set; }

	/// <summary>
	/// Which part of the body the most recent hit struck, for the gore (`ZombieAI.Gore.cs`): "head", "arm left", "arm right",
	/// "leg left", "leg right", "explosion", or "" for anything else. See <see cref="GorePartOf"/>.
	///
	/// ⚠️ SAME CONVENTION AS LastHitPosition — set immediately before OnDamaged fires, so a handler reading it during OnDamaged
	/// or OnKilled sees the hit that caused it.
	/// </summary>
	public string LastHitPart { get; private set; } = "";

	/// <summary>Where a relayed hit carries its body part: `NZNet.HurtRemote` stamps it into `DamageInfo.Extra`.</summary>
	public const string PartKey = "nz_part";

	/// <summary>The tag an explosion's damage carries (the grenade's), so the gore can tell an explosive kill.</summary>
	public const string ExplosionTag = "explosion";

	/// <summary>
	/// Which part of the body this damage struck, as <see cref="LastHitPart"/> names it.
	///
	/// ⛔ THE HITBOX'S TAGS, NOT THE HEADSHOT FLAG. `IsHeadshot` is joined by Lucky Shot and Wide Bore further on, so a chest
	/// hit can SCORE as a headshot — and must still not pop a head. The tags say where the bullet actually landed: `head` for
	/// j_head and j_neck, `arm`/`leg` with `left`/`right` for every bone down to the fingers and toes (see PartMultiplier).
	///
	/// ⚠️ A RELAYED HIT HAS NO HITBOX HERE, so the machine that saw it names the part and `NZNet.HurtRemote` carries it in
	/// `Extra` — read first, because the relay's own `head` tag is the promoted flag, not the hitbox.
	/// </summary>
	public static string GorePartOf( in DamageInfo damage )
	{
		var extra = (damage as SWB.Shared.DamageInfo)?.Extra;
		if ( extra is not null && extra.TryGetValue( PartKey, out var relayed ) ) return relayed ?? "";

		var tags = damage.Tags;
		if ( tags is null ) return "";

		if ( tags.Has( ExplosionTag ) || tags.Has( NZombies.TechBlast.BlastTag ) ) return "explosion";
		if ( tags.Has( "head" ) ) return "head";
		if ( tags.Has( "arm" ) ) return tags.Has( "left" ) ? "arm left" : tags.Has( "right" ) ? "arm right" : "arm";
		if ( tags.Has( "leg" ) ) return tags.Has( "left" ) ? "leg left" : tags.Has( "right" ) ? "leg right" : "leg";

		return "";
	}

	/// <summary>
	/// How much the most recent hit actually dealt, after every multiplier.
	///
	/// ⛔ EXISTS FOR DEADSHOT'S M3, which splashes a fraction of the KILLING HIT. The death
	/// handler in ZombieAI knows a zombie died and where, but not how hard — this is the
	/// same latch-it-for-the-death-path shape `LastHitWasMelee`, `LastHitPosition` and
	/// `LastWeapon` already use, and for the same reason: widening a public callback's
	/// signature for one consumer would break every existing subscriber.
	///
	/// ⚠️ THE POST-MULTIPLIER FIGURE, not `damage.Damage`. A 10% splash off the raw bullet
	/// value would ignore Pack-a-Punch, rarity, the head product and Focus — which is most
	/// of what makes a late-game headshot worth splashing.
	/// </summary>
	public float LastDamage { get; private set; }

	/// <summary>
	/// How long since this body last actually lost health. STATE, NOT A CALLBACK.
	///
	/// ⛔ `HealthRegen` USED TO LEARN THIS BY CHAINING `OnDamaged`, AND ON A CLONED BODY THE
	/// CHAIN WAS SILENTLY CUT. `OnDamaged` is an Action PROPERTY: `NZPlayer.OnStart` ASSIGNS it and
	/// `HealthRegen.OnStart` wraps whatever is already there, so the two only work if NZPlayer runs
	/// first. On the scene's own player it does — NZPlayer creates HealthRegen, so the order is
	/// guaranteed. On a body made with `GameObject.Clone()` every component already exists, the
	/// order is whatever the serialisation happened to be, and if HealthRegen starts first its
	/// wrapper is thrown away by NZPlayer's assignment a moment later.
	///
	/// ⚠️ THE SYMPTOM WAS A PLAYER WHO COULD NOT BE HURT, AND IT DID NOT LOOK LIKE THIS AT ALL.
	/// With the chain cut, `_sinceDamage` never reset, so regen believed the player had been calm
	/// for the whole round and ticked 10% every 0.05s — every hit was undone within half a second.
	/// The host's log read `hit for 30 — 120/150` forty times in a row, never lower, while the
	/// host's OWN body went 120 → 90 → 60 → 30 → down on the same zombies. It reads as a
	/// networking bug and is nothing of the sort.
	///
	/// ⚠️ SO THE FACT LIVES HERE, WHERE THE DAMAGE HAPPENS. A reader cannot lose a value it
	/// pulls, and there is no ordering left to get wrong.
	/// </summary>
	public TimeSince SinceLastDamage { get; private set; }

	/// <summary>
	/// Regeneration waits until this moment (`Time.Now`), whatever <see cref="SinceLastDamage"/> says — 0, so never, unless
	/// something burns (2026-10-06: the Fire Margwa's line, `MargwaBurn`).
	///
	/// ⛔ A BLOCK BESIDE THE DAMAGE CLOCK, NOT A WRITE TO IT. That clock only restarts when health is LOST, and a burn held at
	/// 1 HP (it never kills on its own) takes none — so regen would start in the middle of it. The user: *"teh main goal is to
	/// make sure the player is unable to regen HP"*.
	/// ⚠️ THE OWNER'S MACHINE: `HealthRegen` runs there, on the player's own body.
	/// </summary>
	public float NoRegenUntil { get; set; }

	/// <summary>Throttle for the relayed-hit line — see the relay branch in <see cref="OnDamage"/>.</summary>
	RealTimeSince _sinceHitLog;

	/// <summary>
	/// Take a figure decided somewhere else. NO side effects: no `OnDamaged`, no `OnKilled`, no
	/// regen restart, no damage number.
	///
	/// ⛔ EVERY ONE OF THOSE IS THE POINT OF LEAVING THEM OUT. The host already ran them when the
	/// hit landed — it played the voice line, awarded the points, decided the down. Re-running
	/// them here would double every consequence of a single hit, and `OnKilled` in particular
	/// would put the player down a second time from the mirror of the first.
	///
	/// ⛔ REGEN *IS* RESTARTED, AND THE NOTE THAT USED TO SIT HERE SAYING OTHERWISE IS OBSOLETE.
	///
	/// It argued that "the host is already mirroring the result of ITS regen, so restarting the
	/// clock here would have both machines healing the same body on two different schedules."
	/// That was true until this method was given its refuse-to-heal guard: a mirror can now only
	/// ever LOWER health, so the host's regen result cannot cross at all. The owner's local regen
	/// is the ONLY thing that heals a client — which makes restarting its clock not optional but
	/// the entire mechanism.
	///
	/// ⚠️ WITHOUT IT A MIRRORED HIT ARRIVES ALREADY "SECONDS SINCE I WAS LAST HURT", so regen
	/// begins on the same frame and the bar snaps back up. The blood overlay reads the same clock
	/// and cleared with it. *"health visually regens instantly still, including the bloody
	/// overlay."*
	/// </summary>
	public void MirrorTo( float current, float max )
	{
		// ⛔ AND IT MAY NOT TOUCH `Max` AT ALL. That number is the OWNER'S — Juggernog raises it,
		// and Juggernog is bought and held on the machine whose player bought it. The host's copy
		// has never heard of the perk, so mirroring its `Max` silently pulled a Jugged client back
		// down to the base figure. User: *"unsure if juggernog is working for the client."*
		//
		// ⚠️ IT WAS ASSIGNED UNCONDITIONALLY, ABOVE THE GUARD BELOW — so even a mirror that
		// correctly refused to change `Current` still overwrote `Max` on its way past.

		// ⛔ A MIRROR MAY TAKE HEALTH AWAY. IT MAY NEVER GIVE IT BACK.
		//
		// `NZPlayer.PushHealth` sends the HOST'S COPY of this player's health as an absolute
		// value — and that copy is stale, because regen, armour and revives all run on the
		// machine that OWNS the body and never travel back up. So the host holds a number from
		// whenever it last damaged you, and pushing it as truth HEALS you:
		//
		//   client at 30hp (or downed at 0) · host's copy still reads 150
		//   a zombie hits you for 30        · host pushes 120
		//   → you jump from 30 to 120, and a DOWNED player stands up
		//
		// User: *"the clients sometimes heal when taking damage, resulting in being able to
		// revive a downed client by knifing them."*
		//
		// ⚠️ THIS IS A GUARD, NOT THE FIX. The real answer is that the host should relay the
		// DAMAGE AMOUNT rather than a total — the same lesson `NZNet.AwardPoints` records, where
		// carrying the amount is what lets the owner's own systems run. Until player health has
		// one owner, refusing to heal is what makes a stale number harmless instead of dangerous.
		if ( current >= Current ) return;

		Current = Math.Clamp( current, 0f, Max );

		// ⛔ AND THE DAMAGE CLOCK RESTARTS, OR REGEN UNDOES THIS INSTANTLY. `HealthRegen` reads
		// `SinceLastDamage` and refills once it passes the delay — a mirrored hit never touched
		// that clock, so it arrived already "seconds since I was last hurt" and the bar snapped
		// straight back up. User: *"when damaged a client sees their hp bar refill instantaneously
		// when in reality it hasn't."* It was not a display bug; the client really was healing.
		//
		// ⛔ `SinceLastDamage`, NOT `LastDamage`. The first attempt at this set `LastDamage` — which
		// is the AMOUNT of the last hit, not the clock — so it silently zeroed a figure other code
		// reads and did nothing whatever for regen. Two names one letter apart, both on this class,
		// and the compiler is happy with either.
		//
		// ⚠️ THE SAME FIELD `OnDamage` SETS, so a mirrored hit and a local one leave regen in the
		// same state rather than in two that have to agree.
		SinceLastDamage = 0f;
	}

	/// <summary>
	/// The last hit position, or a sensible point on this body when there wasn't
	/// one. Damage numbers need somewhere to appear even for a scripted hit.
	/// </summary>
	public Vector3 HitPositionOr( float fallbackHeight = 48f )
		=> LastHitPosition.IsNearlyZero()
			? WorldPosition + Vector3.Up * fallbackHeight
			: LastHitPosition;

	/// <summary>
	/// Is this the local player's OWN body — the one thing a client is still authoritative over?
	///
	/// ⚠️ BOTH HALVES ARE NEEDED. "Has an `NZPlayer`" alone would also match the other
	/// player's body; "is mine" alone would also match a zombie nobody owns. Together they name
	/// exactly one object per machine.
	/// </summary>
	bool IsOwnPlayerBody
		=> GameObject.Components.Get<NZPlayer>( FindMode.EverythingInSelf ) is not null
			&& PlayerPresence.Mine( GameObject );

	/// <summary>A player, and not this machine's. Their health is theirs to change.</summary>
	bool IsRemotePlayerBody
		=> GameObject.Components.Get<NZPlayer>( FindMode.EverythingInSelf ) is not null
			&& PlayerPresence.Theirs( GameObject );

	/// <summary>
	/// Is this one player shooting another? Those hits are dropped.
	///
	/// ⚠️ BOTH ENDS MUST BE A PLAYER. A zombie hitting a player is not friendly fire, and a
	/// player hitting a zombie is the game — so the test is the PAIR, not either half.
	///
	/// ⚠️ `EverythingInSelf`, MATCHING `IsOwnPlayerBody` DIRECTLY ABOVE. The body carries the
	/// `NZPlayer`; searching ancestors would start matching weapons and held props that happen to
	/// be parented under it, and searching descendants would match nothing useful.
	///
	/// ⚠️ THE ATTACKER IS THE SHOOTER'S BODY, NOT THE WEAPON. `DamageInfo.FromBullet` is
	/// called with `weapon.Owner.GameObject` as its first argument, so this comparison is between
	/// two player bodies and needs no unwrapping.
	/// </summary>
	bool IsFriendlyFire( in DamageInfo damage )
	{
		if ( GameObject.Components.Get<NZPlayer>( FindMode.EverythingInSelf ) is null )
			return false;

		var attacker = damage.Attacker;
		if ( !attacker.IsValid() || attacker == GameObject ) return false;

		return attacker.Components.Get<NZPlayer>( FindMode.EverythingInSelf ) is not null;
	}

	/// <summary>
	/// EVERYTHING THE ATTACKER CONTRIBUTES TO A HIT, as one multiplier.
	///
	/// ⛔ THE SHOOTER'S PERKS HAVE NEVER REACHED A ZOMBIE THEY DID NOT PERSONALLY HOST. Bullets
	/// are relayed — a client's hit is applied by the host through `NZNet.HurtRemote` — and every
	/// term below reads `damage.Attacker`, which on the host is the PROXY copy of that client: no
	/// perks, no augments, and `FiredBy` resolves to nothing because weapons are
	/// `NetworkMode.Never`. So all of it silently evaluated to 1. `HurtRemote`'s own note admits
	/// as much: *"the perks do not come with it … that is a real gap."*
	///
	/// Dead for every client, on every shot: **Deadshot** M1 Deadeye, M2 First Blood and m1 Lucky
	/// Shot; **Death Perception**'s headshot and boss multipliers; **Vigor Rush** M3/M4/m5;
	/// **Victorious Tortoise** M1 and M4; and the `t2_headshot`, `t5_deadeye` and `t4_bodyshot`
	/// weapon nodes.
	///
	/// ⚠️ THE CLIENT PRE-MULTIPLIES AND THE HOST NEEDS NO CHANGE, WHICH IS WHY THIS IS SAFE.
	/// Every term here is attacker-dependent, so on the host's perk-less proxy they all return 1 —
	/// the host multiplying by one after the client has already applied the real figure cannot
	/// double anything. The alternative, gating each term on a "this was relayed" flag, would have
	/// been six edits inside the most delicate method in the project.
	///
	/// ⚠️ VICTIM-SIDE TERMS ARE DELIBERATELY ABSENT. `HeadshotDamageScale` is a global constant,
	/// `PartMultiplier` is about which limb was hit, and Brutus's helmet MUTATES as it is read —
	/// all three stay where they are and are applied once, by the host, exactly as before.
	///
	/// ⚠️ `headBonus` IS RECOMPUTED HERE RATHER THAN PASSED, from the same two conditions the
	/// host uses. The client owns both: `ImmuneToHeadshotBonus` is on its own copy of the zombie,
	/// and it is holding the weapon.
	///
	/// ⚠️ <paramref name="mod"/> IS THE BULLET'S AMMO MOD (2026-10-06), resolved once by the relay, for Midas V's term below.
	/// </summary>
	float AttackerScale( in DamageInfo damage, bool headshot, in TechEffects.TechRef tech, string mod )
	{
		// ⛔ FIRST BLOOD USED TO BE THE FIRST LINE HERE AND IT WAS A x3 ON EVERY CLIENT BULLET.
		//
		// Its rule is `if ( max <= 0f || current < max ) return 1f;` — it pays only on an UNDAMAGED
		// victim. That is a fact about the VICTIM, and `Health` carries no `[Sync]` at all: a
		// client's copy of a zombie is never damaged locally, because `OnDamage` returns at the
		// relay branch above before `Apply` ever subtracts. So on a client `current >= max` is
		// permanently true and the augment fired on every shot of every magazine.
		//
		// ⚠️ IT IS BACK ON THE HOST, where the victim's health is real — `OnDamage` still computes
		// and applies it there. Only the attacker half needed to travel, and that is a synced
		// scalar now (`NZPlayer.FirstBloodLuck`), the same shape as the four drop multipliers.
		//
		// ⛔ AND TORTOISE'S RING SCALE CAME OUT FOR THE MIRROR-IMAGE REASON. It resolves
		// `RingOf( player )` by sweeping THIS MACHINE'S ring components — a fact about the WORLD,
		// not about the attacker — so it does NOT return 1 on the host and the client pre-applying
		// it was a genuine double whenever the shooter stood in a ring the host also knows about.
		//
		// ⚠️ WHICH RESTORES THE INVARIANT THIS METHOD'S SAFETY ARGUMENT DEPENDS ON: every term
		// below reads ONLY the attacker, so every one of them returns 1 against the host's
		// perk-less proxy and the host multiplying by one after the client cannot double anything.
		// Two terms had quietly stopped obeying it. Any term added here must be checked against it.
		var m = 1f;

		// ⚠️ BODY SHOT STILL GATES THE PERK TERMS FROM HERE, AND THAT IS NOT A DOUBLE. The node
		// means "no headshot bonus", and the two multipliers below are the SHOOTER'S — they exist
		// on no other machine, so the host cannot suppress them on this client's behalf. Reading
		// the node is free of side effects; it decides whether a term the client alone can apply
		// gets applied.
		var headBonus = headshot && !ImmuneToHeadshotBonus
			&& !TechEffects.Has( tech, "t4_bodyshot" );

		// ⛔ AND PRECISION ROUNDS AND DEADEYE CAME OUT OF THIS PRODUCT, WHICH WAS THE OTHER HALF
		// OF FIXING THEM. They were multiplied HERE because the host could not read them — the
		// method's own safety argument is that every term returns 1 against the host's copy of
		// the attacker, and with the tree unreplicated that was true of tech too. It is no longer:
		// `NZPlayer.TechNet` syncs the tree, so the host now applies both itself at the head
		// product in `OnDamage`, and leaving them here would have made every client headshot land
		// for the SQUARE of its node. Anything added to this method must be checked against that
		// same invariant — two terms had already stopped obeying it once before.
		if ( headBonus )
			m *= PerkEffects.HeadshotScaleFor( damage.Attacker )
				* NZombies.DeadshotAugments.HeadshotScale( damage.Attacker );

		// ⚠️ NAPALM'S m1 ACCELERANT IS A TERM HERE TOO. It rides the `burn` vulnerability already
		// applied to the victim and contributes only the delta up to x2.5 — but the question it
		// asks ("does the ATTACKER own m1") is the attacker's, so it belongs with the rest.
		//
		// ⚠️ MELEE-EXCLUDED, MATCHING THE HOST'S OWN GUARD. The knife path computes its damage
		// separately and this term is inside a `!LastHitWasMelee` block there.
		if ( !IsMelee( damage ) )
			m *= NZombies.FireAugments.BurnDamageBonus( damage.Attacker, GameObject );

		m *= NZombies.VigorAugments.DamageScale( damage.Attacker, WorldPosition );
		m *= NZombies.DeathAugments.BossScaleAgainst( damage.Attacker, GameObject );

		// ⚠️ MIDAS V GOLD STANDARD (ammo mod upgrade, 2026-10-06): a bullet from a Midas gun whose owner has level V deals ×(1 +
		// points earned / 1,000,000), the shooter's own scoreboard figure, which only this machine holds exactly.
		// ⛔ IT KEEPS THE INVARIANT ABOVE BY AN EXPLICIT TEST, NOT BY LUCK. Two of its three inputs DO reach the host — the level is
		// synced (`NZPlayer.AmmoUpgradeNet`) and the mod travels with this hit, the trap that caught the tech nodes — and only the
		// third, the earned figure, happens to be 0 on the proxy. `KillMods.GoldStandardScale` answers 1 for any body that is not
		// this machine's, and the host scales its own bullets in `OnDamage`, beside Vigor's terms.
		m *= NZombies.KillMods.GoldStandardScale( damage.Attacker, mod );

		return m;
	}

	/// <summary>Engine entry point — the bullet path calls this.</summary>
	public void OnDamage( in DamageInfo damage )
	{
		// ⚠️ OUTER SCOPE -- contains most of the dmg.* scopes below, so they do NOT sum with it.
		// Outer minus the sum of its inners is cost not yet attributed to anything, which is
		// how the next hot spot gets found.
		using var _cpu = CpuScope.Measure( "dmg.ondamage" );

		// ⛔ NO FRIENDLY FIRE, AND IT NEVER HAD A GUARD — IT SIMPLY HAD NO SYMMETRY EITHER, SO
		// ONLY HALF OF IT WAS VISIBLE. Reported as *"o host dá dano ao client mas o client não ao
		// host"*, which reads as a networking bug and is not one: players were shootable by
		// design-accident, and damage applies on the machine that resolved the bullet. The host
		// has authority over a client's body so its shots landed; a client has none over the
		// host's, so its shots evaporated. Removing the damage removes the asymmetry with it.
		//
		// ⚠️ HERE BECAUSE THIS METHOD IS THE ONE CHOKEPOINT, exactly as the relay below is: the
		// block at the top of this method already states that bullets, the knife, grenades, fire
		// pits and the augments all arrive through `OnDamage`. A guard at the bullet path alone
		// would have left a teammate killable with a grenade.
		//
		// ⚠️ ZOMBIES ARE UNAFFECTED BY CONSTRUCTION. They hurt a player through `Apply`, not
		// through here — the ⚠ note a few lines down says so — so this cannot make anyone
		// invulnerable to the thing that is supposed to kill them.
		//
		// ⚠️ AND SELF-DAMAGE STILL LANDS. `attacker == GameObject` is excluded, so your own
		// grenade and your own fall still hurt you — which PhD Flopper exists to negate and would
		// be pointless against a blanket player-immunity.
		if ( IsFriendlyFire( damage ) ) return;

		// ⚠️ THE WALL-IMPACT GUARD THAT WAS HERE IS GONE, AND ITS ABSENCE IS THE FIX RATHER THAN A
		// REGRESSION. It refused `"fall"`-tagged damage whose motion was horizontal — a filter on
		// the ENGINE's impact damage, keyed on a tag `Armor.Bypasses` itself calls unverified.
		// `ShoveGuard` now turns that engine damage off outright and generates landing damage from
		// downward speed alone, so nothing produces sideways impact damage for a guard to catch.
		// Filtering a source that no longer exists is a test that can only ever be wrong.

		// ── SOMEBODY ELSE'S BODY ───────────────────────────────────
		//
		// ⛔ A PROXY'S HEALTH IS A COPY, AND KILLING A COPY KILLS NOBODY. Zombies replicate
		// from the host, so a client shooting one runs this method against its own copy: that
		// zombie would drop dead on the client's screen, stay alive and chasing on the host's,
		// and then be resurrected the moment the next transform update arrived. Worse, the local
		// `Die()` would eventually destroy an object the client does not own.
		//
		// ⚠️ RELAYED RATHER THAN IGNORED. Refusing the damage here would be consistent and
		// would mean a client's bullets did nothing at all. The hit is real — it happened on a
		// body whose position the host itself sent — so it is forwarded to the machine entitled
		// to act on it.
		//
		// ⚠️ THIS IS THE ONE CHOKEPOINT FOR EVERY SOURCE. Bullets, the knife, grenades, fire
		// pits and the augments all arrive through `OnDamage`; putting the relay here rather than
		// at each caller is what stops the next damage source from silently being local-only.
		//
		// ⚠️ NOT YET TRUE OF DAMAGE TO PLAYERS. Zombies hurt a player through `Apply`, not
		// through here, and a player's health is still per-machine — that is section 9's job
		// (downs and revives) and it is deliberately not being half-done in passing.
		// ⛔ `IsProxy` WAS TOO NARROW HERE FOR THE SAME REASON IT WAS WRONG FOR ZOMBIES: a
		// zombie is `NetworkSpawn()`ed with no owner, so it is nobody's proxy, so a client's
		// bullets were quietly applying to its own copy after all. The rule is the authority
		// model stated plainly — **the host owns everything except each client's own body.**
		if ( NZGame.IsClient && !IsOwnPlayerBody )
		{
			// ⚠️ WIDE BORE PROMOTES ALONGSIDE LUCKY SHOT, and for the same reason it is done
			// HERE: everything downstream reads this one boolean. `||` short-circuits, so a real
			// headshot never pays for the distance test.
			// ⚠️ AND BULLSEYE (sniper tier 3, 2026-10-04), the same kind of promotion, decided on this machine like these two.
			var head = IsHeadshot( damage ) || WideBoreHead( damage ) || ClassTech.BullseyeHead( damage );

			// ⚠️ m1 LUCKY SHOT PROMOTES *HERE*, BEFORE THE FLAG IS SENT, so everything the
			// promotion is supposed to reach actually does: the head damage product, Focus's
			// streak, Trophy's points, Recycler's refund and the 100-point headshot award are all
			// decided from this one boolean on one machine or the other.
			if ( !head && NZombies.DeadshotAugments.RollLuckyShot( damage.Attacker ) )
				head = true;

			var relayTech = FiredBy( damage );

			// ⚠️ AND THE GUN'S AMMO MOD, RESOLVED ONCE (2026-10-06): it goes with the hit below, and Midas V's scale in `AttackerScale`
			// reads it. `AmmoMods.On` builds the catalogue to find it, and this runs per pellet.
			var relayMod = AmmoMods.BulletModOf( damage, relayTech );

			// ⛔ THE SHOOTER'S OWN MULTIPLIERS ARE APPLIED HERE OR NOWHERE. See `AttackerScale`.
			var scale = AttackerScale( damage, head, relayTech, relayMod );

			NZNet.HurtRemote( GameObject.Id,
				damage.Attacker.IsValid() ? damage.Attacker.Id : Guid.Empty,
				damage.Damage * scale,   // ⚠️ `relayed` below is this same figure — see its note
				damage.Position,
				head,
				IsMelee( damage ),
				// ⚠️ THE POINTS VERDICT TRAVELS WITH THE HIT, because the host is what pays and only
				// this machine knows which pellet of which shot this is. `ShotPoints` decided it a
				// few frames of call stack ago, in the bullet loop.
				Pays( damage ),
				// ⛔ AND SO DOES THE WEAPON — AS A PREFAB PATH, WHICH IS THE ONLY FORM OF IT THAT
				// CAN CROSS. Weapon prefabs are `NetworkMode.Never`, so the host has no object to
				// find and eight nodes read as unowned there until this argument existed. See
				// `TechEffects.TechRef`.
				relayTech.Prefab ?? "",
				// ⚠️ ONE STAT GOES WITH IT, FOR BOUNCY ROUNDS' LINK BUDGET, and it is the only
				// one: everything else the host asks is "does this tree own that id", which the
				// tree answers. `GetShootInfo` is a field read, so this costs nothing per pellet.
				NZombies.TechEffects.PenetrationOf( relayTech, damage ),
				// ⚠️ AND WHERE IT LANDED, FOR THE GORE. The host has no hitbox for a relayed hit, so only this machine can say an arm
				// or the head was struck (`GorePartOf`, `ZombieAI.Gore.cs`).
				GorePartOf( damage ),
				// ⚠️ AND THE AMMO MOD OF THE GUN THAT FIRED IT (2026-10-04), which only this machine knows: Bleeder bleeds it on
				// the host, and Midas and the kill mods read it off the corpse (`LastMod`). Bullets only.
				relayMod,
				// ⚠️ AND WHICH BULLET IT WAS, for Follow-Through alone (marksman tier 3, 2026-10-04): the host carries a kill's
				// leftover to the same bullet's next zombie (`ClassTech.FollowThroughCarry`). "" from every other gun.
				ClassTech.FollowThroughShot( relayTech, damage ) );

			// ⚠️ THE PER-HEADSHOT SIDE EFFECTS BELONG TO THE SHOOTER AND SO RUN ON THE SHOOTER.
			// Concussion stuns the zombie — which reaches the host now that `StatusEffects` relays
			// — and Focus's streak is the shooter's own state, which only this machine holds.
			if ( head )
			{
				NZombies.DeadshotAugments.TryConcuss( damage.Attacker, GameObject );
				NZombies.DeadshotAugments.OnHeadshotHit( damage.Attacker );
			}

			// ⛔ NAPALM NECTAR IS GATED ON `HasNapalm( attacker )`, WHICH IS FALSE ON A PROXY — so
			// the base ignite and every augment that modifies it did nothing whatever for a client.
			// Not one of the ten: no burn, no Wildfire, no Scorched Earth pit, no Chain Reaction.
			//
			// ⚠️ THE BURN ITSELF REACHES THE HOST because `StatusEffects` relays now; this only
			// has to decide to light it, on the machine that knows the perk is owned.
			//
			// ⚠️ MELEE-EXCLUDED, matching the host's own `!LastHitWasMelee` guard — the knife
			// ignites through `FireAugments.OnKnifeHit`, which already runs on the attacker.
			// ⚠️ NOT FOR THE PRISMA ON OBERON (2026-10-07): it marks him and hurts him not at all — a client's ignite or ammo mod
			// off its round would (`PrismaChain.Spares`; the host zeroes the hit itself)
			if ( !IsMelee( damage ) && !(NZombies.PrismaChain.FlatDamage( relayTech ) && NZombies.PrismaChain.Spares( GameObject )) )
			{
				if ( PerkEffects.HasNapalm( damage.Attacker ) )
				{
					NZombies.FireAugments.TryIgnite( damage.Attacker, GameObject );
					NZombies.FireAugments.TryPit( damage.Attacker, damage.Position );
				}

				// ⛔ AND EVERY AMMO MOD WAS DEAD FOR CLIENTS FOR A SECOND, INDEPENDENT REASON.
				// `AmmoMods.OnZombieHit` resolves the player from the attacker — a proxy — and then
				// asks `VultureAugments.HeldWeapon( player )`. Weapons are `NetworkMode.Never`, so
				// the host's copy of a client is holding NOTHING, and the method returns before it
				// reaches the roll. Cryofreeze, Dead Wire, Fireworks, Radioactive Decay, Blast
				// Furnace and Elemental Pop's M1 — none of them have ever fired for a client.
				//
				// ⚠️ AND THE COOLDOWN LIVES ON THE PLAYER (`AmmoModReady`), which is another thing
				// only the owner's own copy has. Rolling here is what makes it real state rather
				// than a dictionary on a ghost.
				NZombies.AmmoMods.OnZombieHit( damage.Attacker, GameObject, AmmoMods.IsBullet( damage ) );
			}

			// ⛔ THE PER-HIT HOOK BLOCK IS ONE SEAM, NOT THREE PERK BUGS. Four hooks sit
			// consecutively further down this method, below the `return` above, and every one of
			// them reads `damage.Attacker` to ask what that player owns — so for a client's bullet
			// all four ran on the host against a proxy with no perks and no weapon. Mule Kick's
			// M2 Overflow, Timeslip's M3 Fault Lines and M4 Chrono Rounds, and Vigor Rush's m4
			// Last Round: four augments across three perks, one cause, one place.
			//
			// ⚠️ THE BLOCK'S OWN COMMENT SAYS IT WAS GATHERED HERE so "the two bullet paths"
			// could not diverge. The divergence it did not anticipate is the MACHINE, not the path.
			//
			// ⚠️ NOT MELEE-GATED, matching the host's own placement — these are outside the
			// `!LastHitWasMelee` block there because a knife hit rolls them too.
			//
			// ⚠️ `relayed` IS THE FINAL FIGURE, which `TryLastRound` requires: it splashes what the
			// shot actually dealt, so it has to see the multipliers this branch just applied.
			var relayed = damage.Damage * scale;

			if ( relayed > 0f )
			{
				NZombies.MuleKickAugments.TryOverflow( damage.Attacker );
				NZombies.TimeAugments.OnZombieHit( damage.Attacker, GameObject );
				NZombies.TimeAugments.TryPit( damage.Attacker, damage.Position );
				NZombies.VigorAugments.TryLastRound( damage.Attacker,
					damage.Position, relayed, GameObject );
			}

			// ⛔ THE SHOOTER STILL GETS ITS NUMBER, AND WITHOUT THIS THE RELAY IS UNUSABLE.
			// Returning early skips everything below — including the damage number — so a client
			// shot a zombie and got no flinch, no figure and no hitmarker: *"zombies do not get
			// hit by client shots, or maybe do but there's no way to tell"*. Feedback the player
			// cannot see is the same as no feedback, and a mechanic nobody can observe cannot be
			// tested by anybody.
			//
			// ⚠️ IT IS THE CLIENT'S OWN FIGURE, NOT THE HOST'S VERDICT, and those can differ —
			// the host applies its copy of the attacker's perks, which do not replicate yet. This
			// is a hit CONFIRMATION, not a claim about the zombie's health. When player state
			// syncs the two converge; until then the number is honest about what this machine
			// computed and the health bar is the host's business.
			LastHitPosition = damage.Position;

			// ⛔ THE NUMBER MUST BE SCALED FOR *WHERE* IT LANDED, and it was not. `damage.Damage`
			// is the weapon's base figure; the head and limb multipliers are applied further down
			// this method — which the relay branch returns before ever reaching. So a client saw
			// the same figure for a headshot, a torso hit and a foot. User: *"when hitting a zombie
			// clientside, the damage numbers always display the base damage no matter where i
			// hit."*
			//
			// ⚠️ THE SAME TWO SCALARS THE HOST USES, read from the same fields, so the number a
			// client sees is the shape of the real one rather than a second invented rule. The
			// attacker's PERKS are still not in it — those live on the host's copy and do not
			// replicate — which the note above already says this figure is honest about.
			// ⚠️ THE NUMBER NOW INCLUDES THE SHOOTER'S OWN MULTIPLIERS, because the hit does. It
			// used to be honest about a figure that had no perks in it; that figure is no longer
			// what the zombie takes, and a damage number that disagrees with the damage is worse
			// than one that is merely incomplete.
			// ⚠️ AND THE TORTOISE RING THE SHOOTER STANDS IN (2026-09-27). The host applies it — it was
			// taken out of `AttackerScale` because it would double there — so it is not in `scale`, and a
			// client in a Dig In ring saw two-thirds of what it dealt. Every ring is mirrored here, so
			// this machine can read it; for the NUMBER only, never for the hit, which the host scales.
			// ⚠️ AND THE PER-CLASS AUGMENTS' HEAD, BODY AND VICTIM TERMS (2026-10-04) — One Shot One Kill, Silver Bullets, Point
			// Blank — which the host applies; here for the number only, like the ring.
			var shown = damage.Damage * scale * (head
				? HeadshotDamageScale * ClassTech.HeadScale( relayTech )
				: PartMultiplier( damage, relayTech ) * ClassTech.BodyScale( relayTech ))
				* ClassTech.VictimScale( relayTech, GameObject, damage.Position )
				* NZombies.TortoiseAugments.DamageScale( damage.Attacker );

			DamageNumbers.Report( this, shown, head );

			// ⛔ SO "NO DAMAGE NUMBERS" STOPS BEING TWO DIFFERENT BUGS. The client reported no
			// numbers at all, and that has two causes with opposite fixes: the hit never reached
			// this method (nothing to draw) or it did and the HUD did not draw it. One line here
			// separates them permanently — if this prints and no number appears, the fault is in
			// `DamageNumbers`; if it never prints, the shot never landed.
			//
			// ⚠️ THROTTLED, because an automatic weapon would otherwise write ten lines a
			// second. Two a second is enough to tell "hits are landing" from "nothing is".
			if ( _sinceHitLog > 0.5f )
			{
				_sinceHitLog = 0f;

				Log.Info( $"[nz-net] relayed a hit on '{GameObject.Name}' for {damage.Damage:0}"
					+ $"{(head ? " (head)" : "")} — damage number reported locally" );
			}

			return;
		}

		// ── PhD FLOPPER ──────────────────────────────────────────────────────
		// ⛔ BLOCKED HERE, IN OnDamage, AND DELIBERATELY NOT IN Apply. Zombies hurt
		// the player through `Health.Apply` (ZombieAI:3495); grenades, bullets and
		// anything else arrive through THIS method. Blocking in Apply would make
		// the player immune to zombies too — which is not a perk, it is a cheat.
		//
		// ⚠️ POSITIVE CHECK ON THE ATTACKER, not "nobody uses OnDamage but the
		// grenade". If zombies are ever routed through OnDamage — for hitboxes,
		// say — an absence-based test would silently make PhD block zombie damage.
		// Asking "is the attacker a zombie" stays correct through that change.
		if ( PhdBlocks( damage ) )
		{
			Log.Info( $"[nz-perk] PhD Flopper absorbed {damage.Damage:0.#}" );
			return;
		}

		LastHitWasMelee = IsMelee( damage );
		LastHitPays = Pays( damage );
		LastHitForced = false;
		LastHitPosition = damage.Position;
		LastAttacker = damage.Attacker;
		LastHitPart = GorePartOf( damage );

		// ⚠️ THE WEAPON IS LATCHED TOO, and it was not until 2026-08-20. `DamageInfo`
		// has carried a `Weapon` field all along and this method only ever read
		// `Attacker`, so anything asking "what killed this" after the fact could name
		// the player but not the gun. Bounty had to fall back to whatever the killer
		// happens to be HOLDING, which with Mule Kick reads the wrong weapon's tech tree
		// whenever a gun is swapped away between the shot and the death.
		//
		// ⚠️ Populated for BULLETS ONLY, because `DamageInfo.FromBullet` is the only
		// thing that sets it — the knife, grenades, traps and status effects hand-build
		// their DamageInfo and leave it null. That is correct rather than a gap: tech is
		// per weapon prefab, so a grenade must not read a rifle's tree.
		LastWeapon = damage.Weapon;

		// ⚠️ AND BULLSEYE (sniper tier 3, 2026-10-04): a tag the shooter's own bullet carries, so a client's arrives already
		// decided, in `head`, like Wide Bore's.
		var headshot = IsHeadshot( damage ) || WideBoreHead( damage ) || ClassTech.BullseyeHead( damage );

		// ⛔ DEADSHOT'S m1 "LUCKY SHOT" PROMOTES A HIT TO A HEADSHOT *HERE*, at the one
		// place headshots are decided, and that is what makes the promotion complete rather
		// than cosmetic. Everything downstream reads this local: the head damage product,
		// Focus's streak, Trophy's points, Recycler's refund, Cranial Detonation's blast and
		// the 100-point headshot kill award. Scaling the damage instead would have handed
		// over the multiplier and none of the rest.
		//
		// ⚠️ SHORT-CIRCUITED ON A REAL HEADSHOT, so the roll only ever promotes. Rolling on
		// a genuine headshot as well would be a 90% chance of DEMOTING one, which is the
		// opposite augment.
		if ( !headshot && NZombies.DeadshotAugments.RollLuckyShot( damage.Attacker ) )
			headshot = true;

		var amount = damage.Damage;

		// ⚠️ THE FIRING WEAPON IS RESOLVED ONCE, then handed to both the head product
		// and PartMultiplier. Four tech nodes now read it on a single hit, and every
		// pellet of a 16-pellet shotgun blast is its own DamageInfo through this method.
		var tech = FiredBy( damage );

		// ⚠️ FOLLOW-THROUGH (marksman tier 3, 2026-10-04): a kill earlier along this same bullet left damage over, and it
		// joins this hit's raw figure, so this victim's own multipliers scale it like the rest of the round. Kept as the
		// bullet's figure for the kill test after `Apply`.
		amount += ClassTech.FollowThroughCarry( tech, damage, this );
		var bullet = amount;

		// ⛔ DAMAGE YOU DO TO YOURSELF TAKES NONE OF YOUR OWN DAMAGE BONUSES (2026-10-03). A fall (`ShoveGuard.Land`) and your
		// own grenade arrive here with YOU as the attacker, so every attacker-side term below read them as your hit on a target:
		// Vigor Rush's M3 Point Blank saw a victim at zero range and DOUBLED it — a 24-damage landing took 49 in the round-88
		// game — and its M4/m5, Deadshot's First Blood and Tortoise's ring bonus would scale it the same way. Those multiply the
		// damage you deal; none of them is meant to make you hurt yourself harder. Victim-side terms are untouched.
		var selfInflicted = damage.Attacker.IsValid() && damage.Attacker.Root == GameObject.Root;

		// ⛔ LATCHED FOR THE DEATH AWARD, WHICH RUNS AFTER THE DamageInfo IS GONE. `ZombieAI`
		// pays Bounty out of the weapon that killed, and it used to rebuild that from `LastWeapon`
		// — which is `damage.Weapon`, so it was null for every client hit and then fell through to
		// `Rarity.HeldBy`, which resolves a component on a body whose weapons are not on this
		// machine either. Two dead ends for the same reason; one ref fixes both.
		LastTech = tech;

		// ⚠️ AND THE HIT'S AMMO MOD (2026-10-04) — this machine's own gun, or the one a client's hit was sent with.
		LastMod = AmmoMods.HitModOf( damage, tech );

		// ⛔ DEADSHOT'S M2 "FIRST BLOOD" IS READ BEFORE ANYTHING SUBTRACTS, which is the
		// whole requirement. It tests whether the victim is UNDAMAGED, so it has to be
		// sampled while `Current` is still the pre-hit value — evaluating it after the
		// subtraction would make it fire only on kills that left the zombie at full health,
		// i.e. never.
		//
		// ⚠️ NOT HEADSHOT-GATED, unlike the original's. Any first hit on a fresh zombie
		// triples, which is what makes it a different major from Deadeye rather than a
		// bigger one.
		//
		// ⚠️ STORED, NOT APPLIED YET. It multiplies below alongside the head product so the
		// two compose in a documented order rather than one silently preceding the other.
		//
		// ⛔ NOT FOR DAMAGE YOU DO TO YOURSELF — see `selfInflicted`. A fall at full health is a "first hit on an undamaged
		// target" in every sense this test can see, and would have tripled.
		var firstBlood = selfInflicted ? 1f : NZombies.DeadshotAugments.FirstBloodScale(
			damage.Attacker, Current, Max );

		// ⛔ INSTA-KILL: ANY hit from a player kills a zombie outright.
		//
		// ⚠️ ZOMBIES ONLY. This is the shared Health component — the PLAYER carries
		// one too, and scaling damage here without the check would have Insta-Kill
		// kill YOU in one hit, which is the opposite of the powerup.
		//
		// ⚠️ Applied as damage rather than a direct kill so the whole death path
		// still runs: the melee flag, the headshot bonus, ragdolls, sounds and the
		// round's alive count all hang off `OnDamage` finishing normally.
		//
		// ⛔ UNLESS THE VARIANT SAYS OTHERWISE. The napalm, the shrieker, Brutus and Oberon carry an
		// `InstaKillMultiplier` and take that many times the hit instead of dying from it — see the
		// property. Everything else is killed exactly as before.
		//
		// ⚠️ APPLIED ONCE, ON THE MACHINE THAT OWNS THE ZOMBIE. The client relay branch above returns
		// before this line, so a client's hit reaches the host raw (times the shooter's own
		// multipliers only) and is multiplied here, once — the same route the kill has always taken.
		// Scaling it on both ends would have made it nine.
		if ( amount > 0f && PowerupEffects.InstaKill )
		{
			var ai = Components.Get<ZombieAI>( FindMode.EverythingInSelfAndAncestors );
			if ( ai.IsValid() )
			{
				// ⚠️ A STAND-IN WHEN IT IS `Max × 10` (not a variant's real multiple): see `LastHitDamage`.
				var scale = PowerupEffects.InstaKillScale( ai.Variant );
				amount = scale is float s ? amount * s : Max * 10f;
				LastHitForced |= scale is null;
			}
		}

		// ⛔ VIGOR RUSH'S M2 "EXECUTIONER" USES THE SAME OVERKILL-DAMAGE IDIOM AS INSTA-KILL
		// ABOVE, AND FOR THE SAME REASON its own comment gives: applied as damage rather
		// than a direct kill so the whole death path still runs — the melee flag, the
		// headshot bonus, ragdolls, sounds, the round's alive count and every on-kill augment
		// all hang off `OnDamage` finishing normally. A `Current = 0` here would skip all of
		// it.
		//
		// ⚠️ THE THRESHOLD IS TESTED AGAINST THE PRE-HIT HEALTH, which is what "below 20%"
		// means. `Current` has not been touched yet at this line.
		//
		// ⚠️ ZOMBIES ONLY, checked the same way — this is the shared Health component and the
		// player carries one too.
		if ( amount > 0f
			&& NZombies.VigorAugments.Executes( damage.Attacker, Current, Max )
			&& Components.Get<ZombieAI>( FindMode.EverythingInSelfAndAncestors ).IsValid() )
		{
			amount = Max * 10f;
			LastHitForced = true;
		}

		// ⛔ HEADSHOT DAMAGE IS SCALED HERE, NOT PER-WEAPON. The original gamemode
		// does exactly this, once, for every weapon:
		//
		//   sh_constructor.lua:
		//     if hitgroup == HITGROUP_HEAD and v != "walker_necromorph"
		//        and v != "special_bot" and v != "walker_xeno" then
		//         dmginfo:ScaleDamage(2.5)
		//     end
		//
		// ⚠️ 2.5, NOT 2.0 — ARC9's per-weapon BodyDamageMults says 2, but nZombies
		// overrides it gamemode-side, so ported weapons must NOT carry their own
		// head multiplier or the two compound.
		//
		// ⚠️ Every OTHER hitgroup in the original drives gibbing, stun and
		// decapitation — NOT damage. There is no arm/leg damage multiplier to port.
		// ⚠️ Death Perception SCALES THIS MULTIPLIER (x1.5), it does not add to
		// it — 2.5x becomes 3.75x. Applied here rather than on the weapon so it
		// rides on whatever head scale is in play, and read off the ATTACKER
		// because this component is on the zombie.
		// ── PRECISION ROUNDS (`t2_headshot`) ─────────────────────────────────
		//
		// ⚠️ A THIRD MULTIPLY ON THE SAME PRODUCT, alongside Death Perception, for
		// the same reason: it rides on whatever head scale is in play instead of
		// replacing it, and it is resolved from the WEAPON rather than from this
		// component — Health is on the zombie, so a `Components.Get` here finds
		// nothing and the node would silently do nothing.
		//
		// ⛔ IT STACKS ON A LIVE DATA BUG AND ITS MAGNITUDE CANNOT BE JUDGED UNTIL
		// THAT IS FIXED. AWM, G3, SVD and WA2000 author `HeadMultiplier: 2.0` in
		// their prefabs, which `ShootInfo.DamageFor` applies IN ADDITION to the 2.5
		// here — so those four already deal x5 headshots while the other 27 deal
		// x2.5, and WeaponTech's own `t2_headshot` row warns about the same
		// compounding. Not fixed here: the fault is in four prefabs, and papering
		// over it in this method would hide it from whoever does fix it.
		//
		// ── DEADEYE (`t5_deadeye`) ───────────────────────────────────────────
		//
		// ⚠️ A FOURTH MULTIPLY ON THE SAME PRODUCT, for the reason the two blocks above
		// give: it rides on whatever head scale is in play rather than replacing it, and
		// it is resolved from the WEAPON because this component is on the zombie.
		//
		// ⚠️ AN IMMUNE ZOMBIE GIVES DEADEYE NOTHING while still charging its x0.5 body
		// penalty, so Deadeye against a necromorph, special_bot or xeno is pure
		// downside. That falls out of the existing immunity rule rather than being
		// chosen here, and it is coherent: the node's whole purchase is head damage.
		//
		// ⛔ THE PIERCE HALF OF THIS NODE IS STILL NOT WIRED, BUT IT IS NO LONGER BLOCKED.
		// Penetration is a ShootInfo field, not one of Health's, so the head and body
		// halves land here and the pierce lands nowhere — owning Deadeye today still gets
		// two thirds of what the catalogue promises.
		//
		// ⚠️ WHAT CHANGED: `NZPlayer.ApplyTech` now writes `PenetrationDepth` from
		// `basePen * penFactor + penBonus`, so granting Deadeye its pierce is one term in
		// that expression rather than a new system. Double Tap's m2 already rides it.
		//
		// ⛔ WHAT IS MISSING IS A NUMBER, NOT A MECHANISM. The node row says only "and
		// pierce" — `WeaponTech.cs:517` names no magnitude, and `Penetration` is already
		// TRUE on all 31 prefabs, so "pierce" has to mean "more pierce" by some amount
		// nobody has specified. Inventing one here would put an unsourced value in a
		// catalogue whose whole discipline is that its numbers come from somewhere.
		//
		// ── BODY SHOT (`t4_bodyshot`) ────────────────────────────────────────
		//
		// ⛔ THE WHOLE HEAD PRODUCT IS SKIPPED, NOT JUST HeadshotDamageScale. The node
		// says the bonus is lost ENTIRELY, and neutralising only the 2.5 would leave
		// Death Perception's x1.5 and Precision Rounds' multiplier still riding on the
		// head — so the head would stay the best place to shoot on a node whose entire
		// trade is "stop aiming". A player owning Precision Rounds AND Body Shot on one
		// weapon therefore loses the tier-2 pick outright; that is the price of a
		// contradictory build, not a bug, and WeaponTech's tier-5 header says in as many
		// words that nothing forbids a bad pair.
		//
		// ⚠️ IT REUSES THE ImmuneToHeadshotBonus PATH rather than adding a branch,
		// because for this hit the two mean the same thing: no head bonus. A head hit
		// then takes NEITHER the head product NOR PartMultiplier — head is deliberately
		// absent from that table — and lands at x1, below the x1.5 torso. That inversion
		// IS the node.
		// ⚠️ THIS HALF IS UNCHANGED BY THE REDESIGN and is now the node's ONLY presence
		// in this file: the head multiplier is forced to 1 by skipping the whole product
		// below. Neutralising just `HeadshotDamageScale` would leave Death Perception and
		// Precision Rounds still riding on the head, so the head would stay the best place
		// to shoot on a node whose entire trade is that it should not be.
		var headBonus = headshot && !ImmuneToHeadshotBonus
			&& !TechEffects.Has( tech, "t4_bodyshot" );

		// ⚠️ DEADSHOT'S M1 AND M4 ARE A FIFTH MULTIPLY ON THE SAME PRODUCT, for the reason
		// the four blocks above give: they ride whatever head scale is in play rather than
		// replacing it, so a player keeps their base 2.5, Death Perception and every node
		// they own. Both come through one call because both are headshot-only.
		// ⚠️ THE WHOLE if/else CHAIN IS WRAPPED, not just the if-branch: a using-block around
		// only the branch would separate the `else` from its `if` and stop compiling. So dmg.part
		// nests inside this scope in the else case -- those two do not sum.
		// ⛔ A FLAT WEAPON SKIPS THE WHOLE PRODUCT, BOTH BRANCHES. Neutralising only the headshot
		// half would leave `PartMultiplier` scaling limb shots down, so the gun would still care
		// where it hit — just in the other direction. The Prisma deals its number wherever it
		// lands, which is the point of a round that plants a fuse rather than doing the killing.
		//
		// ⚠️ THE WHOLE if/else CHAIN IS INSIDE THE GUARD for the reason the note below gives
		// about the `using`: splitting an `if` from its `else` does not compile.
		var flat = NZombies.PrismaChain.FlatDamage( tech );

		// ⛔ THE PRISMA DEALS OBERON NOTHING (2026-10-07, the user: *"make it so the prisma is unable to damage the boss oberon, only applying the debuff that does not damage but allows it to get more damaged"*). Zeroed here, before
		// every multiplier, so nothing below can turn it back into damage; its fuse still lights (`PrismaChain.OnHit`, off the
		// round's own figure), and the Napalm ignite and the ammo mods do not roll off it (the non-melee block). With nothing
		// dealt there is no hit reaction and no points, which is the request: it marks him, it does not hurt him.
		var spared = flat && NZombies.PrismaChain.Spares( GameObject );
		if ( spared ) amount = 0f;

		using ( CpuScope.Measure( "dmg.headscale" ) )
		{
			if ( flat ) { /* no head bonus, no part multiplier */ }
			else
			// ⚠️ AND THESE TWO NOW APPLY TO A CLIENT'S SHOTS AS WELL, which is what makes this
			// the ONLY place they are applied. The relay used to pre-multiply them on the shooter
			// because the host could not read the tree; it can, so `AttackerScale` no longer
			// does — the two edits are one change and must not be separated.
			if ( headBonus )
				amount *= HeadshotDamageScale
					* PerkEffects.HeadshotScaleFor( damage.Attacker )
					* NZombies.DeadshotAugments.HeadshotScale( damage.Attacker )
					* TechEffects.Factor( tech, "t2_headshot" )
					* TechEffects.Factor( tech, "t5_deadeye" )
					// ⚠️ THE PER-CLASS AUGMENTS' HEAD (2026-10-04): every `s.head`, and One Shot One Kill's x5.
					* ClassTech.HeadScale( tech );
			else if ( !headshot )
				amount *= PartMultiplier( damage, tech )
					// ⚠️ and One Shot One Kill's fifth on everything else.
					* ClassTech.BodyScale( tech );
		}

		// ⚠️ VIGOR RUSH'S M3, M4 AND m5 MULTIPLY HERE, beside First Blood, because all four
		// are conditions on the finished hit rather than on where it landed. The perk's own
		// base multiplier is elsewhere — in the bullet-tagged block below — deliberately:
		// the base is bullets only, whereas these apply to whatever the perk applied to.
		// ⛔ NOT ON YOURSELF — `selfInflicted` (M3 doubled every fall).
		using ( CpuScope.Measure( "dmg.vigor" ) )
			if ( !selfInflicted )
				amount *= NZombies.VigorAugments.DamageScale( damage.Attacker, WorldPosition );

		// ⚠️ MIDAS V GOLD STANDARD (ammo mod upgrade, 2026-10-06) BESIDE THEM, the shooter's own term on the finished hit: a bullet
		// from a Midas gun whose owner has level V deals ×(1 + points earned this game / 1,000,000). The user: *"tier V: your damage
		// scales with the ammount of points you have had in the entire game / 100% for each 1000000, measured from the points in
		// the scoreboard"*. `LastMod` is this hit's mod, latched above, so only that gun's bullets.
		// ⛔ ONCE A HIT. The host's own bullets (solo included) take it HERE; a client's arrive with it already in, from the
		// shooter's machine (`AttackerScale`), and read ×1 here, because `KillMods.GoldStandardScale` answers only for this
		// machine's own body. Before the helmets and Last Round, as a client's figure has it.
		// ⛔ And like Vigor's, never on yourself — `selfInflicted`.
		using ( CpuScope.Measure( "dmg.ammomods" ) )
			if ( !selfInflicted )
				amount *= KillMods.GoldStandardScale( damage.Attacker, LastMod );

		// ⚠ TORTOISE'S RING BONUS SITS BESIDE VIGOR'S for the same reason: both are damage terms
		// read off the ATTACKER, and both must be in before the subtraction. M1's x1.5 and M4's
		// kill stacks resolve together inside that one call. ⛔ And like Vigor's, never on yourself.
		float ringScale;
		using ( CpuScope.Measure( "dmg.tortoise" ) )
			ringScale = selfInflicted ? 1f : NZombies.TortoiseAugments.DamageScale( damage.Attacker );

		amount *= ringScale;

		// ⛔ BRUTUS'S HELMET TABLE, AND DEATH PERCEPTION'S BOSS MULTIPLIER, BOTH LAND HERE. Both are
		// conditions on the finished hit rather than on where it landed, which is the same argument
		// the two blocks above give for sitting at this point in the chain.
		//
		// ⛔ THE HELMET CALL MUTATES AND MUST HAPPEN EXACTLY ONCE PER HIT. `ScaleOn` decrements the
		// helmet as well as returning the multiplier, because splitting those into two calls would
		// either chip it twice or compute the scale against a helmet that already absorbed this same
		// bullet. Do not "tidy" it into a query plus an apply.
		//
		// ⚠️ AND IT IS GIVEN THE PRE-SCALE `amount`, NOT the raw DamageInfo value. The helmet is
		// meant to absorb the shot the player actually fired - so it should see rarity, Pack-a-Punch,
		// Double Tap and every tech node, which are all already folded in by this line. What it must
		// NOT see is its own 1.5% reduction, which is why this is the argument and not the result.
		using ( CpuScope.Measure( "dmg.helmet" ) )
			amount *= NZombies.BrutusHelmet.ScaleOn( GameObject, amount, headshot );

		// ⛔ THE MARGWA'S TABLE (2026-10-06), the helmet's shape and its exactly-once rule: an open mouth takes 0.75, everything
		// else 0.01, and the hit that finds a mouth at the right health takes that head off. Judged by WHERE it landed (the
		// latched `damage.Position` against the open head's bone, upstream's own test), so a blast or a body shot reads as body.
		using ( CpuScope.Measure( "dmg.margwa" ) )
			amount *= NZombies.MargwaBoss.ScaleOn( GameObject, damage.Position, damage.Attacker );

		// ⛔ AVOGADRO'S TABLE (2026-10-06), the same exactly-once rule: x0.1 for everything, and a melee hit x2 that stuns him —
		// one counted every five seconds, x0 between (`AvogadroBoss.ScaleFor`).
		using ( CpuScope.Measure( "dmg.avogadro" ) )
			amount *= NZombies.AvogadroBoss.ScaleOn( GameObject, IsMelee( damage ), damage.Attacker );

		// ⛔ THE DIRECTOR'S TABLE (2026-10-06): x0.095 for everything, and a player's hit asks for his rage (`DirectorBoss.ScaleFor`
		// sets a flag; the rage itself starts outside this call).
		using ( CpuScope.Measure( "dmg.director" ) )
			amount *= NZombies.DirectorBoss.ScaleOn( GameObject, damage.Attacker );

		// ⛔ THE PANZER SOLDAT'S ARMOUR (2026-10-06), the helmet's exactly-once rule AND its argument: the pre-scale `amount`,
		// which the faceplate or the power core takes raw when the hit lands on it (`PanzerBoss.ScaleFor`).
		using ( CpuScope.Measure( "dmg.panzer" ) )
			amount *= NZombies.PanzerBoss.ScaleOn( GameObject, amount, damage.Position, damage.Attacker );

		// ⛔ THE THIRD BATCH OF BOSSES (2026-10-06): one hook for all of them — each one built on `BossBase` answers its own hit
		// through one virtual (Brenner's x0.25 off anything but bullets and blades, Zaballa's x0.75, the Thrasher's spore sacs …),
		// under the helmet's exactly-once rule, since it may count the hit (`BossBase.ScaleOn`).
		using ( CpuScope.Measure( "dmg.boss3" ) )
			amount *= NZombies.BossBase.ScaleOn( GameObject, amount, damage );

		// ⛔ THE NAPALM ZOMBIE'S ARMOUR, WHICH IS OPEN ONLY WHILE IT IS CHARGING. Same shape as the
		// helmet line above and for the same reason — a victim-side condition on the finished hit —
		// but this one is a pure QUERY and mutates nothing, so it does not carry the
		// exactly-once warning that one does.
		//
		// ⚠️ IT SCALES THE BLAST AND THE FIRE TOO, not just bullets, because `OnDamage` is the one
		// entry every source funnels through. That is correct: a napalm zombie standing in another's
		// fire pool should be as hard to burn as it is to shoot.
		//
		// ⚠️ IT TAKES THE ATTACKER because Cryofreeze doubles it, and the mod lives on the gun the
		// attacker is holding. Damage with no attacker — a fire pool, a fall — simply reads as not
		// cryo, which is right.
		using ( CpuScope.Measure( "dmg.napalm" ) )
			amount *= NZombies.NapalmZombie.ScaleOn( GameObject, damage.Attacker );

		// ⚠️ M1 BOSS SLAYER WAS CATALOGUED AND UNWIRED UNTIL A BOSS EXISTED; Brutus is that boss and
		// this is the line that pays it — x2 as of 2026-09-13, down from x3. `BossScaleAgainst`
		// returns 1 for a non-boss and for a player who does not own it, so it costs a component
		// lookup and nothing else.
		using ( CpuScope.Measure( "dmg.boss" ) )
			amount *= NZombies.DeathAugments.BossScaleAgainst( damage.Attacker, GameObject );

		// ⚠️ SILVER BULLETS AND POINT BLANK (2026-10-04): who was hit and from how close — conditions on the finished hit,
		// beside Boss Slayer for the reason the blocks above give.
		using ( CpuScope.Measure( "dmg.tech" ) )
			amount *= ClassTech.VictimScale( tech, GameObject, HitPositionOr() );

		// ⚠️ AFTER the head product and the limb table, so First Blood triples whatever the
		// hit was already worth rather than replacing the location bonus. A headshot on a
		// fresh zombie therefore gets both — which is the intended reward for opening on a
		// new target with a good shot.
		amount *= firstBlood;

		// ⚠️ m3 CONCUSSION FIRES ON THE HIT, NOT ON THE KILL, so it can stagger a survivor
		// — which is the point of a stun. It is rolled before the damage lands because a
		// killing blow would leave nothing to stun, and `TryConcuss` resolves the ZombieAI
		// itself rather than trusting this component's object.
		// ⚠️ ONE SCOPE COVERS BOTH DEADSHOT CALLS IN THIS BLOCK. RollLuckyShot and FirstBloodScale
		// ABOVE ARE NOT COVERED: one sits in an `if` condition and the other in a declaration, and
		// hoisting either into a local would change when its RNG runs. They show up as
		// unattributed time inside dmg.ondamage rather than being silently counted here.
		using ( CpuScope.Measure( "dmg.deadshot" ) )
		if ( headshot )
		{
			NZombies.DeadshotAugments.TryConcuss( damage.Attacker, GameObject );

			// ⚠ M4 FOCUS BUILDS ON THE HIT, beside Concussion, for the same reason and in
			// the same block: both are per-headshot side effects rather than damage terms, and
			// both have to run before anything can early-return on a death - a headshot that
			// kills still counts toward the streak.
			//
			// ⚠ BELOW the `headBonus` term above, so this shot is scaled by the streak it
			// arrived with and the increment lands for the next one.
			NZombies.DeadshotAugments.OnHeadshotHit( damage.Attacker );
		}

		// ⚠️ MULE KICK'S M2 "OVERFLOW" ROLLS ON ANY DAMAGING HIT, headshot or not, and on
		// hits that do not kill — it is a sustain augment, not a reward for accuracy. It sits
		// beside Concussion because both are per-hit side effects rather than damage terms,
		// and both must run before anything can early-return on a death.
		using ( CpuScope.Measure( "dmg.mulekick" ) )
		if ( amount > 0f )
			NZombies.MuleKickAugments.TryOverflow( damage.Attacker );

		// ══ TIMESLIP TONIC — M4 CHRONO ROUNDS AND M3 FAULT LINES ══════════════════
		//
		// ⚠ BESIDE MULE KICK'S OVERFLOW, because all three are per-hit side effects rather than
		// damage terms, and this is the one place every damaging route funnels through. Hooking
		// the two bullet paths separately is what left an earlier Vigor Rush helper uncalled
		// entirely — its own comment warned about exactly that.
		//
		// ⚠ BEFORE THE SUBTRACTION, so a killing shot still stacks and still rolls. A zombie
		// that dies to this hit having its speed reduced is harmless; a pit that failed to spawn
		// because the shot happened to kill would make the 1% quietly lower than 1%.
		using ( CpuScope.Measure( "dmg.time" ) )
		if ( amount > 0f )
		{
			NZombies.TimeAugments.OnZombieHit( damage.Attacker, GameObject );
			NZombies.TimeAugments.TryPit( damage.Attacker, HitPositionOr() );
		}

		// ⚠️ THE PER-CLASS WEAPON TECH'S HIT EFFECTS (2026-10-04): the stuns, Suppressive Fire's slow, Spotter's and Marker's
		// marks — per-hit side effects like the two above, here on the host, where a client's hit arrives too.
		using ( CpuScope.Measure( "dmg.tech" ) )
		if ( amount > 0f )
			ClassTech.OnZombieHit( tech, damage.Attacker, HitPositionOr(), GameObject );

		// ⚠️ VIGOR RUSH'S m4 "LAST ROUND" SPLASHES THE FINISHED AMOUNT, so it carries every
		// multiplier the shot earned — including the perk's own and any augment above. The
		// original splashed `dmg:GetDamage()`, which in its damage model is the same thing.
		//
		// ⚠️ AFTER the amount is final and BEFORE the subtraction, so a killing shot still
		// splashes what it dealt.
		using ( CpuScope.Measure( "dmg.vigor" ) )
		if ( amount > 0f )
			// ⛔ WITHOUT THE RING (2026-09-27). Each splash hit comes back through `OnDamage` with the same
			// attacker, standing in the same ring, and gets the ring's bonus there — so splashing the
			// ringed amount gave a HOST shooter the bonus twice (×2.25 in Dig In), while a client's
			// splash, built from `relayed`, which has no ring in it, got it once.
			NZombies.VigorAugments.TryLastRound( damage.Attacker,
				HitPositionOr(), ringScale > 0f ? amount / ringScale : amount, GameObject );

		// ── VIGOR RUSH ───────────────────────────────────────────
		//
		// ⛔ APPLIED HERE, AND UNTIL NOW APPLIED NOWHERE. Vigor Rush's multiplier
		// was read by exactly one thing — WeaponStatsPanel — so the damage figure on
		// the stats screen doubled while every bullet did its normal damage. Double
		// Tap and Deadshot were dead the same way; see PATTERNS OF MISTAKES §2.
		//
		// ⛔ BULLETS ONLY, AND THE TAG IS WHAT MAKES THAT EXACT. `TagsHelper.Bullet`
		// is stamped in ONE place in the whole codebase — `DamageInfo.FromBullet`,
		// which both bullet paths funnel through and nothing else calls. So this test
		// is not a convention that could drift: the knife, grenades, traps, status
		// effects and the test commands all build their DamageInfo by hand and cannot
		// accidentally acquire it. The original's Vigor Rush is "double bullet damage",
		// and widening it to everything would quietly make it the best perk in the game.
		//
		// ⚠️ ATTACKER-SIDE, because this component is on the ZOMBIE — the same
		// reason Death Perception and Napalm Nectar below read damage.Attacker. A
		// Components.Get<NZPlayer> here finds nothing and the perk silently does
		// nothing, which is the trap PerkEffects.HeadshotScaleFor records.
		//
		// ⚠️ BEFORE the vulnerability and incendiary multipliers, so a burning
		// zombie's bonus scales the perked damage rather than the base. Multiplication
		// commutes, so the order does not change the result — it is grouped this way
		// because the shooter's own perks belong together, above the victim's state.
		if ( amount > 0f && damage.Tags is not null
			&& damage.Tags.Has( SWB.Shared.TagsHelper.Bullet ) )
		{
			using ( CpuScope.Measure( "dmg.perk" ) )
				amount *= PerkEffects.BulletDamageFor( damage.Attacker );
		}

		// ── NAPALM NECTAR ────────────────────────────────────────────────────
		// Ported from enemies/sv_hooks.lua:485-560. See Docs/NAPALM_NECTAR.md.
		//
		// ⚠️ ALREADY BURNING IS CHECKED FIRST, and applies to EVERY source — the
		// original's `NapalmVulnMult` is a property of the victim, not of who is
		// shooting it. A second player shooting a burning zombie gets the bonus
		// too, which is the point of lighting things up in co-op.
		using ( CpuScope.Measure( "dmg.status" ) )
			amount *= StatusEffects.VulnerabilityOf( GameObject );

		// ⚠️ CRYOFREEZE III'S SHATTER IS DECIDED HERE, BEFORE THE AMMO MODS ROLL (2026-10-05, the review), as the freeze's own ×1.3
		// just was: a host's bullet that procs Cryofreeze freezes this zombie inside the roll below, and must not then shatter
		// it at `Apply` — "the NEXT hit", and a client's proccing hit never could (`Cryofreeze.Shatters`).
		var shatters = Cryofreeze.Shatters( this );

		// ══ NAPALM NECTAR ═════════════════════════════════════════════════
		//
		// ⛔ ONE CALL NOW. This was thirty lines implementing TWO unrelated mechanics — a 1-in-6
		// double-damage roll with no fire in it, and a per-zombie hit counter (5 hits then 1-in-3)
		// that did the igniting — with the counter living on this component as `_napalmHits`. It
		// froze while the target burned and reset on each ignition, so the real cost was ~7 hits on
		// ONE zombie and spraying a crowd built five counters and lit nothing.
		//
		// All of it moved to `FireAugments`, so the base ignite and the nine augments that modify
		// it are in one file — and so the knife can ignite through the same cooldown.
		//
		// ⚠ m1 ACCELERANT RIDES THE VULNERABILITY APPLIED ABOVE, not this call. That line has
		// already multiplied in the `burn` rule's x2, so the augment contributes only the delta up
		// to x2.5. See `FireAugments.BurnDamageBonus`.
		// ⚠️ NOT FOR THE PRISMA ON OBERON (`spared`, 2026-10-07): its ignite and its ammo mod would hurt him by the back door
		if ( !LastHitWasMelee && !spared )
		{
			using ( CpuScope.Measure( "dmg.fire" ) )
				amount *= NZombies.FireAugments.BurnDamageBonus( damage.Attacker, GameObject );

			using ( CpuScope.Measure( "dmg.fire" ) )
			if ( PerkEffects.HasNapalm( damage.Attacker ) )
			{
				NZombies.FireAugments.TryIgnite( damage.Attacker, GameObject );
				NZombies.FireAugments.TryPit( damage.Attacker, HitPositionOr() );
			}

			// ⚠️ AMMO MODS ROLL HERE, INSIDE THE SAME NON-MELEE BLOCK. An ammo mod is
			// ammo, so a knife must not proc one — and the melee exclusion already
			// wraps this block for Napalm's benefit, which is why the call belongs in here
			// rather than beside it.
			//
			// ⚠️ OUTSIDE THE NAPALM GATE, though. That `if` above tests for the perk;
			// ammo mods are bought from a machine and belong to nobody's perk.
			using ( CpuScope.Measure( "dmg.ammomods" ) )
				AmmoMods.OnZombieHit( damage.Attacker, GameObject, AmmoMods.IsBullet( damage ) );
		}

		// ⚠️ THE PRISMA'S FUSE, BESIDE THE AMMO MODS AND NOT INSIDE THEM. It is the weapon's
		// own behaviour rather than a mod fitted to it, so it has no proc chance, no cooldown and
		// no interaction with what is in the ammo-mod slot — a Prisma with Dead Wire does both.
		// `PrismaChain.OnHit` checks the weapon and returns immediately for every other gun.
		//
		// ⚠️ `damage.Damage`, THE ROUND'S OWN FIGURE, NOT `amount`. A special's fuse ticks a share of
		// the WEAPON'S damage (`ZombieVariant.ResonanceShare`), and by here `amount` is already this
		// victim's helmet, burn and Insta-Kill arithmetic. A client's relayed hit arrives with the
		// shooter's own multipliers in it (`AttackerScale`) — the one way the two machines' figures
		// can differ.
		PrismaChain.OnHit( tech, GameObject, damage.Damage );

		// ── BLEEDER (ammo mod, 2026-10-04) ──────────────────────────
		//
		// ⚠️ HERE, ABOVE PERFORATOR: `amount` is what this hit deals, every multiplier in it, and that is what the bleed pays
		// again over eight seconds — before Perforator turns the hit into its instant tenth. `LastMod` is the hit's own mod.
		if ( amount > 0f && LastMod == "bleeder"
			&& Components.Get<ZombieAI>( FindMode.EverythingInSelfAndAncestors ).IsValid() )
		{
			using ( CpuScope.Measure( "dmg.ammomods" ) )
				Bleeder.Start( this, amount, damage.Attacker );
		}

		// ── PERFORATOR (`t5_perforator`) ────────────────────────────
		//
		// ⛔ THE LAST LINE THAT CAN STILL CHANGE WHAT THE BULLET DEALS, WHICH IS WHY IT IS HERE.
		// Everything above has finished scaling `amount` — hit group, perks, augments, burn
		// vulnerability, ammo mods — and the next statement commits it. The node replaces the
		// PAYOUT, not the arithmetic, so it has to sit exactly in the gap between the two.
		//
		// ⚠️ `Absorb` RETURNS WHAT TO DEAL NOW and queues the rest on the victim, so the
		// assignment below is the whole integration. A version that queued the pool and zeroed
		// `amount` would fire no `OnDamaged` at all: no flinch, no hit reaction, and no points for
		// shooting — see `Perforator`'s header for why one tick has to stay behind.
		//
		// ⚠️ BULLETS ONLY, BY THE SAME TAG TEST BOUNCY USES BELOW. Bouncy's links and
		// Flechette's shards reach this method carrying the same weapon, and letting them bleed too
		// would be defensible — but it would also mean a bounce link deals a tenth up front, so
		// `IsDead` is false, so the chain stops at link one. Two tier-5 nodes silently cancelling
		// each other is worse than the literal reading of "BULLETS deal damage over time".
		if ( amount > 0f
			&& damage.Tags is not null
			&& damage.Tags.Has( SWB.Shared.TagsHelper.Bullet )
			&& TechEffects.Has( tech, "t5_perforator" )
			&& Components.Get<ZombieAI>( FindMode.EverythingInSelfAndAncestors ).IsValid() )
		{
			using ( CpuScope.Measure( "dmg.tech" ) )
				amount = Perforator.Absorb( this, amount,
					TechEffects.Factor( tech, "t5_perforator", 1f ), damage.Attacker );
		}

		// ⛔ MARKER (handgun tier 5, 2026-10-04): IS IT MARKED, ASKED BEFORE `Apply`. A killing hit clears every status on the
		// corpse (`ZombieAI.Die` → `StatusEffects.ClearAll`), so asked afterwards the answer was "no" for exactly the hit that
		// killed it — and early on, when one pistol shot kills, that is every hit.
		var marked = amount > 0f && StatusEffects.Has( GameObject, "marked" );

		// ⚠️ Armor exemption resolved HERE, the only place the DamageInfo exists.
		// ⚠️ COUP DE GRÂCE (sniper tier 3, 2026-10-04) CHANGES ONLY WHAT THE ZOMBIE LAYS DOWN: a hit on a zombie already below
		// 30% takes the rest of its health (`ClassTech.CoupDeGrace`), and `amount` stays the hit for what follows.
		// ⚠️ AND CRYOFREEZE III'S SHATTER THE SAME WAY (2026-10-05): a zombie a level-III Cryofreeze froze, already under 25%, dies
		// to any hit from anyone, never a boss (`Cryofreeze.Shatter`, decided above the roll). Here on the host, where a client's
		// hit arrives too.
		var taken = Apply( Cryofreeze.Shatter( this, ClassTech.CoupDeGrace( tech, this, amount ), shatters ), headshot, null,
			Armor.Bypasses( damage ), source: DescribeDamage( damage ) );

		// ⚠️ FOLLOW-THROUGH (marksman tier 3, 2026-10-04): a kill keeps what it did not need for its bullet's next zombie.
		ClassTech.FollowThroughKill( tech, damage, bullet, this, taken );

		// ⛔ MARKER: a hit on a marked zombie, by anyone, lands on every marked zombie — the full hit, after this one has landed.
		// The copies go through `Apply`, so they never copy again (`ClassTech.ShareMarked`).
		if ( marked )
			ClassTech.ShareMarked( this, amount, damage.Attacker );

		LastHitWasMelee = false;
		LastHitPays = true;

		// ⚠️ AND THE PART, LATCHED FOR THIS HIT ALONE (2026-09-28). The gore reads it inside `Apply` (`OnDamaged`, the death), and
		// damage that does not come through here — a burn, a bleed, a Nuke — found the last bullet's part still set: a zombie shot in
		// the head once, then burned to death, lost its head to the fire.
		LastHitPart = "";

		// ── ADRENALINE ROUNDS — GONE FROM HERE ───────────────────────
		//
		// ⛔ THE NODE WAS REDESIGNED AND IS NO LONGER A ZOMBIE STATUS AT ALL. It used to speed up
		// whatever it failed to kill, which is why it lived on this line — "did the hit FAIL to
		// kill" is only knowable after `Apply`. It now speeds up the PLAYER instead, so it is
		// stamped on the shooter in `HitScan` and nothing about it belongs in the victim's
		// damage path. See `AdrenalineRounds`.
		//
		// ⚠️ BOUNCY ROUNDS INHERITED THE SPOT, and inherited the argument with it: it asks the
		// mirror-image question ("did it kill"), which is knowable at exactly the same moment.

		// ⚠️ AND EVERY GATE THE OLD BLOCK REASONED ITS WAY TO SURVIVES IN IT VERBATIM —
		// `taken > 0` so pellets 2..16 of a killing blast do not each fire, and the `ZombieAI`
		// test because `Health` is shared and a downed team-mate carries one.

		// ── BOUNCY ROUNDS (`t5_bouncy`) ──────────────────────────────
		//
		// ⛔ THE MIRROR IMAGE OF THE BLOCK ABOVE, AND IT HAS TO BE HERE FOR THE SAME REASON.
		// Adrenaline asks "did this FAIL to kill"; this asks "did it kill", and neither question
		// exists until `Apply` has committed the damage and fired `OnKilled`.
		//
		// ⛔ AND IT CANNOT LIVE IN `HitScan`, WHICH IS WHERE IT STARTED. On a CLIENT this
		// machine's copy of a zombie never takes the damage at all — `OnDamage` relays and
		// returns hundreds of lines above — so `IsDead` read on the shooter was permanently
		// false. The kill is knowable on the machine that owns the zombie and nowhere else.
		//
		// ⚠️ `amount`, NOT `taken`, IS WHAT THE CHAIN DOUBLES. `taken` is clipped to the
		// health that was left, so a round that overkills by 900 would hand the next link 100 —
		// and overkill is precisely what this node is built to spend. `amount` is also the fully
		// scaled figure, which is the other half of what moving here bought: `HitScan` could only
		// offer the damage before hit groups, perks and ammo mods touched it.
		//
		// ⚠️ FOUR GATES, CHEAPEST FIRST, AND EACH IS LOAD-BEARING:
		//   `Tags.Has(Bullet)` — only a fired round chains. A link's own damage carries the
		//                        `bounce` tag and no bullet tag, which is what makes the chain
		//                        terminate at its cap instead of restarting at every link; the
		//                        same test excludes Flechette shards and the Tech blast.
		//   `taken > 0f`      — `Apply` returns 0 for an already-dead body, so pellets 2..16 of
		//                        the blast that killed it cannot each start their own chain.
		//   `IsDead`          — the trigger itself.
		//   `ZombieAI`        — Health is shared and the PLAYER carries one. Without this, a
		//                        downed team-mate would chain lightning into the horde.
		//
		// ⚠️ THE CAP IS READ OFF THE PRIMARY ShootInfo, because by here the pellet is gone and
		// with it the `isPrimary` that produced it. Every prefab in the pack authors penetration on
		// primary fire; a secondary-only pierce stat would read the wrong number, and no weapon has
		// one.
		if ( taken > 0f && IsDead
			&& damage.Tags is not null
			&& damage.Tags.Has( SWB.Shared.TagsHelper.Bullet )
			&& TechEffects.Has( tech, "t5_bouncy" )
			&& Components.Get<ZombieAI>( FindMode.EverythingInSelfAndAncestors ).IsValid() )
		{
			// ⚠️ THE CAP COMES THROUGH `PenetrationOf` RATHER THAN OFF THE WEAPON, because on
			// a relayed hit there is no weapon here to read it from — the shooter sends the
			// number with the hit. It is the one weapon STAT that travels; see its note.
			using ( CpuScope.Measure( "dmg.tech" ) )
				BouncyRounds.Chain( tech, GameObject, amount,
					TechEffects.PenetrationOf( tech, damage ) );
		}
	}

	/// <summary>Would PhD Flopper absorb this hit entirely.
	///
	/// ⚠️ Its own method because the inline version mixed &amp;&amp; and || and got the
	/// precedence wrong on the first attempt — a condition that read correctly and
	/// meant something else. Two plain questions in order is not clever, and that
	/// is the point.</summary>
	bool PhdBlocks( in DamageInfo damage )
	{
		// ⚠️ Runs before anything else in OnDamage, on every hit, player or zombie.
		using var _cpu = CpuScope.Measure( "dmg.phd" );
		var victim = Components.Get<NZPlayer>( FindMode.EverythingInSelfAndAncestors );
		if ( !victim.IsValid() ) return false;
		if ( !PerkEffects.ImmuneToNonZombieDamage( victim ) ) return false;

		// No attacker at all — a grenade, the world, a future fall. Not a zombie.
		if ( !damage.Attacker.IsValid() ) return true;

		var fromZombie = damage.Attacker.Components
			.Get<ZombieAI>( FindMode.EverythingInSelfAndAncestors ).IsValid();

		return !fromZombie;
	}

	/// <summary>
	/// Does this damage come from a melee swing?
	///
	/// Tag-based like the headshot check, so any future knife just adds "melee"
	/// to its damage rather than needing a code path here.
	/// </summary>
	public static bool IsMelee( in DamageInfo damage )
	{
		var tags = damage.Tags;
		if ( tags is null ) return false;

		return tags.Has( "melee" ) || tags.Has( "knife" );
	}

	/// <summary>
	/// Apply damage. Returns the amount actually taken — 0 when already dead or
	/// still inside the immunity window, which callers use to decide whether to
	/// play a reaction at all.
	/// </summary>
	/// <summary>⚠️ `from` is the ATTACKER, and optional so every existing caller
	/// still compiles. It exists because Victorious Tortoise needs to know WHERE a
	/// hit came from, and zombie melee arrives through this method with no
	/// DamageInfo to read it out of. Multiplayer wants the same thing later —
	/// "who dealt this" is worth having on the one path that all damage crosses.</summary>
	/// <summary>
	/// DAMAGE THAT MUST NOT PAY THE PER-HIT POINTS DRIP — a tick of something the shot that
	/// started it has already been paid for.
	///
	/// ⛔ `LastHitPays` IS PRIVATE AND THAT IS DELIBERATE, so this exists rather than opening it:
	/// there is exactly one legitimate reason to write it from outside (a follow-up payment on a
	/// hit already awarded), and a method that says so is harder to misuse than a setter that says
	/// nothing. `Perforator`'s bleed is the first caller; a burn tick is the obvious second.
	///
	/// ⚠️ RESTORED TO TRUE AFTERWARDS, matching what `OnDamage` does around its own write.
	/// The flag is LATCHED for `ZombieAI` to read during `OnDamaged`/`OnKilled`, so leaving it
	/// false would silently unpay the next knife, trap or grenade to touch this body.
	/// </summary>
	public float ApplyUnpaid( float amount, GameObject from = null )
	{
		LastHitPays = false;
		var taken = Apply( amount, false, from );
		LastHitPays = true;
		return taken;
	}

	/// <summary>⚠️ `ignoreArmor` exists because Apply takes a float and cannot see
	/// the damage type. The original exempts fall/drown/poison/radiation from armor,
	/// and only OnDamage — which has the DamageInfo — can tell. Defaults false so
	/// every existing caller keeps armor applied, which is the safe direction: a
	/// missed exemption over-protects, a missed armor call silently disables the
	/// system.</summary>
	public float Apply( float amount, bool headshot = false, GameObject from = null,
		bool ignoreArmor = false, bool blast = false, Vector3 blastAt = default, bool tick = false, bool cost = false,
		string source = null )
	{
		// ⚠️ `cost`: HEALTH SPENT, NOT A HIT TAKEN (2026-10-04) — Blood Price's 10 a shot. None of what answers a hit runs:
		// no Bulwark or ring to shrink the price, no PhD or Vigor hit trigger on every shot, no Phase Shift to refuse it. It
		// still lands, still restarts regen, and still downs you, which the user asked for (*"It can down you"*).
		//
		// ⚠️ `blast`: AN ENEMY'S AREA HIT — Oberon's leap, black hole and bombs (`OberonBoss.Blast`), gone off at `blastAt`. Armor
		// spends on it as on any enemy's hit — *"armor needs to react to the other attacks too"* (2026-09-27) — and so do Bulwark,
		// Vigor and the rest; Victorious Tortoise judges "from behind" by where it went off, not by where he stands (*"fix that
		// too"*); what answers a CLAW does not: Widow's Wine's web, and Juggernog's Retaliate, which would have stunned him for
		// five seconds with every one of his own bombs.
		//
		// ⚠️ `tick`: ONE TICK OF DAMAGE OVER TIME — a damage wall's (`DamageWallVolume`), the boss arena's flood
		// (`HexPlatforms.TickHazards`). It lands whatever the victim's post-hit window says and opens none: that window is the
		// crowd's, and lava at 10 every 0.2s would otherwise land one tick in three while shielding its victim from the zombies.
		// ⚠️ OUTER SCOPE, and `using var` matters here specifically: Apply has SEVEN early returns.
		// A using-BLOCK around the body would have to be closed before every one of them.
		using var _cpu = CpuScope.Measure( "dmg.apply" );

		// ⛔ ON A CLIENT, A ZOMBIE IS THE HOST'S — ITS COPY HERE TAKES NO DAMAGE AT ALL. The host owns everything but each
		// client's own body (`OnDamage`'s rule, which already sends a client's hits on to the host and never lays them on the
		// copy). But a status's tick (`StatusEffects`, run on every machine) came straight here, so a client's copy lost health
		// on its own, and at zero ran the whole of `ZombieAI.Die` on the client: non-solid, a second Prisma burst, a soul box
		// fed, drops rolled, and a `ZombieDied` that felled every other client's copy. A client's Oberon died that way to
		// radiation ticks, and no client's shot could hit him after (the co-op audit, 2026-09-27). Players' bodies are untouched:
		// the owner's own, and the relay below for somebody else's.
		if ( NZGame.IsClient && GameObject.Components.Get<NZPlayer>( FindMode.EverythingInSelf ) is null ) return 0f;

		// ⛔ A BODY ON A TELEPORT TAKES NOTHING, WHOSEVER IT IS (the co-op audit, 2026-09-27 — *"fix it so clients also dont
		// get hit mid teleport"*). Each machine made only its own riders invulnerable, so on the host a zombie that reached a
		// client's rider hit it — the swing sounded, the cursed flame went out. The host keeps the list (`Teleporter.InTransit`),
		// and a hit refused here is never sent on.
		if ( Teleporter.InTransit( GameObject ) ) return 0f;

		// ⛔ SOMEBODY ELSE'S HEALTH IS THEIRS TO CHANGE, AND EVERY LINE BELOW THIS COMPUTES THE
		// WRONG ANSWER FOR IT. Zombies live on the host, so a client being eaten is resolved
		// against the HOST'S COPY of that client — a copy that has never heard of the perks they
		// bought. Juggernog is the loudest case: the copy's `Max` is the base 150, so its `Current`
		// after a hit is a number out of 150, and pushing that as an absolute pulled a 250hp
		// player down to it. User: *"juggernog seems to somehow reduce the health for clients."*
		//
		// It is not only the total. `Apply` below runs Widow's Wine, Victorious Tortoise, the
		// Juggernog augments and the armour plates — all read off the victim — so on a proxy every
		// one of them is asking a body with no perks and no vest. The hit was mis-sized before it
		// was ever mis-stored.
		//
		// ⚠️ THE RAW AMOUNT TRAVELS, AND THE OWNER'S OWN `Apply` RESIZES IT. That is the whole
		// point: their perks, their armour, their maximum, their regen clock. The same shape as
		// `NZNet.AwardPoints` and `PlayerStats.RecordStat`, and the exact thing `MirrorTo`'s own
		// comment said the real fix would be — *"the host should relay the DAMAGE AMOUNT rather
		// than a total"*.
		//
		// ⚠️ BEFORE `IsDead` AND `Invulnerable`. Those read the local copy, which after this
		// change never takes damage and so is never dead — but a stale copy that HAD gone to zero
		// would silently swallow every hit and the player would be immortal.
		if ( Networking.IsActive && IsRemotePlayerBody )
		{
			var owner = NZPlayers.OwnerOf( GameObject );

			if ( !string.IsNullOrEmpty( owner ) )
			{
				// ⚠️ `from` TRAVELS AS AN ID. Tortoise needs its position, Retaliate needs to stun
				// it and WebSnare needs to cancel the hit — all three are dead without it.
				// ⚠️ AND WHAT IT WAS, IN WORDS (2026-10-04), for the owner's hit line: `from` alone cannot say "a blast with no
				// attacker" once it has crossed as an empty id.
				NZNet.HurtPlayer( owner, amount, headshot,
					from.IsValid() ? from.Id : Guid.Empty, blast, blastAt, tick, source ?? DescribeHit( from, blast, tick, cost ) );

				// ⚠️ REPORTED AS DEALT so the caller's bookkeeping is unchanged. `ZombieAI` gates
				// its impact sound on a non-zero return — a swing that connected must sound like
				// one here even though the arithmetic happens elsewhere.
				return amount;
			}
		}

		if ( IsDead ) return 0f;

		// ⚠️ BEFORE THE IMMUNITY WINDOW AND EVERYTHING ELSE. See `Invulnerable` - the whole point
		// is that nothing below this line runs.
		if ( Invulnerable ) return 0f;

		if ( !tick && ImmunityAfterHit > 0f && _immuneUntil > 0f ) return 0f;

		amount = MathF.Max( 0f, amount );
		if ( amount <= 0f ) return 0f;

		// ⛔ BASALT'S BOSS FIGHT: OBERON TAKES NOTHING BETWEEN PHASES, AND NO HIT CARRIES HIM PAST THE NEXT PHASE'S LINE
		// (`HexPlatforms.BossCap`) — here, where every route in ends: a bullet, the Shrieker's death pulse, a nuke's share.
		amount = NZombies.HexPlatforms.BossCap( GameObject, Current, Max, amount );
		if ( amount <= 0f ) return 0f;

		// ⚠️ GUARDED, not unconditional — see LastAttacker. OnDamage passes null
		// here on purpose, having already recorded the real attacker itself.
		if ( from.IsValid() )
		{
			// ⛔ SOMEBODY ELSE'S DAMAGE — a bleed, a burn, a Marker copy — TAKES THE LAST BULLET'S MOD WITH IT (2026-10-04,
			// the review). `LastMod` is the mod of the last bullet, and the credit is about to move to `from`: kept, a client's
			// Bleeder tick finishing a zombie the host had shot with Midas was paid the host's +50%.
			if ( from != LastAttacker ) LastMod = "";

			LastAttacker = from;
		}

		// ⚠️ WHAT DEALT IT, FOR THE PLAYER'S HIT LINE (2026-10-04): the caller's own words when it has them (`OnDamage`'s
		// DamageInfo, a forwarded hit's sender), otherwise what this method was given.
		LastHitSource = source ?? DescribeHit( from, blast, tick, cost );

		// ⚠️ ONE NZPlayer LOOKUP FOR BOTH the perk reduction and armor. They used
		// to be separate reads; two places answering "which player is this" is the
		// shape INSTRUCTIONS.md §3 warns diverges — and here they cannot, because
		// there is now exactly one answer.
		var victim = Components.Get<NZPlayer>( FindMode.EverythingInSelfAndAncestors );

		// ⛔ WIDOW'S WINE CANCELS THE HIT, AND IT RUNS BEFORE EVERYTHING ELSE THAT COULD
		// BILL FOR IT. Placed after Tortoise it would scale damage that is about to be
		// discarded; placed after armor it would spend a plate on a hit the perk prevented.
		// Returning zero here means the claw never lands — the grenade paid for it.
		//
		// ⚠️ THE FULL AMOUNT IS NEGATED, not reduced. That matches the original, whose
		// hook ends in `return false` on the damage event.
		//
		// ⚠️ `_immuneUntil` IS DELIBERATELY NOT SET. A negated hit is not an
		// invulnerability window; the next zombie in the same tick should still get its own
		// chance to trigger the perk, and if the charges are gone it should hurt.
		if ( !blast && from.IsValid() && victim.IsValid() && PerkEffects.WebSnare( victim, from ) )
			return 0f;

		// ⚠️ VICTORIOUS TORTOISE APPLIES HERE, in Apply rather than OnDamage,
		// because zombie melee — the only thing that hits you from behind — comes
		// through Apply. Putting it in OnDamage would cover grenades and miss
		// zombies, which is precisely backwards for a perk about being surrounded.
		//
		// ⚠️ A BLAST BY WHERE IT WENT OFF (`blastAt`): a bomb behind you is behind you, wherever he stands.
		if ( blast && victim.IsValid() )
			amount *= PerkEffects.TortoiseScale( victim, blastAt );
		else if ( from.IsValid() && victim.IsValid() )
			amount *= PerkEffects.TortoiseScale( victim, from.WorldPosition );

		// ⛔ AUGMENTS RUN AFTER THE BASE PERKS AND BEFORE ARMOR, and the position is
		// the same argument Tortoise makes above. Juggernog's M3 Bulwark cuts the hit
		// first so armor is billed only for what got through; reversed, a Bulwark player
		// would burn plates at the full rate while taking less damage.
		//
		// ⚠️ ONE CALL FOR ALL AUGMENTS, dispatched inside AugmentEffects. Seventeen perks
		// each reaching into this method would be seventeen edits here and an ordering
		// that depended on file order.
		//
		// ⚠️ `from` may be NULL — Retaliate needs an attacker, Bulwark and Adrenal Surge
		// do not, so the null case is handled in there rather than by skipping the call.
		// ⚠️ THE GUN IN HAND (2026-10-04): Juggernaut's cut and Pain Reload's reload, beside the perks and before armor.
		if ( victim.IsValid() && !cost )
			amount = ClassTech.OnPlayerDamaged( victim, amount );

		if ( victim.IsValid() && !cost )
			amount = AugmentEffects.OnPlayerDamaged( victim, from, amount, clawed: !blast );

		// ⛔ ARMOR RUNS AFTER THE PERK, AND THE ORDER IS LOAD-BEARING. Tortoise cuts
		// the hit first, then armor pays for what is left — so a halved back-hit only
		// costs half as much armor. Reversed, armor would be billed for damage the
		// perk had already prevented, and a Tortoise player would burn through plates
		// at the same rate as everyone else while taking less damage.
		//
		// ⚠️ Armor also DEPLETES here, not just reduces — see Armor.Absorb. This
		// line both mutates the player's armor and returns the reduced amount.
		// ⛔ ONLY AN ENEMY SPENDS YOUR PLATES. Armor was billed for anything that reached this
		// method — fall damage, damage walls, easter-egg traps, your own grenade. `ignoreArmor`
		// was meant to cover that and could not: it is set only on the DamageInfo route, and
		// defaults FALSE for every direct `Apply` caller, which is exactly how a damage wall
		// reaches a player.
		//
		// ⚠️ THE TEST BELONGS HERE, NOT IN `Armor.Bypasses`. This is the one line all damage
		// passes through; `Bypasses` sees only half of it. Writing it there first looked right
		// and would have left every trap in the game still eating plates.
		//
		// ⚠️ A ZOMBIE'S CLAW PASSES ITS ROOT GameObject (`DoAttackDamage`: `hp.Apply( damage,
		// false, GameObject )`), which is where the "zombie" tag is — so real hits resolve.
		// Checked, because getting this backwards disables armor entirely rather than
		// over-applying it.
		using ( CpuScope.Measure( "dmg.absorb" ) )
		if ( victim.IsValid() && !ignoreArmor && ZombieAI.IsEnemyDamage( from ) )
			amount = Armor.Absorb( victim, amount );

		// ⛔ LEECH'S OVERHEAL TAKES THE HIT NEXT (2026-10-04, `Over`). It is health above the max, under the plates: armor bills
		// first, this pays what armor let through, and only the rest reaches `Current`. Phase Shift below is asked about that
		// rest alone, so a hit the overheal soaks up is never "fatal". The hit is still reported whole — the number, the
		// reaction and the regen clock all saw a hit land.
		var overTaken = 0f;
		if ( Over > 0f && amount > 0f )
		{
			overTaken = MathF.Min( Over, amount );
			Over -= overTaken;
		}

		// ══ QUICK REVIVE m5 PHASE SHIFT ═════════════════════════════════════
		//
		// ⛔ IT READS THE REDUCED AMOUNT, WHICH IS WHY IT IS DOWN HERE AND NOT AT THE TOP OF THIS
		// METHOD. Placed above the reductions it tested the RAW damage against remaining health, so a
		// 200-damage claw that armor and Bulwark were about to cut to 30 counted as fatal to a player
		// on 50 HP — and the augment DROPPED THEM TO 1 HP on a hit they would have walked off with
		// 20. An augment that makes you worse off the more armor you own.
		//
		// ⚠ AND IT RUNS AFTER WIDOW'S WINE'S CANCEL, for the reason the Widow comment above states
		// about itself: nothing should bill for a hit another perk prevents for free. Above it, a
		// snared claw spent a two-minute cooldown on damage that never landed.
		//
		// ⛔ IT SETS HEALTH TO EXACTLY 1, IT DOES NOT NEGATE THE HIT. Negating leaves you at
		// whatever health you already had, which for a player on 80 HP is not "kept at 1 HP" by any
		// reading. The write goes straight to `Current` because this method IS the only way health
		// moves down — `Current` has a private setter and `Heal` clamps negatives away.
		//
		// ⚠ NO ARMOR IS SPENT AND NO REDUCTION IS BILLED. `Armor.Absorb` above already ran and
		// already depleted, which is correct: you were hit, the plates took it, and the augment only
		// catches what got through.
		//
		// ⚠ `OnDamaged` STILL FIRES, with the real amount lost. You were hit, so the regen delay
		// must restart — otherwise a phase shift is followed by regen that thinks it has been calm.
		using ( CpuScope.Measure( "dmg.revive" ) )
		if ( victim.IsValid() && !cost && NZombies.ReviveAugments.TryPhaseShift( victim, amount - overTaken ) )
		{
			var lost = MathF.Max( 0f, Current - 1f ) + overTaken;
			Current = 1f;
			LastDamage = lost;
			LastLost = lost;

			if ( !tick && ImmunityAfterHit > 0f ) _immuneUntil = ImmunityAfterHit;

			if ( lost > 0f )
			{
				SinceLastDamage = 0f;
				OnDamaged?.Invoke( lost, headshot );
			}

			return lost;
		}

		// ⚠️ LATCHED BEFORE THE SUBTRACTION, so a killing blow still reports what it dealt.
		// Recording it after would be identical today and would break the moment anything
		// clamped `amount` against remaining health.
		LastDamage = amount;

		var beforeHit = Current;

		Current = Math.Clamp( Current - (amount - overTaken), 0f, Max );

		// ⚠️ WHAT IT REALLY TOOK, for a stand-in killing blow (`LastHitDamage`).
		LastLost = MathF.Max( 0f, beforeHit - Current ) + overTaken;

		// ⚠️ STAMPED AT THE ONE PLACE HEALTH IS LOST, so every route in — bullets, the knife,
		// a zombie's melee, a fire pit — restarts the regen delay without having to remember to.
		//
		// ⛔ BUT ONLY IF HEALTH ACTUALLY MOVED, AND IT DID NOT USED TO CHECK. `HealthRegen` refuses
		// to heal until `SinceLastDamage` passes the delay, so ANY caller reaching this line resets
		// that wait — including one that cost the player nothing. A hit fully absorbed by armour, or
		// a `DamageWallVolume` whose `Spot.Damage` is authored 0 (the call site clamps with
		// `MathF.Max( 0f, ... )`, so zero is an expected value there), lands here with `amount` 0,
		// takes no health, and silently pushes the regen delay out again. Standing in a 0-damage
		// volume would block regeneration forever with nothing on screen to explain it.
		//
		// ⚠️ A *HEALTH* REGEN TIMER HAS NO BUSINESS RESTARTING FOR A HIT THAT COST NO HEALTH.
		// That is the whole argument — armour absorbing a swing is armour doing its job, not a
		// reason to punish the health bar as well.
		//
		// ⚠️ THE IMMUNITY WINDOW IS NOT AFFECTED. A swallowed hit returns far above this line, so
		// this changes nothing about being surrounded.
		if ( Current < beforeHit || overTaken > 0f ) SinceLastDamage = 0f;

		if ( !tick && ImmunityAfterHit > 0f )
			_immuneUntil = ImmunityAfterHit;

		// ⚠️ ON A ZOMBIE VICTIM THIS IS THE HIT REACTION -- flinch, pain voice, aggro. ZombieAI
		// subscribes OnDamaged, so the delegate is very much not free.
		using ( CpuScope.Measure( "dmg.react" ) )
			OnDamaged?.Invoke( amount, headshot );

		// ⚠️ A DIRECT CALL, not an event. `OnDamaged` is an Action PROPERTY, so a
		// second consumer assigning it silently clobbers ZombieAI's handler — and
		// a static event would survive hotload with stale subscribers attached.
		// PointsPopups.Add is called the same way from the points path; this
		// follows it.
		// ⛔ NOT FOR SOMEBODY ELSE'S SHOT. A damage number is feedback to the person who pulled
		// the trigger, and on the host it was being drawn for every client's hit as well:
		// `NZNet.HurtRemote` is `[Rpc.Host]`, so a client's relayed bullet lands here on the host
		// and reaches this line exactly like a local one. The host's screen filled with numbers
		// for shots it did not fire. User: *"damage numbers from all players show on all players,
		// i want players to only see their own."*
		//
		// ⚠️ PHRASED AS "NOT THEIRS" RATHER THAN "MINE", WHICH IS THE SAFER HALF. Grenades,
		// napalm pits, Retaliate and the augments all arrive here with an attacker that is not a
		// player at all — requiring a positive match to my own body would silently delete their
		// numbers too. This removes only what another player caused.
		//
		// ⚠️ THE CLIENT'S OWN NUMBER IS UNAFFECTED — it is reported in `OnDamage`'s relay branch,
		// on the shooter's machine, before the hit is ever sent.
		var byAnotherPlayer = LastAttacker.IsValid()
			&& LastAttacker.Components.Get<NZPlayer>( FindMode.EverythingInSelfAndAncestors )
				is { } shooter
			&& PlayerPresence.Theirs( shooter.GameObject );

		if ( !byAnotherPlayer )
			using ( CpuScope.Measure( "dmg.numbers" ) )
				DamageNumbers.Report( this, amount, headshot );

		// ⚠️ ZombieAI.Die RUNS INSIDE THIS, so on a killing hit dmg.die dwarfs everything else in
		// dmg.apply. Read it against dmg_hits: at high penetration most hits are not kills.
		using ( CpuScope.Measure( "dmg.die" ) )
		if ( Current <= 0f )
			OnKilled?.Invoke( headshot );

		return amount;
	}

	/// <summary>Set both current and max — for spawning something with a
	/// round-scaled pool rather than the prefab's default.</summary>
	public void Reset( float max )
	{
		Max = max;
		Current = max;
		_immuneUntil = 0f;
	}

	/// <summary>
	/// Set what is left of the pool, within it — basalt's beast carried from one of his comings to the next
	/// (`HexPlatforms.BossFight.cs`), which a `Reset` alone would hand back whole.
	/// </summary>
	public void SetCurrent( float current ) => Current = Math.Clamp( current, 0f, Max );

	/// <summary>
	/// Restore health.
	///
	/// ⚠️ Use this, NOT Apply with a negative amount — Apply clamps its argument
	/// to zero, so Apply(-50) silently heals nothing. It also fires OnDamaged,
	/// which healing must not: HealthRegen listens to that to know when it was
	/// last hit, so healing through Apply would reset its own delay forever.
	/// </summary>
	public void Heal( float amount )
	{
		if ( IsDead ) return;
		Current = Math.Clamp( Current + MathF.Max( 0f, amount ), 0f, Max );
	}

	/// <summary>
	/// OVERHEAL — health above <see cref="Max"/>, which Leech gives (2026-10-04, `KillMods`): *"kills recover 5hp, including
	/// overheal, meaning if my max hp is 150, then i can kill and regen up to a total 0f 200hp"*, and it *"does not drain
	/// away"*.
	///
	/// ⛔ A POOL OF ITS OWN, NOT `Current` ABOVE `Max`. Everything that writes `Current` clamps it to `Max` — a hit, `Heal`,
	/// `SetCurrent`, Juggernog's refresh — and each would have quietly taken it away. Only damage spends this (`Apply`, after
	/// armor), only Leech fills it, and `Reset` leaves it alone, so a perk's refill does not cost it. A new run clears it
	/// (`RoundManager`).
	///
	/// ⚠️ THE OWNER'S, like the rest of a player's health. Not mirrored to the other machines.
	/// </summary>
	public float Over { get; private set; }

	/// <summary>Heal, and past <see cref="Max"/> into <see cref="Over"/>, up to <paramref name="overCap"/> over. Returns what was added.</summary>
	public float HealOver( float amount, float overCap )
	{
		if ( IsDead || amount <= 0f ) return 0f;

		var toMax = MathF.Min( amount, MathF.Max( 0f, Max - Current ) );
		Current += toMax;

		var toOver = MathF.Min( amount - toMax, MathF.Max( 0f, overCap - Over ) );
		Over += toOver;

		return toMax + toOver;
	}

	/// <summary>Drop the overheal — a new run.</summary>
	public void ClearOver() => Over = 0f;

	/// <summary>
	/// Was this a headshot? The engine merges the struck hitbox's tags into the
	/// damage (BaseCombatWeapon.MergeHitboxTags), so "head" arrives here without
	/// us tracing for it.
	/// </summary>
	public static bool IsHeadshot( in DamageInfo damage )
	{
		// ⚠️ Hitbox tag resolution, per hit. Separate from the head-damage product, which is
		// measured as dmg.headscale.
		using var _cpu = CpuScope.Measure( "dmg.headshot" );
		var tags = damage.Tags;
		return tags is not null && tags.Has( "head" );
	}

	/// <summary>
	/// WIDE BORE (`t5_widebore`) — was this hit close enough to the head to count as one?
	///
	/// ⛔ AN INSTANCE METHOD, BECAUSE THE STATIC ONE CANNOT SEE THE VICTIM. `IsHeadshot` reads
	/// only the DamageInfo, which carries where the bullet landed but nothing about whose head is
	/// where. This runs on the victim, so `GameObject` is the zombie and the head is a measurement
	/// rather than a lookup.
	///
	/// ⚠️ HEAD HEIGHT IS A FRACTION OF `BodyHeight`, NOT A BONE NAME. The roster is ported from
	/// several packs through ModelDoc and shares no skeleton convention — this project has been
	/// bitten by guessing bone axes before. `StatusEffects.FlameHeightFraction` (0.92) is the
	/// established answer to "where is this zombie's head", used to place the burning effect, and
	/// reusing it means the two cannot disagree about the same zombie.
	///
	/// ⚠️ IT SCALES WITH THE ZOMBIE, because `BodyHeight` is what `nz_zscale` moves — a giant's
	/// head is measured at a giant's height rather than at a constant sitting in its chest.
	///
	/// ⚠️ AND IT IS ONLY EVER A PROMOTION. A hit already carrying the head tag is a headshot
	/// whatever this says; this turns a body shot into a headshot and never the reverse.
	/// </summary>
	bool WideBoreHead( in DamageInfo damage )
	{
		// ⚠️ THE CHEAP TEST FIRST, AND IT IS NOT AN OPTIMISATION FLOURISH. This runs on every
		// body shot in the game — `||` only skips it for hits that already carry the head tag — so
		// the component lookup on self goes before `FiredBy`, which walks to find the weapon.
		// Players are the common non-zombie victim and leave immediately.
		var zombie = Components.Get<NZombies.ZombieAI>( FindMode.EverythingInSelf );
		if ( zombie is null ) return false;

		var radius = NZombies.TechEffects.Factor( FiredBy( damage ), "t5_widebore", 0f );
		if ( radius <= 0f ) return false;

		var head = WorldPosition
			+ Vector3.Up * (zombie.BodyHeight * NZombies.StatusEffects.FlameHeightFraction);

		return damage.Position.DistanceSquared( head ) <= radius * radius;
	}
}