Zombies/WustlingZombie.cs

A game component representing the Wustling special zombie. It handles its tuning, state, animations, charge behavior (tell, run, slam), weak spot damage scaling, stun shaking, and debugging console commands.

NetworkingFile Access
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

namespace NZombies;

/// <summary>
/// THE WÜSTLING — CoD WWII's charging brute, ported 2026-10-07 from GMod nZombies' `nz_zombie_special_follower` (GhostlyMoo's port;
/// "Wustling" upstream, "follower" in its assets — the name of no behaviour). A SPECIAL, not a boss: he arrives during ordinary
/// rounds (`AmbientSpecials`) and the round waits for him (`IsBoss` false). Built on `BossBase` for its helpers alone (`Queue`,
/// `PlayShared`, `HurtPlayer`, `MaxHit`, `SwingDamage`, `SetPart`, `ScaleDamage`) — nothing in it assumes a boss.
///
///   • HE WALKS, one of two walks rolled per Wüstling (55.0 or 65.9 u/s, each its own tier), and swings for x1.3 a walker (upstream's
///     65 against 50) — the standing set at a target all but still within <see cref="StandRange"/>, upstream's base split.
///   • SHOOT HIM AND HE MAY CHARGE: every hit has a <see cref="ChargeChance"/> chance once the cooldown is out (the first
///     <see cref="FirstCooldown"/> s after he arrives). Its clock is <see cref="MadMin"/>-<see cref="MadMax"/> s from that hit.
///     THE TELL: he flinches (`charge_react`, 2.23 s — a pain yelp, then at 1.77 s the roar), then RUNS at 274.6 u/s (`charge_run`)
///     for what is left of the clock (2-2.7 s).
///   • THE SLAM ENDS IT: `charge_attack` (2.9 s), a lunge of up to <see cref="SlamTravel"/> forward to its blow at 0.77 s — his
///     target, within <see cref="SlamReach"/>, takes x<see cref="HeavyWeight"/> of his swing (upstream's 95 against 65; never over
///     `BossBase.MaxHit`) and the hulk's thud; a punch, a jolt within 500 and dust at his feet for everyone. He slams the moment his
///     target is in sight within <see cref="ChargeReach"/> (no charge then for <see cref="HitCooldown"/> s), or where he stands when
///     the clock runs out (<see cref="StopCooldown"/> s) — each counted from the slam's start, upstream's.
///     ⚠️ NO KNOCKBACK: upstream has none for him (the base's shove is a ZCT personality's, and a special never gets one).
///     ⚠️ THE SLAM IS HIS, NOT `ZombieAI`'S SWING: upstream plays the one clip and blow at both ends, so here both are this move,
///     timed by its own frame 23 (the AI swings at 1.8x and hits 10% in). His swing is off while he charges (`MeleeDisabled`).
///     ⚠️ OFF THE GROUND (a drop, a climb) THE CHARGE ENDS — its clock runs out and he slams once he is down. Upstream's comment says
///     so; its line set the COOLDOWN (`ManIsMadCooldown`) where it meant the clock (`ManIsMadTime`), and never stopped anything.
///     ⚠️ A BOARDED WINDOW IN HIS WAY ENDS IT TOO, with no slam: he stops to tear it with his ordinary swings — the comment's "moving
///     into an object", which upstream never wrote.
///     ⚠️ NOT UPSTREAM'S 100 °/s TURN WHILE CHARGING: the agent here steers by its path, and the body's turn is the AI's (`MaxYawRate`
///     + 0.85 x speed — 233 °/s at the charge's speed even at 0), so he cannot be baited off a ledge.
///   • HIS WEAK SPOT: a hit within <see cref="WeakReach"/> of `tag_back_weakspot` (the QC's: 6.5 along `j_spine4`, his upper back)
///     takes x<see cref="WeakScale"/> and a wet impact; anything else x<see cref="BodyScale"/> — 16 walkers' health from the front.
///   • A STUN SHAKES HIM: while the `stun` status holds him (Dead Wire's stun lock, Elemental Pop, Concussion…) he plays his
///     `stunned_electrobolt` loop instead of standing still — upstream's `SparkySequences`, its idle under `PerformStun`.
///     ⚠️ His shock DEATH (upstream's electrobolt as `DMG_SHOCK`'s death clip) is not here: nothing in this game kills by type.
///   • PAIN: a hit voices it at most every <see cref="PainGap"/> s, and so does the flinch that starts his charge.
///     ⚠️ Upstream's hit pain needs the map's stumbling setting and a head or arm hit; the flinch's own `vox_follower_pain` is an
///     event of the clip that no code of upstream's plays.
///   • HE RISES out of the ground (`trav_riser`, 6.4 s; its dirt-crawl at 1.1 s), crosses climbs with his window vault
///     (`trav_win_v1`), and hits the floor a second into his death.
///   ⚠️ LURES STILL PULL HIM (Banana Colada's stand, the Re-Animator's human); upstream's specials ignore monkey bombs — no variant
///   here can say so.
///
/// ⚠️ HOST AUTHORITY, EVERY MACHINE DRAWS: clips through `PlayShared`, sounds `NZSound.PlayShared`, the jolt `NZNet.ShakeAt`, the dust
/// `ShockRing.FireShared`; his pace (walk v1 / v2 / the charge) travels as a part (`SetPart( PacePart )` → <see cref="ShowPart"/>),
/// the Panzerhund's way, so a watching machine's legs match his.
/// ⛔ NO HIT OF HIS DOWNS ANYONE FROM FULL HEALTH: the slam goes through `HurtPlayer`, his swing under `ZombieAI.MaxHitDamage`.
/// </summary>
public sealed class WustlingZombie : BossBase
{
	protected override string Tag => "[nz-wustling]";
	protected override float VoiceHeight => 66f;

