Zombies/MimicBoss.cs

Boss component for the Mimic enemy and its projectile. MimicBoss implements the Mimic boss AI (grabbing players with a tentacle, spitting a projectile, rage behavior, damage scaling, sounds, FX and diagnostics). MimicSpit is the network-spawned projectile the Mimic lobs, handles flight arc, collision, burst damage and visual/audio effects.

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

namespace NZombies;

/// <summary>
/// The Mimic — Black Ops 6's shape-shifter, ported 2026-10-07 from GMod nZombies' `nz_zombie_boss_mimic` (GhostlyMoo's port; its
/// behaviour lives in the special, `nz_zombie_special_mimic`), the third batch. Spec: `Sbox nzombies/Docs/boss_specs/mimic.md`.
///
///   • IT CLIMBS OUT OF THE GROUND (`riser_01`), untouchable for its first <see cref="SpawnProtection"/> s, and RUNS (213.3 u/s).
///   • THE TENTACLE GRAB, its main move, at a player in sight within <see cref="GrabRange"/> (a clear lane at the tentacle's
///     height too), every <see cref="GrabGapMin"/>-<see cref="GrabGapMax"/> whole seconds (the first 5 s after it arrives):
///     `command_grab_in` lunges 34 forward and whips the tentacle out to 192; its tip within <see cref="CatchRadius"/> of your
///     eyes, from frame 11 to the clip's end, catches you. CAUGHT, ON YOUR OWN MACHINE (the Panzer claw's way, `MimicGrab`): no
///     legs of your own, reeled along the floor to its mouth and held there, your view pulled to its jaw; 0.8 s into `success` it
///     BITES (x<see cref="HeavyWeight"/> of its swing — upstream's 75 against 50), and at 1.0 s it lets go and throws you off.
///     Missed: `miss`, 0.73 s. ⛔ IT TAKES NOTHING WHILE IT HOLDS YOU (upstream's): nobody shoots you free.
///     ⚠️ KNIFE YOUR WAY OUT (the Panzer claw's rule, the user's 2026-10-06; upstream's only way out was a stun):
///     <see cref="KnifeToFree"/> swings, hit or miss, and it lets go before the bite, recoils (`miss`) and cries out (its pain
///     lines, which upstream never plays). Two where the claw takes 3-5: this hold lasts about a second, a knife swings every 0.75.
///     ⚠️ Held on the floor, <see cref="HoldNear"/> out: upstream lifts you to its mouth 6 units from its middle — inside its body
///     here, and the Panzer's claw learned not to pull anyone up.
///   • THE SPIT (the boss's own): at its target 200-750 away in sight, every 6.25-10.5 s (the first 1 s after it arrives),
///     `ranged_attack_01/02` lobs a ball of electricity from its jaw onto their feet in 0.75 s (`MimicSpit`): x<see
///     cref="SpitWeight"/> of its swing at the middle of a <see cref="SpitRadius"/> burst (upstream's 90), nothing at the rim,
///     and a <see cref="SpitDaze"/> s daze (upstream's `NZSonicBlind(1)`, the Shrieker's daze here). ⚠️ It leads them by
///     its flight (<see cref="SpitLead"/>; upstream's 1.2 s overshot a 0.75 s flight), turns to them first, and its arc starts
///     where it leaves (upstream solved it from the head and launched it from the jaw).
///     ⚠️ BIGGER, LONGER, AND WORSE AS IT WEAKENS (2026-10-07, the user: *"the mimic's projectile should have a far bigger area
///     of effect, and stun for longer"* and *"the less health the mimic has, the more frequently it fires the projectile and the
///     longer it stuns for"*): a burst of radius 125 (upstream's 50 x2.5), a 2 s daze at full health (upstream's 1 s x2), and with
///     the health it has lost the gap between spits shrinks to x<see cref="SpitGapLow"/> and the daze grows to x<see
///     cref="SpitDazeLow"/> (<see cref="HealthLost"/>). And a head you can see (<see cref="SpitLook"/>, `BossShotLook`).
///   • ITS CLAWS: the run attacks, x<see cref="HeavyWeight"/> on the heavy one (`run_attack_03`'s `melee_heavy`), and the five
///     standing ones at a target standing still within 110 (upstream's split, through `ZombieAI.AttackOverride`).
///   • BELOW <see cref="RageAt"/> OF ITS HEALTH IT ENRAGES, for good: the rage run, 284.4 u/s. Nothing else changes (upstream's).
///   • IT TAKES x<see cref="HeadScale"/> WITHIN <see cref="HeadReach"/> OF ITS HEAD, x<see cref="BodyScale"/> ANYWHERE ELSE.
///   • IT DIES HEADLESS: its head pops, `stun_in` or `death_01` plays, and it bursts in gore and is gone. ⚠️ Every time: upstream's
///     burst never came for a Mimic killed mid-move (it ragdolled, and the ragdoll hook skipped the boss).
///
/// ⚠️ NO PROP DISGUISE: upstream's BOSS never hides as a power-up (`!self.IsMooBossZombie`) — only the special Mimic does — so
/// neither does this; the prop clips stay on the model, unplayed.
/// ⚠️ HOST AUTHORITY, EVERY MACHINE DRAWS: the hold is the victim's own machine's (`NZNet.MimicHold`/`MimicRelease`, the knife's
/// `NZNet.MimicCut`), the spit a network-spawned `MimicSpit`, the head `NZNet.BossPart`, the gore each machine's own.
/// ⛔ NO HIT OF ITS DOWNS ANYONE FROM FULL HEALTH (`BossBase.MaxHit`), Oberon's rule — the bite and the spit included.
/// </summary>
public sealed class MimicBoss : BossBase
{
	protected override string Tag => "[nz-mimic]";

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

	static float? _grabRange, _catch, _heavy, _toss, _tossUp, _near, _view, _lunge;
	static int? _gapMin, _gapMax, _knives;
	/// <summary>It grabs at a player in sight within 215 (upstream's `TargetInRange(215)`), every 6-12 whole seconds.</summary>
	public static float GrabRange { get => _grabRange ?? 215f; set => _grabRange = value; }
	public static int GrabGapMin { get => _gapMin ?? 6; set => _gapMin = value; }
	public static int GrabGapMax { get => _gapMax ?? 12; set => _gapMax = value; }
	/// <summary>The tentacle's tip this near your eyes catches you: 52, upstream's.</summary>
	public static float CatchRadius { get => _catch ?? 52f; set => _catch = value; }
	/// <summary>Its bite and its heavy swing against its swing: 1.5 — upstream's `HeavyAttackDamage` 75 against 50.</summary>
	public static float HeavyWeight { get => _heavy ?? 1.5f; set => _heavy = value; }
	/// <summary>Knife swings that cut you loose, hit or miss: 2.</summary>
	public static int KnifeToFree { get => _knives ?? 2; set => _knives = value; }
	/// <summary>Its throw as it lets go: your distance from it x7 (upstream's), and 150 up (upstream's 10 — it held you in the air).</summary>
	public static float TossScale { get => _toss ?? 7f; set => _toss = value; }
	public static float TossUp { get => _tossUp ?? 150f; set => _tossUp = value; }
	/// <summary>The nearest its hold keeps you to its middle: 40.</summary>
	public static float HoldNear { get => _near ?? 40f; set => _near = value; }
	/// <summary>How hard your view is pulled to its mouth while it holds you, per second: 6 (0, never) — upstream's half a tick.</summary>
	public static float ViewPull { get => _view ?? 6f; set => _view = value; }
	/// <summary>The grab's lunge: 34 forward, the clip's own root motion (stripped by the port).</summary>
	public static float LungeDistance { get => _lunge ?? 34f; set => _lunge = value; }

