Weapons/AmmoModDeaths.cs

Host-side death hook for ammo-mod effects. When a ZombieAI dies (before its status effects are cleared) this calls SilkShot.OnWebbedDeath and Bloodhound.OnMarkedDeath and logs any exceptions so one mod failing does not abort the whole death handling.

Networking
using Sandbox;
using System;

namespace NZombies;

/// <summary>
/// THE AMMO MODS' DEATH HOOK, ON THE HOST (2026-10-06, `Sbox nzombies/Docs/AMMO_MODS.md` "Tiers IV and V"): a zombie died, and a
/// mod that left something ON it passes it on.
///
/// | mod | upgrade | the hook is for |
/// |---|---|---|
/// | **Silk Shot** | V Brood | a webbed zombie that dies webs up to 3 within 140u (`SilkShot.OnWebbedDeath`) |
/// | **Bloodhound** | V Blood Trail | a marked zombie's mark jumps to the nearest unmarked one (`Bloodhound.OnMarkedDeath`) |
///
/// ⛔ CALLED FROM `ZombieAI.Die`, ON THE HOST (OR SOLO), BEFORE THE CORPSE'S STATUSES ARE CLEARED — beside `PrismaChain.OnDied`,
/// for its reason: what is on the body (the web, the mark, and who put them there: `StatusEffects.SourceOf`) is read off it, and
/// after `StatusEffects.ClearAll` there is nothing left to read. A puppet never reaches `Die`, so this runs once a death, on the
/// machine whose statuses the AI obeys.
///
/// ⚠️ NOT `AmmoMods.OnZombieKilled`, which runs on the KILLER'S machine, relayed there after the fact when the corpse's statuses
/// are gone, and is about who killed it. These are about what the zombie WORE, whoever killed it — a grenade, a bleed, a
/// teammate.
///
/// ⚠️ EACH HOOK IS FENCED: an exception thrown inside `Die` before `ClearAll` would end the whole death there — no points, no
/// corpse on the other screens (`NZNet.ZombieDied`) — so one mod's fault costs that mod's effect and a warning, nothing else.
/// </summary>
public static class AmmoModDeaths
{
	/// <summary>A zombie died. THE HOST, from `ZombieAI.Die`, before `StatusEffects.ClearAll`.</summary>
	public static void Died( ZombieAI zombie )
	{
		// ⚠️ A CLIENT NEVER GETS HERE (`Die` is the host's alone); the test says so where the next caller would read it.
		if ( !zombie.IsValid() || NZGame.IsClient ) return;

		try { SilkShot.OnWebbedDeath( zombie ); }
		catch ( Exception e ) { Log.Warning( $"[nz-ammo] silk shot's death hook threw on {zombie.GameObject?.Name}: {e.Message}" ); }

		try { Bloodhound.OnMarkedDeath( zombie ); }
		catch ( Exception e ) { Log.Warning( $"[nz-ammo] bloodhound's death hook threw on {zombie.GameObject?.Name}: {e.Message}" ); }
	}
}