Zombies/MargwaBoss.cs

Component controlling the Margwa boss AI and visuals. Manages heads/mouths, attacks (slam, pulse, teleport, fire line, shadow skulls, scream), head popping, sounds, lights, bone overrides, networking of mouth/head events, and designer-tunable parameters with console commands.

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

namespace NZombies;

/// <summary>
/// The Margwa — BO3's three-headed boss, ported 2026-10-06 from GMod nZombies' `nz_zombie_boss_margwa` (GhostlyMoo's port of
/// Shadows of Evil / Revelations). The user picked it first of the enemies chosen on 2026-10-05; its four other entries are
/// the same body in another skin (`margwa_zod`, `margwa_genesis`, `margwa_fire`, `margwa_shadow`).
///
/// | | an OPEN mouth, hit within <see cref="MouthReach"/> of its head | everywhere else |
/// |---|---|---|
/// | **damage** | **×0.75** | **×0.01** |
///
/// ⛔ THOSE TWO NUMBERS ARE UPSTREAM'S (`OnInjured`: `ScaleDamage(0.75)` / `ScaleDamage(0.01)`) AND THEY ARE THE FIGHT. The
/// body is armour; the only way in is a mouth, and only one opens at a time: every 3.5-4.15 s (less a second per head lost)
/// one of the THREE heads, at random, opens for 1.5 s — a head already gone wastes its turn, exactly as upstream rolls
/// `table.Random(self.HeadTbl)` over all three.
///
/// ⛔ A HEAD COMES OFF ONLY AT A THRESHOLD, AND ONLY THROUGH ITS OWN OPEN MOUTH: the first once health is at 65% or less, the
/// second at 35% (`healththreshold1/2`). Each pays the shooter 500, makes him wait 3 s before another can go
/// (`IFrames`), plays the stagger for that head, and from the first one on he CHARGES (upstream's `SetRunSpeed(71)` the tick
/// after any head is lost). The third head is the kill itself.
///
/// ⚠️ THE MOUTH IS A JAW, TURNED HERE, NOT A CLIP. Upstream's mouth clips are delta layers that move exactly one bone each
/// (that head's lower jaw, 53-62°, measured from the decompiled SMDs), and s&amp;box plays one sequence; so the jaw is turned by
/// a bone override on top of whatever is playing (<see cref="OnPreRender"/>), and a light in the element's colour sits in the
/// open mouth — upstream's `zmb_mimic_mouth` glow. `nz_margwa_jaw 0` turns the override off and leaves the light.
///
/// ⚠️ THE SKIN DECIDES WHAT ELSE HE DOES (`MaterialGroup`; the user's picks, 2026-10-06):
///   • skin 0, Shadows of Evil — TELEPORTS: far from his target he goes through a portal and climbs out beside it.
///   • skin 1, Revelations — an AREA PULSE: close to his target he rears up, an amber ring shows its reach, and the ground
///     erupts round him — damage inside, every screen shaking (the user's pick, Oberon's pulse as the model). And he is the
///     QUICK, ANGRY one (the user: *"the genesis one must be a bit faster and more agressive than the others"*): 20% faster on
///     his feet, 50% faster swings, his pulse and slam on short clocks (<see cref="GenesisPace"/>). And from further off he
///     SCREAMS THE SHRIEKER'S SCREAM (the user: *"must also be able to use the shrieker scream that stuns the player"*): every
///     mouth wide — and open to a shot while it lasts — then `SonicWave`, the Shrieker's own dazing wave.
///   • skin 2, Fire — lays a LINE of fire along the floor in front of him that burns for seconds (`MargwaFireLine`; it
///     replaced upstream's rolling fire wave on request).
///   • skin 3, Shadow — opens a portal and sends four homing skulls (`nz_ent_proj_chomper`, `MargwaProjectile`).
///
/// ⚠️ HOST AUTHORITY, EVERY MACHINE DRAWS: the host decides mouths, heads and attacks; `NZNet.MargwaMouth` /
/// `NZNet.MargwaHeadGone` tell everyone, and each machine turns its own jaw, lights its own mouth and switches its own head
/// bodygroup — a renderer property does not replicate after the spawn.
///
/// ⛔ NO ATTACK OF HIS DOWNS ANYONE FROM FULL HEALTH (<see cref="HitCap"/>), Oberon's rule — *"we cant have any attack insta
/// kill like that"* (2026-09-27). The swipe is capped through `ZombieAI.MaxHitDamage`, the slam and the projectiles here.
/// </summary>
public sealed class MargwaBoss : Component
{
	public enum Element { Normal, Fire, Shadow }

	// ══ tuning ═══════════════════════════════════════════════════════════════
	//
	// ⛔ NULLABLE-BACKED, INSTRUCTIONS.md §1: a static's value survives a hotload, its initialiser does not re-run.

	static float? _mouthScale;
	/// <summary>Damage an open mouth takes. 0.75, upstream's.</summary>
	public static float MouthScale { get => _mouthScale ?? 0.75f; set => _mouthScale = value; }

	static float? _bodyScale;
	/// <summary>Damage everything else takes — the body, a closed mouth, a blast. 0.01, upstream's.</summary>
	public static float BodyScale { get => _bodyScale ?? 0.01f; set => _bodyScale = value; }

	static float? _mouthReach;
	/// <summary>How near a hit must land to the open head's bone to count, at model scale 1. 34 (upstream 35 middle, 33 sides).</summary>
	public static float MouthReach { get => _mouthReach ?? 34f; set => _mouthReach = value; }

	static float? _openSeconds;
	/// <summary>How long a mouth stays open. 1.5 s, upstream's `OpenMouthTime`.</summary>
	public static float OpenSeconds { get => _openSeconds ?? 1.5f; set => _openSeconds = value; }

	static float? _gapMin, _gapMax;
	/// <summary>Seconds between mouth rolls, less one per head lost. 3.5-4.15, upstream's `NextMouthOpen`.</summary>
	public static float GapMin { get => _gapMin ?? 3.5f; set => _gapMin = value; }
	public static float GapMax { get => _gapMax ?? 4.15f; set => _gapMax = value; }

	static float? _popFirstAt, _popSecondAt;
	/// <summary>Health share at or under which a head can come off: 0.65 for the first, 0.35 for the second.</summary>
	public static float PopFirstAt { get => _popFirstAt ?? 0.65f; set => _popFirstAt = value; }
	public static float PopSecondAt { get => _popSecondAt ?? 0.35f; set => _popSecondAt = value; }

	static int? _popPoints;
	/// <summary>Points for taking a head off. 500, upstream's `GivePoints(500)`.</summary>
	public static int PopPoints { get => _popPoints ?? 500; set => _popPoints = value; }

	static float? _popGrace;
	/// <summary>Seconds after spawning or losing a head before another can go. 3, upstream's `IFrames`.</summary>
	public static float PopGrace { get => _popGrace ?? 3f; set => _popGrace = value; }

	static float? _chargeSpeed;
	/// <summary>
	/// His ABSOLUTE speed once a head is gone: 195 u/s, `nz_ai_margwa_run_charge`'s own ground speed (442 u over 69 frames).
	/// Before that he walks at the variant's `FixedSpeed`, 88 — the walk clip's 88.7.
	/// </summary>
	public static float ChargeSpeed { get => _chargeSpeed ?? 195f; set => _chargeSpeed = value; }

	static float? _slamRange, _slamRadius, _slamWeight, _slamGapMin, _slamGapMax;
	/// <summary>How near the target must be for the ground slam (`nz_ai_margwa_smash_attack`, upstream's stand attack).</summary>
	public static float SlamRange { get => _slamRange ?? 125f; set => _slamRange = value; }
	/// <summary>How far the slam reaches, from where the arms come down.</summary>
	public static float SlamRadius { get => _slamRadius ?? 170f; set => _slamRadius = value; }
	/// <summary>The slam against one swipe. 1.25 (upstream's heavy hit is 3× a swipe; the cap would eat most of that anyway).</summary>
	public static float SlamWeight { get => _slamWeight ?? 1.25f; set => _slamWeight = value; }
	public static float SlamGapMin { get => _slamGapMin ?? 8f; set => _slamGapMin = value; }
	public static float SlamGapMax { get => _slamGapMax ?? 12f; set => _slamGapMax = value; }