	static float? _spitMin, _spitMax, _spitGapMin, _spitGapMax, _spitWeight, _spitRadius, _daze, _flight, _lead, _spitGapLow, _dazeLow,
		_spitLook;
	/// <summary>It spits at a target in sight 200-750 away, every 6.25-10.5 s at full health — upstream's (sooner as it weakens,
	/// <see cref="SpitGapLow"/>).</summary>
	public static float SpitMin { get => _spitMin ?? 200f; set => _spitMin = value; }
	public static float SpitMax { get => _spitMax ?? 750f; set => _spitMax = value; }
	public static float SpitGapMin { get => _spitGapMin ?? 6.25f; set => _spitGapMin = value; }
	public static float SpitGapMax { get => _spitGapMax ?? 10.5f; set => _spitGapMax = value; }
	/// <summary>
	/// The spit at the middle of its burst against its swing: 1.8 — upstream's 90 against 50 (unchanged 2026-10-07) — and its
	/// burst's radius: ⚠️ 125 (2026-10-07, the user: *"the mimic's projectile should have a far bigger area of effect"* — upstream's 50
	/// x2.5). Still all of it at the middle and nothing at the rim, so the same spot near the middle now takes more of it.
	/// </summary>
	public static float SpitWeight { get => _spitWeight ?? 1.8f; set => _spitWeight = value; }
	public static float SpitRadius { get => _spitRadius ?? 125f; set => _spitRadius = value; }
	/// <summary>How long the spit dazes you at full health: ⚠️ 2 s (2026-10-07, the user: *"and stun for longer"* — upstream's
	/// `NZSonicBlind(1)` x2), longer as it weakens (<see cref="SpitDazeLow"/>).</summary>
	public static float SpitDaze { get => _daze ?? 2f; set => _daze = value; }
	/// <summary>
	/// ⚠️ THE WEAKER IT IS, THE SOONER IT SPITS AND THE LONGER YOU ARE DAZED (2026-10-07, the user: *"the less health the mimic
	/// has, the more frequently it fires the projectile and the longer it stuns for"*). With t the share of its health it has
	/// lost (<see cref="HealthLost"/>, read as each spit is decided), the gap to the next spit is x lerp(1, SpitGapLow, t) and
	/// the daze x lerp(1, SpitDazeLow, t): 0.35 and 2 — every 6.25-10.5 s and 2 s at full health, 4.2-7.1 s and 3 s at half,
	/// 2.6-4.4 s and 3.8 s at a tenth, 2.2-3.7 s and 4 s at its last.
	/// </summary>
	public static float SpitGapLow { get => _spitGapLow ?? 0.35f; set => _spitGapLow = value; }
	public static float SpitDazeLow { get => _dazeLow ?? 2f; set => _dazeLow = value; }
	/// <summary>
	/// How wide the spit's glowing head is, in units: 24 (0, none) — `BossShotLook`, Avogadro's bolt's (2026-10-07: it was a
	/// light and hair-thin sparks, the bolt the user could not see). ⚠️ EACH MACHINE'S OWN: read where it is drawn.
	/// </summary>
	public static float SpitLook { get => _spitLook ?? 24f; set => _spitLook = value; }
	/// <summary>Its flight, 0.75 s (upstream's), and how far ahead it aims along your run: the same 0.75 s (upstream's 1.2).</summary>
	public static float SpitFlight { get => _flight ?? 0.75f; set => _flight = value; }
	public static float SpitLead { get => _lead ?? 0.75f; set => _lead = value; }

	static float? _body, _head, _headReach, _protect, _rageAt;
	/// <summary>What it takes: x0.75 within 20 of its head (`j_head`), x0.4 anywhere else — upstream's.</summary>
	public static float BodyScale { get => _body ?? 0.4f; set => _body = value; }
	public static float HeadScale { get => _head ?? 0.75f; set => _head = value; }
	public static float HeadReach { get => _headReach ?? 20f; set => _headReach = value; }
	/// <summary>Untouchable for this long after it arrives: 1 s, upstream's spawn protection.</summary>
	public static float SpawnProtection { get => _protect ?? 1f; set => _protect = value; }
	/// <summary>The share of its health below which it enrages: 0.5.</summary>
	public static float RageAt { get => _rageAt ?? 0.5f; set => _rageAt = value; }

	// its clips (30 fps) and their events — ⛔ `const`, never `static readonly` (INSTRUCTIONS.md §1)
	const string ClipGrabIn = "nz_ai_mimic_command_grab_in_01", ClipGrabHit = "nz_ai_mimic_command_grab_success_01",
		ClipGrabMiss = "nz_ai_mimic_command_grab_miss_01", ClipSpitA = "nz_ai_mimic_ranged_attack_01",
		ClipSpitB = "nz_ai_mimic_ranged_attack_02", ClipHeavy = "nz_ai_mimic_run_attack_03", ClipDeath = "nz_ai_mimic_death_01",
		ClipStunIn = "nz_ai_mimic_stun_in", ClipMantle = "nz_ai_mimic_run_mantle_36";
	const string StandA = "nz_ai_mimic_stand_attack_01", StandB = "nz_ai_mimic_stand_attack_02",
		StandC = "nz_ai_mimic_stand_attack_03", StandD = "nz_ai_mimic_stand_attack_04", StandE = "nz_ai_mimic_stand_attack_05";
	const float GrabInLen = 26f / 30f, GrabVox = 1f / 30f, GrabOpen = 11f / 30f, GrabWhoosh = 17f / 30f, LungeTime = 9f / 30f,
		HitLen = 54f / 30f, HitScream = 6f / 30f, HitBite = 24f / 30f, HitRelease = 30f / 30f, MissLen = 22f / 30f,
		SpitALen = 60f / 30f, SpitAAt = 16f / 30f, SpitBLen = 50f / 30f, SpitBAt = 13f / 30f, RiserLand = 20f / 30f,
		DeathBurst = 73f / 30f, StunInLen = 68f / 30f;

	/// <summary>Its rage run's own speed (the run's clips at 40 fps) — `mimic.zvar`'s rage tier is picked by it.</summary>
	const float RageSpeed = 284.4f;
	/// <summary>The standing swings: at a target slower than 15 u/s within 110 (upstream's `AttackRange − 15`).</summary>
	const float StandSpeed = 15f, StandRange = 110f;
	/// <summary>A hold ends at most this far from it (a teleport, never a drag), and on the victim's machine after this long.</summary>
	const float HoldLeash = 400f, HoldCeiling = 3f;