	// ══ tuning ═══════════════════════════════════════════════════════════════
	//
	// ⛔ NULLABLE-BACKED, INSTRUCTIONS.md §1.

	static float? _chance, _madMin, _madMax, _firstCd, _hitCd, _stopCd;
	/// <summary>A hit's chance to start the charge once the cooldown is out: 0.5 (upstream's `math.random(100) &lt;= 50`).</summary>
	public static float ChargeChance { get => _chance ?? 0.5f; set => _chance = value; }
	/// <summary>The charge's clock, from the hit that starts it, the tell included: 4.25-4.95 s, upstream's.</summary>
	public static float MadMin { get => _madMin ?? 4.25f; set => _madMin = value; }
	public static float MadMax { get => _madMax ?? 4.95f; set => _madMax = value; }
	/// <summary>No charge for 3 s after he arrives; for 6 s after a slam that reached someone, 3 s after one the clock ran out on.</summary>
	public static float FirstCooldown { get => _firstCd ?? 3f; set => _firstCd = value; }
	public static float HitCooldown { get => _hitCd ?? 6f; set => _hitCd = value; }
	public static float StopCooldown { get => _stopCd ?? 3f; set => _stopCd = value; }

	static float? _chargeReach, _slamReach, _slamTravel, _heavy;
	/// <summary>Charging, he slams at his target in sight this near: 95 (upstream's `AttackRange`, which starts his attack).</summary>
	public static float ChargeReach { get => _chargeReach ?? 95f; set => _chargeReach = value; }
	/// <summary>The slam's blow lands on his target within 95 of him (upstream's `DamageRange`) …</summary>
	public static float SlamReach { get => _slamReach ?? 95f; set => _slamReach = value; }
	/// <summary>… after a lunge of up to 163: `charge_attack`'s own root travel to its frame 23, which the port strips.</summary>
	public static float SlamTravel { get => _slamTravel ?? 163f; set => _slamTravel = value; }
	/// <summary>
	/// The slam's blow against his swing: 1.46 (upstream's `HeavyAttackDamage` 95 against his `AttackDamage` 65) — 57 to round 6, 95
	/// to 15, then `BossBase.MaxHit` (100) on every round after.
	/// </summary>
	public static float HeavyWeight { get => _heavy ?? 95f / 65f; set => _heavy = value; }

