Zombies/ZaballaBoss.cs

A Boss AI component for the Zaballa boss in a nzombies game mode. Controls tuning constants, audio/visual effects, mask-breaking, teleport-mine sequence, area pulse attack, attack speed/frenzy scaling, and console commands for diagnostics and live retuning.

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

namespace NZombies;

/// <summary>
/// Zaballa the Deceiver — MWZ's masked demon, ported 2026-10-07 from GMod nZombies' `nz_zombie_boss_trickster` (GhostlyMoo's
/// port), the third batch. Spec: `Sbox nzombies/Docs/boss_specs/zaballa.md`.
///
///   • HE WALKS (77.8 u/s, the variant's one tier in play) humming and crackling, a blue aura on his robe and fire in his eyes.
///   • THE MINE TELEPORT, every <see cref="MineGapMin"/>-<see cref="MineGapMax"/> s (the first 2.75-5.95 s after he arrives) when
///     his target is within <see cref="MineRange"/>: into a portal (`disappear_01`), then three evil-energy mines at his target's
///     feet <see cref="MineGap"/> s apart (`BossOrb`: a <see cref="MineRadius"/> burst after <see cref="MineFuse"/> s, x<see
///     cref="MineWeight"/> of his swipe at the middle, a daze), and on the third he steps out of a portal behind them (`appear_01`).
///     ⚠️ UNTOUCHABLE WHILE HIDDEN — upstream's intent; its own code cleared the protection a tick after setting it, and bullets
///     at the empty spot still hurt him.
///   • THREE MASKS: at 75% health the right one breaks, at 25% the left one, each with a blast at his feet and, unless he is busy,
///     a stagger (`stun_blend`, 5.7 s) that ends in the mine teleport. The middle one breaks when he dies.
///   • THE PULSE, every <see cref="PulseGapMin"/>-<see cref="PulseGapMax"/> s when his target is in sight within <see
///     cref="PulseRange"/>: 1.43 s of warning (its reach drawn on the floor), then a burst round him — x<see cref="PulseWeight"/>
///     of his swipe at his middle, falling off to the rim, and everyone in it dazed. ⚠️ Upstream's damage only reached 50 of its
///     127 (the mine's falloff, copied); here it falls off over the whole reach it warns of.
///   • HIS SWIPE (the variant's attacks, 75 against a walker's 50, x<see cref="SwipeScale"/>) within 77.
///   • HE TAKES x<see cref="Resist"/> FROM EVERYTHING.
///   • ⚠️ THE USER'S REVIEW (2026-10-07): every attack harder (<see cref="SwipeScale"/>, the mines' and the pulse's weights, their
///     edges no longer fading to nothing), and A FRENZY PER BROKEN MASK — gaps x<see cref="FrenzyGap"/>, x<see cref="FrenzyRate"/>
///     as fast, each step (`nz_zaballa_mask` to try it).
///
/// ⚠️ HOST AUTHORITY, EVERY MACHINE DRAWS: the masks are `NZNet.BossPart`, the vanishing `NZNet.BossHidden`, the mines
/// network-spawned `BossOrb`s, the pulse `NZNet.BossBurstFx` (whose daze each machine puts on its own player).
/// ⛔ NO HIT OF HIS DOWNS ANYONE FROM FULL HEALTH (`BossBase.MaxHit`), Oberon's rule — the mines and the pulse included.
/// </summary>
public sealed class ZaballaBoss : BossBase
{
	protected override string Tag => "[nz-zaballa]";
	protected override float VoiceHeight => 66f;

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