	/// <summary>The tentacle's tip (`player_attach`) and its mouth (`jaw_fx_tag`: an offset in `bone_24a3a588`'s frame, from the QC).</summary>
	const string TipBone = "tag_player", JawBone = "bone_24a3a588";
	static Vector3 JawOffset => new( 7f, -6f, 0f );
	/// <summary>The glow in its mouth: the map's zombie-eye colour upstream, cyan by default.</summary>
	static Color MouthColour => new( 0.1f, 0.9f, 1f );

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

	bool _grabbing, _catchOpen, _raged, _burst;
	NZPlayer _grabFor, _victim, _spitAt;
	float _nextGrab, _nextSpit, _nextThink, _diedAt, _burstAt;
	List<string> _stand;
	SoundHandle _mouth;
	float _nextMouthTry;

	// its target's ground speed, frame to frame (a client's body only arrives as positions): the spit's lead, the standing swings
	GameObject _tracked;
	Vector3 _trackedAt, _trackedVel;

	enum Beat { Unprotect, Land, GrabVox, GrabOpen, Whoosh, GrabEnd, Scream, Bite, Release, GrabDone, Spit }

	/// <summary>Where its mouth is now, on this machine — the spit leaves from it, the hum sits in it, a held view is pulled to it.</summary>
	public Vector3 Jaw => Body.IsValid() && Body.TryGetBoneTransform( JawBone, out var t ) ? t.PointToWorld( JawOffset ) : Voice;

	/// <summary>
	/// Where the tentacle's tip is now, on this machine: `tag_player` if the model kept it, else its path through the grab clip
	/// playing here, at that clip's own time — the same answer on every machine, the clip being relayed (the Mangler's way).
	/// ⛔ THE BONE IS MOST LIKELY NOT ON THE MODEL: nothing is skinned to `tag_player`, ModelDoc's default bone culling drops such
	/// bones (CHANGELOG 08-19, `bone_cull_type`), and `mimic.vmdl` sets none.
	/// </summary>
	Vector3 Tip
	{
		get
		{
			if ( Body.IsValid() && Body.TryGetBoneTransform( TipBone, out var t ) ) return t.Position;
			var (fwd, up) = TipPath();
			return WorldPosition + (Facing * fwd + Vector3.Up * up) * Size;
		}
	}

	/// <summary>`tag_player` in its own frame (forward, up) through the grab clip playing here, at its time; at rest outside them.</summary>
	(float Fwd, float Up) TipPath()
	{
		var clip = Body.IsValid() ? Body.Sequence.Name : null;
		if ( clip != ClipGrabIn && clip != ClipGrabHit ) return (40f, 50f);
		return Along( clip == ClipGrabIn ? InPath : HitPath, Body.Sequence.Time * 30f );
	}

	/// <summary>
	/// `tag_player`'s path as (frame, forward, up) keys, read off the SMDs within 6 units: `grab_in` from frame 11 (the catch's
	/// window) — the whip out to 192 — and `success` to frame 30 (the release) — the reel in to 6. ⛔ PROPERTIES, NOT STATIC
	/// FIELDS (INSTRUCTIONS.md §1).
	/// </summary>
	static float[] InPath => new[] { 11f, 19.6f, 93.8f, 12f, 75.7f, 80.3f, 13f, 143.2f, 53.5f, 14f, 180.7f, 45.2f, 17f, 191.6f, 39f,
		26f, 191.8f, 38.1f };
	static float[] HitPath => new[] { 0f, 191.8f, 38.1f, 4f, 184.7f, 36f, 7f, 163.7f, 38f, 9f, 137.2f, 42.3f, 14f, 27.8f, 54.3f,
		17f, 7.6f, 55f, 25f, 16.4f, 60.8f, 30f, 44f, 57f };

	static (float Fwd, float Up) Along( float[] keys, float frame )
	{
		for ( int i = 3; i < keys.Length; i += 3 )
		{
			if ( frame > keys[i] ) continue;
			var f = Math.Clamp( (frame - keys[i - 3]) / MathF.Max( 0.001f, keys[i] - keys[i - 3] ), 0f, 1f );
			return (keys[i - 2] + (keys[i + 1] - keys[i - 2]) * f, keys[i - 1] + (keys[i + 2] - keys[i - 1]) * f);
		}
		return (keys[^2], keys[^1]);
	}

	/// <summary>
	/// Where the one its tentacle holds is kept, read on EVERY machine (the held player's own drags itself here, `MimicGrab`): under
	/// the tentacle's tip on its floor — out where it caught you, then reeled in with it — but never nearer its middle than
	/// <see cref="HoldNear"/> (upstream's tip ends 6 from it, inside its body here).
	/// </summary>
	public Vector3 HoldPoint
	{
		get
		{
			var f = Facing;
			var off = (Tip - WorldPosition).WithZ( 0f );
			var near = HoldNear * Size;
			var along = off.Dot( f );
			if ( along < near ) off += f * (near - along);
			return WorldPosition + off;
		}
	}

	protected override void BossStart()
	{
		// ⚠️ EVERY MACHINE: the glow in its open mouth (upstream's `zmb_mimic_mouth` on `jaw_fx_tag`, embers rising off it)
		var mouth = Components.Create<BossGlow>();
		mouth.Bone = JawBone;
		mouth.Offset = JawOffset;
		mouth.Colour = MouthColour;
		mouth.Radius = 55f;
		mouth.Brightness = 1.4f;
		mouth.SparkEvery = 0.3f;
		mouth.SparkReach = 8f;
		mouth.SparkColour = new Color( 0.7f, 1f, 1f );
	}

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

		// ⛔ NONE OF THE WALKER'S CLIMB CLIPS ON ITS MODEL: its own mantle (upstream's barricade and default jump)
		Ai.ClimbClipOverride = ClipMantle;
		_stand = new List<string> { StandA, StandB, StandC, StandD, StandE };

		_nextGrab = Time.Now + 5f;
		_nextSpit = Time.Now + 1f;
		if ( Hp.IsValid() ) Hp.Invulnerable = true;
		Queue( SpawnProtection, (int)Beat.Unprotect );
		Queue( RiserLand, (int)Beat.Land );

		// the boss round's sting, to everyone (upstream's `spawnfunc`: the Mangler's spawn, so his event), and the riser's dust
		// (`zombie_spawn_dust_sfx`, frame 1: the walkers' own)
		NZSound.PlayShared( "nz.mangler.intro" );
		NZSound.PlayShared( NZSound.ZombieSpawn, WorldPosition );