	static float? _elementRange;
	/// <summary>How near the target must be for a Fire or Shadow attack. 750, upstream's `TargetInRange(750)`.</summary>
	public static float ElementRange { get => _elementRange ?? 750f; set => _elementRange = value; }

	static float? _hitCap;
	/// <summary>The most one of his hits takes, as a share of the match's base health. Two-thirds, Oberon's.</summary>
	public static float HitCap { get => _hitCap ?? (2f / 3f); set => _hitCap = value; }

	static float? _teleportFrom, _teleportGapMin, _teleportGapMax, _teleportNear, _teleportFar;
	/// <summary>
	/// The Shadows of Evil Margwa goes through a portal when his target is at least this far. 900 — about five seconds of his
	/// charge, ten of his walk. Not scaled by his size: it is a distance across the map, not about his body.
	/// </summary>
	public static float TeleportFrom { get => _teleportFrom ?? 900f; set => _teleportFrom = value; }
	/// <summary>Seconds between portals: 14-22.</summary>
	public static float TeleportGapMin { get => _teleportGapMin ?? 14f; set => _teleportGapMin = value; }
	public static float TeleportGapMax { get => _teleportGapMax ?? 22f; set => _teleportGapMax = value; }
	/// <summary>Where he comes out: 160-360 units from his target, on the navmesh, on its floor.</summary>
	public static float TeleportNear { get => _teleportNear ?? 160f; set => _teleportNear = value; }
	public static float TeleportFar { get => _teleportFar ?? 360f; set => _teleportFar = value; }

	static float? _pulseRadius, _pulseWeight, _pulseEdge, _pulseGapMin, _pulseGapMax;
	/// <summary>The Revelations Margwa's pulse: how far it reaches, at size 1. 360 — 450 at his 1.25, near Oberon's 500.</summary>
	public static float PulseRadius { get => _pulseRadius ?? 360f; set => _pulseRadius = value; }
	/// <summary>Its damage at his feet, against one swipe. 1.2.</summary>
	public static float PulseWeight { get => _pulseWeight ?? 1.2f; set => _pulseWeight = value; }
	/// <summary>
	/// The share of that at the rim. 0.5 — the ring on the floor promises a hit anywhere inside it, so the edge must still hurt;
	/// a linear falloff to nothing would make the outer third a lie.
	/// </summary>
	public static float PulseEdge { get => _pulseEdge ?? 0.5f; set => _pulseEdge = value; }
	/// <summary>Seconds between pulses: 4.5-7 (10-15 until he was made the aggressive one; 6-9 until "more frequently").</summary>
	public static float PulseGapMin { get => _pulseGapMin ?? 4.5f; set => _pulseGapMin = value; }
	public static float PulseGapMax { get => _pulseGapMax ?? 7f; set => _pulseGapMax = value; }

	static float? _screamRange, _screamMinRange, _screamGapMin, _screamGapMax, _screamWindUp, _screamHold;
	/// <summary>
	/// The Revelations Margwa's scream reaches this far: 650, the Shrieker's (`ShriekerZombie.ScreamRange`). He screams only at a
	/// target he can see — the Shrieker's rule, what makes the range fair.
	/// </summary>
	public static float ScreamRange { get => _screamRange ?? 650f; set => _screamRange = value; }
	/// <summary>He will not scream closer than this: 90, the Shrieker's — walking at him shuts him up (and brings the pulse).</summary>
	public static float ScreamMinRange { get => _screamMinRange ?? 90f; set => _screamMinRange = value; }
	/// <summary>Seconds between screams: 8-12 (the Shrieker's 7, for a boss with two other moves).</summary>
	public static float ScreamGapMin { get => _screamGapMin ?? 8f; set => _screamGapMin = value; }
	public static float ScreamGapMax { get => _screamGapMax ?? 12f; set => _screamGapMax = value; }
	/// <summary>How long he winds up before the wave leaves: 0.9 s, the Shrieker's; he holds the scream 0.8 s after.</summary>
	public static float ScreamWindUp { get => _screamWindUp ?? 0.9f; set => _screamWindUp = value; }
	public static float ScreamHold { get => _screamHold ?? 0.8f; set => _screamHold = value; }

	static float? _genesisPace, _genesisSwing, _genesisSlamGapMin, _genesisSlamGapMax;
	/// <summary>
	/// The Revelations Margwa's feet against the others': 1.2 — a walk of 106 for their 88, a charge of 234 for their 195. Both
	/// stay inside their animation tiers (walk under 130, charge over 190), so his legs simply cycle a little faster.
	/// </summary>
	public static float GenesisPace { get => _genesisPace ?? 1.2f; set => _genesisPace = value; }
	/// <summary>
	/// His swings against the others': 1.5 — `ZombieAI.AttackSpeed`, the windup and the recovery both, so he swings again half as
	/// soon. 1.25 until the user asked for him to *"attack more frequently"* (2026-10-06).
	/// </summary>
	public static float GenesisSwing { get => _genesisSwing ?? 1.5f; set => _genesisSwing = value; }
	/// <summary>Seconds between his slams: 4-6, where the others wait 8-12 (5-8 before "more frequently").</summary>
	public static float GenesisSlamGapMin { get => _genesisSlamGapMin ?? 4f; set => _genesisSlamGapMin = value; }
	public static float GenesisSlamGapMax { get => _genesisSlamGapMax ?? 6f; set => _genesisSlamGapMax = value; }

	static bool? _jawBones;
	/// <summary>Turn the jaw bone of the open mouth (`nz_margwa_jaw`). On.</summary>
	public static bool JawBones { get => _jawBones ?? true; set => _jawBones = value; }

	static Angles? _jawMid, _jawLeft, _jawRight;
	/// <summary>
	/// Each lower jaw's open turn, in its own frame: upstream's mouth clips at their widest, SMD Euler (x, y, z) read as
	/// (pitch y, yaw z, roll x) — Source's own order, so `Rotation.From` builds the same matrix.
	/// </summary>
	public static Angles JawMid { get => _jawMid ?? new Angles( 53.44f, -4.22f, -3.33f ); set => _jawMid = value; }
	public static Angles JawLeft { get => _jawLeft ?? new Angles( 62.39f, 0.26f, 3.32f ); set => _jawLeft = value; }
	public static Angles JawRight { get => _jawRight ?? new Angles( 62.09f, 2.96f, 4.70f ); set => _jawRight = value; }

	// ══ the heads ════════════════════════════════════════════════════════════
	//
	// ⛔ SWITCHES, NOT `static readonly` ARRAYS: an array built in an initialiser cannot be corrected in a live session (§1).
	// Head 0 is the middle, 1 the left, 2 the right — upstream's `HeadTbl` 1/2/3.

	public static string HeadBone( int h ) => h switch { 0 => "j_head", 1 => "j_head_le", _ => "j_head_ri" };
	static string JawBone( int h ) => h switch { 0 => "j_jaw_lower_1", 1 => "j_jaw_lower_1_le", _ => "j_jaw_lower_1_ri" };
	/// <summary>The lower jaw's one child (the inner lip), which must turn with it or tear off.</summary>
	static string JawChild( int h ) => h switch { 0 => "j_jaw_lower_upper_1", 1 => "j_jaw_lower_upper_1_le", _ => "j_jaw_lower_upper_1_ri" };
	/// <summary>The bodygroup per head — `margwa.vmdl`'s `midhead`/`lefthead`/`righthead`, choice 1 the stump.</summary>
	static string Group( int h ) => h switch { 0 => "midhead", 1 => "lefthead", _ => "righthead" };
	static string ShotClip( int h ) => h switch
	{
		0 => "nz_ai_margwa_shot_middle_head",
		1 => "nz_ai_margwa_shot_left_head",
		_ => "nz_ai_margwa_shot_right_head",
	};
	static Angles JawOpen( int h ) => h switch { 0 => JawMid, 1 => JawLeft, _ => JawRight };
	static string HeadName( int h ) => h switch { 0 => "middle", 1 => "left", _ => "right" };