	static float? _weak, _body, _weakReach, _painGap;
	/// <summary>A hit within 10 of his weak spot takes x3, anything else x0.25 — upstream's table.</summary>
	public static float WeakScale { get => _weak ?? 3f; set => _weak = value; }
	public static float BodyScale { get => _body ?? 0.25f; set => _body = value; }
	public static float WeakReach { get => _weakReach ?? 10f; set => _weakReach = value; }
	/// <summary>A hit voices his pain at most this often: 8 s (the base's `LastStun + 8`); 0, never.</summary>
	public static float PainGap { get => _painGap ?? 8f; set => _painGap = value; }

	static float? _standRange, _standSpeed;
	/// <summary>He plants for a standing swing at a target this near and this still: 80 (`AttackRange − 15`), 3.9 u/s (`velocity² &lt; 15`).</summary>
	public static float StandRange { get => _standRange ?? 80f; set => _standRange = value; }
	public static float StandSpeed { get => _standSpeed ?? 3.9f; set => _standSpeed = value; }

	static bool? _shockClips;
	/// <summary>A stun shakes him in his electrobolt loop (upstream's `SparkySequences`); off, he stands rooted like a walker.</summary>
	public static bool ShockClips { get => _shockClips ?? true; set => _shockClips = value; }

	// his clips (30 fps) and their events — ⛔ `const`, never `static readonly` (INSTRUCTIONS.md §1)
	const string ClipReact = "nz_base_follower_charge_react", ClipSlam = "nz_base_follower_charge_attack",
		ClipShockA = "nz_base_follower_stunned_electrobolt_v1", ClipShockB = "nz_base_follower_stunned_electrobolt_v2",
		ClipVault = "nz_base_follower_trav_win_v1", ClipStandA = "nz_base_follower_stand_attack_v1",
		ClipStandB = "nz_base_follower_stand_attack_v2", ClipStandC = "nz_base_follower_stand_attack_v3";
	const float ReactLen = 67f / 30f, ReactPain = 3f / 30f, ReactRoar = 53f / 30f, SlamLen = 87f / 30f, SlamWhoosh = 18f / 30f,
		SlamHit = 23f / 30f, RiserCrawl = 33f / 30f, BodyfallAt = 30f / 30f;

	/// <summary>His clips' own speeds — `wustling.zvar`'s tiers are picked by them.</summary>
	const float WalkSlow = 55.0f, WalkFast = 65.9f, ChargeSpeed = 274.6f;
	/// <summary>The electrobolt is held this long past the last frame he was stunned.</summary>
	const float ShakeTail = 0.25f;
	/// <summary>The lunge stops this far short of the player it is aimed at, and of a wall.</summary>
	const float SlamGap = 30f;

	/// <summary>Not a bodygroup: the pace the host tells every machine (0 walk v1, 1 walk v2, 2 the charge), taken by <see cref="ShowPart"/>.</summary>
	const string PacePart = "wustling_pace";

	/// <summary>`tag_back_weakspot`, from the QC: 6.5 along `j_spine4`'s Y — his upper back (measured: model (0, 7.7, 60.4); the rig faces −Y).</summary>
	static Vector3 WeakOffset => new( 0f, 6.5f, 0f );

	static Color Dust => new( 0.72f, 0.64f, 0.52f );

	// ══ state ════════════════════════════════════════════════════════════════

	bool _mad, _chargeDue, _shaking, _paceSent, _fell;
	int _walkPace, _pace, _shownPace;
	float _madUntil, _cooldownUntil, _painAt, _nextWet, _diedAt;
	string _shakeClip;
	List<string> _standing;
	GameObject _tracked;
	Vector3 _trackedAt;
	float _targetSpeed = 999f;

	enum Beat { Pain, Roar, Whoosh, Slam, Crawl }

	public bool Charging => _mad;

	/// <summary>Free to start something: walking, swinging or standing — not in a clip of his own, a spawn or a fall.</summary>
	bool Free => Ai.State is ZombieState.Chasing or ZombieState.Attacking or ZombieState.Idle;

	static float SpeedOf( int pace ) => pace switch { 1 => WalkFast, 2 => ChargeSpeed, _ => WalkSlow };

	/// <summary>The standing swings (⚠️ LAZILY: a hotload into a live one finds this unset).</summary>
	List<string> Standing => _standing ??= new List<string> { ClipStandA, ClipStandB, ClipStandC };