		Say( $"spawned — {Hp?.Max ?? 0f:0} hp (x{BodyScale:0.##} body, x{HeadScale:0.##} head) · grabs within {GrabRange:0}"
			+ $" every {GrabGapMin}-{GrabGapMax}s (knifed free in {KnifeToFree}) · spits at {SpitMin:0}-{SpitMax:0}"
			+ $" every {SpitGapMin:0.##}-{SpitGapMax:0.##}s (x{SpitGapLow:0.##} at its last) · rage below {RageAt * 100f:0}%"
			+ $" · hitboxes {Ai.HitboxCount}" );
	}

	protected override void EveryFrame()
	{
		// the hum in its mouth, looped while you are near enough to hear it (Zaballa's hum) — upstream's client re-emitted it every
		// frame (`IsValid` on a string)
		if ( Time.Now < _nextMouthTry ) { if ( _mouth.IsValid() ) _mouth.Position = Jaw; return; }
		_nextMouthTry = Time.Now + 1f;
		if ( _mouth.IsValid() && _mouth.IsPlaying ) return;
		if ( NZSound.Ear.Distance( WorldPosition ) > 1000f ) return;
		_mouth = NZSound.Play( "nz.mimic.mouth", Jaw );
	}

	protected override void HostFrame()
	{
		Ai.MaxHitDamage = MaxHit;
		if ( Ai.State == ZombieState.Spawning ) return;

		TrackTarget();

		// run_attack_03's `melee_heavy`: that swing x1.5, upstream's 75
		Ai.SwingScale = Ai.CurrentClip == ClipHeavy ? HeavyWeight : 1f;

		if ( _grabbing )
		{
			// ⚠️ THE GRAB BROKEN OFF — anything that takes the clip from it (upstream's `SequenceMonitor`): whoever it holds is let
			// go and thrown, upstream's
			if ( Ai.State != ZombieState.Special )
			{
				if ( _victim.IsValid() ) LetGo( true );
				EndGrab();
			}
			else if ( _victim.IsValid() ) TickHold();
			else if ( _catchOpen ) TickCatch();
			return;
		}

		TickRage();
		if ( Time.Now < _nextThink ) return;
		_nextThink = Time.Now + 0.1f;
		TickStand();

		if ( Ai.State != ZombieState.Chasing ) return;
		var p = TargetPlayer;
		if ( !IsUp( p ) ) return;
		var d = WorldPosition.Distance( p.WorldPosition );

		// ⚠️ THE GRAB FIRST, as upstream's `AI()` asks it first; a lane that is not clear costs no wait, only the next look
		if ( Time.Now >= _nextGrab && d < GrabRange * Size && !HeldElsewhere( p ) && CanSee( p.GameObject ) && LaneClear( p ) )
		{
			StartGrab( p );
			return;
		}

		if ( Time.Now >= _nextSpit && d >= SpitMin * Size && d < SpitMax * Size && CanSee( p.GameObject ) ) StartSpit( p );
	}

	protected override void Died( bool host )
	{
		_diedAt = Time.Now;
		if ( _mouth.IsValid() ) _mouth.Stop();

		// its head goes first (upstream's death, off the ragdoll path): blood out of its mouth on every machine
		Blood( Jaw, 36, 190f, 0.85f );
		if ( !host ) return;

		if ( _victim.IsValid() ) LetGo( false );
		_grabbing = false;
		_catchOpen = false;
		Ai.SwingScale = 1f;
		SetPart( "head", 1 );
		NZSound.PlayShared( NZSound.ZombieGoreHead, Jaw );
		Say( "killed" );
	}

	/// <summary>At the end of its death clip (`death_01`'s `mimic_explode`, frame 73) it bursts in gore and is gone — on every machine.</summary>
	protected override void DeadFrame()
	{
		if ( _burst ) return;
		if ( _burstAt <= 0f )
		{
			var clip = Ai.CurrentClip;
			_burstAt = _diedAt + (clip == ClipDeath ? DeathBurst : clip == ClipStunIn ? StunInLen : 2.4f);
		}
		if ( Time.Now < _burstAt ) return;
		_burst = true;

		// upstream's `zmb_monster_explosion` at its middle: no fire, a gore burst
		var at = WorldPosition + Vector3.Up * 45f * Size;
		MargwaFx.SetHidden( GameObject, true );
		Blood( at, 70, 320f, 1f );
		Blood( at, 30, 150f, 1.6f );
		if ( !IsHost ) return;

		NZSound.PlayShared( "nz.mimic.explode", at );
		NZSound.PlayShared( "nz.margwa.headpop", at );
		NZNet.ShakeAt( at, 0.5f, 700f );
		Ai.CorpseLinger = 0.2f;
	}

	protected override void OnDestroy()
	{
		if ( _mouth.IsValid() ) _mouth.Stop();
	}

	/// <summary>Blood thrown up from <paramref name="at"/> on THIS machine (the walkers' gore, `ZombieAI.GoreBurst`), unless it is off.</summary>
	public static void Blood( Vector3 at, int count, float speed, float spread )
	{
		if ( ZombieAI.GoreEnabled ) ZombieAI.GoreBurst( at, Vector3.Up, count, speed, spread );
	}

	/// <summary>Its table: nothing while it holds someone, x<see cref="HeadScale"/> by its head, x<see cref="BodyScale"/> anywhere else.</summary>
	protected override float ScaleDamage( float amount, in DamageInfo damage )
	{
		// ⛔ NOTHING WHILE IT HOLDS SOMEONE (upstream's): their friends cannot shoot them free — the knife is the way out
		if ( _victim.IsValid() ) return 0f;

		// upstream's test: where the hit landed, against its head's bone (hit groups computed and never read)
		var head = false;
		if ( !damage.Position.IsNearlyZero() && Body.IsValid() && Body.TryGetBoneTransform( "j_head", out var h ) )
			head = damage.Position.Distance( h.Position ) <= HeadReach * Size;
		return head ? HeadScale : BodyScale;
	}

	protected override void OnBeat( int beat )
	{
		switch ( (Beat)beat )
		{
			case Beat.Unprotect:
				if ( Hp.IsValid() ) Hp.Invulnerable = false;
				break;

			case Beat.Land:
				// the riser's `CoDZ_Zombie.JumpLand_Heavy`, frame 20
				NZSound.PlayShared( "nz.mimic.land", WorldPosition );
				NZNet.ShakeAt( WorldPosition, 0.3f, 600f );
				break;

			case Beat.GrabVox:
				NZSound.PlayShared( "nz.mimic.grab", Voice );
				break;

			case Beat.GrabOpen:
				// frame 11: `mimic_grapple_whoosh` and `mimic_grapple_hitbox_enable` — the tip catches from now to the clip's end
				_catchOpen = true;
				NZSound.PlayShared( "nz.mimic.tentacle", Voice );
				break;

			case Beat.Whoosh:
				NZSound.PlayShared( "nz.mimic.tentacle", Voice );
				break;

			case Beat.GrabEnd:
				_catchOpen = false;
				if ( Ai.State != ZombieState.Special ) break;
				if ( _victim.IsValid() )
				{
					SwapClip( ClipGrabHit );
					Ai.HoldSpecialFor( HitLen );
					Queue( HitScream, (int)Beat.Scream );
					Queue( HitBite, (int)Beat.Bite );
					Queue( HitRelease, (int)Beat.Release );
					Queue( HitLen, (int)Beat.GrabDone );
				}
				else
				{
					SwapClip( ClipGrabMiss );
					Ai.HoldSpecialFor( MissLen );
					Queue( MissLen, (int)Beat.GrabDone );
				}
				break;

			case Beat.Scream:
				// `mimic_scream`, frame 6, on the one it holds
				if ( _victim.IsValid() ) NZSound.PlayShared( "nz.mimic.scream", _victim.WorldPosition + Vector3.Up * 50f );
				break;

			case Beat.Bite:
			{
				// frame 24, `melee_heavy`: the bite
				var p = _victim;
				if ( !IsUp( p ) ) break;
				var at = p.WorldPosition + Vector3.Up * 50f;
				HurtPlayer( p, SwingDamage( HeavyWeight ), source: "mimic bite" );
				NZSound.PlayShared( "nz.mimic.bite", at );
				NZNet.ShakeAt( at, 0.6f, 150f );
				break;
			}

			case Beat.Release:
				// frame 30, `mimic_grapple_release_player`: let go and thrown off
				if ( _victim.IsValid() ) LetGo( true );
				break;

			case Beat.GrabDone:
				if ( _victim.IsValid() ) LetGo( true );
				EndGrab();
				break;

			case Beat.Spit:
				Spit();
				break;
		}
	}

	// ══ the grab ═════════════════════════════════════════════════════════════

	/// <summary>Face them once (upstream's: the lunge does not track), lunge, and whip the tentacle out. THE HOST.</summary>
	bool StartGrab( NZPlayer p )
	{
		Face( p.WorldPosition );
		// ⚠️ HELD FOR THE LONGER ENDING (the catch's `success`); a miss cuts it to `miss` at the end of `grab_in`
		if ( !PlayShared( ClipGrabIn, GrabInLen + HitLen + 0.2f ) ) return false;

		_nextGrab = Time.Now + Game.Random.Int( GrabGapMin, Math.Max( GrabGapMin, GrabGapMax ) );   // whole seconds, upstream's
		_grabbing = true;
		_catchOpen = false;
		_grabFor = p;
		_victim = null;

		// ⚠️ THE LUNGE: the clip's 34 units forward are stripped by the port — a short arc along its facing instead, stopping short
		// of a wall and of them
		var dir = Facing;
		var reach = MathF.Min( LungeDistance * Size, WorldPosition.Distance( p.WorldPosition ) - 40f * Size );
		var from = WorldPosition + Vector3.Up * 40f * Size;
		var wall = Scene.Trace.Ray( from, from + dir * MathF.Max( 1f, reach ) ).IgnoreGameObjectHierarchy( GameObject )
			.WithoutTags( "zombie", "player", "trigger", "ragdoll" ).Run();
		if ( wall.Hit ) reach = MathF.Min( reach, wall.Distance - 20f * Size );
		if ( reach > 8f ) Ai.BeginArc( WorldPosition + dir * reach, LungeTime, 2f * Size );

		Queue( GrabVox, (int)Beat.GrabVox );
		Queue( GrabOpen, (int)Beat.GrabOpen );
		Queue( GrabWhoosh, (int)Beat.Whoosh );
		Queue( GrabInLen, (int)Beat.GrabEnd );
		Say( $"grabbing at {p.GameObject.Name}" );
		return true;
	}

	/// <summary>
	/// The tip against the one it grabs at, every frame of the window: still its target, nobody else's, its tip within <see
	/// cref="CatchRadius"/> of their eyes, and upstream's two looks clear — the tip to their eyes, its eyes to theirs. THE HOST.
	/// </summary>
	void TickCatch()
	{
		var p = _grabFor;
		if ( !IsUp( p ) || TargetPlayer != p || HeldElsewhere( p ) ) return;
		var tip = Tip;
		var eye = EyeOf( p );
		if ( tip.Distance( eye ) > CatchRadius * Size ) return;
		if ( !Clear( tip, eye ) || !CanSee( p.GameObject ) ) return;
		Catch( p );
	}

	/// <summary>Caught: held on their own machine until it lets go, or they knife it enough. THE HOST.</summary>
	void Catch( NZPlayer p )
	{
		_victim = p;
		_catchOpen = false;
		var knives = Math.Max( 1, KnifeToFree );
		NZNet.MimicHold( p.GameObject.Id, GameObject.Id, knives, HoldCeiling );
		Say( $"CAUGHT {p.GameObject.Name} — bitten {HitBite:0.##}s into `success` unless they knife it {knives} times" );
	}

	/// <summary>A down (upstream's `PlayerDeath` hook) or a body carried impossibly far (a teleport): let go, no throw. THE HOST.</summary>
	void TickHold()
	{
		var p = _victim;
		if ( !IsUp( p ) || (p.WorldPosition - WorldPosition).WithZ( 0f ).Length > HoldLeash * Size ) LetGo( false );
	}

	/// <summary>
	/// Let go of the one it holds — thrown, upstream's `(victim − mimic) x7 + 10 up` added to their speed, or not — and tell their
	/// machine (`NZNet.MimicRelease`; after a cut theirs has let go already, and it finds nothing to undo). THE HOST.
	/// </summary>
	void LetGo( bool toss )
	{
		var p = _victim;
		_victim = null;
		if ( !p.IsValid() ) return;

		var push = Vector3.Zero;
		if ( toss && IsUp( p ) )
			push = (p.WorldPosition - WorldPosition).WithZ( 0f ) * TossScale + Vector3.Up * TossUp;
		NZNet.MimicRelease( p.GameObject.Id, push );
	}

	void EndGrab()
	{
		_grabbing = false;
		_catchOpen = false;
		_grabFor = null;
		ClearBeats();
	}

	/// <summary>Its tentacle knifed off (`CutBy`): it lets go before the bite, recoils (`miss`) and cries out. THE HOST.</summary>
	void Cut( NZPlayer by )
	{
		LetGo( false );
		ClearBeats();
		_catchOpen = false;
		NZSound.PlayShared( "nz.mimic.pain", Voice );
		if ( Ai.State == ZombieState.Special )
		{
			SwapClip( ClipGrabMiss );
			Ai.HoldSpecialFor( MissLen );
		}
		Queue( MissLen, (int)Beat.GrabDone );
		Say( $"tentacle knifed by {by.GameObject.Name} — it lets go" );
	}

	/// <summary>
	/// A player knifed their way out (`NZNet.MimicCut`, counted on their own machine): the Mimic that holds THEM lets go — nobody
	/// else's. False when none does (a message arriving after the bite, a down or its death already let go). THE HOST.
	/// </summary>
	public static bool CutBy( NZPlayer p )
	{
		if ( !p.IsValid() ) return false;
		foreach ( var m in All.ToList() )
		{
			if ( m._victim != p ) continue;
			m.Cut( p );
			return true;
		}
		return false;
	}

	/// <summary>Is this player in another Mimic's tentacle or a Panzer's claw — one hold at a time.</summary>
	bool HeldElsewhere( NZPlayer p )
		=> All.Any( m => m != this && m._victim == p ) || Scene.GetAllComponents<PanzerClaw>().Any( c => c.Holds( p ) );

	/// <summary>
	/// Room for the tentacle: a clear line at its height (36 up) to them. ⚠️ Upstream's sweep was a world-aligned box 500 long, so
	/// a wall anywhere in it — behind them too — called the grab off; this asks what it meant: nothing in the way.
	/// </summary>
	bool LaneClear( NZPlayer p )
	{
		var to = p.WorldPosition + Vector3.Up * 36f;
		var tr = Scene.Trace.Ray( WorldPosition + Vector3.Up * 36f * Size, to ).IgnoreGameObjectHierarchy( GameObject )
			.WithoutTags( "zombie", "player", "trigger", "ragdoll" ).Run();
		return !tr.Hit || tr.HitPosition.Distance( to ) < 24f;
	}

	bool Clear( Vector3 a, Vector3 b )
		=> !Scene.Trace.Ray( a, b ).IgnoreGameObjectHierarchy( GameObject ).WithoutTags( "zombie", "player", "trigger", "ragdoll" )
			.Run().Hit;

	static Vector3 EyeOf( NZPlayer p )
	{
		var c = p.Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants );
		return c.IsValid() ? c.EyePosition : p.WorldPosition + Vector3.Up * 64f;
	}

	// ══ the spit ═════════════════════════════════════════════════════════════

	/// <summary>
	/// The share of its health it has lost: 0 at full, 1 at none — what shortens the gap between its spits and lengthens their daze
	/// (2026-10-07). 0 without a health to read.
	/// </summary>
	float HealthLost => Hp.IsValid() && Hp.Max > 1f ? Math.Clamp( 1f - Hp.Current / Hp.Max, 0f, 1f ) : 0f;

	/// <summary>The gap to its next spit, x lerp(1, <see cref="SpitGapLow"/>) by the health it has lost now.</summary>
	float SpitGapScale => 1f + (SpitGapLow - 1f) * HealthLost;

	/// <summary>How long a spit launched now dazes: <see cref="SpitDaze"/> x lerp(1, <see cref="SpitDazeLow"/>) by the health lost.</summary>
	float SpitDazeNow => MathF.Max( 0f, SpitDaze * (1f + (SpitDazeLow - 1f) * HealthLost) );

	/// <summary>
	/// One of its two spits, turned to them first (⚠️ upstream spat wherever it faced). THE HOST.
	/// ⚠️ THE GAP SHRINKS AS IT WEAKENS (2026-10-07): read now, from this spit's start, as upstream's cooldown was. At its last
	/// (x0.35) the shortest gap, 2.2 s, is still longer than its longest spit clip (2 s), and a spit only starts from a chase —
	/// so one never cuts into another, and the 200-750 band, the sight check and the lead are asked afresh each time.
	/// </summary>
	bool StartSpit( NZPlayer p )
	{
		var second = Game.Random.Int( 0, 1 ) == 1;
		Face( p.WorldPosition );
		if ( !PlayShared( second ? ClipSpitB : ClipSpitA, second ? SpitBLen : SpitALen ) ) return false;

		_nextSpit = Time.Now + Game.Random.Float( SpitGapMin, SpitGapMax ) * MathF.Max( 0f, SpitGapScale );
		_spitAt = p;
		Queue( second ? SpitBAt : SpitAAt, (int)Beat.Spit );
		return true;
	}

	/// <summary>
	/// `mimic_ranged_attack`: lobbed from its jaw onto their feet, where their run will have taken them. THE HOST. Its daze is
	/// read as it leaves the jaw (<see cref="SpitDazeNow"/>), and travels with it.
	/// </summary>
	void Spit()
	{
		var p = _spitAt;
		_spitAt = null;
		if ( Ai.State != ZombieState.Special || !IsUp( p ) ) return;

		var vel = TargetPlayer == p ? _trackedVel.WithZ( 0f ) : Vector3.Zero;
		var to = p.WorldPosition + vel * SpitLead;
		MimicSpit.Lob( Scene, Jaw, to, SpitFlight, GameObject, SwingDamage( SpitWeight ), SpitRadius * Size, SpitDazeNow );
	}

	// ══ the rest ═════════════════════════════════════════════════════════════

	/// <summary>Below half health, the rage run for good (upstream's `SetRunSpeed(71)`, its rage tier). THE HOST.</summary>
	void TickRage()
	{
		if ( _raged || !Hp.IsValid() || Hp.Max <= 1f || Hp.Current >= Hp.Max * RageAt ) return;
		if ( Ai.State is not (ZombieState.Chasing or ZombieState.Attacking) ) return;
		Rage();
	}

	void Rage()
	{
		if ( _raged ) return;
		_raged = true;
		SetSpeed( RageSpeed );
		Say( $"RAGE — running at {Ai.SpeedOverride:0} u/s for good" );
	}

	/// <summary>
	/// Its five standing swings at a target standing still within 110, its run attacks otherwise (upstream's split; the variant
	/// states one set a tier, so the standing one rides `ZombieAI.AttackOverride`, read at each swing). THE HOST.
	/// </summary>
	void TickStand()
	{
		var t = Ai.Target;
		var still = t.IsValid() && t == _tracked && _trackedVel.WithZ( 0f ).Length < StandSpeed
			&& WorldPosition.Distance( t.WorldPosition ) < StandRange * Size;
		var want = still ? _stand : null;
		if ( !ReferenceEquals( Ai.AttackOverride, want ) ) Ai.AttackOverride = want;
	}

	/// <summary>Its target's ground speed, frame to frame, smoothed (Avogadro's `TrackTarget`). THE HOST.</summary>
	void TrackTarget()
	{
		var t = Ai.Target;
		if ( !t.IsValid() ) { _tracked = null; _trackedVel = Vector3.Zero; return; }

		var pos = t.WorldPosition;
		if ( t != _tracked )
		{
			_tracked = t;
			_trackedAt = pos;
			_trackedVel = Vector3.Zero;
			return;
		}

		var dt = MathF.Max( 0.001f, Time.Delta );
		var v = (pos - _trackedAt) / dt;
		_trackedAt = pos;
		if ( v.Length > 1200f ) return;   // a teleport or a respawn, not a run
		_trackedVel = Vector3.Lerp( _trackedVel, v, 1f - MathF.Exp( -8f * dt ) );
	}

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

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

	/// <summary>`nz_mimic` — every Mimic alive, and the tuning.</summary>
	[ConCmd( "nz_mimic" )]
	public static void Report()
	{
		Log.Info( $"[nz-mimic] grab within {GrabRange:0} every {GrabGapMin}-{GrabGapMax}s, catch {CatchRadius:0}u, bite x{HeavyWeight:0.##},"
			+ $" knifed free in {KnifeToFree}, toss x{TossScale:0.#} +{TossUp:0} up, held {HoldNear:0} out, view pull {ViewPull:0.#}"
			+ $" · spit {SpitMin:0}-{SpitMax:0}u every {SpitGapMin:0.##}-{SpitGapMax:0.##}s (x{SpitGapLow:0.##} at its last),"
			+ $" x{SpitWeight:0.##} in {SpitRadius:0}u, daze {SpitDaze:0.##}s (x{SpitDazeLow:0.##} at its last), flight"
			+ $" {SpitFlight:0.##}s, lead {SpitLead:0.##}s, head {SpitLook:0}u · x{BodyScale:0.##} body, x{HeadScale:0.##}"
			+ $" head (within {HeadReach:0}) · rage below {RageAt * 100f:0}% · hit cap {MaxHit:0}" );

		var list = All.ToList();
		Log.Info( $"[nz-mimic] {list.Count} alive" );
		foreach ( var m in list )
			Log.Info( $"[nz-mimic]   hp {m.Hp?.Current ?? 0f:0}/{m.Hp?.Max ?? 0f:0} · state {m.Ai?.State} · speed {m.Ai?.MoveSpeed ?? 0f:0}"
				+ $"{(m._raged ? " · RAGE" : "")}{(m._victim.IsValid() ? $" · HOLDING {m._victim.GameObject.Name}" : m._grabbing ? " · grabbing" : "")}"
				+ $" · next grab in {MathF.Max( 0f, m._nextGrab - Time.Now ):0.#}s · next spit in {MathF.Max( 0f, m._nextSpit - Time.Now ):0.#}s"
				+ $" · spits now every {SpitGapMin * m.SpitGapScale:0.#}-{SpitGapMax * m.SpitGapScale:0.#}s, daze {m.SpitDazeNow:0.#}s"
				+ $" ({m.HealthLost * 100f:0}% lost)"
				+ $" · tentacle tip from {(m.Body.IsValid() && m.Body.TryGetBoneTransform( TipBone, out _ ) ? "its bone" : "the clip's path")}"
				+ $" · hitboxes {m.Ai?.HitboxCount ?? 0}" );
	}

	/// <summary>`nz_mimic_grab` — every Mimic grabs at its target now, whatever the range and the wait.</summary>
	[ConCmd( "nz_mimic_grab" )]
	public static void GrabCmd()
	{
		if ( NZGame.IsClient ) { Log.Info( "[nz-mimic] the host decides" ); return; }
		foreach ( var m in All.Where( m => m.Ai.IsValid() && !m.Dead && !m._grabbing ).ToList() )
		{
			var p = m.TargetPlayer;
			if ( IsUp( p ) ) m.StartGrab( p );
		}
	}

	/// <summary>`nz_mimic_spit` — every Mimic spits at its target now, whatever the range and the wait.</summary>
	[ConCmd( "nz_mimic_spit" )]
	public static void SpitCmd()
	{
		if ( NZGame.IsClient ) { Log.Info( "[nz-mimic] the host decides" ); return; }
		foreach ( var m in All.Where( m => m.Ai.IsValid() && !m.Dead && !m._grabbing ).ToList() )
		{
			var p = m.TargetPlayer;
			if ( IsUp( p ) ) m.StartSpit( p );
		}
	}

	/// <summary>`nz_mimic_rage` — every Mimic enraged now.</summary>
	[ConCmd( "nz_mimic_rage" )]
	public static void RageCmd()
	{
		if ( NZGame.IsClient ) { Log.Info( "[nz-mimic] the host decides" ); return; }
		foreach ( var m in All.Where( m => m.Ai.IsValid() && !m.Dead ).ToList() ) m.Rage();
	}

	/// <summary>`nz_mimic_set &lt;key&gt; &lt;value&gt;` — retune it live.</summary>
	[ConCmd( "nz_mimic_set" )]
	public static void SetCmd( string key = "", float value = 0f )
	{
		switch ( key.ToLowerInvariant() )
		{
			case "range": GrabRange = value; break;
			case "gapmin": GrabGapMin = Math.Max( 0, (int)value ); break;
			case "gapmax": GrabGapMax = Math.Max( 0, (int)value ); break;
			case "catch": CatchRadius = value; break;
			case "heavy": HeavyWeight = value; break;
			case "knives": KnifeToFree = Math.Max( 1, (int)value ); break;
			case "toss": TossScale = value; break;
			case "tossup": TossUp = value; break;
			case "near": HoldNear = value; break;
			case "view": ViewPull = value; break;
			case "lunge": LungeDistance = value; break;
			case "spitmin": SpitMin = value; break;
			case "spitmax": SpitMax = value; break;
			case "spitgapmin": SpitGapMin = value; break;
			case "spitgapmax": SpitGapMax = value; break;
			case "spit": SpitWeight = value; break;
			case "spitradius": SpitRadius = value; break;
			case "daze": SpitDaze = value; break;
			case "flight": SpitFlight = value; break;
			case "lead": SpitLead = value; break;
			case "spitgaplow": SpitGapLow = MathF.Max( 0.05f, value ); break;
			case "dazelow": SpitDazeLow = MathF.Max( 0f, value ); break;
			case "spitlook": SpitLook = MathF.Max( 0f, value ); break;
			case "body": BodyScale = value; break;
			case "head": HeadScale = value; break;
			case "headreach": HeadReach = value; break;
			case "protect": SpawnProtection = value; break;
			case "rage": RageAt = value; break;
			default:
				Log.Info( "[nz-mimic] nz_mimic_set <range|gapmin|gapmax|catch|heavy|knives|toss|tossup|near|view|lunge|spitmin|spitmax"
					+ "|spitgapmin|spitgapmax|spitgaplow|spit|spitradius|daze|dazelow|spitlook|flight|lead|body|head|headreach|protect|rage>"
					+ " <value>" );
				return;
		}
		Log.Info( $"[nz-mimic] {key} = {value:0.###}" );
		Report();
	}
}