	static float? _mineRange, _mineGapMin, _mineGapMax, _mineWeight, _mineRadius, _mineFuse, _mineGap, _daze;
	/// <summary>He teleports at a target within 750 (upstream's `TargetInRange(750)`, no sight needed), every 10.75-18.95 s.</summary>
	public static float MineRange { get => _mineRange ?? 750f; set => _mineRange = value; }
	public static float MineGapMin { get => _mineGapMin ?? 10.75f; set => _mineGapMin = value; }
	public static float MineGapMax { get => _mineGapMax ?? 18.95f; set => _mineGapMax = value; }
	/// <summary>A mine at its middle against his swipe: 1.6 (upstream's 90 against 75 was 1.2; raised by the user's review,
	/// 2026-10-07 — see <see cref="SwipeScale"/>).</summary>
	public static float MineWeight { get => _mineWeight ?? 1.6f; set => _mineWeight = value; }
	/// <summary>A mine's reach (50), its fuse (1.15 s) and the gap between his three (0.75 s), upstream's.</summary>
	public static float MineRadius { get => _mineRadius ?? 50f; set => _mineRadius = value; }
	public static float MineFuse { get => _mineFuse ?? 1.15f; set => _mineFuse = value; }
	public static float MineGap { get => _mineGap ?? 0.75f; set => _mineGap = value; }
	/// <summary>How long a mine or the pulse dazes you: 0.75 s (upstream's `NZSonicBlind(0.75)`).</summary>
	public static float Daze { get => _daze ?? 0.75f; set => _daze = value; }

	static float? _pulseRange, _pulseGapMin, _pulseGapMax, _pulseWeight, _resist, _vox;
	/// <summary>He pulses at a target in sight within 127 (his reach + 50), every 5.95-9.99 s.</summary>
	public static float PulseRange { get => _pulseRange ?? 127f; set => _pulseRange = value; }
	public static float PulseGapMin { get => _pulseGapMin ?? 5.95f; set => _pulseGapMin = value; }
	public static float PulseGapMax { get => _pulseGapMax ?? 9.99f; set => _pulseGapMax = value; }
	/// <summary>The pulse at his middle against his swipe: 1.6 (upstream's 90 against 75 was 1.2; raised by the user's review,
	/// 2026-10-07 — see <see cref="SwipeScale"/>).</summary>
	public static float PulseWeight { get => _pulseWeight ?? 1.6f; set => _pulseWeight = value; }
	/// <summary>What he takes from anything: 0.75, upstream's.</summary>
	public static float Resist { get => _resist ?? 0.75f; set => _resist = value; }
	/// <summary>The chance of a line with a teleport, a reappearance or a broken mask: 0.24 (upstream's `math.random(100) &lt; 25`).</summary>
	public static float VoxChance { get => _vox ?? 0.24f; set => _vox = value; }

	static float? _swipe, _mineFloor, _pulseFloor, _frenzyGap, _frenzyRate;
	/// <summary>
	/// HARDER HITS (2026-10-07, the user's review: *"zaballa, each attack should deal more damage"*): his swipe x1.35 on top of his
	/// variant's 1.5 (`ZombieAI.SwingScale`: 75 → 100 at round 12), the mines and the pulse 1.6 of his swipe at the middle (was
	/// 1.2), and neither fades to nothing at its edge any more — a mine's rim deals <see cref="MineFloor"/> of its middle (was
	/// none), the pulse's <see cref="PulseFloor"/> (was 0.1). ⚠️ `BossBase.MaxHit` STILL CAPS EVERY HIT AT 100 (the no-insta-kill
	/// rule): from round 16 his swipe was already there, so the later rounds feel the change in the falloff.
	/// </summary>
	public static float SwipeScale { get => _swipe ?? 1.35f; set => _swipe = value; }
	public static float MineFloor { get => _mineFloor ?? 0.35f; set => _mineFloor = value; }
	public static float PulseFloor { get => _pulseFloor ?? 0.4f; set => _pulseFloor = value; }
	/// <summary>
	/// THE FRENZY, A STEP PER BROKEN MASK (2026-10-07, the user: *"for each mask missing it should become far more agressive with
	/// the attacks, they become more frequent and faster"*). Each mask gone multiplies the gaps between his attacks by
	/// <see cref="FrenzyGap"/> (0.6 — the teleport's, the pulse's and the rest between swipes) and their speed by
	/// <see cref="FrenzyRate"/> (1.3 — the swipe, the pulse and its warning, the way out, the mines' spacing and fuses, the way
	/// back in). One mask gone: gaps x0.6, x1.3 as fast; two gone: x0.36, x1.69.
	/// ⚠️ THE STAGGER A BREAKING MASK CAUSES KEEPS ITS LENGTH: it is the opening the break earns, not an attack.
	/// </summary>
	public static float FrenzyGap { get => _frenzyGap ?? 0.6f; set => _frenzyGap = value; }
	public static float FrenzyRate { get => _frenzyRate ?? 1.3f; set => _frenzyRate = value; }

