Boss component for the Panzermorder enemy in the NZombies gamemode. Implements health bars/lives, knock-down and spend logic, roar and ground-slam attacks, arrival/idle behaviours, gore effects and console commands for diagnostics and live tuning.
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
namespace NZombies;
/// <summary>
/// The Panzermorder — CoD WWII's brute, a flower of meat for a chest, ported 2026-10-07 from GMod nZombies'
/// `nz_zombie_boss_meatflower` (Laby and GhostlyMoo; the boss round's "Panzermorder"), the third batch. Spec:
/// `Sbox nzombies/Docs/boss_specs/meatflower.md`. ⚠️ NOT upstream's `nz_zombie_boss_panzermorder`: the same creature written
/// earlier, on a model nobody has.
///
/// • IT ARRIVES GETTING UP OFF THE FLOOR (`s2_zom_brt_stun_up_v1`, the variant's entrance) in a ring of dust, the ground
/// shaking.
/// • IT WALKS (101.1 u/s) — AND RUNS (325.9) ON ITS LAST BAR. Every step shakes the ground (the variant's step shake).
/// ⚠️ One sound and one shake a footfall: upstream's right foot had its handler twice.
/// • FOUR BARS OF HEALTH, THREE KNOCK-DOWNS (`meatflower.zvar`'s pool in quarters): the hit that would empty a bar puts it
/// down instead — untouchable while it falls (2.77 s) and lies (4.2 s), touchable again a frame into getting up (5.1 s),
/// 7 s after the hit. The first hit a player lands while it lies there (a later one than the blast that put it down) spends
/// a life, and it gets up on the next bar; no hit, and it gets up on the same bar, whole again. With no life left the last
/// bar is its last: emptied, it bursts.
/// ⚠️ UPSTREAM'S RULE NEVER RAN: the gamemode refuses every hit on an invulnerable boss before its `OnInjured` sees one, so
/// no life was ever spent and it got up whole forever (it died only to a hit that emptied a bar EXACTLY). Here, its intent.
/// ⚠️ UPSTREAM SPENDS A LIFE ON A SHOCK (`DMG_SHOCK`); HERE ANY PLAYER'S HIT DOES — this game's only shock is Dead Wire's,
/// and without it the boss could not die. <see cref="ShockOnly"/> (`nz_morder_set shockonly 1`) asks for a hit while it
/// wears the `shock` status, upstream's rule.
/// • THE ROAR, while its target is out of its reach (beyond <see cref="RoarMinRange"/>): each of upstream's path time-outs
/// (0.5-1 s near, 1.5-15 s far) rolls <see cref="RoarChance"/>, and a roar waits 1-<see cref="RoarGapMax"/> s for the next.
/// 2.4 s into it everyone up within <see cref="RoarRadius"/> it can see takes x<see cref="RoarWeight"/> of its swing
/// (upstream's 25 against 65), and everyone in it is dazed <see cref="Daze"/> s (upstream's `NZSonicBlind(3)`).
/// ⚠️ Its reach is drawn on the floor while it builds (`NZNet.BossTelegraph`), which upstream does not; and the daze, a
/// burst, also finds whoever stands behind a wall inside it — the damage does not.
/// • THE GROUND SLAM, one walking swing in three (`walk_attack_ground_v2`, at its `zmb_brute_slam`): everyone up within
/// <see cref="SlamRadius"/>, walls or not, takes x<see cref="SlamWeight"/> of its swing (upstream's 75 against 65), the
/// ground shaking. ⚠️ Upstream's hit every NPC and nextbot round it as well: the horde is spared here.
/// • ITS SWING, 65 against a walker's 50 (the variant's multiplier). ⚠️ Upstream's swings are homing lunges of 530-700 u
/// (root motion, facing you every tick); the port strips the travel and the moving attack closes on you instead.
/// • IT BURSTS WHEN IT DIES — no clip, the body gone at once in a spray of blood (upstream's `nbnz_gib_explosion`) — and
/// every player up gets a perk they lack (upstream's Perk Bottle, the Sawrunner's way). ⚠️ Upstream's death skipped the
/// boss kill's rewards (it never went through `OnKilled`); here it is an ordinary death.
/// • ITS OWN IDLE when nobody is left to chase. ⚠️ Upstream asks for `idle`, which the model lacks: its reference pose.
///
/// ⚠️ HOST AUTHORITY, EVERY MACHINE DRAWS: the knock-down's clips are `PlayShared`/`SwapClip`, the roar's blast is
/// `NZNet.BossBurstFx` (whose daze each machine puts on its own player), the dust `ShockRing.FireShared`, the burst `Died`'s on
/// every machine. ⛔ NO HIT OF ITS DOWNS ANYONE FROM FULL HEALTH (`BossBase.MaxHit`), Oberon's rule — the roar and slam too.
/// </summary>
public sealed class PanzermorderBoss : BossBase
{
protected override string Tag => "[nz-morder]";
/// <summary>
/// ⚠️ ITS EYES WHERE UPSTREAM'S WERE: `EyePos` ~61 off its 72-unit hull (the model has no `j_head`), so it sees what
/// upstream's saw. On a 233-unit body its voice comes from the knees, which no ear can tell at range.
/// </summary>
protected override float VoiceHeight => 64f;
// ══ tuning ═══════════════════════════════════════════════════════════════
//
// ⛔ NULLABLE-BACKED, INSTRUCTIONS.md §1.
static bool? _shockOnly;
/// <summary>Only a hit while it wears the `shock` status spends a life (upstream's `DMG_SHOCK`); off, any player's hit does.</summary>
public static bool ShockOnly { get => _shockOnly ?? false; set => _shockOnly = value; }
static float? _roarChance, _roarGap, _roarMin, _roarRadius, _roarWeight, _daze;
/// <summary>A path time-out rolls a roar at 0.1 (upstream's `math.random(10) == 1`); then 1-20 s before the next can.</summary>
public static float RoarChance { get => _roarChance ?? 0.1f; set => _roarChance = value; }
public static float RoarGapMax { get => _roarGap ?? 20f; set => _roarGap = value; }
/// <summary>It roars only at a target beyond 245, upstream's attack range: a path times out only while you are out of reach.</summary>
public static float RoarMinRange { get => _roarMin ?? 245f; set => _roarMin = value; }
/// <summary>The roar's blast: 300 round it, x0.385 of its swing (upstream's 25 against 65), and a 3 s daze.</summary>
public static float RoarRadius { get => _roarRadius ?? 300f; set => _roarRadius = value; }
public static float RoarWeight { get => _roarWeight ?? 0.385f; set => _roarWeight = value; }
public static float Daze { get => _daze ?? 3f; set => _daze = value; }
static float? _slamRadius, _slamWeight;
/// <summary>The ground slam: 350 round it, walls or not, x1.15 of its swing (upstream's 75 against 65).</summary>
public static float SlamRadius { get => _slamRadius ?? 350f; set => _slamRadius = value; }
public static float SlamWeight { get => _slamWeight ?? 1.15f; set => _slamWeight = value; }
/// <summary>Its lives (upstream's `revives`): three knock-downs, so four bars.</summary>
const int Lives = 3;
// its clips (30 fps) and their events — ⛔ `const`, never `static readonly` (INSTRUCTIONS.md §1)
const string ClipDown = "s2_zom_brt_stun_down_v1", ClipLoop = "s2_zom_brt_stun_loop_v1", ClipUp = "s2_zom_brt_stun_up_v1",
ClipRoar = "s2_zom_brt_roar", ClipSlam = "s2_zom_brt_walk_attack_ground_v2", ClipIdle = "s2_zom_brt_idle",
ClipTraverse = "s2_zom_brt_walk_trav_exit_v1";
/// <summary>
/// ⚠️ EACH CLIP OF THE KNOCK-DOWN IS SWAPPED A FRAME EARLY: a one-shot that runs out wraps to its first frame for a frame
/// (`ZombieAI.TickCorpse`'s stand-up flash).
/// </summary>
const float Wrap = 0.03f;
const float DownLen = 83f / 30f - Wrap, LoopLen = 126f / 30f, UpLen = 153f / 30f - Wrap, DownTotal = DownLen + LoopLen + UpLen,
FallA = 46f / 30f, FallB = 52f / 30f, RiseAt = DownLen + LoopLen + 1f / 30f,
RoarLen = 211f / 30f, RoarBlastAt = 72f / 30f,
SlamLen = 181f / 30f, SlamEvent = 50f / 30f, SlamAt = 50f / 181f;
/// <summary>Its clips' own speeds — `meatflower.zvar`'s tiers are picked by them.</summary>
const float WalkSpeed = 101.1f, RunSpeed = 325.9f;
const float IdleHold = 2f;
/// <summary>
/// ⚠️ A LIFE GOES TO A LATER HIT, NOT THE BLAST THAT PUT IT DOWN: a shotgun's other pellets land in the same frame, and they
/// would spend it at once.
/// </summary>
const float SpendGrace = 0.25f;
static Color Dust => new( 0.7f, 0.62f, 0.5f );
static Color Flesh => new( 0.55f, 0.06f, 0.05f );
static Color Sonic => new( 1f, 0.55f, 0.4f );
// ══ state ════════════════════════════════════════════════════════════════
int _lives = Lives, _downs;
bool _down, _spent, _sprint, _slammed;
float _downAt, _nextThink, _nextRoar, _nextIdle, _clipT;
string _clip;
enum Beat { Fall, Loop, Up, Rise, Stand, RoarBlast, Slam }
public bool Down => _down;
public int LivesLeft => _lives;
/// <summary>One bar: the pool in quarters.</summary>
float Bar => Hp.IsValid() ? Hp.Max / (Lives + 1) : 0f;
/// <summary>The line under the bar it is on — emptied to here, it goes down. 0 on its last bar.</summary>
float Floor => _lives * Bar;
protected override void HostStart()
{
Ai.MaxHitDamage = MaxHit;
// ⛔ NONE OF THE WALKER'S CLIMB CLIPS ON ITS MODEL: its own traversal (upstream's `JumpSequences`)
Ai.ClimbClipOverride = ClipTraverse;
SetSpeed( WalkSpeed );
_nextThink = Time.Now + 1f;
// upstream's arrival: `bo3_panzer_landing`'s dust round it, and the shake (amplitude 5 within 2048)
ShockRing.FireShared( WorldPosition + Vector3.Up * 4f, 200f * Size, Dust );
NZNet.ShakeAt( WorldPosition, 0.5f, 2048f );
Say( $"spawned — {Hp?.Max ?? 0f:0} hp in {Lives + 1} bars of {Bar:0} · {(ShockOnly ? "a shocked hit" : "any player's hit")} while"
+ $" down spends a life · roars beyond {RoarMinRange:0} · slam {SlamRadius:0}u · hitboxes {Ai.HitboxCount}" );
}
protected override void HostFrame()
{
Ai.MaxHitDamage = MaxHit;
if ( Ai.State == ZombieState.Spawning ) return;
// ⚠️ A BAR EMPTIED BY ANYTHING BUT A HIT — a burn's tick, a bleed, a nuke's share: `Health.Apply`, never `ScaleDamage` —
// puts it down too, the pool under the line taking whatever went past it
if ( !_down && _lives > 0 && Hp.IsValid() && Hp.Current <= Floor + 0.5f ) KnockDown();
if ( _down ) return;
TickSlam();
TickRoar();
TickIdle();
}
/// <summary>
/// Its table: nothing lands while it is down — and the first player's hit then spends a life — and the hit that would empty
/// a bar puts it down instead (during its entrance, that hit takes it to the line and no further; it goes down when the
/// entrance is over). THE HOST, once a hit.
/// </summary>
protected override float ScaleDamage( float amount, in DamageInfo damage )
{
if ( _down )
{
if ( !_spent && Time.Now >= _downAt + SpendGrace && FromPlayer( damage )
&& (!ShockOnly || StatusEffects.Has( GameObject, "shock" )) ) Spend();
return 0f;
}
if ( _lives <= 0 || !Hp.IsValid() || amount <= 0f ) return 1f;
var left = Hp.Current - Floor;
if ( amount < left ) return 1f;
if ( Ai.State == ZombieState.Spawning ) return MathF.Max( 0f, left ) / amount;
KnockDown();
return 0f;
}
static bool FromPlayer( in DamageInfo damage )
=> damage.Attacker.IsValid() && damage.Attacker.Components.Get<NZPlayer>( FindMode.EverythingInSelfAndAncestors ).IsValid();
protected override void OnBeat( int beat )
{
switch ( (Beat)beat )
{
case Beat.Fall:
// `bodyfall_large`, twice as it lands: the slam's thud, dust and shake — no damage (upstream's)
NZSound.PlayShared( "nz.margwa.slam.far", WorldPosition );
ShockRing.FireShared( WorldPosition + Vector3.Up * 4f, 220f * Size, Dust );
NZNet.ShakeAt( WorldPosition, 0.8f, 2000f );
break;
case Beat.Loop:
SwapClip( ClipLoop );
break;
case Beat.Up:
SwapClip( ClipUp );
break;
case Beat.Rise:
// `ps s2_zom_brt_stun_up_v1`: touchable again — on the next bar if a life went, else on the same one, whole
_down = false;
if ( Hp.IsValid() )
{
Hp.Invulnerable = false;
Hp.SetCurrent( (_lives + 1) * Bar );
}
Say( _spent ? $"up — bar {Lives + 1 - _lives} of {Lives + 1}" : "up WHOLE — nobody hit it while it lay there" );
break;
case Beat.Stand:
if ( _sprint ) SetSpeed( RunSpeed );
break;
case Beat.RoarBlast:
RoarBlast();
break;
case Beat.Slam:
Slam();
break;
}
}
/// <summary>
/// It bursts: on every machine the body gone at once and blood thrown up and out of it (upstream's `nbnz_gib_explosion`);
/// on the host the gore's sounds, and a perk for every player up (upstream's Perk Bottle — on their own machines, where
/// perks live).
/// </summary>
protected override void Died( bool host )
{
Splatter();
MargwaFx.SetHidden( GameObject, true );
if ( !host ) return;
Ai.CorpseLinger = 0.2f;
NZSound.PlayShared( "nz.morder.gore", Voice );
NZSound.PlayShared( "nz.morder.merge", Voice );
var n = 0;
foreach ( var p in Scene.GetAllComponents<NZPlayer>().ToList() )
{
if ( !IsUp( p ) ) continue;
NZNet.DirectorPerk( p.GameObject.Id );
n++;
}
Say( $"killed — it burst after {_downs} knock-down(s); a perk for each of {n} player(s)" );
}
// ══ the knock-downs ══════════════════════════════════════════════════════
/// <summary>
/// Down: untouchable, on the line under its bar, its three clips one hold (down, lying, up), two thuds as it lands.
/// ⚠️ GOING DOWN WITH ITS LAST LIFE, IT RUNS WHEN IT IS UP (upstream's `SetRunSpeed(230)` at the third down). THE HOST.
/// </summary>
void KnockDown()
{
if ( _down || _lives <= 0 || !Hp.IsValid() ) return;
_down = true;
_spent = false;
_downAt = Time.Now;
_downs++;
Hp.Invulnerable = true;
Hp.SetCurrent( Floor );
ClearBeats();
// ⚠️ MID-ROAR OR MID-IDLE IT IS IN A SPECIAL ALREADY: the clip is swapped and the hold re-armed
if ( Ai.State == ZombieState.Special )
{
SwapClip( ClipDown );
Ai.HoldSpecialFor( DownTotal );
}
else PlayShared( ClipDown, DownTotal );
if ( _lives < 2 ) _sprint = true;
Queue( FallA, (int)Beat.Fall );
Queue( FallB, (int)Beat.Fall );
Queue( DownLen, (int)Beat.Loop );
Queue( DownLen + LoopLen, (int)Beat.Up );
Queue( RiseAt, (int)Beat.Rise );
Queue( DownTotal + 0.1f, (int)Beat.Stand );
NZSound.PlayShared( "nz.morder.merge", Voice );
NZSound.PlayShared( "nz.morder.down", Voice );
Say( $"DOWN ({_downs}) — {_lives} life/lives left; {(ShockOnly ? "a shocked hit" : "a hit")} while it lies there spends one" );
}
/// <summary>A life spent while it lies there: its death voice (upstream's, on the shock), and it gets up on the next bar. THE HOST.</summary>
void Spend()
{
_spent = true;
_lives--;
NZSound.PlayShared( "nz.morder.death", Voice );
BossFx.BurstShared( WorldPosition + Vector3.Up * 30f * Size, 70f * Size, Flesh, 10 );
Say( $"a life spent — {_lives} left{(_lives == 0 ? ": its last bar" : "")}" );
}
// ══ the roar ═════════════════════════════════════════════════════════════
/// <summary>
/// Upstream's `OnPathTimeOut`: while it chases a target out of its reach, each path time-out (`d²/295²` held to 0.5-1 s
/// within 850, `d²/1000²` held to 1.5-15 s beyond) rolls <see cref="RoarChance"/> once the last roar's gap is out. THE HOST.
/// </summary>
void TickRoar()
{
if ( Ai.State != ZombieState.Chasing || Time.Now < _nextThink ) return;
var p = TargetPlayer;
if ( !IsUp( p ) ) { _nextThink = Time.Now + 0.5f; return; }
var d = WorldPosition.Distance( p.WorldPosition );
_nextThink = Time.Now + (d > 850f ? Math.Clamp( d * d / (1000f * 1000f), 1.5f, 15f ) : Math.Clamp( d * d / (295f * 295f), 0.5f, 1f ));
if ( d <= RoarMinRange * Size || Time.Now < _nextRoar || Game.Random.Float( 0f, 1f ) >= RoarChance ) return;
Roar();
}
void Roar()
{
if ( !PlayShared( ClipRoar, RoarLen ) ) return;
// upstream's `NextAction = CurTime() + math.random(20)`, from the roar's start
_nextRoar = Time.Now + Game.Random.Int( 1, Math.Max( 1, (int)RoarGapMax ) );
NZSound.PlayShared( "nz.morder.roar", Voice );
NZNet.BossTelegraph( WorldPosition, RoarRadius * Size, RoarBlastAt, 1f, 0.55f, 0.4f );
Queue( RoarBlastAt, (int)Beat.RoarBlast );
}
/// <summary>
/// `ps s2_zom_brt_roar`: everyone up within <see cref="RoarRadius"/> it can see takes x<see cref="RoarWeight"/> of its swing,
/// and everyone inside is dazed (each machine its own player, `BossFx.Burst`). THE HOST.
/// </summary>
void RoarBlast()
{
var at = WorldPosition + Vector3.Up * 40f * Size;
var radius = RoarRadius * Size;
var damage = SwingDamage( RoarWeight );
foreach ( var p in PlayersWithin( at, radius ).ToList() )
if ( CanSee( p.GameObject ) ) HurtPlayer( p, damage, blast: true, at: at, source: "panzermorder roar" );
BossFx.BurstShared( at, radius, Sonic, 12, Daze );
NZNet.ShakeAt( WorldPosition, 0.35f, 2048f );
}
// ══ the ground slam ══════════════════════════════════════════════════════
/// <summary>
/// The slam lands at `zmb_brute_slam`, 50 of the swing's 181 frames in — read off the clip as it plays, so the swing's
/// speed (`ZombieAI.ScaledAttackSpeed`) carries it with the arm. The same swing twice in a row restarts the clip. THE HOST.
/// </summary>
void TickSlam()
{
var seq = Body.IsValid() ? Body.Sequence : null;
if ( seq is null ) return;
var clip = seq.Name;
var t = seq.TimeNormalized;
if ( clip != _clip || t < _clipT - 0.25f ) _slammed = false;
_clip = clip;
_clipT = t;
if ( _slammed || clip != ClipSlam || Ai.State != ZombieState.Attacking || t < SlamAt ) return;
_slammed = true;
Slam();
}
/// <summary>
/// `zmb_brute_slam`: everyone up within <see cref="SlamRadius"/> — walls or not, upstream's sphere — takes x<see
/// cref="SlamWeight"/> of its swing; the thud heard far, dust round it and where its hand came down, the ground shaking.
/// THE HOST.
/// </summary>
void Slam()
{
var at = WorldPosition;
var radius = SlamRadius * Size;
var damage = SwingDamage( SlamWeight );
foreach ( var p in PlayersWithin( at + Vector3.Up * 40f, radius ).ToList() )
HurtPlayer( p, damage, blast: true, at: at, source: "panzermorder slam" );
NZSound.PlayShared( "nz.margwa.slam.far", at );
ShockRing.FireShared( at + Vector3.Up * 4f, radius, Dust );
if ( Body.IsValid() && Body.TryGetBoneTransform( "j_wrist_ri", out var hand ) )
ShockRing.FireShared( hand.Position.WithZ( at.z + 4f ), 90f * Size, Dust );
NZNet.ShakeAt( at, 0.9f, 2000f );
}
// ══ the rest ═════════════════════════════════════════════════════════════
/// <summary>Its own idle after a second with nobody to chase (Oberon's way). THE HOST.</summary>
void TickIdle()
{
if ( Ai.Target.IsValid() ) { _nextIdle = MathF.Max( _nextIdle, Time.Now + 1f ); return; }
if ( Time.Now < _nextIdle || Ai.State is not (ZombieState.Idle or ZombieState.Chasing) ) return;
_nextIdle = Time.Now + IdleHold + 0.15f;
PlayShared( ClipIdle, IdleHold );
}
/// <summary>
/// The burst, on THIS machine: a ring of blood on the floor and the spray out of its chest.
/// ⚠️ `ZombieAI.GoreBurst` IS PRIVATE TODAY: this asks for it to be made public (notes.md).
/// </summary>
void Splatter()
{
var chest = Body.IsValid() && Body.TryGetBoneTransform( "j_spine4", out var t )
? t.Position
: WorldPosition + Vector3.Up * 150f * Size;
ShockRing.Fire( WorldPosition + Vector3.Up * 4f, 180f * Size, Flesh );
if ( !ZombieAI.GoreEnabled ) return;
ZombieAI.GoreBurst( chest, Vector3.Up, 70, 320f, 1.2f );
ZombieAI.GoreBurst( chest, Vector3.Up, 40, 160f, 1.8f );
ZombieAI.GoreBurst( WorldPosition + Vector3.Up * 60f * Size, Vector3.Up, 30, 200f, 1.5f );
}
// ══ diagnostics ══════════════════════════════════════════════════════════
static IEnumerable<PanzermorderBoss> All
=> Game.ActiveScene?.GetAllComponents<PanzermorderBoss>() ?? Enumerable.Empty<PanzermorderBoss>();
/// <summary>`nz_morder` — every Panzermorder alive, and the tuning.</summary>
[ConCmd( "nz_morder" )]
public static void Report()
{
Log.Info( $"[nz-morder] {Lives + 1} bars, {Lives} lives · {(ShockOnly ? "a hit while SHOCKED" : "any player's hit")} while down spends one"
+ $" · roar beyond {RoarMinRange:0}: {RoarChance * 100f:0.#}% a time-out, then 1-{RoarGapMax:0}s, {RoarRadius:0}u x{RoarWeight:0.###}"
+ $" + {Daze:0.#}s daze · slam {SlamRadius:0}u x{SlamWeight:0.##} · hit cap {MaxHit:0}" );
var list = All.ToList();
Log.Info( $"[nz-morder] {list.Count} alive" );
foreach ( var m in list )
Log.Info( $"[nz-morder] hp {m.Hp?.Current ?? 0f:0}/{m.Hp?.Max ?? 0f:0} · bar {Lives + 1 - m._lives} of {Lives + 1} ({m.Bar:0} each)"
+ $" · lives {m._lives} · state {m.Ai?.State} · speed {m.Ai?.MoveSpeed ?? 0f:0}"
+ $"{(m._down ? (m._spent ? " · DOWN (a life spent)" : " · DOWN") : "")}{(m._sprint ? " · RUNNING" : "")}"
+ $" · next roar in {MathF.Max( 0f, m._nextRoar - Time.Now ):0.#}s · hitboxes {m.Ai?.HitboxCount ?? 0}" );
}
/// <summary>`nz_morder_roar` — every Panzermorder roars now; `nz_morder_slam` — slams now; `nz_morder_down` — its bar emptied now.</summary>
[ConCmd( "nz_morder_roar" )]
public static void RoarCmd()
{
if ( NZGame.IsClient ) { Log.Info( "[nz-morder] the host decides" ); return; }
foreach ( var m in All.Where( m => m.Ai.IsValid() && !m.Dead && !m._down ).ToList() ) m.Roar();
}
[ConCmd( "nz_morder_slam" )]
public static void SlamCmd()
{
if ( NZGame.IsClient ) { Log.Info( "[nz-morder] the host decides" ); return; }
foreach ( var m in All.Where( m => m.Ai.IsValid() && !m.Dead && !m._down ).ToList() )
if ( m.PlayShared( ClipSlam, SlamLen ) ) m.Queue( SlamEvent, (int)Beat.Slam );
}
[ConCmd( "nz_morder_down" )]
public static void DownCmd()
{
if ( NZGame.IsClient ) { Log.Info( "[nz-morder] the host decides" ); return; }
foreach ( var m in All.Where( m => m.Ai.IsValid() && !m.Dead && !m._down ).ToList() )
{
if ( m._lives <= 0 ) { Log.Info( "[nz-morder] on its last bar — no knock-down left: empty it and it bursts" ); continue; }
m.KnockDown();
}
}
/// <summary>`nz_morder_set <key> <value>` — retune it live.</summary>
[ConCmd( "nz_morder_set" )]
public static void SetCmd( string key = "", float value = 0f )
{
switch ( key.ToLowerInvariant() )
{
case "shockonly": ShockOnly = value != 0f; break;
case "roarchance": RoarChance = value; break;
case "roargap": RoarGapMax = value; break;
case "roarrange": RoarMinRange = value; break;
case "roarradius": RoarRadius = value; break;
case "roar": RoarWeight = value; break;
case "daze": Daze = value; break;
case "slamradius": SlamRadius = value; break;
case "slam": SlamWeight = value; break;
default:
Log.Info( "[nz-morder] nz_morder_set <shockonly|roarchance|roargap|roarrange|roarradius|roar|daze|slamradius|slam> <value>" );
return;
}
Log.Info( $"[nz-morder] {key} = {value:0.###}" );
Report();
}
}