/// <summary>
/// The Mimic's spit (2026-10-07), upstream's `nz_proj_mimic_spit`: a ball of blue electricity lobbed from its jaw, falling under
/// upstream's gravity onto the spot it was aimed at after its flight, bursting on the first thing it meets but a zombie (upstream's
/// bounced off them unburst): every player up within <see cref="Radius"/> of the burst hurt — all of it at the middle, nothing at
/// the rim (upstream's falloff; measured to the body, shin to head, as Avogadro's bolt is, where upstream measured to its middle)
/// — and dazed on their own machine (`BossFx.BurstShared`). No look through walls, as upstream's had none.
///
/// ⚠️ THE HOST FLIES IT; EVERYONE ELSE WATCHES (Avogadro's bolt's way): <see cref="Lob"/> network-spawns it, its transform
/// replicates, and its look — a blue light, sparks in its wake, the flash it leaves the jaw in, its hiss — is built on every
/// machine. ⚠️ A 3 s LIFE: upstream's had none.
/// ⚠️ A HEAD YOU CAN SEE (2026-10-07): the light and the hair-thin sparks were all it had — what Avogadro's bolt was when the
/// user could not see it — so it carries `BossShotLook`'s glowing electric-blue head and trail too (<see cref="MimicBoss.SpitLook"/>).
/// Its burst grew with it (radius 125, `MimicBoss.SpitRadius`): the ring runs out to the burst's edge, and the sparks fill it.
/// </summary>
public sealed class MimicSpit : Component
{
	/// <summary>Upstream's `sv_gravity`, which its arc is solved for.</summary>
	public const float Gravity = 600f;
	const float Life = 3f, Touch = 20f;