	protected override void HostStart()
	{
		Ai.MaxHitDamage = MaxHit;

		// ⛔ NONE OF THE WALKER'S CLIMB CLIPS ON HIS MODEL: his window vault (upstream's `JumpSequences`)
		Ai.ClimbClipOverride = ClipVault;

		// upstream rolls one walk clip of two; each is its own tier here
		_walkPace = Game.Random.Int( 0, 1 );
		_pace = _walkPace;
		SetSpeed( SpeedOf( _pace ) );
		_cooldownUntil = Time.Now + FirstCooldown;

		// the riser's dirt-crawl, its frame 33 (`CoDZ_Zombie.Ground_ClimbOut`) — its frame 0 is the variant's SpawnSound
		if ( Ai.State == ZombieState.Spawning ) Queue( RiserCrawl, (int)Beat.Crawl );

		Say( $"spawned — {Hp?.Max ?? 0f:0} hp (x{BodyScale:0.##}, x{WeakScale:0.##} on his back) · walks {SpeedOf( _pace ):0.#} u/s"
			+ $" · charges on {ChargeChance * 100f:0}% of hits · hitboxes {Ai.HitboxCount}" );
	}

	/// <summary>
	/// ⚠️ A WATCHING MACHINE'S LEGS: the host's `SetSpeed` does not travel (the Panzerhund's note), so its puppet re-picks its clip from
	/// the pace it was told (<see cref="ShowPart"/>). Every machine.
	/// </summary>
	protected override void EveryFrame()
	{
		if ( !Ai.IsPuppet ) return;
		var speed = MathF.Max( 1f, SpeedOf( _shownPace ) * Size );
		if ( MathF.Abs( Ai.SpeedOverride - speed ) <= 0.01f ) return;
		Ai.SpeedOverride = speed;
		Ai.RepickAnimations();
	}

	protected override void HostFrame()
	{
		Ai.MaxHitDamage = MaxHit;

		// ⚠️ EVERY FRAME, the Napalm Zombie's way: his swing is off exactly while he charges — the slam is his
		Ai.MeleeDisabled = _mad;

		if ( Ai.State == ZombieState.Spawning ) { _chargeDue = false; return; }

		// ⚠️ THE PACE ONCE MORE AS HE STANDS UP: a watching machine may not have had him yet when he spawned
		if ( !_paceSent )
		{
			_paceSent = true;
			SetPart( PacePart, _pace );
		}

		TickShock();

		if ( _chargeDue )
		{
			_chargeDue = false;
			if ( !_mad && Free ) StartCharge();
		}

		if ( _mad ) TickCharge();
		PickAttackSet();
	}

	/// <summary>
	/// His table: x<see cref="WeakScale"/> on the weak spot, x<see cref="BodyScale"/> anywhere else — and the hit that may voice his
	/// pain or start his charge. THE HOST, once a hit.
	/// </summary>
	protected override float ScaleDamage( float amount, in DamageInfo damage )
	{
		var weak = OnWeakSpot( damage.Position );

		// the only sign you found it: a wet hit (upstream's `WeakImpactSounds`, the Margwa's), throttled per bullet
		if ( weak && Time.Now >= _nextWet )
		{
			_nextWet = Time.Now + 0.07f;
			NZSound.PlayShared( "nz.margwa.weakspot", damage.Position );
		}

		if ( PainGap > 0f && Time.Now >= _painAt + PainGap && Ai.State is not (ZombieState.Spawning or ZombieState.Dead) ) Pain();

		// ⚠️ ASKED FOR HERE, STARTED IN `HostFrame` — outside the damage call (the Director's way)
		if ( !_mad && Time.Now >= _cooldownUntil && Game.Random.Float( 0f, 1f ) < ChargeChance ) _chargeDue = true;

		return weak ? WeakScale : BodyScale;
	}

	/// <summary>A watching machine told his pace (`NZNet.BossPart`); the host's own echo is harmless (it never reads it).</summary>
	public override bool ShowPart( string group, int choice )
	{
		if ( group != PacePart ) return false;
		_shownPace = choice;
		return true;
	}

