Zombies/WalkerDeaths.cs

Classification of walker death animation names. Declares lists/sets grouping animation clip names by cause (Fire, Ice, Explosion, Electric, OtherConditional), computes Normal as generated WalkerAnimations.Death minus all conditional sets, and exposes AllConditional concatenation for reporting.

Reflection
using System.Collections.Generic;
using System.Linq;

namespace NZombies;

/// <summary>
/// Which death clips may play for which cause of death.
///
/// ⛔ `WalkerAnimations.Death` IS THE WHOLE SET, AND MOST OF IT MUST NOT PLAY ON
/// A BULLET. 34 of its 78 clips are authored for a specific way of dying —
/// burning, frozen, blown up, electrocuted, bayoneted — and picking from the
/// full list at random is why a shot zombie sometimes froze solid, or flailed
/// away from a blast that never happened.
///
/// The original does the same split: a generic set for an ordinary kill, and the
/// elemental sets only when that damage type caused it. We have no damage types
/// yet, so ordinary kills use <see cref="Normal"/> and the rest sit here named
/// and ready — each is one line away from being used the day fire, electricity
/// or explosives exist.
///
/// ⚠️ SEPARATE FILE ON PURPOSE. `WalkerAnimations.cs` says "GENERATED by
/// Tools/build_walker_vmdl.py — do not hand-edit", and it means it: the clip
/// lists come from whatever actually compiled into the .vmdl. Putting this
/// classification in there would be silently destroyed the next time the walker
/// is rebuilt, which — given the model has been rebuilt twice today — is not
/// hypothetical.
/// </summary>
public static class WalkerDeaths
{
	/// <summary>Burning to death. Needs a fire damage type to be reachable.</summary>
	public static readonly List<string> Fire = new()
	{
		"nz_flame_death_a", "nz_flame_death_e", "nz_flame_death_f",
		"nz_flame_death_g", "nz_flame_death_h",
		"nz_firestaff_death_collapse_a", "nz_firestaff_death_collapse_b",
	};

	/// <summary>
	/// Frozen solid.
	///
	/// ⚠️ THESE END UPRIGHT ON PURPOSE — a frozen corpse stands. That is correct
	/// for an ice kill and looks exactly like the "zombie will not fall over" bug
	/// on a bullet kill, which is how they were first noticed: of 105 death clips
	/// measured, the ones that stayed vertical were these plus the blast deaths.
	/// </summary>
	public static readonly List<string> Ice = new()
	{
		"nz_dth_freeze_1", "nz_dth_freeze_2", "nz_dth_freeze_3",
		"nz_dth_freeze_4", "nz_dth_freeze_5",
		"nz_icestaff_death_a", "nz_icestaff_death_b", "nz_icestaff_death_c",
		"nz_icestaff_death_d", "nz_icestaff_death_e",
	};

	/// <summary>Thrown by a blast — the body is launched, not folded.</summary>
	public static readonly List<string> Explosion = new()
	{
		"nz_death_blast_1", "nz_death_blast_2",
		"nz_death_blast_from_back", "nz_death_blast_from_left",
		"nz_death_blast_from_right",
		"nz_death_explosion_run_b_v2", "nz_death_explosion_stand_b_v3",
	};

	/// <summary>Electrocuted — tesla trap, wonder weapons.</summary>
	public static readonly List<string> Electric = new()
	{
		"nz_death_elec_1", "nz_death_elec_2", "nz_death_elec_3",
		"nz_death_elec_4", "nz_zombie_tesla_death_e",
	};

	/// <summary>
	/// Everything else that must never be picked at random.
	///
	/// The bayonet death belongs to a melee animation that has to be playing, the
	/// cellbreaker deaths belong to a different zombie type entirely, and
	/// neckgrab / nerve / exposed_twist read as scripted set-pieces.
	///
	/// ⚠️ UNKNOWNS GO HERE, NOT INTO THE NORMAL POOL. A death that never plays is
	/// invisible; a wrong one is not.
	/// </summary>
	public static readonly HashSet<string> OtherConditional = new()
	{
		"nz_bayonet_thrust_death",
		"nz_base_zombie_cellbreaker_death_01",
		"nz_base_zombie_cellbreaker_death_02",
		"nz_base_zombie_cellbreaker_death_mg",
		"nz_base_zombie_cellbreaker_death_explode",
		"nz_death_neckgrab",
		"nz_death_nerve",
		"nz_exposed_death_twist",
	};

	/// <summary>
	/// Deaths for an ordinary kill — what a bullet should produce.
	///
	/// ⚠️ Built by SUBTRACTION from the generated list, never hand-copied. A
	/// hand-copied list silently stops matching the day a clip is added or
	/// renamed, and these names came out of a 105-clip package that has already
	/// been regenerated twice. Add a clip to the model and it lands here on its
	/// own unless a conditional set explicitly claims it.
	/// </summary>
	public static readonly List<string> Normal = WalkerAnimations.Death
		.Where( d => !Fire.Contains( d )
			&& !Ice.Contains( d )
			&& !Explosion.Contains( d )
			&& !Electric.Contains( d )
			&& !OtherConditional.Contains( d ) )
		.ToList();

	/// <summary>Every clip claimed by a condition — for reporting.</summary>
	public static IEnumerable<string> AllConditional =>
		Fire.Concat( Ice ).Concat( Explosion ).Concat( Electric )
			.Concat( OtherConditional );
}