	/// <summary>Upstream's trail, `Color(100, 100, 255)`.</summary>
	public static Color Colour => new( 0.4f, 0.45f, 1f );

	/// <summary>Its head: the arcs of upstream's `bo3_panzer_elec_nade`, `(138, 185, 255)` — an electric blue, paler than its trail.</summary>
	public static Color HeadColour => new( 0.54f, 0.73f, 1f );

	// ── the host's ──
	public GameObject Owner { get; set; }
	public float Damage { get; set; }
	public float Radius { get; set; } = 50f;
	public float Daze { get; set; } = 1f;

	Vector3 _from, _v0, _last;
	float _born, _nextSpark;
	bool _done;
	GameObject _lightGo;
	SoundHandle _loop;

	/// <summary>
	/// Lob one from <paramref name="from"/> to land on <paramref name="to"/> after <paramref name="seconds"/> — upstream's `getvel`:
	/// across in that time, and up enough that its gravity brings it down on the spot. THE HOST; a client only watches.
	/// </summary>
	public static MimicSpit Lob( Scene scene, Vector3 from, Vector3 to, float seconds, GameObject owner, float damage, float radius,
		float daze )
	{
		if ( !scene.IsValid() || NZGame.IsClient ) return null;
		var t = MathF.Max( 0.1f, seconds );

		var go = new GameObject( true, "nz_mimic_spit" );
		go.Flags |= GameObjectFlags.NotSaved;
		go.WorldPosition = from;

		var s = go.Components.Create<MimicSpit>();
		s.Owner = owner;
		s.Damage = damage;
		s.Radius = radius;
		s.Daze = daze;
		s._from = from;
		s._last = from;
		s._born = Time.Now;
		s._v0 = (to - from) / t + Vector3.Up * (0.5f * Gravity * t);

		// ⚠️ UNCONDITIONAL (a no-op solo), and after it is configured, as Avogadro's bolt is
		go.NetworkSpawn();
		return s;
	}