	protected override void OnBeat( int beat )
	{
		switch ( (Beat)beat )
		{
			case Beat.Pain:
				// the flinch's own `vox_follower_pain` (frame 3) — unless the hit that started it just voiced one
				if ( Time.Now >= _painAt + 1f ) Pain();
				break;

			case Beat.Roar:
				// `vox_follower_charge` (frame 53): THE TELL, half a second before he runs
				NZSound.PlayShared( "nz.wustling.charge", Voice );
				break;

			case Beat.Whoosh:
				// `melee_whoosh` (frame 18): the base's heavy swipe
				NZSound.PlayShared( "nz.zaballa.whoosh", Voice );
				break;

			case Beat.Slam:
				SlamBlow();
				break;

			case Beat.Crawl:
				NZSound.PlayShared( "nz.tesla.climbout", WorldPosition + Vector3.Up * 20f );
				break;
		}
	}

	protected override void Died( bool host )
	{
		_diedAt = Time.Now;
		if ( !host ) return;

		_mad = false;
		Ai.MeleeDisabled = false;
		Ai.AttackOverride = null;
		Say( "killed" );
	}

	/// <summary>The thud a second into either death clip (the base's `bodyfall_heavy`, frame 30/31). THE HOST.</summary>
	protected override void DeadFrame()
	{
		if ( _fell || !IsHost || Time.Now < _diedAt + BodyfallAt ) return;
		_fell = true;
		NZSound.PlayShared( "nz.megaton.bodyfall", WorldPosition );
	}

	// ══ the weak spot ════════════════════════════════════════════════════════

	/// <summary>Where `tag_back_weakspot` is now: on `j_spine4`, so it follows his lean; without the bone, behind his middle.</summary>
	Vector3 WeakSpot => Body.IsValid() && Body.TryGetBoneTransform( "j_spine4", out var spine )
		? spine.PointToWorld( WeakOffset )
		: WorldPosition + (Vector3.Up * 60f - Facing * 7.7f) * Size;

	/// <summary>Upstream's test: the hit's position within <see cref="WeakReach"/> of the spot (a blast's centre is not on it).</summary>
	bool OnWeakSpot( Vector3 at ) => !at.IsNearlyZero() && at.Distance( WeakSpot ) <= WeakReach * Size;

	void Pain()
	{
		_painAt = Time.Now;
		NZSound.PlayShared( "nz.wustling.pain", Voice );
	}

	// ══ the charge ═══════════════════════════════════════════════════════════

	/// <summary>The charge: the flinch and the roar (the tell), then the run until the clock — the slam ends it. THE HOST.</summary>
	void StartCharge()
	{
		if ( !PlayShared( ClipReact, ReactLen ) ) return;

		_mad = true;
		_madUntil = Time.Now + Game.Random.Float( MadMin, MathF.Max( MadMin, MadMax ) );
		Ai.MeleeDisabled = true;
		ApplyPace();

		Queue( ReactPain, (int)Beat.Pain );
		Queue( ReactRoar, (int)Beat.Roar );
		Say( $"CHARGING — runs at {ChargeSpeed:0.#} u/s once he has roared; slams in {_madUntil - Time.Now:0.##}s, or at his target"
			+ $" within {ChargeReach:0}" );
	}

	/// <summary>
	/// The charge, every frame of it: off the ground the clock runs out (he slams once down); a board in his way ends it; his target
	/// in sight within <see cref="ChargeReach"/> takes the slam now; at the clock's end, the slam where he stands. THE HOST.
	/// </summary>
	void TickCharge()
	{
		var airborne = Ai.OnLink || Ai.CrossingLink || Ai.Arcing;

		// ⚠️ UPSTREAM'S INTENT, NOT ITS LINE: `ManIsMadCooldown = CurTime() - 999` was meant for the clock
		if ( airborne && _madUntil > Time.Now )
		{
			_madUntil = Time.Now;
			Say( "off the ground — the charge ends as he lands" );
		}

		// his swing is off, so this is a board or a wall he stopped to tear
		if ( Ai.State == ZombieState.Attacking )
		{
			Calm( StopCooldown, "a barricade in his way" );
			return;
		}

		if ( airborne || Ai.State is not (ZombieState.Chasing or ZombieState.Idle) ) return;

		var p = TargetPlayer;
		if ( IsUp( p ) && WorldPosition.Distance( p.WorldPosition ) <= ChargeReach * Size && CanSee( p.GameObject ) )
		{
			Slam( true );
			return;
		}

		if ( Time.Now >= _madUntil ) Slam( false );
	}