	// his clips (30 fps) and their events — ⛔ `const`, never `static readonly` (INSTRUCTIONS.md §1)
	const string ClipOut = "nz_base_deceiver_disappear_01", ClipStun = "nz_base_deceiver_stun_blend",
		ClipIn = "nz_base_deceiver_appear_01", ClipPulse = "nz_base_deceiver_special_attack_01", ClipIdle = "nz_base_deceiver_idle";
	const float OutLen = 52f / 30f, OutPortal = 28f / 30f, StunLen = 172f / 30f, StunPortal = 149f / 30f,
		InLen = 87f / 30f, PulseLen = 96f / 30f, PulseCharge = 20f / 30f, PulseBlast = 63f / 30f;

	/// <summary>"Behind the player": a spot on the navmesh this near and this far from them, behind where they look.</summary>
	const float BehindNear = 25f, BehindFar = 115f;

	static Color Red => new( 1f, 0.12f, 0.08f );
	static Color Aura => new( 0.15f, 0.65f, 1f );

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

	bool _rightMask = true, _leftMask = true, _hidden, _away;
	int _masks = 3, _mine;
	float _nextMine, _nextPulse;
	GameObject _victim;
	Vector3 _vanishedAt;
	SoundHandle _hum;
	float _nextHumTry;
	float _actRate, _pulseRate, _baseSwing, _baseRest;

	enum Beat { PortalOut, Vanish, Mine, Reappear, Visible, PulseCharge, PulseBlast }

	public bool Hidden => _hidden;
	public int Masks => _masks;

	/// <summary>Masks gone while he lives: 0, 1 or 2 (the middle one goes as he dies).</summary>
	int Broken => Math.Clamp( 3 - _masks, 0, 2 );
	/// <summary>How much faster his attacks run now, and what their gaps are multiplied by — <see cref="FrenzyRate"/> and
	/// <see cref="FrenzyGap"/>, once per broken mask.</summary>
	float Faster => MathF.Pow( MathF.Max( 0.1f, FrenzyRate ), Broken );
	float Sooner => MathF.Pow( Math.Clamp( FrenzyGap, 0.05f, 1f ), Broken );
	/// <summary>The teleport's and the pulse's speed, fixed as each starts (never 0: a hotload into a live one finds these unset).</summary>
	float ActRate => _actRate > 0.01f ? _actRate : 1f;
	float PulseRate => _pulseRate > 0.01f ? _pulseRate : 1f;

	protected override void BossStart()
	{
		// ⚠️ EVERY MACHINE: the aura on his robe (upstream's `zmb_trickster_aura`, blue arcs and embers) and the fire in his eyes
		// (`zmb_zct_fire_red` on `eyes`)
		var aura = Components.Create<BossGlow>();
		aura.Bone = "j_spineupper";
		aura.Colour = Aura;
		aura.Radius = 150f;
		aura.Brightness = 1.6f;
		aura.SparkEvery = 0.12f;
		aura.SparkReach = 22f;
		aura.SparkColour = new Color( 0.5f, 0.9f, 1f );

		var eyes = Components.Create<BossGlow>();
		eyes.Bone = "j_head";
		eyes.Offset = new Vector3( 3.6f, 2.67f, 0f );
		eyes.Colour = Red;
		eyes.Radius = 60f;
		eyes.Brightness = 2.2f;
	}

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

		// ⛔ NONE OF THE WALKER'S CLIMB CLIPS ON HIS MODEL: his own mantle (upstream's barricade jump, type 0)
		Ai.ClimbClipOverride = "nz_base_deceiver_mantle_over_36";

		_nextMine = Time.Now + Game.Random.Float( 2.75f, 5.95f );
		_nextPulse = Time.Now + Game.Random.Float( PulseGapMin, PulseGapMax );

		// he climbs out of a portal (`nz_base_deceiver_spawn`, its `deceiver_portal` at frame 1)
		Portal( WorldPosition );