	protected override void OnStart()
	{
		_lightGo = new GameObject { Parent = GameObject, Name = "light" };
		_lightGo.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.NotNetworked;
		_lightGo.NetworkMode = NetworkMode.Never;   // ⛔ THIS MACHINE'S OWN (INSTRUCTIONS.md §39)
		var light = _lightGo.Components.Create<PointLight>();
		light.LightColor = Colour * 3f;
		light.Radius = 120f;
		light.Shadows = false;

		// `bo3_mangler_blast_glow`: a pale cyan flash at the jaw as it leaves, and its hiss in flight
		BossFx.Flash( WorldPosition, new Color( 0.6f, 0.9f, 1f ), 200f );
		_loop = NZSound.Play( "nz.mimic.spit.loop", WorldPosition );

		// ⚠️ A BALL YOU CAN SEE (2026-10-07), Avogadro's bolt's fix: a glowing electric-blue head with a fading trail, every machine
		if ( MimicBoss.SpitLook > 0f )
			BossShotLook.On( GameObject, HeadColour, MimicBoss.SpitLook, trailWidth: 7f, trailSeconds: 0.2f, brightness: 4f );
	}

	protected override void OnUpdate()
	{
		// ── every machine: its hiss follows it, sparks in its wake (upstream's `trails/electric`) ──
		if ( _loop.IsValid() ) _loop.Position = WorldPosition;
		if ( !_done && Time.Now >= _nextSpark )
		{
			_nextSpark = Time.Now + 0.03f;
			var c = WorldPosition;
			AvogadroSpark.Between( c + Vector3.Random * 3f, c + Vector3.Random.Normal * Game.Random.Float( 6f, 12f ),
				Game.Random.Float( 0.05f, 0.12f ), 0.14f, Colour );
		}

		if ( _done || NZGame.IsClient ) return;

		// ── the host flies it, on the arc itself (no drift), a wall or a player ending it ──
		var t = Time.Now - _born;
		var next = _from + _v0 * t + Vector3.Down * (0.5f * Gravity * t * t);
		var from = _last;

		var wall = Scene.Trace.Ray( from, next ).IgnoreGameObjectHierarchy( GameObject )
			.WithoutTags( "zombie", "player", "trigger", "ragdoll" ).Run();
		if ( wall.Hit )
		{
			Burst( wall.HitPosition - (next - from).Normal * 4f );
			return;
		}

		foreach ( var p in Scene.GetAllComponents<NZPlayer>() )
		{
			if ( !BossBase.IsUp( p ) || DistanceToBody( p, from, next ) > Touch ) continue;
			Burst( next );
			return;
		}

		WorldPosition = next;
		_last = next;
		if ( t >= Life ) Burst( next );
	}