	/// <summary>The mouth glow per element — upstream's `PostDraw` colours.</summary>
	public static Color GlowFor( Element e ) => e switch
	{
		Element.Fire => new Color( 1f, 0.235f, 0f ),
		Element.Shadow => new Color( 0.12f, 0.55f, 1f ),
		_ => new Color( 1f, 0.82f, 0f ),
	};

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

	public Element Kind { get; private set; }

	/// <summary>Does he go through portals — the Shadows of Evil Margwa (skin 0).</summary>
	public bool Teleports { get; private set; }

	/// <summary>Does he pulse — the Revelations Margwa (skin 1).</summary>
	public bool Pulses { get; private set; }

	/// <summary>His pace against the others': <see cref="GenesisPace"/> for the Revelations Margwa, 1 for the rest.</summary>
	float Pace => Pulses ? MathF.Max( 0.1f, GenesisPace ) : 1f;

	readonly bool[] _alive = { true, true, true };
	readonly float[] _jaw = new float[3];

	/// <summary>The head whose mouth is open, or -1. The host's decision, mirrored on every machine.</summary>
	public int OpenHead { get; private set; } = -1;

	float _openUntil;
	float _nextMouth;
	float _noPopUntil;
	float _nextSlam;
	float _nextElement;
	float _nextWeakPing;
	float _nextTeleport;
	float _nextPulse;
	float _nextScream;
	float _allOpenUntil;
	bool _paceApplied;

	/// <summary>`ShowMouth`'s head for EVERY mouth at once — the scream. -1 closes them all.</summary>
	const int AllMouths = 3;
	bool _vanished;
	bool _shielded;
	bool _deadHandled;
	bool _overriding;

	ZombieAI _ai;
	Health _hp;
	SkinnedModelRenderer _body;
	PointLight _mouthLight;
	GameObject _mouthLightGo;

	/// <summary>A timed beat inside one of his moves — data, not a lambda, so a hotload mid-move cannot strand a closure.</summary>
	enum Beat { SlamHit, FireTell, FireThrow, ShadowTell, ShadowPortal, ShadowSkull, PortalOut, Vanish, Arrive, PulseHit, ScreamLand }

	readonly List<(float At, Beat What, int N)> _pending = new();

	public int HeadsLost => _alive.Count( a => !a );

	public bool HeadAlive( int h ) => h >= 0 && h < 3 && _alive[h];

	/// <summary>The most one hit of his takes now (<see cref="HitCap"/> of the match's base health).</summary>
	public static float MaxHit => MathF.Max( 1f, Difficulty.MaxHealth * Math.Clamp( HitCap, 0.1f, 0.95f ) );

	/// <summary>Which way his BODY faces — the object's rotation carries the rig's yaw correction (`ZombieAI.ModelTurn`).</summary>
	Vector3 Facing => _ai.IsValid() ? (WorldRotation * _ai.ModelTurn.Inverse).Forward.WithZ( 0 ).Normal : WorldRotation.Forward;

	/// <summary>
	/// His size against the authored model: the variant's `ModelScale` (1.25 since 2026-10-06, the user: *"increase their size by
	/// 25%"*), which `ZombieAI.ApplyModelScale` puts on the object. Every distance of his own here is authored at size 1 and
	/// multiplied by this, as the capsule, the swipe's reach and <see cref="MouthReach"/> are.
	/// </summary>
	float Size => MathF.Max( 0.1f, WorldScale.x );

	/// <summary>Where his voice comes from: head height.</summary>
	Vector3 Voice => WorldPosition + Vector3.Up * 90f * Size;

	protected override void OnStart()
	{
		_ai = Components.Get<ZombieAI>( FindMode.EverythingInSelfAndAncestors );
		_hp = Components.Get<Health>( FindMode.EverythingInSelfAndAncestors );
		_body = Components.Get<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants );

		// ⚠️ THE ELEMENT FROM THE SKIN THE HOST ROLLED: `ZombieAI.ApplySkin` writes `skinN` on the renderer, which travels in
		// the network spawn, so every machine reads the same answer here.
		var group = _body.IsValid() ? _body.MaterialGroup ?? "" : "";
		Kind = group == "skin2" ? Element.Fire : group == "skin3" ? Element.Shadow : Element.Normal;

		// ⚠️ SKIN 0 IS THE ONE WITH NO GROUP: `ApplySkin` leaves the renderer alone for skin 0, so "not skin1-3" is Shadows of Evil
		Teleports = Kind == Element.Normal && group != "skin1";
		Pulses = group == "skin1";

		var now = Time.Now;
		_nextMouth = now + Game.Random.Float( GapMin, 7.5f );   // upstream's first roll: 3.5-7.5
		_noPopUntil = now + PopGrace;
		_nextElement = now + 5f;                                // upstream's `ElementalAttackCooldown = CurTime() + 5`
		_nextSlam = now + Game.Random.Float( 4f, 8f );
		_nextTeleport = now + 6f;
		_nextPulse = now + (Pulses ? 3f : 5f);
		_nextScream = now + 7f;   // the Shrieker's `ArmDelay`: never a scream the moment he arrives

		// ⚠️ THE REVELATIONS MARGWA'S PACE: a quicker swing now, and an absolute walk (`SpeedOverride`, as a lost head sets his
		// charge) that `TickPace` hands to the animation once he has climbed out — `RepickAnimations` also pushes the agent's
		// speed, which must not move a body still in its entrance.
		if ( Pulses && _ai.IsValid() && !NZGame.IsClient )
		{
			_ai.AttackSpeed *= MathF.Max( 0.1f, GenesisSwing );
			_ai.SpeedOverride = MathF.Max( 1f, (_ai.Variant?.FixedSpeed ?? 88f) * Pace );
		}

		if ( _ai.IsValid() )
		{
			_ai.MaxHitDamage = MaxHit;

			// ⛔ HE HAS NONE OF THE WALKER'S CLIMB CLIPS, and an unknown sequence is the bind pose rather than an error (Oberon's
			// note). The 48-unit mantle is his: in place, since `margwa_port.py` strips its travel.
			_ai.ClimbClipOverride = "nz_ai_margwa_mantle_over_48";
		}

		EnsureMouthLight();