	/// <summary>The charge over without a slam. THE HOST.</summary>
	void Calm( float cooldown, string why )
	{
		_mad = false;
		_cooldownUntil = Time.Now + cooldown;
		Ai.MeleeDisabled = false;
		ApplyPace();
		Say( $"charge over ({why}) — walking at {SpeedOf( _pace ):0.#} u/s" );
	}

	/// <summary>
	/// His speed for what he is doing — the charge, or his walk — and every machine told on a change. ⚠️ MID-SPECIAL ONLY THE NUMBER
	/// (Krasny's rule): a re-pick pushes the agent's speed back where the special roots it; the special's end re-picks. THE HOST.
	/// </summary>
	void ApplyPace()
	{
		var pace = _mad ? 2 : _walkPace;
		var speed = SpeedOf( pace );
		if ( Ai.State is ZombieState.Special or ZombieState.Spawning ) Ai.SpeedOverride = MathF.Max( 1f, speed * Size );
		else SetSpeed( speed );

		if ( pace == _pace ) return;
		_pace = pace;
		SetPart( PacePart, pace );
	}

	/// <summary>
	/// The slam, both of the charge's ends (upstream plays the one clip and blow for both): facing his target when he reached them,
	/// the lunge to the blow at frame 23, his walk again after. THE HOST.
	/// </summary>
	void Slam( bool reached )
	{
		var p = TargetPlayer;
		if ( reached && IsUp( p ) ) Face( p.WorldPosition );

		_mad = false;
		_cooldownUntil = Time.Now + (reached ? HitCooldown : StopCooldown);
		Ai.MeleeDisabled = false;

		var played = PlayShared( ClipSlam, SlamLen );
		ApplyPace();
		if ( !played ) return;

		// ⚠️ THE LUNGE: the clip's 163 to its blow is root motion the port strips — an arc along his facing, short of a wall and of the
		// player ahead (upstream's body stopped against them)
		var dir = Facing;
		var reach = SlamTravel * Size;
		if ( IsUp( p ) )
		{
			var to = (p.WorldPosition - WorldPosition).WithZ( 0 );
			if ( to.Length > 1f && Vector3.Dot( to.Normal, dir ) > 0.7f ) reach = MathF.Min( reach, to.Length - SlamGap * Size );
		}
		if ( reach > 16f )
		{
			var from = WorldPosition + Vector3.Up * 40f * Size;
			var wall = Scene.Trace.Ray( from, from + dir * reach ).IgnoreGameObjectHierarchy( GameObject )
				.WithoutTags( "zombie", "player", "trigger", "ragdoll" ).Run();
			if ( wall.Hit ) reach = MathF.Min( reach, wall.Distance - SlamGap * Size );
			if ( reach > 16f ) Ai.BeginArc( WorldPosition + dir * reach, SlamHit, 0f );
		}

		Queue( SlamWhoosh, (int)Beat.Whoosh );
		Queue( SlamHit, (int)Beat.Slam );
		Say( reached
			? $"SLAM at {(p.IsValid() ? p.GameObject.Name : "nobody")} — no charge for {HitCooldown:0.#}s"
			: $"the clock ran out — SLAM where he stands, no charge for {StopCooldown:0.#}s" );
	}

	/// <summary>
	/// The slam's blow (`melee_heavy` + `evt_follower_slam`, frame 23): the vox, the punch, the jolt within 500, the dust — and his
	/// target, if within <see cref="SlamReach"/>, takes x<see cref="HeavyWeight"/> of his swing (never over `MaxHit`) and the hulk's
	/// thud. Upstream's `DoAttackDamage`: his target alone. THE HOST.
	/// </summary>
	void SlamBlow()
	{
		var at = WorldPosition;
		NZSound.PlayShared( "nz.wustling.melee", Voice );
		NZSound.PlayShared( "nz.wustling.slam", at + Vector3.Up * 20f );
		NZNet.ShakeAt( at, 0.5f, 500f );
		ShockRing.FireShared( at + Vector3.Up * 4f, 110f * Size, Dust );

		var p = TargetPlayer;
		if ( !IsUp( p ) || p.WorldPosition.Distance( at ) > SlamReach * Size ) return;
		if ( HurtPlayer( p, SwingDamage( HeavyWeight ), source: "wustling slam" ) )
			NZSound.PlayShared( "nz.wustling.impact", p.WorldPosition + Vector3.Up * 40f );
	}