	protected override void OnDestroy()
	{
		if ( _loop.IsValid() ) _loop.Stop();
		if ( _lightGo.IsValid() ) _lightGo.Destroy();
	}

	/// <summary>How near the segment passes to a player's body (a line from shin to head) — Avogadro's bolt's measure.</summary>
	static float DistanceToBody( NZPlayer p, Vector3 a, Vector3 b )
	{
		var lo = p.WorldPosition + Vector3.Up * 12f;
		var hi = p.WorldPosition + Vector3.Up * 64f;
		var best = float.MaxValue;
		for ( int i = 0; i <= 4; i++ )
		{
			var q = Vector3.Lerp( a, b, i / 4f );
			var z = Math.Clamp( q.z, lo.z, hi.z );
			best = MathF.Min( best, q.Distance( new Vector3( lo.x, lo.y, z ) ) );
		}
		return best;
	}

	/// <summary>The burst. THE HOST; destroying a network object ends it everywhere.</summary>
	void Burst( Vector3 at )
	{
		if ( _done ) return;
		_done = true;

		foreach ( var p in Scene.GetAllComponents<NZPlayer>().ToList() )
		{
			if ( !BossBase.IsUp( p ) ) continue;
			var share = 1f - DistanceToBody( p, at, at ) / MathF.Max( 1f, Radius );
			if ( share <= 0f ) continue;
			var hp = p.Components.Get<Health>( FindMode.EverythingInSelfAndDescendants );
			if ( !hp.IsValid() || hp.IsDead ) continue;
			hp.Apply( MathF.Min( Damage * share, BossBase.MaxHit ), false, Owner.IsValid() ? Owner : GameObject, blast: true,
				blastAt: at, source: "mimic spit" );
		}

		// `bo3_mangler_blast_core_2`: a blue burst — its ring out to the radius, its sparks more as it grows (10 at upstream's 50, 20
		// at 125) — and everyone in it dazed on their own machine
		BossFx.BurstShared( at, Radius, Colour, Math.Clamp( (int)(Radius / 6f), 10, 24 ), Daze );
		NZSound.PlayShared( "nz.mimic.spit.hit", at );
		GameObject.Destroy();
	}
}