		if ( !NZGame.IsClient )
			Log.Info( $"[nz-margwa] spawned — {Kind}, {_hp?.Max ?? 0f:0} hp · mouth x{MouthScale:0.##}, body x{BodyScale:0.###}"
				+ $" · heads come off at {PopFirstAt * 100f:0}% / {PopSecondAt * 100f:0}% · hitboxes {_ai?.HitboxCount ?? 0}" );
	}

	protected override void OnUpdate()
	{
		if ( !_ai.IsValid() ) return;

		if ( _ai.State == ZombieState.Dead )
		{
			OnDeath();
			return;
		}

		// ── the host decides ──
		if ( !_ai.IsPuppet )
		{
			_ai.MaxHitDamage = MaxHit;
			TickShield();
			TickPace();
			TickMouths();
			TickPending();
			TickMoves();
		}

		// ── every machine draws ──
		if ( OpenHead >= 0 && Time.Now >= _openUntil ) OpenHead = -1;

		float step = Time.Delta / 0.18f;
		for ( int h = 0; h < 3; h++ )
		{
			var want = _alive[h] && (h == OpenHead || Time.Now < _allOpenUntil) ? 1f : 0f;
			_jaw[h] = want > _jaw[h] ? MathF.Min( want, _jaw[h] + step ) : MathF.Max( want, _jaw[h] - step );
		}

		TickMouthLight();
	}

	// ══ spawn ════════════════════════════════════════════════════════════════

	/// <summary>The Revelations Margwa's walk, handed to the animation once he is out of his entrance (`RepickAnimations`).</summary>
	void TickPace()
	{
		if ( _paceApplied || !Pulses ) return;
		if ( _ai.State is not (ZombieState.Chasing or ZombieState.Attacking) ) return;

		_paceApplied = true;
		_ai.RepickAnimations();
	}

	/// <summary>
	/// Untouchable while he climbs out — upstream's `SetInvulnerable(true)` for the length of `nz_ai_margwa_spawn`.
	/// </summary>
	void TickShield()
	{
		var spawning = _ai.State == ZombieState.Spawning;
		if ( spawning == _shielded || !_hp.IsValid() ) return;

		_shielded = spawning;
		_hp.Invulnerable = spawning;

		// ⚠️ THE ARRIVAL IS A BOSS BEAT, heard wherever you stand: upstream plays `spawn_2d` through every client, beside the
		// positional roar the variant's `SpawnSound` already gives.
		if ( spawning ) NZSound.PlayShared( "nz.margwa.spawn2d" );
		else NZSound.PlayShared( "nz.margwa.teleport", WorldPosition + Vector3.Up * 50f );
	}

	// ══ the mouths ═══════════════════════════════════════════════════════════

	void TickMouths()
	{
		if ( Time.Now < _nextMouth ) return;

		// ⚠️ ALL THREE HEADS ROLL, DEAD ONES INCLUDED, as upstream's `table.Random(self.HeadTbl)` does: a lost head wastes its
		// turn. Rolling only the living ones would leave the last head open nearly all the time, since the gap also shrinks by
		// a second per head lost.
		var h = Game.Random.Int( 0, 2 );
		_nextMouth = Time.Now + MathF.Max( 0.5f, Game.Random.Float( GapMin, GapMax ) - HeadsLost );

		if ( _alive[h] ) OpenMouth( h, OpenSeconds );
	}

	/// <summary>Open one mouth (or close them all with -1) on every machine. THE HOST.</summary>
	void OpenMouth( int h, float seconds )
	{
		ShowMouth( h, seconds );
		if ( Networking.IsActive ) NZNet.MargwaMouth( GameObject.Id, h, seconds );
	}

	/// <summary>This machine's half of a mouth opening or closing (`NZNet.MargwaMouth`).</summary>
	public void ShowMouth( int h, float seconds )
	{
		// ⚠️ 3 IS EVERY MOUTH AT ONCE (the Revelations Margwa's scream), beside whichever single one is open; -1 closes them all
		if ( h == AllMouths )
		{
			_allOpenUntil = Time.Now + MathF.Max( 0f, seconds );
			return;
		}
		if ( h < 0 ) _allOpenUntil = 0f;

		OpenHead = h >= 0 && h < 3 && _alive[h] ? h : -1;
		_openUntil = Time.Now + MathF.Max( 0f, seconds );
	}

	void EnsureMouthLight()
	{
		if ( _mouthLight.IsValid() ) return;

		_mouthLightGo = new GameObject { Parent = GameObject, Name = "nz_margwa_mouth" };
		_mouthLightGo.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.NotNetworked;
		_mouthLight = _mouthLightGo.Components.Create<PointLight>();
		_mouthLight.LightColor = Color.Black;
		_mouthLight.Radius = 110f;
		_mouthLight.Shadows = false;
	}

	void TickMouthLight()
	{
		if ( !_mouthLight.IsValid() ) return;

		int h = -1;
		float w = 0f;
		for ( int i = 0; i < 3; i++ )
			if ( _jaw[i] > w ) { w = _jaw[i]; h = i; }

		if ( h < 0 || w <= 0.01f || !_body.IsValid() || !_body.TryGetBoneTransform( HeadBone( h ), out var head ) )
		{
			_mouthLight.LightColor = Color.Black;
			return;
		}

		// in front of the head, where the mouth opens — upstream's `*mouth_fx_tag` sits 9 units ahead of the head bone
		_mouthLightGo.WorldPosition = head.Position + Facing * 10f * Size;
		var c = GlowFor( Kind );
		_mouthLight.LightColor = c * (3.2f * w);
		_mouthLight.Radius = 110f * Size;
	}

	/// <summary>Turn the open jaw — after this frame's animation, on top of it.</summary>
	protected override void OnPreRender()
	{
		if ( !_body.IsValid() || _body.Model is null ) return;

		var sm = _body.SceneModel;
		if ( !sm.IsValid() ) return;

		var active = JawBones && _jaw.Any( j => j > 0.001f );
		if ( !active )
		{
			if ( _overriding ) { sm.ClearBoneOverrides(); _overriding = false; }
			return;
		}

		sm.ClearBoneOverrides();
		_overriding = true;

		var bones = _body.Model.Bones;
		var root = sm.Transform;
		for ( int h = 0; h < 3; h++ )
		{
			if ( _jaw[h] <= 0.001f || !_alive[h] ) continue;

			var jb = bones.GetBone( JawBone( h ) );
			if ( jb is null || !_body.TryGetBoneTransformAnimation( jb, out var jawWorld ) ) continue;

			// ⚠️ MODEL SPACE: `SetBoneOverride` takes a transform relative to the scene model.
			var jaw = root.ToLocal( jawWorld );
			var open = Rotation.Slerp( Rotation.Identity, Rotation.From( JawOpen( h ) ), Ease( _jaw[h] ) );
			var turned = jaw.WithRotation( jaw.Rotation * open );
			sm.SetBoneOverride( jb.Index, turned );

			// ⛔ AND ITS CHILD WITH IT: an override is the bone's FINAL transform, so the inner lip would stay where the
			// animation left it and tear away from the jaw it hangs off.
			var kb = bones.GetBone( JawChild( h ) );
			if ( kb is not null && _body.TryGetBoneTransformAnimation( kb, out var kidWorld ) )
				sm.SetBoneOverride( kb.Index, turned.ToWorld( jaw.ToLocal( root.ToLocal( kidWorld ) ) ) );
		}
	}

	static float Ease( float x ) => x * x * (3f - 2f * x);

	// ══ the damage table ═════════════════════════════════════════════════════

	/// <summary>
	/// The multiplier for one hit, and the head it takes off when it is the one. THE HOST (`Health.OnDamage`).
	///
	/// ⛔ IT MUTATES, SO EXACTLY ONCE PER HIT, the helmet's rule: a head comes off in here.
	/// </summary>
	public float ScaleFor( Vector3 hitPos, GameObject attacker )
	{
		if ( _deadHandled ) return 1f;
		if ( _shielded ) return 0f;

		var h = MouthHit( hitPos );
		if ( h < 0 ) return MathF.Max( 0f, BodyScale );

		// ⛔ THE ONLY SIGN YOU FOUND IT: a wet hit, throttled per bullet (upstream plays `WeakImpactSounds` on both ends)
		if ( Time.Now >= _nextWeakPing )
		{
			_nextWeakPing = Time.Now + 0.07f;
			NZSound.PlayShared( "nz.margwa.weakspot", hitPos );
		}

		TryPop( h, attacker );
		return MathF.Max( 0f, MouthScale );
	}

	/// <summary>The open head this hit landed on, or -1.</summary>
	int MouthHit( Vector3 hitPos )
	{
		if ( hitPos.IsNearlyZero() || !_body.IsValid() ) return -1;
		var reach = MouthReach * Size;

		// ⚠️ WHILE HE SCREAMS EVERY MOUTH IS OPEN, AND OPEN IS OPEN: the live head nearest the hit, within reach. Jaws drawn wide
		// that a bullet could not hurt would read as broken — and it is the price of the stun, a window to answer it in.
		if ( Time.Now < _allOpenUntil )
		{
			int best = -1;
			var bestD = reach;
			for ( int i = 0; i < 3; i++ )
			{
				if ( !_alive[i] || !_body.TryGetBoneTransform( HeadBone( i ), out var hd ) ) continue;
				var d = hitPos.Distance( hd.Position );
				if ( d <= bestD ) { bestD = d; best = i; }
			}
			if ( best >= 0 ) return best;
		}

		var h = OpenHead;
		if ( h < 0 || !_alive[h] ) return -1;
		if ( !_body.TryGetBoneTransform( HeadBone( h ), out var head ) ) return -1;

		return hitPos.Distance( head.Position ) <= reach ? h : -1;
	}

	void TryPop( int h, GameObject attacker )
	{
		if ( Time.Now < _noPopUntil || !_hp.IsValid() || _hp.Max <= 0f ) return;

		var share = _hp.Current / _hp.Max;
		var lost = HeadsLost;
		var ready = (lost == 0 && share <= PopFirstAt) || (lost == 1 && share <= PopSecondAt);
		if ( !ready ) return;

		PopHead( h, attacker, react: true );
	}

	/// <summary>Take a head off. THE HOST.</summary>
	void PopHead( int h, GameObject attacker, bool react )
	{
		if ( !_alive[h] ) return;

		_noPopUntil = Time.Now + PopGrace;
		OpenMouth( -1, 0f );

		SetHeadGone( h );
		if ( Networking.IsActive ) NZNet.MargwaHeadGone( GameObject.Id, h );

		var at = _body.IsValid() && _body.TryGetBoneTransform( HeadBone( h ), out var head ) ? head.Position : WorldPosition + Vector3.Up * 90f * Size;
		NZSound.PlayShared( "nz.margwa.headpop", at );
		NZSound.PlayShared( "nz.margwa.pain", at );

		// ⚠️ THE PAY GOES THROUGH `AddPoints`, which relays to a client's own machine (`NZNet.AwardPoints`)
		var shooter = attacker.IsValid() ? attacker.Components.Get<NZPlayer>( FindMode.EverythingInSelfAndAncestors ) : null;
		if ( react && shooter.IsValid() && PopPoints > 0 ) shooter.AddPoints( PopPoints );

		// ⛔ FROM THE FIRST HEAD ON HE CHARGES, AT AN ABSOLUTE SPEED (`SpeedOverride`, the helmet's way) — and `RepickAnimations`
		// or nothing reads it.
		if ( HeadsLost >= 1 && _ai.IsValid() )
		{
			_ai.SpeedOverride = MathF.Max( 1f, ChargeSpeed * Pace );
			_ai.RepickAnimations();
		}

		if ( react && HeadsLost <= 2 && _ai.IsValid() )
			_ai.PlaySpecial( ShotClip( h ), 101f / 30f );

		Log.Info( $"[nz-margwa] {HeadName( h ).ToUpperInvariant()} HEAD OFF — {HeadsLost}/3 · {_hp?.Current ?? 0f:0}/{_hp?.Max ?? 0f:0} hp"
			+ $" · {(shooter.IsValid() ? $"+{PopPoints} to {shooter.GameObject.Name}" : "no shooter")}"
			+ $" · speed {(_ai?.SpeedOverride ?? 0f):0}" );
	}

	/// <summary>This machine's half of a head coming off (`NZNet.MargwaHeadGone`): the stump, and the jaw let go.</summary>
	public void SetHeadGone( int h )
	{
		if ( h < 0 || h > 2 ) return;

		_alive[h] = false;
		_jaw[h] = 0f;
		if ( OpenHead == h ) OpenHead = -1;

		if ( _body.IsValid() ) _body.SetBodyGroup( Group( h ), 1 );
	}

	void OnDeath()
	{
		if ( _deadHandled ) return;
		_deadHandled = true;

		_pending.Clear();
		OpenHead = -1;
		for ( int i = 0; i < 3; i++ ) _jaw[i] = 0f;
		if ( _mouthLight.IsValid() ) _mouthLight.LightColor = Color.Black;

		var sm = _body.IsValid() ? _body.SceneModel : null;
		if ( sm.IsValid() ) sm.ClearBoneOverrides();
		_overriding = false;

		// ⚠️ NEVER A HIDDEN CORPSE: killed between the portals (he is untouchable there, but a nuke or a command is not), each
		// machine shows its own body again
		_vanished = false;
		MargwaFx.SetHidden( GameObject, false );

		// ⚠️ THE LAST HEAD GOES WITH HIM, as upstream's `PerformDeath` pops the first one still on. Each machine does its own,
		// from the same state: the heads that are gone already arrived over the network.
		for ( int i = 0; i < 3; i++ )
		{
			if ( !_alive[i] ) continue;
			SetHeadGone( i );
			if ( !NZGame.IsClient && _body.IsValid() && _body.TryGetBoneTransform( HeadBone( i ), out var head ) )
				NZSound.PlayShared( "nz.margwa.headpop", head.Position );
			break;
		}
	}

	// ══ his moves ════════════════════════════════════════════════════════════

	void TickMoves()
	{
		if ( _ai.State is not (ZombieState.Chasing or ZombieState.Attacking) ) return;

		var target = _ai.Target;
		if ( !target.IsValid() ) return;

		var dist = WorldPosition.Distance( target.WorldPosition );
		var now = Time.Now;

		// ── the Shadows of Evil Margwa's portal: far from his target, he goes through one and comes out beside it ──
		if ( Teleports && now >= _nextTeleport && dist >= TeleportFrom && StartTeleport() ) return;

		// ── Fire / Shadow ──
		if ( Kind != Element.Normal && now >= _nextElement && dist <= ElementRange && CanSee( target ) )
		{
			Face( target.WorldPosition );

			if ( Kind == Element.Fire && _ai.PlaySpecial( "nz_ai_margwa_fire_attack", 63f / 30f ) )
			{
				_nextElement = now + Game.Random.Float( 10f, 16f );
				Queue( 0.05f, Beat.FireTell );
				Queue( 33f / 30f, Beat.FireThrow );
				return;
			}

			if ( Kind == Element.Shadow && _ai.PlaySpecial( "nz_ai_margwa_elec_attack", 96f / 30f ) )
			{
				_nextElement = now + Game.Random.Float( 12f, 18f );
				Queue( 0.05f, Beat.ShadowTell );
				Queue( 48f / 30f, Beat.ShadowPortal );
				// four skulls, 0.35 s apart, upstream's `timer.Simple(i * 0.35)`
				for ( int i = 1; i <= 4; i++ ) Queue( 48f / 30f + i * 0.35f, Beat.ShadowSkull, i );
				return;
			}
		}

		// ── the Revelations Margwa's pulse: close enough, he rears up and the ground erupts round him ──
		// ⚠️ FROM THE RING'S FULL REACH (80% until he was made the aggressive one): he goes for it the moment you are inside it
		if ( Pulses && now >= _nextPulse && dist <= PulseRadius * Size && StartPulse() ) return;

		// ── the Revelations Margwa's scream: further off and in sight, the Shrieker's dazing wave ──
		if ( Pulses && now >= _nextScream && dist >= ScreamMinRange && dist <= ScreamRange && CanSee( target ) && StartScream() )
			return;

		// ── the ground slam ──
		if ( now >= _nextSlam && dist <= SlamRange * Size )
		{
			Face( target.WorldPosition );
			if ( _ai.PlaySpecial( "nz_ai_margwa_smash_attack", 56f / 30f ) )
			{
				_nextSlam = now + (Pulses
					? Game.Random.Float( GenesisSlamGapMin, GenesisSlamGapMax )
					: Game.Random.Float( SlamGapMin, SlamGapMax ));
				NZSound.PlayShared( "nz.margwa.attack", Voice );
				Queue( 27f / 30f, Beat.SlamHit );   // `melee_heavy` + `slam` at frame 27
			}
		}
	}

	void Queue( float after, Beat what, int n = 0 ) => _pending.Add( (Time.Now + after, what, n) );

	void TickPending()
	{
		for ( int i = _pending.Count - 1; i >= 0; i-- )
		{
			var p = _pending[i];
			if ( Time.Now < p.At ) continue;
			_pending.RemoveAt( i );
			Run( p.What, p.N );
		}
	}

	void Run( Beat what, int n )
	{
		var target = _ai.Target;
		var front = WorldPosition + Facing * 100f * Size;

		switch ( what )
		{
			case Beat.SlamHit:
			{
				var at = WorldPosition + Facing * 45f * Size;
				NZSound.PlayShared( "nz.margwa.slam", at );
				NZSound.PlayShared( "nz.margwa.slam.far", at );
				NZNet.ShakeAt( at, 0.45f, 2000f );
				Blast( at, SlamRadius * Size, SlamWeight );
				break;
			}

			case Beat.FireTell:
				NZSound.PlayShared( "nz.margwa.fire.start", Voice );
				NZSound.PlayShared( "nz.margwa.warn", Voice );
				break;

			case Beat.FireThrow:
			{
				// ⛔ A LINE OF FIRE SINCE 2026-10-06, NOT UPSTREAM'S ROLLING WAVE — the user: *"it makes an entire line in front of it
				// a fire zone"*. From his feet, aimed at his target at the moment his arms come down (`fire_slam`, frame 33).
				var dir = target.IsValid() ? (target.WorldPosition - WorldPosition).WithZ( 0 ) : Facing;
				dir = dir.Length > 0.01f ? dir.Normal : Facing;
				CastFireLine( WorldPosition + dir * 50f * Size, dir );
				break;
			}

			case Beat.ScreamLand:
			{
				// the Shrieker's own `Land`: its scream, a shake (on every machine here — the Shrieker's shakes the host alone), and
				// `SonicWave`, which every machine builds and which dazes the player it reaches on that player's machine
				NZSound.PlayShared( NZSound.ShriekerScream, Voice );
				NZSound.PlayShared( "nz.margwa.attack", Voice );
				NZNet.ShakeAt( WorldPosition, 0.6f, ScreamRange );

				var from = Voice;
				var to = target.IsValid() ? target.WorldPosition + Vector3.Up * 36f : from + Facing * ScreamRange;
				SonicWave.Fire( from, to );
				break;
			}

			case Beat.PulseHit:
			{
				var at = WorldPosition;
				var radius = PulseRadius * Size;
				NZNet.MargwaPulse( at, radius );
				NZSound.PlayShared( "nz.margwa.slam", at );
				NZSound.PlayShared( "nz.margwa.slam.far", at );
				Pulse( at, radius );
				break;
			}

			case Beat.PortalOut:
				// `teleport_portal`, frame 13: the portal opens under him
				NZNet.MargwaPortal( WorldPosition, 70f * Size, 1.6f );
				NZSound.PlayShared( "nz.margwa.warp", WorldPosition + Vector3.Up * 40f );
				break;

			case Beat.Vanish:
			{
				// ⚠️ NOWHERE TO COME OUT (no navmesh near his target), AND HE STAYS: the clip ends where he stands
				var spot = target.IsValid() ? TeleportSpot( target ) : null;
				if ( spot is null ) break;

				NZNet.MargwaVanish( GameObject.Id, true );
				_vanished = true;
				if ( _hp.IsValid() ) _hp.Invulnerable = true;

				WarpTo( spot.Value );
				if ( target.IsValid() ) Face( target.WorldPosition );

				NZNet.MargwaPortal( spot.Value, 70f * Size, 2.2f );
				NZSound.PlayShared( "nz.margwa.teleport", spot.Value + Vector3.Up * 40f );
				break;
			}

			case Beat.Arrive:
				if ( !_vanished ) break;
				_vanished = false;

				NZNet.MargwaVanish( GameObject.Id, false );
				if ( _hp.IsValid() ) _hp.Invulnerable = false;
				if ( target.IsValid() ) Face( target.WorldPosition );

				// he climbs out with his own entrance, as he first came in
				_ai.PlaySpecial( "nz_ai_margwa_spawn", 54f / 30f );
				break;

			case Beat.ShadowTell:
				NZSound.PlayShared( "nz.margwa.shadow.start", Voice );
				NZSound.PlayShared( "nz.margwa.warn", Voice );
				break;

			case Beat.ShadowPortal:
				NZSound.PlayShared( "nz.margwa.shadow.portal", front );
				NZSound.PlayShared( "nz.margwa.slam", WorldPosition );
				NZNet.ShakeAt( WorldPosition, 0.35f, 1800f );
				break;

			case Beat.ShadowSkull:
			{
				// ⚠️ OUT OF THE PORTAL, 100 AHEAD AND 50 UP, upstream's `direction + Vector3(0,0,50)`; even a skull launched after
				// he died still flies, as upstream's timers do
				var from = front + Vector3.Up * 50f * Size + Facing.Cross( Vector3.Up ) * ((n - 2.5f) * 12f * Size);
				var aim = target.IsValid() ? (target.WorldPosition + Vector3.Up * 50f - from).Normal : Facing;
				MargwaProjectile.Launch( Scene, MargwaProjectile.Kinds.Skull, from, aim, target, GameObject );
				break;
			}
		}
	}

	/// <summary>What one of his swipes deals on this round — the fire line's and the slam's yardstick.</summary>
	float SwipeDamage()
	{
		int round = Math.Max( 1, RoundManager.Instance?.Round ?? 1 );
		return ZombieStats.AttackDamageForRound( round ) * (_ai?.Variant?.DamageMultiplier ?? 2f) * Difficulty.ZombieDamage;
	}

	/// <summary>Light the line of fire, here (the copy that burns) and on every other machine (copies that only draw). THE HOST.</summary>
	void CastFireLine( Vector3 start, Vector3 dir )
	{
		MargwaFireLine.Draw( Scene, start, dir, Size, true, SwipeDamage(), GameObject );
		if ( Networking.IsActive ) NZNet.MargwaFireLine( start, dir, Size );

		NZSound.PlayShared( "nz.margwa.slam", WorldPosition );
		NZSound.PlayShared( "nz.margwa.fire.impact", start );
		NZSound.PlayShared( "nz.margwa.fire.whoosh", start + dir * 300f * Size );
		NZNet.ShakeAt( WorldPosition, 0.4f, 1800f );
	}

	/// <summary>
	/// Scream: he stops, every mouth opens, and after the Shrieker's wind-up the wave leaves (`ScreamLand`). THE HOST.
	/// </summary>
	bool StartScream()
	{
		var target = _ai.Target;
		if ( !target.IsValid() ) return false;

		_nextScream = Time.Now + Game.Random.Float( ScreamGapMin, ScreamGapMax );
		Face( target.WorldPosition );
		if ( !_ai.PlaySpecial( "nz_ai_margwa_idle_01", ScreamWindUp + ScreamHold ) ) return false;

		OpenMouth( AllMouths, ScreamWindUp + ScreamHold );
		NZSound.PlayShared( NZSound.ShriekerCharge, Voice );
		NZSound.PlayShared( "nz.margwa.warn", Voice );
		Queue( ScreamWindUp, Beat.ScreamLand );
		return true;
	}

	/// <summary>
	/// Rear up for the pulse: `nz_ai_margwa_elec_attack`, whose two-handed slam (`elec_slam`, frame 48) is the release — 1.6 s of
	/// warning, the ring on the floor at the full reach meanwhile. THE HOST.
	/// </summary>
	bool StartPulse()
	{
		var target = _ai.Target;
		if ( !target.IsValid() ) return false;

		_nextPulse = Time.Now + Game.Random.Float( PulseGapMin, PulseGapMax );
		Face( target.WorldPosition );
		if ( !_ai.PlaySpecial( "nz_ai_margwa_elec_attack", 96f / 30f ) ) return false;

		const float charge = 48f / 30f;
		NZNet.MargwaPulseCharge( WorldPosition, PulseRadius * Size, charge );
		NZSound.PlayShared( "nz.margwa.warn", Voice );
		Queue( charge, Beat.PulseHit );
		return true;
	}

	/// <summary>
	/// The pulse's damage: everyone inside the circle on his floor, in his line of sight, <see cref="PulseWeight"/> of a swipe at
	/// his feet down to <see cref="PulseEdge"/> of that at the rim, under his cap, as an area hit with his name on it. THE HOST.
	/// </summary>
	void Pulse( Vector3 at, float radius )
	{
		var swipe = SwipeDamage();

		foreach ( var p in Scene.GetAllComponents<NZPlayer>().ToList() )
		{
			if ( !p.IsValid() || (p.IsDown || p.DownedNet) ) continue;

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

			// ⚠️ HIS FLOOR, NOT THE ONE ABOVE OR BELOW: the ring is drawn on the ground he stands on
			var flat = (p.WorldPosition - at).WithZ( 0 ).Length;
			if ( flat > radius || MathF.Abs( p.WorldPosition.z - at.z ) > 96f ) continue;

			var to = p.WorldPosition + Vector3.Up * 32f;
			var tr = Scene.Trace.Ray( at + Vector3.Up * 32f, to ).IgnoreGameObjectHierarchy( GameObject )
				.WithoutTags( "zombie", "player", "trigger" ).Run();
			if ( tr.Hit && tr.HitPosition.Distance( to ) > 24f ) continue;

			var t = Math.Clamp( flat / MathF.Max( 1f, radius ), 0f, 1f );
			var share = 1f - (1f - PulseEdge) * t;
			hp.Apply( MathF.Min( swipe * PulseWeight * share, MaxHit ), false, GameObject, blast: true, blastAt: at );
		}
	}

	/// <summary>Go into the portal: `nz_ai_margwa_teleport_out`, then out beside his target. THE HOST.</summary>
	bool StartTeleport()
	{
		var target = _ai.Target;
		if ( !target.IsValid() ) return false;

		_nextTeleport = Time.Now + Game.Random.Float( TeleportGapMin, TeleportGapMax );
		Face( target.WorldPosition );
		if ( !_ai.PlaySpecial( "nz_ai_margwa_teleport_out", 41f / 30f ) ) return false;

		Queue( 13f / 30f, Beat.PortalOut );        // `teleport_portal`
		Queue( 36f / 30f, Beat.Vanish );           // `teleport` is frame 38: gone just before it
		Queue( 36f / 30f + 0.35f, Beat.Arrive );   // after the clip, so the entrance can play
		return true;
	}

	/// <summary>Where to come out: on the navmesh, 160-360 from the target, on its floor; else a ring point snapped to the mesh.</summary>
	Vector3? TeleportSpot( GameObject target )
	{
		var center = target.WorldPosition;
		var nav = Scene.NavMesh;

		if ( nav is not null )
			for ( int i = 0; i < 12; i++ )
			{
				var p = nav.GetRandomPoint( center, TeleportFar );
				if ( !p.HasValue ) continue;

				var flat = (p.Value - center).WithZ( 0 ).Length;
				if ( flat < TeleportNear || flat > TeleportFar || MathF.Abs( p.Value.z - center.z ) > 64f ) continue;
				return p.Value;
			}

		var a = Game.Random.Float( 0f, MathF.PI * 2f );
		var g = ZombieAI.NavGround( Scene, center + new Vector3( MathF.Cos( a ), MathF.Sin( a ), 0f ) * ((TeleportNear + TeleportFar) * 0.5f) );
		return (g - center).WithZ( 0 ).Length >= TeleportNear * 0.5f && MathF.Abs( g.z - center.z ) <= 64f ? g : null;
	}

	/// <summary>Put him somewhere else, the agent with him, as a snap on every machine.</summary>
	void WarpTo( Vector3 to )
	{
		WorldPosition = to;

		var agent = Components.Get<NavMeshAgent>( FindMode.EverythingInSelfAndDescendants );
		if ( agent.IsValid() ) agent.SetAgentPosition( to );

		// ⛔ A SNAP, NOT A SLIDE: without this every other machine interpolates the body across the map
		GameObject.Network.ClearInterpolation();
	}

	/// <summary>Turn to face a point — his own facing, the rig's yaw correction included (`ZombieAI.FaceMovement`'s rule).</summary>
	void Face( Vector3 at )
	{
		var dir = (at - WorldPosition).WithZ( 0 );
		if ( dir.Length < 1f || !_ai.IsValid() ) return;
		WorldRotation = Rotation.LookAt( dir.Normal, Vector3.Up ) * _ai.ModelTurn;
	}

	bool CanSee( GameObject target )
	{
		var from = WorldPosition + Vector3.Up * 80f * Size;
		var to = target.WorldPosition + Vector3.Up * 50f;
		var tr = Scene.Trace.Ray( from, to ).IgnoreGameObjectHierarchy( GameObject ).WithoutTags( "zombie", "trigger" ).Run();
		return !tr.Hit || tr.HitPosition.Distance( to ) < 40f;
	}

	/// <summary>
	/// Damage every player in reach, with line of sight and linear falloff, as an area hit with his name on it — Oberon's
	/// `Blast`, the same 24 units of trace slack and the same cap.
	/// </summary>
	public void Blast( Vector3 at, float radius, float weight ) => BlastFrom( Scene, GameObject, _ai, at, radius, weight );

	public static void BlastFrom( Scene scene, GameObject source, ZombieAI ai, Vector3 at, float radius, float weight )
	{
		if ( !scene.IsValid() || radius <= 0f ) return;

		int round = Math.Max( 1, RoundManager.Instance?.Round ?? 1 );
		var damage = ZombieStats.AttackDamageForRound( round )
			* (ai?.Variant?.DamageMultiplier ?? 2f)
			* weight
			* Difficulty.ZombieDamage;

		foreach ( var p in scene.GetAllComponents<NZPlayer>().ToList() )
		{
			if ( !p.IsValid() ) continue;

			var hp = p.Components.Get<Health>( FindMode.EverythingInSelfAndDescendants );
			if ( !hp.IsValid() ) continue;

			var to = p.WorldPosition + Vector3.Up * 32f;
			var dist = at.Distance( to );
			if ( dist > radius ) continue;

			var tr = scene.Trace.Ray( at, to ).IgnoreGameObjectHierarchy( source ).Run();
			if ( tr.Hit && tr.HitPosition.Distance( to ) > 24f ) continue;

			var falloff = 1f - MathX.Clamp( dist / MathF.Max( radius, 1f ), 0f, 1f );
			hp.Apply( MathF.Min( damage * falloff, MaxHit ), false, source, blast: true, blastAt: at );
		}
	}

	/// <summary>The scale for a hit on this object: the Margwa's table, or 1 when it is not one (`Health.OnDamage`).</summary>
	public static float ScaleOn( GameObject victim, Vector3 hitPos, GameObject attacker )
	{
		if ( !victim.IsValid() ) return 1f;

		var m = victim.Components.Get<MargwaBoss>( FindMode.EverythingInSelfAndAncestors );
		return m.IsValid() ? m.ScaleFor( hitPos, attacker ) : 1f;
	}

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

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

	/// <summary>`nz_margwa` — every Margwa alive and the table.</summary>
	[ConCmd( "nz_margwa" )]
	public static void Report()
	{
		Log.Info( $"[nz-margwa] table — open mouth x{MouthScale:0.##}, else x{BodyScale:0.###} · reach {MouthReach:0}u"
			+ $" · mouth {OpenSeconds:0.##}s every {GapMin:0.##}-{GapMax:0.##}s less 1 per head lost"
			+ $" · heads at {PopFirstAt * 100f:0}%/{PopSecondAt * 100f:0}% (+{PopPoints}) · charge {ChargeSpeed:0} u/s"
			+ $" · slam {SlamRange:0}u/{SlamRadius:0}u x{SlamWeight:0.##} (at size 1) · hit cap {MaxHit:0} · jaw {(JawBones ? "on" : "off")}" );

		var list = All.ToList();
		Log.Info( $"[nz-margwa] {list.Count} alive" );
		foreach ( var m in list )
		{
			var hp = m._hp;
			Log.Info( $"[nz-margwa]   {m.Kind}{(m.Teleports ? " (teleports)" : m.Pulses ? $" (pulses + screams, pace x{m.Pace:0.##})" : "")} · size x{m.Size:0.##} · hp {hp?.Current ?? 0f:0}/{hp?.Max ?? 0f:0}"
				+ $" · heads {(m._alive[0] ? "M" : "-")}{(m._alive[1] ? "L" : "-")}{(m._alive[2] ? "R" : "-")}"
				+ $" · open {(m.OpenHead >= 0 ? HeadName( m.OpenHead ) : "none")} · state {m._ai?.State}"
				+ $" · speed {m._ai?.MoveSpeed ?? 0f:0} u/s · hitboxes {m._ai?.HitboxCount ?? 0}"
				+ ((m._ai?.HitboxCount ?? 0) == 0 ? " ⛔ NO HITBOXES: only the capsule can be hit, and no mouth" : "") );
		}
	}

	/// <summary>`nz_margwa_open &lt;0|1|2&gt; [seconds]` — open a mouth on every Margwa (0 middle, 1 left, 2 right).</summary>
	[ConCmd( "nz_margwa_open" )]
	public static void OpenCmd( int head = 0, float seconds = 5f )
	{
		if ( NZGame.IsClient ) { Log.Info( "[nz-margwa] the host decides the mouths" ); return; }
		foreach ( var m in All ) m.OpenMouth( head, seconds );
		Log.Info( $"[nz-margwa] {HeadName( head )} mouth open for {seconds:0.#}s" );
	}

	/// <summary>`nz_margwa_pop &lt;0|1|2&gt;` — take a head off every Margwa, as a shot would (no points).</summary>
	[ConCmd( "nz_margwa_pop" )]
	public static void PopCmd( int head = 0 )
	{
		if ( NZGame.IsClient ) { Log.Info( "[nz-margwa] the host decides the heads" ); return; }
		foreach ( var m in All ) m.PopHead( Math.Clamp( head, 0, 2 ), null, react: true );
	}

	/// <summary>`nz_margwa_scream` — every Revelations Margwa screams the Shrieker's scream at its target now.</summary>
	[ConCmd( "nz_margwa_scream" )]
	public static void ScreamCmd()
	{
		if ( NZGame.IsClient ) { Log.Info( "[nz-margwa] the host decides the screams" ); return; }

		var n = 0;
		foreach ( var m in All )
			if ( m.Pulses && m._ai.IsValid() && m.StartScream() ) n++;

		Log.Info( $"[nz-margwa] {n} Margwa(s) screaming · {ScreamMinRange:0}-{ScreamRange:0}u, in sight · {SonicWave.DazeSeconds:0.#}s daze"
			+ $" · every {ScreamGapMin:0}-{ScreamGapMax:0}s (Revelations only)" );
	}

	/// <summary>`nz_margwa_pulse` — every Revelations Margwa rears up and pulses now.</summary>
	[ConCmd( "nz_margwa_pulse" )]
	public static void PulseCmd()
	{
		if ( NZGame.IsClient ) { Log.Info( "[nz-margwa] the host decides the pulses" ); return; }

		var n = 0;
		foreach ( var m in All )
			if ( m.Pulses && m._ai.IsValid() && m.StartPulse() ) n++;

		Log.Info( $"[nz-margwa] {n} Margwa(s) pulsing · {PulseRadius:0}u at size 1 · x{PulseWeight:0.##} of a swipe at the middle,"
			+ $" x{PulseWeight * PulseEdge:0.##} at the rim · every {PulseGapMin:0}-{PulseGapMax:0}s (Revelations only)" );
	}

	/// <summary>`nz_margwa_teleport` — every Shadows of Evil Margwa goes through its portal now, to beside its target.</summary>
	[ConCmd( "nz_margwa_teleport" )]
	public static void TeleportCmd()
	{
		if ( NZGame.IsClient ) { Log.Info( "[nz-margwa] the host decides the portals" ); return; }

		var n = 0;
		foreach ( var m in All )
			if ( m.Teleports && m._ai.IsValid() && m.StartTeleport() ) n++;

		Log.Info( $"[nz-margwa] {n} Margwa(s) into the portal · comes out {TeleportNear:0}-{TeleportFar:0}u from its target"
			+ $" · on its own past {TeleportFrom:0}u, every {TeleportGapMin:0}-{TeleportGapMax:0}s (Shadows of Evil only)" );
	}

	/// <summary>`nz_margwa_fire` — the nearest Margwa lays its line of fire at you now, whatever his skin.</summary>
	[ConCmd( "nz_margwa_fire" )]
	public static void FireCmd()
	{
		if ( NZGame.IsClient ) { Log.Info( "[nz-margwa] the host lights it" ); return; }

		var me = NZPlayer.Local;
		var m = All.Where( b => b._ai.IsValid() )
			.OrderBy( b => me.IsValid() ? b.WorldPosition.Distance( me.WorldPosition ) : 0f ).FirstOrDefault();
		if ( !m.IsValid() || !me.IsValid() ) { Log.Info( "[nz-margwa] no Margwa, or no player of yours" ); return; }

		var dir = (me.WorldPosition - m.WorldPosition).WithZ( 0 );
		dir = dir.Length > 0.01f ? dir.Normal : m.Facing;
		m.CastFireLine( m.WorldPosition + dir * 50f * m.Size, dir );
		Log.Info( $"[nz-margwa] fire line: {MargwaFireLine.Length * m.Size:0}u long, {MargwaFireLine.Width * m.Size:0}u wide,"
			+ $" {MargwaFireLine.Seconds:0.#}s, {MargwaFireLine.Dps:0.##} of a swipe a second" );
	}

	/// <summary>`nz_margwa_jaw &lt;0|1&gt;` — turn the open jaw's bone, or leave only the light.</summary>
	[ConCmd( "nz_margwa_jaw" )]
	public static void JawCmd( int on = 1 )
	{
		JawBones = on != 0;
		Log.Info( $"[nz-margwa] jaw bones {(JawBones ? "ON" : "OFF — the mouth light alone shows an open mouth")}" );
	}

	/// <summary>`nz_margwa_jaw_angles &lt;0|1|2&gt; &lt;pitch&gt; &lt;yaw&gt; &lt;roll&gt;` — retune one jaw's open turn live.</summary>
	[ConCmd( "nz_margwa_jaw_angles" )]
	public static void JawAnglesCmd( int head = 0, float pitch = 0f, float yaw = 0f, float roll = 0f )
	{
		var a = new Angles( pitch, yaw, roll );
		if ( head == 0 ) JawMid = a; else if ( head == 1 ) JawLeft = a; else JawRight = a;
		Log.Info( $"[nz-margwa] {HeadName( head )} jaw opens by {a}" );
	}

	/// <summary>`nz_margwa_set &lt;key&gt; &lt;value&gt;` — retune the table live.</summary>
	[ConCmd( "nz_margwa_set" )]
	public static void SetCmd( string key = "", float value = 0f )
	{
		switch ( key.ToLowerInvariant() )
		{
			case "mouth": MouthScale = value; break;
			case "body": BodyScale = value; break;
			case "reach": MouthReach = value; break;
			case "open": OpenSeconds = value; break;
			case "gapmin": GapMin = value; break;
			case "gapmax": GapMax = value; break;
			case "first": PopFirstAt = value; break;
			case "second": PopSecondAt = value; break;
			case "points": PopPoints = (int)value; break;
			case "charge": ChargeSpeed = value; break;
			case "slam": SlamWeight = value; break;
			case "slamrange": SlamRange = value; break;
			case "slamradius": SlamRadius = value; break;
			case "element": ElementRange = value; break;
			case "hitcap": HitCap = value; break;
			default:
				Log.Info( "[nz-margwa] nz_margwa_set <mouth|body|reach|open|gapmin|gapmax|first|second|points|charge|slam|slamrange"
					+ "|slamradius|element|hitcap> <value>" );
				Log.Info( "[nz-margwa]   upstream: mouth 0.75, body 0.01, reach 33-35, open 1.5, gap 3.5-4.15, heads at 0.65 / 0.35, 500 points" );
				return;
		}

		Log.Info( $"[nz-margwa] {key} = {value:0.###}" );
		Report();
	}
}