	// ══ the stun and the standing swing ══════════════════════════════════════

	/// <summary>
	/// A stun shakes him in his electrobolt loop, held while the `stun` status lasts and <see cref="ShakeTail"/> after (upstream's
	/// `SparkySequences`). THE HOST.
	/// </summary>
	void TickShock()
	{
		var stunned = ShockClips && StatusEffects.Has( GameObject, "stun" );
		if ( _shaking )
		{
			if ( stunned && Ai.State == ZombieState.Special && Ai.CurrentClip == _shakeClip ) Ai.HoldSpecialFor( ShakeTail );
			else _shaking = false;
			return;
		}

		if ( !stunned || !Free ) return;
		var clip = Game.Random.Int( 0, 1 ) == 0 ? ClipShockA : ClipShockB;
		if ( !PlayShared( clip, ShakeTail ) ) return;
		_shaking = true;
		_shakeClip = clip;
	}

	/// <summary>
	/// Which swings his next attack draws from — upstream's base `Attack`: the standing set at a target all but still within
	/// <see cref="StandRange"/>, else the tier's moving ones (none while he charges). The AI reads it as each swing starts. THE HOST.
	/// </summary>
	void PickAttackSet()
	{
		var t = Ai.Target;
		if ( !t.IsValid() )
		{
			_tracked = null;
			_targetSpeed = 999f;
		}
		else if ( t != _tracked )
		{
			_tracked = t;
			_trackedAt = t.WorldPosition;
			_targetSpeed = 999f;
		}
		else
		{
			var dt = MathF.Max( 0.001f, Time.Delta );
			var v = (t.WorldPosition - _trackedAt).WithZ( 0 ).Length / dt;
			_trackedAt = t.WorldPosition;
			if ( v <= 1200f ) _targetSpeed += (v - _targetSpeed) * (1f - MathF.Exp( -10f * dt ));   // past 1200: a teleport, not a run
		}

		var stand = !_mad && t.IsValid() && _targetSpeed < StandSpeed && WorldPosition.Distance( t.WorldPosition ) <= StandRange * Size;
		var want = stand ? Standing : null;
		if ( !ReferenceEquals( Ai.AttackOverride, want ) ) Ai.AttackOverride = want;
	}

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

	static IEnumerable<WustlingZombie> All
		=> Game.ActiveScene?.GetAllComponents<WustlingZombie>() ?? Enumerable.Empty<WustlingZombie>();

	/// <summary>"walk v1", "walk v2" or "CHARGING (Ns on the clock)" — a watching machine knows only the pace the host told it.</summary>
	string PaceReport
		=> IsHost
			? (_mad ? $"CHARGING ({MathF.Max( 0f, _madUntil - Time.Now ):0.#}s on the clock)" : _walkPace == 0 ? "walk v1" : "walk v2")
			: (_shownPace == 2 ? "CHARGING (the host's)" : _shownPace == 1 ? "walk v2" : "walk v1");