		Say( $"spawned — {Hp?.Max ?? 0f:0} hp (x{Resist:0.##} from everything) · mines within {MineRange:0} every {MineGapMin:0.#}-{MineGapMax:0.#}s"
			+ $" · pulse within {PulseRange:0} every {PulseGapMin:0.#}-{PulseGapMax:0.#}s · hitboxes {Ai.HitboxCount}" );
	}

	protected override void EveryFrame()
	{
		// his hum, looped while you are near enough to hear it (the Sawrunner's saw), and quiet while he is gone
		if ( Time.Now < _nextHumTry ) { if ( _hum.IsValid() ) _hum.Position = Voice; return; }
		_nextHumTry = Time.Now + 1f;
		if ( _hum.IsValid() && _hum.IsPlaying ) return;
		if ( NZSound.Ear.Distance( WorldPosition ) > 1400f ) return;
		_hum = NZSound.Play( "nz.zaballa.amb", Voice );
	}

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

		TickMasks();
		if ( _away || Ai.State != ZombieState.Chasing ) return;

		var p = TargetPlayer;
		if ( !IsUp( p ) ) return;
		var d = WorldPosition.Distance( p.WorldPosition );

		// ⚠️ THE TELEPORT FIRST, as upstream's `AI()` asks it first
		if ( Time.Now >= _nextMine && d <= MineRange * Size )
		{
			if ( Game.Random.Float( 0f, 1f ) < VoxChance ) NZSound.PlayShared( "nz.zaballa.taunt", Voice );
			BeginTeleport( false );
			return;
		}

		if ( Time.Now >= _nextPulse && d <= PulseRange * Size && CanSee( p.GameObject ) ) StartPulse();
	}

	protected override void Died( bool host )
	{
		if ( _hum.IsValid() ) _hum.Stop();
		if ( !host ) return;

		// the middle mask comes off as he goes (upstream's `BreakMask(3)`)
		SetHidden( false );
		SetPart( "mmask", 1 );
		MaskBlast();
		Say( "killed" );
	}

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

	protected override float ScaleDamage( float amount, in DamageInfo damage ) => Resist;

	protected override void OnBeat( int beat )
	{
		switch ( (Beat)beat )
		{
			case Beat.PortalOut:
				Portal( WorldPosition );
				break;

			case Beat.Vanish:
				// gone: hidden on every machine, untouchable, and the first mine at his target's feet
				_hidden = true;
				_vanishedAt = WorldPosition;
				SetHidden( true );
				if ( Hp.IsValid() ) Hp.Invulnerable = true;
				SwapClip( ClipIdle );
				_mine = 0;
				LayMine();
				Queue( MineGap / ActRate, (int)Beat.Mine );
				Queue( MineGap * 2f / ActRate, (int)Beat.Reappear );
				break;

			case Beat.Mine:
				LayMine();
				break;

			case Beat.Reappear:
				LayMine();
				Reappear();
				break;

			case Beat.Visible:
				_away = false;
				break;

			case Beat.PulseCharge:
				// `deceiver_attack_charge`: the tell, 1.43 s before it goes off — its reach drawn on the floor
				NZSound.PlayShared( "nz.zaballa.charge", Voice );
				NZNet.BossTelegraph( WorldPosition, PulseRange * Size, (PulseBlast - PulseCharge) / PulseRate, 1f, 0.15f, 0.35f );
				BossFx.BurstShared( Voice, 40f * Size, new Color( 0.7f, 0.3f, 1f ), 14 );
				break;

			case Beat.PulseBlast:
				Pulse();
				break;
		}
	}

	// ══ the masks ════════════════════════════════════════════════════════════

	/// <summary>The right mask at 75%, the left at 25% — each a blast and, unless he is busy, a stagger into the teleport. THE HOST.</summary>
	void TickMasks()
	{
		if ( !Hp.IsValid() || Hp.Max <= 1f ) return;
		var share = Hp.Current / Hp.Max;
		if ( _rightMask && share <= 0.75f ) BreakMask( "rmask" );
		else if ( _leftMask && share <= 0.25f ) BreakMask( "lmask" );
	}

	void BreakMask( string group )
	{
		if ( group == "rmask" ) _rightMask = false;
		else _leftMask = false;
		_masks--;

		SetPart( group, 1 );
		MaskBlast();
		if ( Game.Random.Float( 0f, 1f ) < VoxChance )
			NZSound.PlayShared( _masks == 2 ? "nz.zaballa.mask1" : "nz.zaballa.mask2", Voice );
		Say( $"{group} broken — {_masks} mask(s) left" );

		// ⚠️ ONLY IF HE IS FREE: mid-pulse or mid-teleport the mask still breaks, without the stagger (upstream's)
		if ( !_away && Ai.State is ZombieState.Chasing or ZombieState.Attacking or ZombieState.Idle ) BeginTeleport( true );
	}

	/// <summary>
	/// His swipe now: <see cref="SwipeScale"/> harder always, and per broken mask <see cref="FrenzyRate"/> faster with
	/// <see cref="FrenzyGap"/> of the rest between swings — on the authored values, kept the first time this runs (⚠️ LAZILY: a
	/// hotload into a live Zaballa finds them unset, before anything here has changed them). THE HOST, every frame.
	/// </summary>
	void TickSwipe()
	{
		if ( _baseSwing <= 0f )
		{
			_baseSwing = Ai.AttackSpeed;
			_baseRest = Ai.AttackCooldownPlaceholder;
		}

		Ai.SwingScale = SwipeScale;
		Ai.AttackSpeed = _baseSwing * Faster;
		Ai.AttackCooldownPlaceholder = _baseRest * Sooner;
	}

	/// <summary>`zmb_disciple_blast` at his feet and the crack of the mask.</summary>
	void MaskBlast()
	{
		BossFx.BurstShared( WorldPosition + Vector3.Up * 6f, 90f * Size, Red, 10 );
		NZSound.PlayShared( "nz.zaballa.maskbreak", Voice );
	}

	// ══ the mine teleport ════════════════════════════════════════════════════

	/// <summary>Into a portal — the plain way out, or the stagger after a mask — then the mines and out behind his target. THE HOST.</summary>
	void BeginTeleport( bool stun )
	{
		// ⚠️ THE FRENZY'S SPEED FOR THE WHOLE ACT, fixed as it starts — all but a stagger's own clip, which keeps its length
		var rate = Faster;
		var outRate = stun ? 1f : rate;
		var outLen = (stun ? StunLen : OutLen) / outRate;
		// ⚠️ THE WHOLE ACT IS ONE HOLD: out, three mines, in — the appearance re-arms it for its own length
		if ( !PlayShared( stun ? ClipStun : ClipOut, outLen + MineGap / rate * 2f + 0.5f, outRate ) ) return;

		_actRate = rate;
		_away = true;
		_victim = Ai.Target;
		_nextMine = Time.Now + Game.Random.Float( MineGapMin, MineGapMax ) * Sooner;
		if ( stun ) NZSound.PlayShared( "nz.zaballa.taunt", Voice );

		Queue( (stun ? StunPortal : OutPortal) / outRate, (int)Beat.PortalOut );
		Queue( outLen, (int)Beat.Vanish );
		Say( stun ? "staggered — teleporting" : "teleporting" );
	}

	/// <summary>One mine at his target's feet (where he vanished, with no target), 25 up — its fuse the frenzy's. THE HOST.</summary>
	void LayMine()
	{
		_mine++;
		var at = (_victim.IsValid() ? _victim.WorldPosition : _vanishedAt) + Vector3.Up * 25f;
		BossOrb.Lay( Scene, at, Red, MineFuse / ActRate, GameObject, SwingDamage( MineWeight ), MineRadius * Size, Daze, "nz.zaballa.blast",
			look: 1.2f, floor: Math.Clamp( MineFloor, 0f, 1f ) );
		NZSound.PlayShared( "nz.zaballa.charge", at );
	}

	/// <summary>Out of a portal behind his target, facing them (`appear_01`), visible and touchable again. THE HOST.</summary>
	void Reappear()
	{
		var target = _victim.IsValid() ? _victim : Ai.Target;
		var spot = target.IsValid() ? BehindSpot( target ) : _vanishedAt;

		Warp( spot );
		if ( target.IsValid() ) Face( target.WorldPosition );

		_hidden = false;
		SetHidden( false );
		if ( Hp.IsValid() ) Hp.Invulnerable = false;

		// ⚠️ THE RATE ON THE HOST'S RENDERER TOO: `SwapClip` only relays it, and a stagger's act started at 1
		SwapClip( ClipIn, ActRate );
		if ( Body.IsValid() ) Body.PlaybackRate = ActRate;
		Ai.HoldSpecialFor( InLen / ActRate );
		Queue( InLen / ActRate, (int)Beat.Visible );
		Portal( spot );
		if ( Game.Random.Float( 0f, 1f ) < VoxChance ) NZSound.PlayShared( "nz.zaballa.laugh", Voice );
	}

	/// <summary>
	/// Upstream's `FindSpotBehindPlayer`: random points on the navmesh within <see cref="BehindFar"/> of them, kept when they lie
	/// behind where the player looks; the nearest wins. None found, the spot on the mesh at their own feet.
	/// </summary>
	Vector3 BehindSpot( GameObject target )
	{
		var centre = target.WorldPosition;
		var p = target.Components.Get<NZPlayer>( FindMode.EverythingInSelfAndAncestors );
		var look = p.IsValid() ? p.EyeAngles.ToRotation().Forward.WithZ( 0 ).Normal : Vector3.Zero;

		var nav = Scene.NavMesh;
		Vector3? best = null;
		var bestD = float.MaxValue;
		if ( nav is not null )
			for ( int i = 0; i < 25; i++ )
			{
				var q = nav.GetRandomPoint( centre, BehindFar );
				if ( !q.HasValue ) continue;
				var off = (q.Value - centre).WithZ( 0 );
				var d = off.Length;
				if ( d < BehindNear || d > BehindFar || MathF.Abs( q.Value.z - centre.z ) > 64f ) continue;
				if ( look.Length > 0.5f && look.Dot( off.Normal ) >= 0f ) continue;
				if ( d >= bestD ) continue;
				bestD = d;
				best = q;
			}

		return best ?? ZombieAI.NavGround( Scene, centre );
	}

	/// <summary>His red portal, on every machine: a flash, a ring on the floor, and its sound.</summary>
	void Portal( Vector3 at )
	{
		BossFx.BurstShared( at + Vector3.Up * 4f, 80f * Size, Red, 12 );
		NZSound.PlayShared( "nz.zaballa.portal", at + Vector3.Up * 40f );
	}

	// ══ the pulse ════════════════════════════════════════════════════════════

	/// <summary>The pulse — its clip, its warning and its gap the frenzy's, fixed as it starts. THE HOST.</summary>
	void StartPulse()
	{
		var rate = Faster;
		_nextPulse = Time.Now + Game.Random.Float( PulseGapMin, PulseGapMax ) * Sooner;
		if ( !PlayShared( ClipPulse, PulseLen / rate, rate ) ) return;
		_pulseRate = rate;
		Queue( PulseCharge / rate, (int)Beat.PulseCharge );
		Queue( PulseBlast / rate, (int)Beat.PulseBlast );
	}

	/// <summary>
	/// `deceiver_attack`: x<see cref="PulseWeight"/> of his swipe at his middle, falling off to <see cref="PulseRange"/>, everyone
	/// in it dazed (each machine its own player, `BossFx.Burst`). THE HOST.
	/// </summary>
	void Pulse()
	{
		var at = WorldPosition + Vector3.Up * 40f * Size;
		var radius = PulseRange * Size;
		var damage = SwingDamage( PulseWeight );
		foreach ( var p in PlayersWithin( at, radius ).ToList() )
		{
			var d = (p.WorldPosition + Vector3.Up * 40f).Distance( at );
			HurtPlayer( p, damage * Math.Clamp( 1f - d / radius, Math.Clamp( PulseFloor, 0f, 1f ), 1f ), blast: true, at: at, source: "zaballa pulse" );
		}

		BossFx.BurstShared( at, radius, Red, 16, Daze );
		NZSound.PlayShared( "nz.zaballa.blast", at );
		NZNet.ShakeAt( WorldPosition, 0.45f, 600f );
	}

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

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

	/// <summary>`nz_zaballa` — every Zaballa alive, and the tuning.</summary>
	[ConCmd( "nz_zaballa" )]
	public static void Report()
	{
		Log.Info( $"[nz-zaballa] mines: within {MineRange:0} every {MineGapMin:0.#}-{MineGapMax:0.#}s, {MineRadius:0}u x{MineWeight:0.##} after {MineFuse:0.##}s,"
			+ $" {MineGap:0.##}s apart · pulse: within {PulseRange:0} every {PulseGapMin:0.#}-{PulseGapMax:0.#}s x{PulseWeight:0.##} · daze {Daze:0.##}s"
			+ $" · x{Resist:0.##} from everything · hit cap {MaxHit:0}" );
		Log.Info( $"[nz-zaballa] swipe x{SwipeScale:0.##} · edges: mines {MineFloor:0.##}, pulse {PulseFloor:0.##} of the middle"
			+ $" · per broken mask: gaps x{FrenzyGap:0.##}, x{FrenzyRate:0.##} as fast" );

		var list = All.ToList();
		Log.Info( $"[nz-zaballa] {list.Count} alive" );
		foreach ( var z in list )
			Log.Info( $"[nz-zaballa]   hp {z.Hp?.Current ?? 0f:0}/{z.Hp?.Max ?? 0f:0} · state {z.Ai?.State} · masks {z._masks}"
				+ $" (x{z.Faster:0.##} as fast, gaps x{z.Sooner:0.##}; swing {z.Ai?.AttackSpeed ?? 0f:0.##}, rest {z.Ai?.AttackCooldownPlaceholder ?? 0f:0.##}s)"
				+ $"{(z._hidden ? " · HIDDEN" : z._away ? " · teleporting" : "")} · next teleport in {MathF.Max( 0f, z._nextMine - Time.Now ):0.#}s"
				+ $" · next pulse in {MathF.Max( 0f, z._nextPulse - Time.Now ):0.#}s" );
	}

	/// <summary>`nz_zaballa_teleport` — every Zaballa teleports now; `nz_zaballa_pulse` — every one pulses now.</summary>
	[ConCmd( "nz_zaballa_teleport" )]
	public static void TeleportCmd()
	{
		if ( NZGame.IsClient ) { Log.Info( "[nz-zaballa] the host decides" ); return; }
		foreach ( var z in All.Where( z => z.Ai.IsValid() && !z.Dead && !z._away ).ToList() ) z.BeginTeleport( false );
	}

	[ConCmd( "nz_zaballa_pulse" )]
	public static void PulseCmd()
	{
		if ( NZGame.IsClient ) { Log.Info( "[nz-zaballa] the host decides" ); return; }
		foreach ( var z in All.Where( z => z.Ai.IsValid() && !z.Dead && !z._away ).ToList() ) z.StartPulse();
	}

	/// <summary>`nz_zaballa_mask` — every Zaballa loses his next mask now (the right, then the left), whatever his health: the
	/// blast, the stagger and a step of the frenzy.</summary>
	[ConCmd( "nz_zaballa_mask" )]
	public static void MaskCmd()
	{
		if ( NZGame.IsClient ) { Log.Info( "[nz-zaballa] the host decides" ); return; }
		foreach ( var z in All.Where( z => z.Ai.IsValid() && !z.Dead ).ToList() )
		{
			if ( z._rightMask ) z.BreakMask( "rmask" );
			else if ( z._leftMask ) z.BreakMask( "lmask" );
			else Log.Info( "[nz-zaballa] no mask left to break but the last (it goes as he dies)" );
		}
	}

	/// <summary>`nz_zaballa_set &lt;key&gt; &lt;value&gt;` — retune him live.</summary>
	[ConCmd( "nz_zaballa_set" )]
	public static void SetCmd( string key = "", float value = 0f )
	{
		switch ( key.ToLowerInvariant() )
		{
			case "range": MineRange = value; break;
			case "gapmin": MineGapMin = value; break;
			case "gapmax": MineGapMax = value; break;
			case "mine": MineWeight = value; break;
			case "mineradius": MineRadius = value; break;
			case "fuse": MineFuse = value; break;
			case "minegap": MineGap = value; break;
			case "daze": Daze = value; break;
			case "pulserange": PulseRange = value; break;
			case "pulse": PulseWeight = value; break;
			case "resist": Resist = value; break;
			case "vox": VoxChance = value; break;
			case "swipe": SwipeScale = value; break;
			case "minefloor": MineFloor = value; break;
			case "pulsefloor": PulseFloor = value; break;
			case "frenzygap": FrenzyGap = value; break;
			case "frenzyrate": FrenzyRate = value; break;
			default:
				Log.Info( "[nz-zaballa] nz_zaballa_set <range|gapmin|gapmax|mine|mineradius|fuse|minegap|daze|pulserange|pulse|resist|vox"
					+ "|swipe|minefloor|pulsefloor|frenzygap|frenzyrate> <value>" );
				return;
		}
		Log.Info( $"[nz-zaballa] {key} = {value:0.###}" );
		Report();
	}
}