Boss NPC component for the Meúchler boss in an NZombies game mode. Implements movement, ambush (burrow/raise), enraging, shove reaction, flight (flee) behavior, damage scaling and networked sound/particle events, plus console commands for diagnostics and tuning.
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
namespace NZombies;
/// <summary>
/// The Meúchler — CoD WWII's assassin, ported 2026-10-07 from GMod nZombies' `nz_zombie_boss_assassin` (GhostlyMoo's port), the
/// third batch. Spec: `Sbox nzombies/Docs/boss_specs/meuchler.md`. Two bodies, one rolled per spawn (`meuchler.zvar` lists both,
/// upstream's 50/50): the elite is the same rig, clips and events in other meshes, so this one component runs both.
///
/// • HE RISES (`stand_spawn_idle`) in an orange burst — upstream's `doom_caco_blast` — with a zap, and his alert heard map-wide.
/// • HE WALKS (one of three walks, upstream's roll: 97 or 35.1 u/s) UNTIL HE IS ANGRY — you in his sight within <see
/// cref="EnrageRange"/>, the first hit on him, or <see cref="EnrageAfter"/> s after he arrives: `walk_2_sprint`, then one of
/// five sprints (220-276 u/s) for good. AT HALF HEALTH he moves x<see cref="HalfSpeed"/>, for good.
/// • THE AMBUSH, when you are past <see cref="AmbushRange"/> or out of his sight, at most every <see cref="AmbushGapMin"/>-<see
/// cref="AmbushGapMax"/> s (counted from its start, upstream's; the first 1 s after he arrives): he crawls into the ground
/// (`trav_crawl_exit`) and out of it at the open spawn nearest you (`trav_crawl_enter`), ~5 s in all, with a click heard
/// map-wide as he goes down and as he comes up.
/// • THE SHOVE, upstream's counterplay: a melee hit staggers him away from where it came from (`stand_stumble_*`; from the
/// front, one time in three, the knockdown) — at most <see cref="ShovesPerWindow"/> a <see cref="ShoveWindow"/> s window, the
/// next window 2 s longer for each, and never while he is busy with a move of his own.
/// • HIS SWING (the variant's attacks, 75 against a walker's 50), and the LATER BLOW of a two-blow sprint swing, through the
/// hit window (upstream's `IgnoreImmunity`).
/// • HE TAKES x<see cref="BulletScale"/> FROM BULLETS, x<see cref="MeleeScale"/> FROM MELEE, x<see cref="OtherScale"/> FROM
/// ANYTHING ELSE (upstream's table), and nothing for his first <see cref="SpawnProtection"/> s (the base's).
/// • THE FLIGHT (2026-10-07, the user: *"everytime it loses 20% of its max hp, it runs away into a normal zombie spawnpoint
/// insanely quickly, and after a random ammount from 10-40 seconds later, it spawns again in the nearest normal zombie spawn
/// point from a random player with the hp it had before disapearing"*): at 80, 60, 40 and 20% of his health (<see
/// cref="FleeStep"/>), each once, he breaks off — no swing, no player — and runs at <see cref="FleeSpeed"/> u/s to the
/// round's nearest zombie spawn (not a special's or a boss's) that is <see cref="FleeClear"/> from everyone up (none, the
/// nearest), and into the ground there, the ambush's way. <see cref="FleeMin"/>-<see cref="FleeMax"/> s later he comes up out
/// of the zombie spawn nearest a random player (nobody up: he tries again every 2 s), the ambush's rise, with the health he
/// went down with. While away he is hidden, untouchable and still; no ambush, shove or enrage starts until he is back up.
///
/// ⚠️ THE AMBUSH STARTS WHERE HE STANDS. Upstream walks to the crawl spawner nearest him first (giving up after 3 s and snapping
/// there); a spawn here has no crawl type, and both crawl clips came through the port in place — into and out of the floor,
/// their 121 and 146 u of travel stripped — so he goes down where he is and comes up at the open zombie, special or boss spawn
/// nearest you (upstream's `FindHiddenSpawn`: doors only, not the round or the power). Upstream's checks on the entry spawner
/// go with it (the spec reads its visibility test as inverted); ⚠️ instead he only goes when that spawn is <see
/// cref="AmbushGain"/> nearer you than he is — a crawl that brings him no closer is no ambush.
/// ⚠️ UNTOUCHABLE WHILE ALL OF HIM IS UNDER THE FLOOR: upstream sets no flag, but its crawling mesh was up to ~150 u from its 30 u
/// hit bounds, so bullets missed it. The visible part of each crawl takes hits as ever.
/// ⛔ ONE SWING IS ONE ATTACK: the later blow and the AI's first one together never take more than <see cref="ComboCap"/> x
/// `MaxHit` from a player (the AI's blow counted as landed in full). Upstream's lands in full — 150 from a two-blow swing, a full
/// bar — which the user's rule forbids (*"we cant have any attack insta kill like that"*). From round 16 his first blow alone is
/// at the cap (base health 150, the match's x1 damage), so the later one is felt in the early rounds only, unless
/// `nz_meuchler_set combo` says otherwise.
/// ⚠️ NO STANDING SWINGS: a variant states one attack set a tier (`ZombieAI.AttackClipsForNow`) and the AI keeps closing through
/// a variant's swing, so a `stand_attack_*` would glide. Walk attacks walking, sprint attacks sprinting; the 2- and 3-blow
/// standing swings are out.
/// ⚠️ NO TURN CLIPS: `react_turn_90_*` turn his mesh inside the clip while the AI turns his body to his target during any
/// special — the two would add up and snap back. A target found again gets `walk_2_sprint` toward them, with the turn's taunt
/// when they stand to his side.
/// ⚠️ `walk_2_sprint`'s 93 u are carried by an arc (the port strips a clip's travel). The stumbles' push-back (34-92 u) is not:
/// an arc faces along its course, and he reels backward.
/// ⚠️ AT HALF HEALTH HIS LEGS KEEP UP: upstream raised only the move speed, so the feet slid; here every sprint lands in his
/// fastest tier (`sprint_01`) with its legs sped up to match.
/// ⚠️ THE FLIGHT IS TWO HOLDS. The run is `sprint_01` at the flight's speed (x2.35 at 650) and the burrow the ambush's crawl into
/// the ground at that same rate — a hold plays every clip at the rate it began with — so the exit is quick too: gone in 0.94 s,
/// 1.18 s in all. The time away and the rise are a second hold at x1, so he comes up at the ambush's own pace.
/// ⚠️ ONLY A SPAWN HE CAN REACH: of the nearest eight, the first with a whole path on the navmesh, clear of everyone first. None,
/// he goes into the ground where he stands, as he does when he gets there, stops short, or runs out of <see cref="FleeTimeout"/>.
/// ⚠️ A FLIGHT WAITS UNTIL HE IS FREE: not mid-stagger, mid-ambush, on a link or at a window (the ambush's own rule). A share lost
/// while it waits, while he runs, or while he is away is spent with it: one flight.
/// ⚠️ NOT SOLID WHILE HIDDEN, on every machine (`ZombieAI.SolidBody`, keyed on the hiding itself): 10-40 s at a spawn would be an
/// invisible wall. The ambush's hidden moment goes the same way.
/// ⚠️ AWAY, HE STILL CLICKS: his idle voice (`nz.meuchler.clickfar`, every 8-15 s) is the AI's, with no switch, and comes from
/// where he went down.
///
/// ⚠️ HOST AUTHORITY, EVERY MACHINE DRAWS: the clips are relayed (`PlayShared`, `SwapClip`), the vanishing is `NZNet.BossHidden`,
/// the burst `NZNet.BossBurstFx`, the dirt `SpawnDirt`'s own announcement, the voices `NZSound.PlayShared`. The flight's pose
/// held under the floor while away is `NZNet.ZombieFreeze` on the other machines, sent again with the hiding every 2 s (a
/// player who joins meanwhile), and its run re-sent each stride (a relayed clip plays once on a watching machine).
/// ⛔ NO HIT OF HIS DOWNS ANYONE FROM FULL HEALTH (`BossBase.MaxHit`), Oberon's rule — the swing's later blow included.
/// </summary>
public sealed class MeuchlerBoss : BossBase
{
protected override string Tag => "[nz-meuchler]";
/// <summary>His head, hunched: 47-58 u through his idle, walk and sprint.</summary>
protected override float VoiceHeight => 56f;
// ══ tuning ═══════════════════════════════════════════════════════════════
//
// ⛔ NULLABLE-BACKED, INSTRUCTIONS.md §1.
static float? _ambushRange, _ambushChance, _ambushGapMin, _ambushGapMax, _ambushGain;
/// <summary>He ambushes a target past 575 or out of his sight (upstream's `!TargetInRange(575) or IsAttackBlocked()`).</summary>
public static float AmbushRange { get => _ambushRange ?? 575f; set => _ambushRange = value; }
/// <summary>The chance one pass (every 0.25 s) goes: 0.49, upstream's `math.random(100) < 50` a pass.</summary>
public static float AmbushChance { get => _ambushChance ?? 0.49f; set => _ambushChance = value; }
/// <summary>Then not again for 9.46-13.34 s, counted from when he goes down (upstream's).</summary>
public static float AmbushGapMin { get => _ambushGapMin ?? 9.46f; set => _ambushGapMin = value; }
public static float AmbushGapMax { get => _ambushGapMax ?? 13.34f; set => _ambushGapMax = value; }
/// <summary>⚠️ Not upstream's: how much nearer his target the spawn he comes out of must be than he is — 150.</summary>
public static float AmbushGain { get => _ambushGain ?? 150f; set => _ambushGain = value; }
static float? _enrageRange, _enrageAfter, _halfSpeed;
/// <summary>He enrages at a target in his sight within 750, at the first hit, or 20 s after he arrives (upstream's).</summary>
public static float EnrageRange { get => _enrageRange ?? 750f; set => _enrageRange = value; }
public static float EnrageAfter { get => _enrageAfter ?? 20f; set => _enrageAfter = value; }
/// <summary>At half health he moves x1.35, for good (upstream's `MovementSpeedMultiplier`).</summary>
public static float HalfSpeed { get => _halfSpeed ?? 1.35f; set => _halfSpeed = value; }
static float? _bullet, _melee, _other, _protect, _window, _shoves, _combo;
/// <summary>What he takes from bullets: 0.085; from melee: 0.95; from anything else: 0.5 (upstream's `PostTookDamage`).</summary>
public static float BulletScale { get => _bullet ?? 0.085f; set => _bullet = value; }
public static float MeleeScale { get => _melee ?? 0.95f; set => _melee = value; }
public static float OtherScale { get => _other ?? 0.5f; set => _other = value; }
/// <summary>Nothing at all for his first second (the base's spawn protection), and no anger from it.</summary>
public static float SpawnProtection { get => _protect ?? 1f; set => _protect = value; }
/// <summary>A melee hit staggers him at most twice a 10 s window, the next window 2 s longer for each (upstream's `ShoveCount`).</summary>
public static float ShoveWindow { get => _window ?? 10f; set => _window = value; }
public static float ShovesPerWindow { get => _shoves ?? 2f; set => _shoves = value; }
/// <summary>
/// ⛔ The most one swing's blows take from one player together, against `MaxHit`: 1 — the user's rule, a swing is one attack.
/// Under 1.5 no swing downs anyone from full base health; upstream's every-blow-lands is about 3.
/// </summary>
public static float ComboCap { get => _combo ?? 1f; set => _combo = value; }
static float? _fleeSpeed, _fleeMin, _fleeMax, _fleeStep, _fleeTime, _fleeClear;
/// <summary>
/// The flight (2026-10-07): 650 u/s — the user's *"insanely quickly"*, 2.35x his fastest sprint (276) and 1.74x that at half
/// health (373). His legs keep up to x4 (`sprint_01` at <see cref="FleeSpeed"/> / 276, never under x1).
/// </summary>
public static float FleeSpeed { get => _fleeSpeed ?? 650f; set => _fleeSpeed = value; }
/// <summary>Then away 10-40 s, the user's, before he comes back.</summary>
public static float FleeMin { get => _fleeMin ?? 10f; set => _fleeMin = value; }
public static float FleeMax { get => _fleeMax ?? 40f; set => _fleeMax = value; }
/// <summary>A flight each time this share of his health goes: 0.2, the user's 20% — at 80, 60, 40 and 20%.</summary>
public static float FleeStep { get => _fleeStep ?? 0.2f; set => _fleeStep = value; }
/// <summary>At most 5 s running (3,250 u at 650); then he goes into the ground wherever he got to.</summary>
public static float FleeTimeout { get => _fleeTime ?? 5f; set => _fleeTime = value; }
/// <summary>The spawn he runs to stands 500 from everyone up, when one he can reach does.</summary>
public static float FleeClear { get => _fleeClear ?? 500f; set => _fleeClear = value; }
// his clips (30 fps) and their events — ⛔ `const`, never `static readonly` (INSTRUCTIONS.md §1)
const string ClipToSprint = "nz_s2_zom_asn_walk_2_sprint", ClipCrawlOut = "nz_s2_zom_asn_trav_crawl_exit",
ClipCrawlIn = "nz_s2_zom_asn_trav_crawl_enter", ClipMantle = "nz_s2_zom_asn_trav_mantle_40",
ClipKnockdown = "nz_s2_zom_asn_stand_pain_react_knockdown", Stumble = "nz_s2_zom_asn_stand_stumble_";
const float ToSprintLen = 62f / 30f, CrawlOutLen = 83f / 30f, CrawlInLen = 68f / 30f, StumbleLen = 60f / 30f,
KnockdownLen = 100f / 30f, DeathFall = 20f / 30f;
/// <summary>
/// The travel `walk_2_sprint` carries (stripped by the port), and the crawl-out frame from which all of him is under the floor
/// (its highest bone below 0, measured on the clip).
/// </summary>
const float ToSprintTravel = 93f, SunkFrame = 66f;
/// <summary>His clips' own speeds — `meuchler.zvar`'s tiers are picked by them: walk_03, walk_01/02, sprint_03/04/05, sprint_02, sprint_01.</summary>
const float WalkSlow = 35.1f, Walk = 97f, Sprint = 220.4f, SprintB = 251.5f, SprintA = 276.3f;
/// <summary>
/// The flight's run (2026-10-07): his fastest sprint, `sprint_01` (a 62-frame cycle, its `walkframe` — the clip's own length is
/// read first) — and what ends it: within 48 u of the spot, under 40 u gained in half a second (stopped short), and at most 8
/// spawns asked for a path.
/// </summary>
const string ClipFlee = "nz_s2_zom_asn_sprint_01";
const float FleeCycle = 62f / 30f, FleeArrive = 48f, FleeStall = 40f;
const int FleePaths = 8;
/// <summary>Upstream's arrival burst, `doom_caco_blast`'s orange (255, 136, 8).</summary>
static Color Orange => new( 1f, 0.53f, 0.03f );
// ══ state ════════════════════════════════════════════════════════════════
bool _enraged, _hurt, _halfSpeedOn, _crawling, _under, _hadTarget = true, _blowDone, _fell;
float _pace, _bornAt, _enrageAt, _nextThink, _nextAmbush, _windowEnd, _idleSince, _clipT, _diedAt;
int _shovesTaken;
string _clip;
Vector3 _exit;
/// <summary>
/// The flight (2026-10-07). ⚠️ FIELDS A HOTLOAD LEAVES AT 0: `_fleeArmed` false means "not counted yet" — the first look counts
/// the shares already gone as spent, with no flight (`TickShares`); `_fleeRate` 0 reads as 1.
/// </summary>
Flight _flight;
bool _fleePending, _fleeArmed, _awayHeld, _ghost;
int _fleesSpent;
float _fleeRate, _fleeStart, _fleeEnds, _fleeRepathAt, _fleeCheckAt, _relayAt, _backAt, _fleeHp, _refreshAt, _fleeRetryAt;
Vector3 _fleeTo, _fleeFrom, _fleeCheckPos;
enum Beat { Alert, Click, Crawl, Bodyfall, Fall, Dirt, Under, Emerge, Out, Gone }
/// <summary>The flight's steps: running, going into the ground, away (hidden, held), coming back up.</summary>
enum Flight { None, Run, Dig, Away, Rise }
public bool Enraged => _enraged;
public bool Crawling => _crawling;
public bool Fleeing => _flight != Flight.None;
public bool Elite => Body.IsValid() && (Body.Model?.ResourcePath?.Contains( "meuchler_elite" ) ?? false);
/// <summary>
/// Carried by something the AI drives — a nav link, a window (held at one, or climbing through it on his mantle), an arc —
/// upstream's `GetIsBusy`: no move of his starts, no shove lands, and his gait waits.
/// </summary>
bool Busy => Ai.CrossingLink || Ai.OnLink || Ai.ParkedAt is not null || Ai.Arcing || Ai.CurrentClip == ClipMantle;
/// <summary>Free for a move of his own: walking, swinging or idle, and not carried by anything (<see cref="Busy"/>).</summary>
bool Free => Ai.State is (ZombieState.Chasing or ZombieState.Attacking or ZombieState.Idle) && !Busy;
protected override void HostStart()
{
Ai.MaxHitDamage = MaxHit;
// ⛔ NONE OF THE WALKER'S CLIMB CLIPS ON HIS MODEL: his own mantle, upstream's every barricade jump
Ai.ClimbClipOverride = ClipMantle;
_bornAt = Time.Now;
_enrageAt = Time.Now + EnrageAfter;
_nextAmbush = Time.Now + 1f;
_windowEnd = Time.Now + 5f;
// the flight's shares counted from full health: none spent
_fleeArmed = true;
_fleesSpent = 0;
// upstream's `SpeedChanged`: one of his three walks (walk_01/02 share a tier, so the AI rolls between them)
_pace = Game.Random.Int( 0, 2 ) == 0 ? WalkSlow : Walk;
ApplyPace();
// upstream's arrival (`doom_caco_blast`, an orange electric burst; the zap is the variant's `SpawnSound`), and his alert
// at frame 17 of his rise
BossFx.BurstShared( WorldPosition + Vector3.Up * 50f * Size, 140f * Size, Orange, 14 );
Queue( Frames( 17f ), (int)Beat.Alert );
Say( $"spawned — the {(Elite ? "elite" : "plain")} body · {Hp?.Max ?? 0f:0} hp (x{BulletScale:0.###} from bullets, x{MeleeScale:0.##}"
+ $" melee, x{OtherScale:0.##} else) · walks at {_pace:0} · ambush past {AmbushRange:0} or unseen, {OpenSpawns().Count()} open"
+ $" spawn(s) · flees at {FleeLeft()} to {NormalSpawns().Count} zombie spawn(s) · hitboxes {Ai.HitboxCount}" );
}
protected override void HostFrame()
{
Ai.MaxHitDamage = MaxHit;
if ( Ai.State == ZombieState.Spawning ) return;
WatchClip();
TickWindow();
TickHalfHealth();
TickShares();
// ⚠️ THE FLIGHT OWNS HIM from the run to the end of his rise (2026-10-07): no ambush, enrage or shove starts meanwhile
if ( _flight != Flight.None )
{
TickFlight();
return;
}
// the ambush owns him until he is out of the ground — a hold that ended early hands him back, seen
if ( _crawling )
{
if ( Ai.State != ZombieState.Special ) EndCrawl();
return;
}
// a flight that is due goes the moment he is free, before anything of his own
if ( _fleePending && Time.Now >= _fleeRetryAt && Free && BeginFlight() ) return;
if ( Time.Now < _nextThink ) return;
_nextThink = Time.Now + 0.25f;
Think();
}
/// <summary>
/// ⚠️ NOT SOLID WHILE HIDDEN (2026-10-07), on every machine: the capsule a player walks into is off while the body is down
/// to its shadow (`BossFx.SetHidden`, which reaches every machine), so 10-40 s away at a spawn is no invisible wall. Keyed on
/// the hiding itself, so it needs no message of its own. His `SolidBody` is the default `true`; nothing else writes it.
/// </summary>
protected override void EveryFrame()
{
if ( !Body.IsValid() ) return;
var hidden = Body.RenderType == ModelRenderer.ShadowRenderType.ShadowsOnly;
if ( hidden == _ghost ) return;
_ghost = hidden;
Ai.SolidBody = !hidden;
}
protected override void Died( bool host )
{
_diedAt = Time.Now;
if ( !host ) return;
// under the floor, the ambush's or away on the flight (only a kill that ignores his invulnerability): seen again
if ( _under ) Surface();
Say( "killed" );
}
/// <summary>The heavy bodyfall of his death clips (their frames 19-21), on every machine.</summary>
protected override void DeadFrame()
{
if ( _fell || Time.Now < _diedAt + DeathFall ) return;
_fell = true;
NZSound.Play( "nz.meuchler.fall", WorldPosition );
}
/// <summary>
/// His table, upstream's `PostTookDamage`: x<see cref="BulletScale"/> from bullets, x<see cref="MeleeScale"/> from melee (and the
/// shove), x<see cref="OtherScale"/> from anything else — and every hit angers him. Nothing for his first second, nothing while
/// all of him is under the floor. THE HOST, once a hit.
/// </summary>
protected override float ScaleDamage( float amount, in DamageInfo damage )
{
if ( _under || Time.Now < _bornAt + SpawnProtection ) return 0f;
_hurt = true;
if ( Health.IsMelee( damage ) )
{
TryShove( damage.Attacker.IsValid() ? damage.Attacker.WorldPosition : damage.Position );
return MeleeScale;
}
return (damage.Tags?.Has( SWB.Shared.TagsHelper.Bullet ) ?? false) ? BulletScale : OtherScale;
}
protected override void OnBeat( int beat )
{
switch ( (Beat)beat )
{
case Beat.Alert:
NZSound.PlayShared( "nz.meuchler.alert", Voice );
break;
case Beat.Click:
NZSound.PlayShared( "nz.meuchler.click", Voice );
break;
case Beat.Crawl:
NZSound.PlayShared( "nz.meuchler.crawl", WorldPosition );
break;
case Beat.Bodyfall:
NZSound.PlayShared( "nz.meuchler.bodyfall", WorldPosition );
break;
case Beat.Fall:
NZSound.PlayShared( "nz.meuchler.fall", WorldPosition );
break;
case Beat.Dirt:
// the ground he goes into and comes out of, on every machine (`SpawnDirt` announces itself)
SpawnDirt.Burst( Scene, WorldPosition, 10 );
break;
case Beat.Under:
// all of him under the floor: hidden on every machine, and untouchable — the ambush's, or the flight's burrow
if ( !_crawling && _flight != Flight.Dig ) break;
_under = true;
SetHidden( true );
if ( Hp.IsValid() ) Hp.Invulnerable = true;
break;
case Beat.Emerge:
Emerge();
break;
case Beat.Out:
_crawling = false;
if ( _flight == Flight.Rise ) EndFlight();
break;
case Beat.Gone:
// the flight's burrow is over: away
if ( _flight == Flight.Dig ) BeginAway();
break;
}
}
// ══ the host's pass ══════════════════════════════════════════════════════
/// <summary>Upstream's `AI()`, a pass every quarter second: the enrage first, then the ambush, then a target found again. THE HOST.</summary>
void Think()
{
var p = TargetPlayer;
var has = IsUp( p );
var found = has && !_hadTarget && Time.Now - _idleSince >= 1f;
if ( !has && _hadTarget ) _idleSince = Time.Now;
_hadTarget = has;
if ( Ai.State != ZombieState.Chasing || Busy ) return;
if ( TryEnrage( p ) || TryAmbush( p ) ) return;
if ( found && CanSee( p.GameObject ) ) React( p );
}
void ApplyPace() => SetSpeed( _pace * (_halfSpeedOn ? HalfSpeed : 1f) );
/// <summary>At half health, x<see cref="HalfSpeed"/> for good — once he is free to change his gait. THE HOST.</summary>
void TickHalfHealth()
{
if ( _halfSpeedOn || !Hp.IsValid() || Hp.Max <= 1f || Hp.Current > Hp.Max * 0.5f ) return;
// ⚠️ NOT MID-FLIGHT (2026-10-07): the hold hands him back for a frame while he is away, and his gait waits until he is up
if ( Ai.State is not (ZombieState.Chasing or ZombieState.Attacking) || Busy || _flight != Flight.None ) return;
_halfSpeedOn = true;
ApplyPace();
Say( $"half health — x{HalfSpeed:0.##}, {Ai.SpeedOverride:0} u/s" );
}
// ══ the enrage ═══════════════════════════════════════════════════════════
bool TryEnrage( NZPlayer p )
{
if ( _enraged ) return false;
var seen = IsUp( p ) && WorldPosition.Distance( p.WorldPosition ) <= EnrageRange * Size && CanSee( p.GameObject );
if ( !seen && !_hurt && Time.Now < _enrageAt ) return false;
Enrage();
return true;
}
/// <summary>Angry, once: `walk_2_sprint`, then one of his five sprints for good (upstream's `SetRunSpeed(71)`). THE HOST.</summary>
void Enrage()
{
if ( _enraged ) return;
_enraged = true;
ToSprint( true );
// one of five, picked once (sprint_03/04/05 share a tier, so the AI rolls between them)
var roll = Game.Random.Int( 0, 4 );
_pace = roll == 0 ? SprintA : roll == 1 ? SprintB : Sprint;
ApplyPace();
Say( $"ENRAGED — sprinting at {Ai.SpeedOverride:0} u/s" );
}
/// <summary>
/// `walk_2_sprint`, its alert at frame 15 — and its 93 u forward, which the port stripped: an arc along his facing, short of any
/// wall (the Panzerhund's dash). THE HOST.
/// </summary>
bool ToSprint( bool alert )
{
if ( !PlayShared( ClipToSprint, ToSprintLen ) ) return false;
if ( alert ) Queue( Frames( 15f ), (int)Beat.Alert );
var dir = Facing;
var from = WorldPosition + Vector3.Up * 40f * Size;
var reach = ToSprintTravel * Size;
var wall = Scene.Trace.Ray( from, from + dir * reach ).IgnoreGameObjectHierarchy( GameObject )
.WithoutTags( "zombie", "player", "trigger", "ragdoll" ).Run();
if ( wall.Hit ) reach = MathF.Max( 0f, wall.Distance - 30f * Size );
if ( reach > 16f ) Ai.BeginArc( WorldPosition + dir * reach, ToSprintLen, 0f );
return true;
}
/// <summary>
/// A target found after none (upstream's `IsIdle` reaction): `walk_2_sprint` straight at them — with the taunt their turn clips
/// voiced when they stand to his side, his alert when they do not. THE HOST.
/// </summary>
void React( NZPlayer p )
{
var (dot, dot2) = Sides( p.WorldPosition );
var side = (dot2 < -0.5f && dot >= -0.5f) || (dot2 > 0.5f && dot <= 0.5f);
Face( p.WorldPosition );
if ( !ToSprint( !side ) ) return;
if ( side ) NZSound.PlayShared( "nz.meuchler.taunt", Voice );
Say( "found his target again" );
}
/// <summary>
/// Upstream's side test, from <paramref name="from"/> to him: `Dot` against his forward, `Dot2` against his right — Dot below 0,
/// it is in front of him; Dot2 below -0.5, on his right; above 0.5, on his left.
/// </summary>
(float Dot, float Dot2) Sides( Vector3 from )
{
var normal = (WorldPosition - from).WithZ( 0 );
normal = normal.Length > 1f ? normal.Normal : -Facing;
var right = Vector3.Cross( Facing, Vector3.Up );
return (Facing.Dot( normal ), right.Dot( normal ));
}
// ══ the ambush ═══════════════════════════════════════════════════════════
bool TryAmbush( NZPlayer p )
{
if ( Time.Now < _nextAmbush || !IsUp( p ) ) return false;
var d = WorldPosition.Distance( p.WorldPosition );
if ( d <= AmbushRange * Size && CanSee( p.GameObject ) ) return false;
if ( Game.Random.Float( 0f, 1f ) >= AmbushChance ) return false;
// ⚠️ ONLY WHEN IT BRINGS HIM NEARER — and a failed look waits a second, so a map without open spawns costs no search a pass
var exit = ExitNear( p.WorldPosition );
if ( exit is null || exit.Value.Distance( p.WorldPosition ) + AmbushGain * Size > d )
{
_nextAmbush = Time.Now + 1f;
return false;
}
return BeginAmbush( exit.Value );
}
/// <summary>
/// Into the ground where he stands (`trav_crawl_exit`, in place: he drops, crawls, turns and sinks) with the click as he goes,
/// heard map-wide — and out at <paramref name="exit"/> when it ends (<see cref="Emerge"/>). THE HOST.
/// </summary>
bool BeginAmbush( Vector3 exit )
{
// ⚠️ THE WHOLE ACT IS ONE HOLD: down, gone, up — the rise re-arms it for its own length (Zaballa's teleport)
if ( !PlayShared( ClipCrawlOut, CrawlOutLen + CrawlInLen + 0.5f ) ) return false;
_crawling = true;
_exit = exit;
// ⚠️ FROM THE START, upstream's: another is possible ~4-8 s after he comes up
_nextAmbush = Time.Now + Game.Random.Float( AmbushGapMin, AmbushGapMax );
NZSound.PlayShared( "nz.meuchler.click", Voice );
Queue( Frames( 10f ), (int)Beat.Bodyfall );
Queue( Frames( 29f ), (int)Beat.Crawl );
Queue( Frames( 41f ), (int)Beat.Crawl );
Queue( Frames( 48f ), (int)Beat.Dirt );
Queue( Frames( SunkFrame ), (int)Beat.Under );
Queue( CrawlOutLen, (int)Beat.Emerge );
Say( $"ambush — into the ground, out {exit.Distance( WorldPosition ):0}u away" );
return true;
}
/// <summary>
/// Out of the ground at the open spawn nearest his target NOW (upstream re-picks it, B:478; none, the first pick), facing them:
/// `trav_crawl_enter`, in place — he rises through the floor — and the click at its frame 10. THE HOST.
/// </summary>
void Emerge()
{
if ( !_crawling ) return;
var p = TargetPlayer;
var up = IsUp( p );
Rise( (up ? ExitNear( p.WorldPosition ) : null) ?? _exit, up ? p : null );
}
/// <summary>
/// Up out of the floor at <paramref name="spot"/>, facing <paramref name="facing"/> if any — the ambush's rise, and the flight's
/// return (2026-10-07). The hold he is in re-armed for the clip's length; <see cref="Beat.Out"/> at its end. THE HOST.
/// </summary>
void Rise( Vector3 spot, NZPlayer facing )
{
Warp( spot );
if ( facing.IsValid() ) Face( facing.WorldPosition );
// ⚠️ THE CLIP BEFORE HE IS SHOWN: its first frame has all of him under the floor, so no machine sees the old pose
SwapClip( ClipCrawlIn );
Ai.HoldSpecialFor( CrawlInLen );
Surface();
Queue( Frames( 4f ), (int)Beat.Dirt );
Queue( Frames( 10f ), (int)Beat.Click );
Queue( Frames( 17f ), (int)Beat.Crawl );
Queue( Frames( 31f ), (int)Beat.Crawl );
Queue( Frames( 44f ), (int)Beat.Crawl );
Queue( CrawlInLen, (int)Beat.Out );
}
/// <summary>Seen and touchable again, on every machine. THE HOST.</summary>
void Surface()
{
_under = false;
SetHidden( false );
if ( Hp.IsValid() ) Hp.Invulnerable = false;
}
/// <summary>The ambush cut short, however: him back, seen, and its leftover beats forgotten. THE HOST.</summary>
void EndCrawl()
{
_crawling = false;
ClearBeats();
if ( _under ) Surface();
}
/// <summary>
/// Every spawn a zombie could come out of now: zombie, special and boss spawns whose door is open — upstream's
/// `GetZombieSpawnArray` with its link test (it ignores a spawner's round and power too; a spawn here has no crawl type).
/// </summary>
IEnumerable<SpawnPoint> OpenSpawns()
{
var cfg = ActiveConfig.Current;
if ( cfg is null ) yield break;
foreach ( var list in new[] { cfg.ZombieSpawns, cfg.SpecialSpawns, cfg.BossSpawns } )
{
if ( list is null ) continue;
foreach ( var s in list )
if ( s is not null && DoorLinks.AnyOpen( s.Link, s.Link2, s.Link3 ) ) yield return s;
}
}
/// <summary>
/// Upstream's `FindHiddenSpawn`: the open spawn nearest <paramref name="to"/> (no sight test — upstream's is commented out), and
/// where a zombie from it stands (`ZombieAI.NavGround`, on its own level). Null when none is open.
/// </summary>
Vector3? ExitNear( Vector3 to ) => StandNear( OpenSpawns(), to );
/// <summary>Of <paramref name="spawns"/>, where a zombie from the one nearest <paramref name="to"/> stands; null, none (the ambush's lookup, and the flight's return).</summary>
Vector3? StandNear( IEnumerable<SpawnPoint> spawns, Vector3 to )
{
SpawnPoint best = null;
var bestD = float.MaxValue;
foreach ( var s in spawns )
{
var d = s.Position.DistanceSquared( to );
if ( d >= bestD ) continue;
bestD = d;
best = s;
}
if ( best is null ) return null;
return ZombieAI.NavGround( Scene, best.Position );
}
// ══ the flight (2026-10-07) ══════════════════════════════════════════════
/// <summary>The share of his health one flight costs, kept sane: 0.05-0.95.</summary>
static float Step => Math.Clamp( FleeStep, 0.05f, 0.95f );
/// <summary>How many shares above 0 a step makes: 4 at 0.2 (80, 60, 40, 20%).</summary>
static int Shares( float step ) => Math.Max( 0, (int)MathF.Ceiling( 1f / step - 1e-4f ) - 1 );
/// <summary>His legs' rate for the run, and so the burrow's: the flight's speed over `sprint_01`'s, x1-x4 — x2.35 at 650.</summary>
static float RunRate => Math.Clamp( FleeSpeed / SprintA, 1f, 4f );
/// <summary>This flight's rate (0 after a hotload reads as 1).</summary>
float FleeRate => _fleeRate > 0.01f ? _fleeRate : 1f;
/// <summary>One cycle of the run at its rate — how often a watching machine is sent it again.</summary>
float Stride => (Body.IsValid() && Body.Sequence.Name == ClipFlee && Body.Sequence.Duration > 0.01f ? Body.Sequence.Duration : FleeCycle)
/ FleeRate;
/// <summary>The shares still to come, "60/40/20%", or "none".</summary>
string FleeLeft()
{
var step = Step;
var all = Shares( step );
if ( _fleesSpent >= all ) return "none";
return string.Join( "/", Enumerable.Range( _fleesSpent + 1, all - _fleesSpent ).Select( k => $"{(1f - k * step) * 100f:0}" ) ) + "%";
}
/// <summary>
/// The round's zombie spawns, as its wave picks from them (`RoundManager.EligibleSpawnsFor`: link open, power on if they ask,
/// round reached) — no special's or boss's. Before a round, round 1's.
/// </summary>
static List<SpawnPoint> NormalSpawns()
{
if ( ActiveConfig.Current?.ZombieSpawns is null ) return new();
return RoundManager.EligibleSpawnsFor( Math.Max( 1, RoundManager.Instance?.Round ?? 1 ) );
}
/// <summary>
/// The shares of his health gone, against those spent, every frame: a new one is a flight, once. Several in one hit, one
/// flight; one gone while a flight is due, under way or away, spent with it and no other. Counted from the top, so a new
/// <see cref="FleeStep"/> counts the spent ones in its own shares. THE HOST.
/// </summary>
void TickShares()
{
if ( !Hp.IsValid() || Hp.Max <= 1f ) return;
var step = Step;
var gone = Math.Clamp( (int)MathF.Floor( (1f - Hp.Current / Hp.Max) / step + 1e-4f ), 0, Shares( step ) );
// ⚠️ A HOTLOAD INTO A LIVE ONE (`_fleeArmed` still false): what is already gone counts as spent, with no flight
if ( !_fleeArmed )
{
_fleeArmed = true;
_fleesSpent = gone;
return;
}
if ( gone <= _fleesSpent ) return;
var n = gone - _fleesSpent;
_fleesSpent = gone;
var what = $"{Hp.Current / Hp.Max * 100f:0}% health, {n} share(s) gone";
if ( _flight != Flight.None || _fleePending )
{
Say( $"{what} — spent with the flight {(_fleePending ? "due" : "under way")}, no other" );
return;
}
_fleePending = true;
_fleeRetryAt = 0f;
Say( $"{what} — a flight" );
}
/// <summary>
/// Off at <see cref="FleeSpeed"/> to the spawn <see cref="FleeSpot"/> picks, in his fastest sprint sped to match, with the pain
/// vox as he breaks off — the swing he was in dropped. With nowhere he can reach (or already there), straight into the ground.
/// ⚠️ ONE HOLD for the run and the burrow, so the burrow plays at the run's rate. False when the hold would not start (it
/// tries again in a second). THE HOST.
/// </summary>
bool BeginFlight()
{
var rate = RunRate;
var to = FleeSpot( out var clear );
var here = to is null || (to.Value - WorldPosition).WithZ( 0 ).Length <= FleeArrive * Size;
if ( !PlayShared( here ? ClipCrawlOut : ClipFlee, MathF.Max( 0f, FleeTimeout ) + CrawlOutLen / rate + 1.5f, rate ) )
{
_fleeRetryAt = Time.Now + 1f;
return false;
}
_fleePending = false;
_flight = Flight.Run;
_fleeRate = rate;
_fleeFrom = WorldPosition;
_fleeTo = to ?? WorldPosition;
_fleeStart = Time.Now;
_fleeEnds = Time.Now + MathF.Max( 0f, FleeTimeout );
_fleeRepathAt = 0f;
_fleeCheckAt = Time.Now + 0.75f;
_fleeCheckPos = WorldPosition;
_relayAt = Time.Now + Stride;
NZSound.PlayShared( "nz.meuchler.pain", Voice );
if ( here )
{
Dig( to is null ? "no zombie spawn he can reach" : "already at one", swap: false );
return true;
}
Drive();
Say( $"FLEEING at {FleeSpeed * Size:0} u/s (legs x{rate:0.##}) to the zombie spawn {_fleeTo.Distance( WorldPosition ):0}u away"
+ (clear ? "" : $" (none {FleeClear * Size:0} from everyone up that he can reach: the nearest)") );
return true;
}
/// <summary>
/// Where he runs: of the round's zombie spawns (<see cref="NormalSpawns"/>), where a zombie from each stands
/// (`ZombieAI.NavGround`), nearest him first — the first <see cref="FleeClear"/> from everyone up, else the nearest.
/// ⚠️ ONLY ONE HE HAS A WHOLE PATH TO, the nearest <see cref="FleePaths"/> asked (a path is not free). Null, none he can
/// reach. <paramref name="clear"/>: the spot is clear of everyone. THE HOST.
/// </summary>
Vector3? FleeSpot( out bool clear )
{
clear = false;
var from = WorldPosition;
var start = ZombieAI.NavGround( Scene, from );
var players = Scene.GetAllComponents<NZPlayer>().Where( IsUp ).Select( p => p.WorldPosition ).ToList();
var away = FleeClear * Size;
Vector3? nearest = null;
var asked = 0;
foreach ( var s in NormalSpawns().OrderBy( sp => sp.Position.DistanceSquared( from ) ) )
{
var isClear = players.All( p => p.Distance( s.Position ) >= away );
if ( !isClear && nearest is not null ) continue;
if ( asked++ >= FleePaths ) break;
var to = ZombieAI.NavGround( Scene, s.Position );
if ( !Reaches( start, to ) ) continue;
nearest ??= to;
if ( !isClear ) continue;
clear = true;
return to;
}
return nearest;
}
/// <summary>A whole path on the navmesh from <paramref name="from"/> to <paramref name="to"/>. No navmesh, no opinion.</summary>
bool Reaches( Vector3 from, Vector3 to )
{
var nav = Scene.NavMesh;
if ( nav is null || !nav.IsEnabled || from.Distance( to ) < 1f ) return true;
var path = nav.CalculatePath( new Sandbox.Navigation.CalculatePathRequest { Start = from, Target = to } );
return path.IsValid && path.Status == Sandbox.Navigation.NavMeshPathStatus.Complete;
}
/// <summary>The flight, a frame at a time. THE HOST.</summary>
void TickFlight()
{
switch ( _flight )
{
case Flight.Run:
TickRun();
break;
case Flight.Dig:
// the hold lost before he was gone (nothing of his ends it — a safety): gone at once
if ( Ai.State != ZombieState.Special ) Vanish();
break;
case Flight.Away:
TickAway();
break;
case Flight.Rise:
// the rise's hold ended early: him back, seen, as the ambush's `EndCrawl`
if ( Ai.State != ZombieState.Special )
{
ClearBeats();
EndFlight();
}
break;
}
}
/// <summary>
/// The run: into the ground when he is there (within 48 u), out of <see cref="FleeTimeout"/>, or stopped short (under 40 u
/// gained in half a second: his path ends before the spot). Hurt as ever meanwhile, his table's own. THE HOST.
/// </summary>
void TickRun()
{
if ( Ai.State != ZombieState.Special )
{
Vanish();
return;
}
// ⚠️ CARRIED (a link): its own clip and its own course. Nothing ends the run mid-air, and the agent is told again after.
if ( Busy )
{
_fleeRepathAt = 0f;
_fleeCheckAt = Time.Now + 0.5f;
_fleeCheckPos = WorldPosition;
return;
}
// his run back after a link's clip took the renderer
if ( Body.IsValid() && Body.Sequence.Name != ClipFlee )
{
SwapClip( ClipFlee, FleeRate );
_relayAt = Time.Now + Stride;
}
var there = (WorldPosition - _fleeTo).WithZ( 0 ).Length <= FleeArrive * Size && MathF.Abs( WorldPosition.z - _fleeTo.z ) < 72f;
if ( there ) { Dig( "there" ); return; }
if ( Time.Now >= _fleeEnds ) { Dig( "out of time" ); return; }
if ( Time.Now >= _fleeCheckAt )
{
if ( WorldPosition.Distance( _fleeCheckPos ) < FleeStall * Size ) { Dig( "stopped short" ); return; }
_fleeCheckAt = Time.Now + 0.5f;
_fleeCheckPos = WorldPosition;
}
Drive();
// ⚠️ A RELAYED CLIP PLAYS ONCE on a watching machine (`PlayClipAsPuppet`), then its own walk at its rate cap, sliding: the
// stride sent again as each one ends
if ( Networking.IsActive && Time.Now >= _relayAt )
{
NZNet.ZombieClip( GameObject.Id, ClipFlee, FleeRate );
_relayAt = Time.Now + MathF.Max( 0.25f, Stride );
}
}
/// <summary>
/// His agent at the flight's speed, told where again every quarter second — the AI's own orders (a retarget, a wander) may
/// have told it otherwise. ⚠️ THE AGENT, NOT THE AI: a hold (`ZombieState.Special`) keeps the AI from chasing or swinging, and
/// its frame faces him along his course. THE HOST.
/// </summary>
void Drive()
{
var agent = Components.Get<NavMeshAgent>( FindMode.EverythingInSelfAndDescendants );
if ( !agent.IsValid() ) return;
var speed = MathF.Max( 1f, FleeSpeed * Size );
if ( MathF.Abs( agent.MaxSpeed - speed ) > 0.5f ) agent.MaxSpeed = speed;
if ( Time.Now < _fleeRepathAt ) return;
_fleeRepathAt = Time.Now + 0.25f;
agent.MoveTo( _fleeTo );
}
/// <summary>His agent stopped where he stands, as a hold leaves it (`ZombieAI.PlaySpecial`: `StopMoving`, speed 0). THE HOST.</summary>
void Halt()
{
var agent = Components.Get<NavMeshAgent>( FindMode.EverythingInSelfAndDescendants );
if ( !agent.IsValid() ) return;
agent.MaxSpeed = 0f;
agent.MoveTo( WorldPosition );
}
/// <summary>
/// Into the ground where he is: the ambush's burrow (`trav_crawl_exit` — its click, bodyfall, crawls and dirt) at the run's
/// rate, all of him under the floor at its frame 66 (hidden, untouchable: <see cref="Beat.Under"/>), away at its end
/// (<see cref="Beat.Gone"/>). THE HOST.
/// </summary>
void Dig( string why, bool swap = true )
{
_flight = Flight.Dig;
Halt();
var r = FleeRate;
if ( swap ) SwapClip( ClipCrawlOut, r );
Ai.HoldSpecialFor( CrawlOutLen / r + 1f );
NZSound.PlayShared( "nz.meuchler.click", Voice );
Queue( Frames( 10f ) / r, (int)Beat.Bodyfall );
Queue( Frames( 29f ) / r, (int)Beat.Crawl );
Queue( Frames( 41f ) / r, (int)Beat.Crawl );
Queue( Frames( 48f ) / r, (int)Beat.Dirt );
Queue( Frames( SunkFrame ) / r, (int)Beat.Under );
Queue( CrawlOutLen / r, (int)Beat.Gone );
Say( $"fled {WorldPosition.Distance( _fleeFrom ):0}u in {Time.Now - _fleeStart:0.0}s ({why}) — into the ground" );
}
/// <summary>Gone at once, the hold lost mid-run or mid-burrow (a safety): a burst where he was, the click, then away. THE HOST.</summary>
void Vanish()
{
ClearBeats();
if ( !_under ) BossFx.BurstShared( WorldPosition + Vector3.Up * 40f * Size, 120f * Size, Orange, 10 );
NZSound.PlayShared( "nz.meuchler.click", Voice );
BeginAway();
}
/// <summary>
/// Away: hidden and untouchable on every machine (the ambush's way, if the burrow had not done it), held at crawl-in's first
/// frame — all of him under the floor, so not even his shadow shows — his health noted, and the run's hold handed back so the
/// away can hold at x1 and his rise play at the ambush's pace (<see cref="TickAway"/>). Back in <see cref="FleeMin"/>-<see
/// cref="FleeMax"/> s. THE HOST.
/// </summary>
void BeginAway()
{
ClearBeats();
_flight = Flight.Away;
_awayHeld = false;
if ( !_under )
{
_under = true;
SetHidden( true );
if ( Hp.IsValid() ) Hp.Invulnerable = true;
}
Halt();
if ( Ai.State == ZombieState.Special )
{
SwapClip( ClipCrawlIn );
Ai.CancelSpecial();
}
HoldUnder();
_fleeHp = Hp.IsValid() ? Hp.Current : 0f;
var lo = MathF.Max( 0f, MathF.Min( FleeMin, FleeMax ) );
var hi = MathF.Max( lo, MathF.Max( FleeMin, FleeMax ) );
_backAt = Time.Now + Game.Random.Float( lo, hi );
_refreshAt = Time.Now + 2f;
Say( $"AWAY with {_fleeHp:0}/{Hp?.Max ?? 0f:0} hp — back in {_backAt - Time.Now:0.#}s" );
}
/// <summary>
/// ⚠️ THE OTHER MACHINES HOLD HIM AT THE FRAME HE SHOWS, crawl-in's first (`NZNet.ZombieFreeze`, the pratfall's): a relayed clip
/// plays once and a puppet then walks, and its shadow would stand at the spawn. Re-sent every 2 s while away. THE HOST.
/// </summary>
void HoldUnder()
{
if ( Networking.IsActive ) NZNet.ZombieFreeze( GameObject.Id, 60f );
}
/// <summary>
/// Away, a frame at a time: his own hold at x1 once the run's has gone (re-armed every frame, so it lasts however long he is
/// away; the AI neither chases nor swings meanwhile), the pose pinned under the floor, his agent kept still (with nobody to
/// chase, the AI's wander would walk him), the hiding sent again every 2 s for anyone who joined, and back when the time is
/// up. THE HOST.
/// </summary>
void TickAway()
{
if ( Ai.State != ZombieState.Special )
{
// ⚠️ THE ONE FRAME THE AI HAS HIM between the two holds — hidden, untouchable, and handed straight back
if ( !PlayShared( ClipCrawlIn, 5f, 1f ) ) return;
_awayHeld = true;
HoldUnder();
}
else if ( _awayHeld ) Ai.HoldSpecialFor( 5f );
// crawl-in's first frame — all of him under the floor — however long the hold runs
if ( Body.IsValid() && Body.Sequence.Name == ClipCrawlIn ) Body.Sequence.Time = 0f;
var agent = Components.Get<NavMeshAgent>( FindMode.EverythingInSelfAndDescendants );
if ( agent.IsValid() && agent.MaxSpeed > 0.01f ) Halt();
if ( Networking.IsActive && Time.Now >= _refreshAt )
{
_refreshAt = Time.Now + 2f;
SetHidden( true );
NZNet.ZombieClip( GameObject.Id, ClipCrawlIn, 1f );
HoldUnder();
}
if ( _awayHeld && Time.Now >= _backAt ) ComeBack();
}
/// <summary>
/// Back: up out of the round's zombie spawn nearest a random player who is up (nobody up: again in 2 s), facing them — the
/// ambush's rise (<see cref="Rise"/>) — with the health he went down with: nothing on the way back sets it, and nothing could
/// take it while he was away. No zombie spawn open, the ambush's own (any open spawn); none at all, where he went down. THE HOST.
/// </summary>
void ComeBack()
{
var up = Scene.GetAllComponents<NZPlayer>().Where( IsUp ).ToList();
if ( up.Count == 0 )
{
_backAt = Time.Now + 2f;
Say( "nobody up to come back for — again in 2s" );
return;
}
var p = Game.Random.FromList( up );
var spot = StandNear( NormalSpawns(), p.WorldPosition ) ?? ExitNear( p.WorldPosition ) ?? WorldPosition;
_flight = Flight.Rise;
_awayHeld = false;
// ⚠️ HIS OWN MOVES PICK UP FROM HERE: no ambush for another 9.46-13.34 s, as after one of its own
_nextAmbush = Time.Now + Game.Random.Float( AmbushGapMin, AmbushGapMax );
// the other machines let go of the held pose before the rise reaches them
if ( Networking.IsActive ) NZNet.ZombieFreeze( GameObject.Id, 0f );
Rise( spot, p );
var hp = Hp.IsValid() ? Hp.Current : 0f;
Say( $"BACK at the zombie spawn {spot.Distance( p.WorldPosition ):0}u from a player · {hp:0}/{Hp?.Max ?? 0f:0} hp"
+ (MathF.Abs( hp - _fleeHp ) < 0.5f ? " (as he went)" : $" (!! he went with {_fleeHp:0})") );
}
/// <summary>The flight over, however it ended: him seen and touchable, his own moves his again. THE HOST.</summary>
void EndFlight()
{
_flight = Flight.None;
_awayHeld = false;
if ( _under ) Surface();
}
// ══ the shove ════════════════════════════════════════════════════════════
/// <summary>The window's count back to 0 when it runs out, the next window 2 s longer for each shove it had (upstream's `ShoveCooldown`).</summary>
void TickWindow()
{
if ( Time.Now <= _windowEnd ) return;
_windowEnd = Time.Now + ShoveWindow + 2f * _shovesTaken;
_shovesTaken = 0;
}
/// <summary>
/// Upstream's L4D-style shove: a melee hit from <paramref name="from"/> staggers him away from it — from his right he reels left,
/// from his left right, from the front back (one time in three the knockdown), from behind forward. Not mid-move, mid-air or
/// mid-window (<see cref="Busy"/>), and at most <see cref="ShovesPerWindow"/> a window. THE HOST.
/// </summary>
void TryShove( Vector3 from )
{
if ( _shovesTaken >= ShovesPerWindow || Busy ) return;
if ( Ai.State is ZombieState.Special or ZombieState.Spawning or ZombieState.Dead ) return;
var (dot, dot2) = Sides( from );
var clip = dot2 < -0.5f && dot >= -0.5f ? Stumble + "l"
: dot2 > 0.5f && dot <= 0.5f ? Stumble + "r"
: dot < 0f ? (Game.Random.Int( 0, 2 ) == 2 ? ClipKnockdown : Stumble + "f")
: Stumble + "b";
if ( clip != ClipKnockdown && Game.Random.Int( 0, 1 ) == 1 ) clip += "_alt";
var knockdown = clip == ClipKnockdown;
if ( !PlayShared( clip, knockdown ? KnockdownLen : StumbleLen ) ) return;
_shovesTaken++;
NZSound.PlayShared( "nz.meuchler.pain", Voice );
if ( knockdown )
{
// down on the floor at frame 22, and back up on his hands
Queue( Frames( 22f ), (int)Beat.Fall );
Queue( Frames( 53f ), (int)Beat.Crawl );
Queue( Frames( 73f ), (int)Beat.Crawl );
Queue( Frames( 81f ), (int)Beat.Crawl );
}
Say( $"shoved ({clip}) — {_shovesTaken} this window" );
}
// ══ the clip he is in ════════════════════════════════════════════════════
/// <summary>
/// The clip the AI is playing him in, watched on the host — a model's events never reach code: the later blow of a two-blow
/// sprint swing, and the click as he climbs (upstream's `asn_vox_click`, frame 8 of his mantle).
/// </summary>
void WatchClip()
{
if ( !Body.IsValid() ) return;
var clip = Body.Sequence.Name;
var t = Body.Sequence.TimeNormalized;
if ( clip != _clip || t + 0.05f < _clipT )
{
// a new play of a clip: another swing, or the same one again
_clip = clip;
_blowDone = false;
if ( clip == ClipMantle )
{
NZSound.PlayShared( "nz.meuchler.click", Voice );
NZSound.PlayShared( "nz.meuchler.crawl", WorldPosition );
}
}
_clipT = t;
if ( _blowDone || Ai.State != ZombieState.Attacking ) return;
var at = LaterBlow( clip );
if ( at <= 0f || t < at ) return;
_blowDone = true;
LandLaterBlow();
}
/// <summary>Where a sprint swing's second `melee` event falls, as a share of the clip (frame / last frame); -1, none.</summary>
static float LaterBlow( string clip ) => clip switch
{
"nz_s2_zom_asn_sprint_attack_melee_02" => 24f / 38f,
"nz_s2_zom_asn_sprint_attack_melee_03" => 36f / 51f,
"nz_s2_zom_asn_sprint_attack_melee_04" => 29f / 48f,
"nz_s2_zom_asn_sprint_attack_melee_05" => 12f / 26f,
_ => -1f,
};
/// <summary>
/// The swing's later blow — upstream's `IgnoreImmunity`: it lands through the hit window and opens none (`tick`), within the AI's
/// own reach. ⛔ ONE SWING, ONE CAP: the AI's first blow counted as landed in full, the two never pass <see cref="ComboCap"/> x
/// `MaxHit`. THE HOST.
/// </summary>
void LandLaterBlow()
{
// the AI's own rule: a webbed or stunned swing lands nothing
if ( StatusEffects.IsDisarmed( GameObject ) ) return;
var p = TargetPlayer;
if ( !IsUp( p ) ) return;
var reach = (Ai.AttackRange + Ai.AttackRangePadding) * Ai.ScaledAttackReach;
if ( WorldPosition.Distance( p.WorldPosition ) > reach ) return;
var blow = SwingDamage( Ai.SwingScale );
var damage = MathF.Min( blow, MaxHit * ComboCap - MathF.Min( blow, MaxHit ) );
if ( damage < 1f ) return;
if ( HurtPlayer( p, damage, tick: true, source: "meuchler later blow" ) )
NZSound.PlayShared( "nz.meuchler.whoosh", WorldPosition + Vector3.Up * 40f * Size );
}
// ══ diagnostics ══════════════════════════════════════════════════════════
static IEnumerable<MeuchlerBoss> All
=> Game.ActiveScene?.GetAllComponents<MeuchlerBoss>() ?? Enumerable.Empty<MeuchlerBoss>();
/// <summary>`nz_meuchler` — every Meúchler alive, and the tuning.</summary>
[ConCmd( "nz_meuchler" )]
public static void Report()
{
Log.Info( $"[nz-meuchler] ambush past {AmbushRange:0} or unseen, x{AmbushChance:0.##} a pass, every {AmbushGapMin:0.##}-{AmbushGapMax:0.##}s,"
+ $" exit {AmbushGain:0}u nearer · enrage in sight within {EnrageRange:0}, at a hit or after {EnrageAfter:0}s · half health x{HalfSpeed:0.##}"
+ $" · x{BulletScale:0.###} bullets, x{MeleeScale:0.##} melee, x{OtherScale:0.##} else, x0 for {SpawnProtection:0.#}s"
+ $" · {ShovesPerWindow:0} shoves a {ShoveWindow:0}s window · a swing x{ComboCap:0.##} the hit cap {MaxHit:0}" );
Log.Info( $"[nz-meuchler] flight every {Step * 100f:0}% of his health lost · {FleeSpeed:0} u/s (legs x{RunRate:0.##}) to a zombie spawn"
+ $" {FleeClear:0} from everyone up, {FleeTimeout:0.#}s at most · back in {FleeMin:0.#}-{FleeMax:0.#}s at the zombie spawn nearest a random player"
+ $" · {NormalSpawns().Count} zombie spawn(s) open" );
var list = All.ToList();
Log.Info( $"[nz-meuchler] {list.Count} alive" );
foreach ( var m in list )
Log.Info( $"[nz-meuchler] {(m.Elite ? "elite" : "plain")} · hp {m.Hp?.Current ?? 0f:0}/{m.Hp?.Max ?? 0f:0} · state {m.Ai?.State}"
+ $" · speed {m.Ai?.MoveSpeed ?? 0f:0}{(m._enraged ? " · ENRAGED" : "")}{(m._halfSpeedOn ? " · HALF HEALTH" : "")}"
+ (m._flight switch
{
Flight.Run or Flight.Dig => " · FLEEING",
Flight.Away => $" · AWAY (back in {MathF.Max( 0f, m._backAt - Time.Now ):0}s)",
Flight.Rise => " · back, rising",
_ => m._fleePending ? " · flight due" : m._under ? " · UNDER THE FLOOR" : m._crawling ? " · crawling" : "",
})
+ $" · flees at {m.FleeLeft()}"
+ $" · next ambush in {MathF.Max( 0f, m._nextAmbush - Time.Now ):0.#}s · shoves {m._shovesTaken} · open spawns {m.OpenSpawns().Count()}" );
}
/// <summary>`nz_meuchler_flee` — every Meúchler runs for a zombie spawn now (busy: as soon as he is free). It spends no share of his health.</summary>
[ConCmd( "nz_meuchler_flee" )]
public static void FleeCmd()
{
if ( NZGame.IsClient ) { Log.Info( "[nz-meuchler] the host decides" ); return; }
foreach ( var m in All.Where( m => m.Ai.IsValid() && !m.Dead && m._flight == Flight.None ).ToList() )
{
m._fleePending = true;
m._fleeRetryAt = 0f;
if ( !m._crawling && m.Free && m.BeginFlight() ) continue;
m.Say( "flight due — he goes as soon as he is free" );
}
}
/// <summary>`nz_meuchler_ambush` — every Meúchler goes into the ground now and comes out near his target (no open spawn: 150 u from them).</summary>
[ConCmd( "nz_meuchler_ambush" )]
public static void AmbushCmd()
{
if ( NZGame.IsClient ) { Log.Info( "[nz-meuchler] the host decides" ); return; }
foreach ( var m in All.Where( m => m.Ai.IsValid() && !m.Dead && !m._crawling && !m.Busy && m._flight == Flight.None ).ToList() )
{
var p = m.TargetPlayer;
if ( !IsUp( p ) ) continue;
var toward = (m.WorldPosition - p.WorldPosition).WithZ( 0 );
var exit = m.ExitNear( p.WorldPosition )
?? ZombieAI.NavGround( m.Scene, p.WorldPosition + (toward.Length > 1f ? toward.Normal : Vector3.Forward) * 150f );
m.BeginAmbush( exit );
}
}
/// <summary>`nz_meuchler_enrage` — every Meúchler enraged now.</summary>
[ConCmd( "nz_meuchler_enrage" )]
public static void EnrageCmd()
{
if ( NZGame.IsClient ) { Log.Info( "[nz-meuchler] the host decides" ); return; }
foreach ( var m in All.Where( m => m.Ai.IsValid() && !m.Dead && !m._enraged && !m._crawling && !m.Busy && m._flight == Flight.None ).ToList() )
m.Enrage();
}
/// <summary>`nz_meuchler_shove` — every Meúchler staggers now as if knifed by his target (the window still counts; not mid-flight).</summary>
[ConCmd( "nz_meuchler_shove" )]
public static void ShoveCmd()
{
if ( NZGame.IsClient ) { Log.Info( "[nz-meuchler] the host decides" ); return; }
foreach ( var m in All.Where( m => m.Ai.IsValid() && !m.Dead && m._flight == Flight.None ).ToList() )
{
var p = m.TargetPlayer;
m.TryShove( IsUp( p ) ? p.WorldPosition : m.WorldPosition + m.Facing * 50f );
}
}
/// <summary>`nz_meuchler_set <key> <value>` — retune him live.</summary>
[ConCmd( "nz_meuchler_set" )]
public static void SetCmd( string key = "", float value = 0f )
{
switch ( key.ToLowerInvariant() )
{
case "range": AmbushRange = value; break;
case "chance": AmbushChance = value; break;
case "gapmin": AmbushGapMin = value; break;
case "gapmax": AmbushGapMax = value; break;
case "gain": AmbushGain = value; break;
case "enrage": EnrageRange = value; break;
case "after": EnrageAfter = value; break;
case "half": HalfSpeed = value; break;
case "bullet": BulletScale = value; break;
case "melee": MeleeScale = value; break;
case "other": OtherScale = value; break;
case "protect": SpawnProtection = value; break;
case "window": ShoveWindow = value; break;
case "shoves": ShovesPerWindow = value; break;
case "combo": ComboCap = value; break;
case "fleespeed": FleeSpeed = value; break;
case "fleemin": FleeMin = value; break;
case "fleemax": FleeMax = value; break;
case "fleestep": FleeStep = value; break;
case "fleetime": FleeTimeout = value; break;
case "fleeclear": FleeClear = value; break;
default:
Log.Info( "[nz-meuchler] nz_meuchler_set <range|chance|gapmin|gapmax|gain|enrage|after|half|bullet|melee|other|protect|window|shoves|combo"
+ "|fleespeed|fleemin|fleemax|fleestep|fleetime|fleeclear> <value>" );
return;
}
Log.Info( $"[nz-meuchler] {key} = {value:0.###}" );
Report();
}
}