	/// <summary>`nz_wustling` — every Wüstling alive, and the tuning.</summary>
	[ConCmd( "nz_wustling" )]
	public static void Report()
	{
		Log.Info( $"[nz-wustling] charge on {ChargeChance * 100f:0}% of hits, clock {MadMin:0.##}-{MadMax:0.##}s · cooldowns {FirstCooldown:0.#}s"
			+ $" (arrival) / {HitCooldown:0.#}s (reached) / {StopCooldown:0.#}s (clock) · slams at {ChargeReach:0} in sight, lunge"
			+ $" {SlamTravel:0}, blow within {SlamReach:0} x{HeavyWeight:0.###} a swing · hit cap {MaxHit:0}" );
		Log.Info( $"[nz-wustling] weak spot x{WeakScale:0.##} within {WeakReach:0.#}, else x{BodyScale:0.##} · pain every {PainGap:0.#}s"
			+ $" · stun shake {(ShockClips ? "on" : "off")} · standing swing within {StandRange:0} under {StandSpeed:0.#} u/s" );

		var list = All.ToList();
		Log.Info( $"[nz-wustling] {list.Count} alive" );
		foreach ( var w in list )
		{
			if ( !w.Ai.IsValid() ) continue;

			// ⚠️ THE WEAK SPOT'S SANITY: it must sit ~6.5 BEHIND his spine — a negative number means the bone's frame came out
			// mirrored and `WeakOffset` needs its sign flipped
			var spine = w.Body.IsValid() && w.Body.TryGetBoneTransform( "j_spine4", out var s ) ? s.Position : w.WorldPosition;
			var behind = -Vector3.Dot( w.WeakSpot - spine, w.Facing );
			Log.Info( $"[nz-wustling]   hp {w.Hp?.Current ?? 0f:0}/{w.Hp?.Max ?? 0f:0} · state {w.Ai.State} · speed {w.Ai.MoveSpeed:0}"
				+ $" · {w.PaceReport} · {(w._mad ? "" : $"next charge in {MathF.Max( 0f, w._cooldownUntil - Time.Now ):0.#}s · ")}"
				+ $"weak spot {behind:0.#} behind his spine{(w._shaking ? " · SHAKING (stunned)" : "")}"
				+ $"{(w.Ai.AttackOverride is not null ? " · standing swing" : "")}" );
		}
	}

	/// <summary>`nz_wustling_charge` — every Wüstling charges now (the tell, the run, the slam), whatever the cooldown and the dice.</summary>
	[ConCmd( "nz_wustling_charge" )]
	public static void ChargeCmd()
	{
		if ( NZGame.IsClient ) { Log.Info( "[nz-wustling] the host decides" ); return; }
		foreach ( var w in All.Where( w => w.Ai.IsValid() && !w.Dead && !w._mad ).ToList() )
		{
			if ( w.Free ) w.StartCharge();
			else Log.Info( $"[nz-wustling] busy ({w.Ai.State}) — try again" );
		}
	}

	/// <summary>`nz_wustling_slam` — every Wüstling slams now where he stands (the charge's end), charging or not.</summary>
	[ConCmd( "nz_wustling_slam" )]
	public static void SlamCmd()
	{
		if ( NZGame.IsClient ) { Log.Info( "[nz-wustling] the host decides" ); return; }
		foreach ( var w in All.Where( w => w.Ai.IsValid() && !w.Dead && w.Free ).ToList() ) w.Slam( false );
	}

	/// <summary>`nz_wustling_set &lt;key&gt; &lt;value&gt;` — retune him live (`shock` 0 / 1).</summary>
	[ConCmd( "nz_wustling_set" )]
	public static void SetCmd( string key = "", float value = 0f )
	{
		switch ( key.ToLowerInvariant() )
		{
			case "chance": ChargeChance = value; break;
			case "madmin": MadMin = value; break;
			case "madmax": MadMax = value; break;
			case "firstcd": FirstCooldown = value; break;
			case "hitcd": HitCooldown = value; break;
			case "stopcd": StopCooldown = value; break;
			case "reach": ChargeReach = value; break;
			case "slamreach": SlamReach = value; break;
			case "travel": SlamTravel = value; break;
			case "heavy": HeavyWeight = value; break;
			case "weak": WeakScale = value; break;
			case "body": BodyScale = value; break;
			case "weakreach": WeakReach = value; break;
			case "pain": PainGap = value; break;
			case "shock": ShockClips = value != 0f; break;
			case "standrange": StandRange = value; break;
			case "standspeed": StandSpeed = value; break;
			default:
				Log.Info( "[nz-wustling] nz_wustling_set <chance|madmin|madmax|firstcd|hitcd|stopcd|reach|slamreach|travel|heavy|weak|body"
					+ "|weakreach|pain|shock|standrange|standspeed> <value>" );
				return;
		}
		Log.Info( $"[nz-wustling] {key} = {value:0.###}" );
		Report();
	}
}