A game component implementing the Director boss NPC for a Zombies mode. It handles spawning visuals, rage mechanics, attacks (slam, roar), buffing nearby zombies (speed or electric), damage scaling, state transitions, networking of effects/clips, and console commands for debugging and tuning.
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
namespace NZombies;
/// <summary>
/// The Director — George Romero, Call of the Dead's boss, ported 2026-10-06 from GMod nZombies' `nz_zombie_boss_director`
/// (GhostlyMoo's port). The user picked him with Avogadro, the Astronaut and the Panzer Soldat.
///
/// | | everything |
/// |---|---|
/// | **damage** | **×0.095** (upstream's `OnTakeDamage`) |
///
/// CALM, he walks (30.8 u/s), swings his stage lamp, and taunts. HIT HIM BY A PLAYER and he RAGES:
/// • `nz_boss_enrage_start_coast` (5.9 s) — his body turns zombified, a roar that dazes everyone within 575 for 1 s, and at
/// frame 116 a slam: a heavy hit to anyone within 105, a 3 s daze within 175, and the zombies round him charged.
/// • then he RUNS YOU DOWN at FIVE TIMES his sprint (~985 u/s against your 340; the user, 2026-10-06: *"yes its suposed to be
/// a death sentence, this is not an enemy you should try to kill but avoid instead"*), and calms down
/// (`nz_boss_sprint_to_walk`) the moment he DOWNS SOMEONE (a player going down within <see cref="DownReach"/> of him), or
/// after 30 s. Five seconds later he can rage again. A hit before then makes him stagger (`nz_boss_nuke_react`, once
/// every 7 s) — upstream's only flinch. The 5.9 s it takes him to rage is the time you have to get away.
/// ⚠️ Upstream also ends his rage in water, the Call of the Dead way to beat him; this project's maps have no water to stand in.
/// HE BUFFS THE HORDE with a scream (`nz_boss_enrage_start_scream`), when at least two zombies are within 200:
/// • 3 times in 4 the SPEED buff (every 10 s): the zombies within 175 sprint at the top speed tier;
/// • else the ELECTRIC buff (every 20 s): the zombies within 175 get three times a zombie's health this round (it heals them
/// too, as upstream's does) and electricity crawling over them.
/// HIS LAMP is blue, and orange once he is under half health. HE ARRIVES out of a beam and slams the ground; HE DIES and at
/// frame 131 is gone in a flash (upstream's `boss_remove`) — and his killer gets a free perk (upstream drops a perk bottle;
/// this project has no bottle power-up, so the perk goes straight to whoever landed the kill, through the slot cap).
///
/// ⚠️ HOST AUTHORITY, EVERY MACHINE DRAWS: `NZNet.DirectorBody` / `DirectorCharged` / `DirectorRoar`, and `DirectorFx` /
/// `DirectorLamp` on each machine. His special clips are relayed (`PlayShared`).
/// ⛔ NO ATTACK OF HIS DOWNS ANYONE FROM FULL HEALTH (<see cref="HitCap"/>), Oberon's rule.
/// </summary>
public sealed class DirectorBoss : Component
{
// ══ tuning ═══════════════════════════════════════════════════════════════
//
// ⛔ NULLABLE-BACKED, INSTRUCTIONS.md §1.
static float? _damageScale, _rageSeconds, _rageCooldown, _reactGap, _sprintSpeed, _walkSpeed, _hitCap;
/// <summary>What every hit on him does: ×0.095, upstream's `ScaleDamage(0.095)`.</summary>
public static float DamageScale { get => _damageScale ?? 0.095f; set => _damageScale = value; }
/// <summary>How long a rage lasts when he downs nobody: 30 s (the user's, 2026-10-06; upstream's 45).</summary>
public static float RageSeconds { get => _rageSeconds ?? 30f; set => _rageSeconds = value; }
/// <summary>Seconds after calming before he can rage again: 5, upstream's `NextEnrageTime`.</summary>
public static float RageCooldown { get => _rageCooldown ?? 5f; set => _rageCooldown = value; }
/// <summary>Seconds between staggers: 7, upstream's.</summary>
public static float ReactGap { get => _reactGap ?? 7f; set => _reactGap = value; }
/// <summary>His rage sprint: 197.1 u/s, `nz_boss_sprint_01`'s own. Calm he walks at 30.8 (`FixedSpeed`).</summary>
public static float SprintSpeed { get => _sprintSpeed ?? 197.1f; set => _sprintSpeed = value; }
public static float WalkSpeed { get => _walkSpeed ?? 30.8f; set => _walkSpeed = value; }
static float? _rageSpeedScale, _rageAnimRate, _rageAccel, _downReach;
/// <summary>His rage against his sprint: ×5 — ~985 u/s, the `rage` tier (`nz_boss_sprint_02`, 240.5 u/s of its own).</summary>
public static float RageSpeedScale { get => _rageSpeedScale ?? 5f; set => _rageSpeedScale = value; }
/// <summary>How fast his legs may cycle while raging, over the match's 2.5 (`ZombieAI.MaxAnimRateOverride`): 4.5.</summary>
public static float RageAnimRate { get => _rageAnimRate ?? 4.5f; set => _rageAnimRate = value; }
/// <summary>His acceleration while raging, units a second a second (a zombie's is 900): 5000, so the corners do not fling him.</summary>
public static float RageAccel { get => _rageAccel ?? 5000f; set => _rageAccel = value; }
/// <summary>A player going down this near him while he rages is his doing, and calms him: 200 at size 1 (his swing is 105).</summary>
public static float DownReach { get => _downReach ?? 200f; set => _downReach = value; }
/// <summary>His rage speed now.</summary>
public static float RageSpeed => SprintSpeed * MathF.Max( 0.1f, RageSpeedScale );
/// <summary>The most one hit of his takes, as a share of the match's base health. Two-thirds, Oberon's.</summary>
public static float HitCap { get => _hitCap ?? (2f / 3f); set => _hitCap = value; }
static float? _roarRadius, _roarDaze, _slamRadius, _slamWeight, _slamDazeRadius, _slamDaze;
/// <summary>His roar dazes everyone this near: 575, for 1 s (upstream's `NZSonicBlind(1)`).</summary>
public static float RoarRadius { get => _roarRadius ?? 575f; set => _roarRadius = value; }
public static float RoarDaze { get => _roarDaze ?? 1f; set => _roarDaze = value; }
/// <summary>The rage slam: a hit within 105 (upstream's `DamageRange`), 150 against his 90 swing.</summary>
public static float SlamRadius { get => _slamRadius ?? 105f; set => _slamRadius = value; }
public static float SlamWeight { get => _slamWeight ?? (150f / 90f); set => _slamWeight = value; }
/// <summary>…and a 3 s daze within 175, upstream's `NZSonicBlind(3)`.</summary>
public static float SlamDazeRadius { get => _slamDazeRadius ?? 175f; set => _slamDazeRadius = value; }
public static float SlamDaze { get => _slamDaze ?? 3f; set => _slamDaze = value; }
static float? _speedBuffGap, _elecBuffGap, _buffQualify, _buffReach, _elecHealth;
/// <summary>Seconds between speed buffs: 10; electric: 20 — upstream's.</summary>
public static float SpeedBuffGap { get => _speedBuffGap ?? 10f; set => _speedBuffGap = value; }
public static float ElecBuffGap { get => _elecBuffGap ?? 20f; set => _elecBuffGap = value; }
/// <summary>Zombies this near count toward a buff (two needed): 200; the buff reaches 175 — upstream's.</summary>
public static float BuffQualify { get => _buffQualify ?? 200f; set => _buffQualify = value; }
public static float BuffReach { get => _buffReach ?? 175f; set => _buffReach = value; }
/// <summary>A charged zombie's health, in zombies of this round: 3, upstream's `GetZombieHealth() * 3`.</summary>
public static float ElecHealth { get => _elecHealth ?? 3f; set => _elecHealth = value; }
public static float MaxHit => MathF.Max( 1f, Difficulty.MaxHealth * Math.Clamp( HitCap, 0.1f, 0.95f ) );
// clip lengths, frames at 30
const float RageClip = 176f / 30f;
const float CalmClip = 53f / 30f;
const float ReactClip = 115f / 30f;
const float ScreamClip = 51f / 30f;
const float RemoveAt = 131f / 30f;
/// <summary>Hidden while the beam comes down, shown just before his entrance clip's slam (frame 39).</summary>
const float RevealAt = 1.0f;
// ══ state ════════════════════════════════════════════════════════════════
ZombieAI _ai;
Health _hp;
SkinnedModelRenderer _body;
DirectorLamp _lamp;
bool _raging, _hitPending, _shielded, _deadHandled, _revealed, _removed, _rewarded;
/// <summary>Who was down last frame (`IsDown` on the host's own player, `DownedNet` on a client's) — a NEW down near him calms him.</summary>
readonly Dictionary<NZPlayer, bool> _wasDown = new();
/// <summary>He downed someone this rage; he calms the moment his current move lets him.</summary>
bool _gotOne;
float _rageUntil, _nextRage, _nextReact, _nextSpeedBuff, _nextElecBuff, _nextThink, _born, _deathAt, _nextBehind;
bool _start0, _start2;
enum Beat { Roar, Fall, Slam, React, Boom, Weak, Yell, SpeedYell, ElecYell, EntranceSlam }
readonly List<(float At, Beat What)> _pending = new();
public bool Raging => _raging;
Vector3 Facing => _ai.IsValid() ? (WorldRotation * _ai.ModelTurn.Inverse).Forward.WithZ( 0 ).Normal : WorldRotation.Forward;
float Size => MathF.Max( 0.1f, WorldScale.x );
Vector3 Voice => WorldPosition + Vector3.Up * 66f * Size;
protected override void OnStart()
{
_ai = Components.Get<ZombieAI>( FindMode.EverythingInSelfAndAncestors );
_hp = Components.Get<Health>( FindMode.EverythingInSelfAndAncestors );
_body = Components.Get<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants );
_lamp = Components.GetOrCreate<DirectorLamp>();
_born = Time.Now;
// ⚠️ EVERY MACHINE: hidden while the summoning beam comes down (upstream's `NoDraw` for 1.5 s), each machine its own copy
MargwaFx.SetHidden( GameObject, true );
DirectorFx.Beam( WorldPosition );
if ( NZGame.IsClient || !_ai.IsValid() ) return;
_nextRage = _born + 7f; // upstream's `NextEnrageTime = CurTime() + 7`
_nextReact = _born + 5f;
_nextSpeedBuff = _born + 2f;
_nextElecBuff = _born + 7f;
_ai.MaxHitDamage = MaxHit;
// ⛔ NO CLIMB CLIP OF THE WALKER'S ON HIS MODEL (Oberon's note): his jump-slam stands in, in place
_ai.ClimbClipOverride = "nz_boss_slamjump";
Log.Info( $"[nz-director] spawned — {_hp?.Max ?? 0f:0} hp at x{DamageScale:0.###} · rage {RageSeconds:0}s at {SprintSpeed:0} u/s"
+ $" · hitboxes {_ai.HitboxCount}" );
}
protected override void OnUpdate()
{
if ( !_ai.IsValid() ) return;
TickReveal();
if ( _ai.State == ZombieState.Dead )
{
OnDeath();
TickRemove();
return;
}
if ( _ai.IsPuppet ) return;
_ai.MaxHitDamage = MaxHit;
TickShield();
TickPending();
TickHit();
TickDowns();
TickRage();
TickBuffs();
TickVoice();
}
// ══ every machine ════════════════════════════════════════════════════════
/// <summary>Out of the beam, on every machine, just before his entrance slams the ground.</summary>
void TickReveal()
{
if ( _revealed || Time.Now < _born + RevealAt ) return;
_revealed = true;
MargwaFx.SetHidden( GameObject, false );
AvogadroFx.Flash( WorldPosition + Vector3.Up * 50f * Size, 1.4f );
if ( NZGame.IsClient ) return;
// upstream's reveal: the zap and the rubble, the electric water, the beam
NZSound.PlayShared( "nz.director.zap", WorldPosition );
NZSound.PlayShared( "nz.director.electricwater", WorldPosition );
NZSound.PlayShared( "nz.director.beam", WorldPosition );
Queue( 39f / 30f - RevealAt, Beat.EntranceSlam );
}
/// <summary>Dead, on every machine: the lamp stays lit until he goes; the host owes his killer a perk.</summary>
void OnDeath()
{
if ( _deadHandled ) return;
_deadHandled = true;
_deathAt = Time.Now;
_pending.Clear();
if ( _ai.IsPuppet ) return;
_ai.CorpseLinger = 0.2f;
Reward();
}
/// <summary>Gone in a flash at frame 131 of his death, upstream's `boss_remove` — on every machine.</summary>
void TickRemove()
{
if ( _removed || Time.Now < _deathAt + RemoveAt ) return;
_removed = true;
AvogadroFx.Flash( WorldPosition + Vector3.Up * 40f * Size, 1.4f );
MargwaFx.SetHidden( GameObject, true );
if ( _lamp.IsValid() ) _lamp.TurnOff();
if ( !NZGame.IsClient ) NZSound.PlayShared( "nz.director.zap", WorldPosition );
}
// ══ the host ═════════════════════════════════════════════════════════════
/// <summary>Untouchable while he arrives (upstream's 1.5 s, here his entrance).</summary>
void TickShield()
{
var want = _ai.State == ZombieState.Spawning;
if ( want == _shielded || !_hp.IsValid() ) return;
_shielded = want;
_hp.Invulnerable = want;
}
/// <summary>A player's hit asked for a rage or a stagger (`ScaleFor`); decided here, outside the damage call.</summary>
void TickHit()
{
if ( !_hitPending ) return;
_hitPending = false;
if ( _raging || _ai.State is not (ZombieState.Chasing or ZombieState.Attacking) ) return;
var now = Time.Now;
if ( now >= _nextRage ) StartRage();
else if ( now >= _nextReact ) React();
}
void TickRage()
{
if ( !_raging || (!_gotOne && Time.Now < _rageUntil) ) return;
if ( _ai.State is not (ZombieState.Chasing or ZombieState.Attacking) ) return;
CalmDown();
}
/// <summary>
/// A player going down near him while he rages: his kill, and the end of his rage (the user: *"when it kills a player it calms
/// down"*). ⚠️ A CLIENT'S DOWN REACHES THE HOST AS `DownedNet` (its owner publishes it); `IsDown` on the host's copy of a client
/// is always false. THE HOST.
/// </summary>
void TickDowns()
{
foreach ( var p in Scene.GetAllComponents<NZPlayer>() )
{
if ( !p.IsValid() ) continue;
var down = p.IsDown || p.DownedNet;
var was = _wasDown.TryGetValue( p, out var w ) && w;
_wasDown[p] = down;
if ( !down || was || !_raging || _gotOne ) continue;
if ( p.WorldPosition.Distance( WorldPosition ) > DownReach * Size ) continue;
_gotOne = true;
Log.Info( $"[nz-director] he DOWNED {p.GameObject.Name} — calming down" );
}
}
/// <summary>The rage: the zombified body, the roar, the slam, then the sprint for 45 s. THE HOST.</summary>
void StartRage()
{
if ( !PlayShared( "nz_boss_enrage_start_coast", RageClip ) ) return;
_raging = true;
_gotOne = false;
_rageUntil = Time.Now + RageClip + RageSeconds;
NZNet.DirectorBody( GameObject.Id, 1 );
_ai.SpeedOverride = MathF.Max( 1f, RageSpeed * Size );
_ai.RepickAnimations();
SetAccel( RageAccel );
Queue( 2f / 30f, Beat.Roar );
Queue( 107f / 30f, Beat.Fall );
Queue( 116f / 30f, Beat.Slam );
Queue( 150f / 30f, Beat.React );
Log.Info( $"[nz-director] RAGE — {RageSeconds:0}s at {_ai.SpeedOverride:0} u/s" );
}
/// <summary>Calm again: `nz_boss_sprint_to_walk`, the calm body, the walk; a rage again in 5 s. THE HOST.</summary>
void CalmDown()
{
_raging = false;
_gotOne = false;
_nextRage = Time.Now + RageCooldown;
NZNet.DirectorBody( GameObject.Id, 0 );
_ai.SpeedOverride = MathF.Max( 1f, WalkSpeed * Size );
_ai.RepickAnimations();
SetAccel( _ai.Acceleration );
if ( PlayShared( "nz_boss_sprint_to_walk", CalmClip ) )
{
Queue( 4f / 30f, Beat.Boom );
Queue( 41f / 30f, Beat.Weak );
}
Log.Info( "[nz-director] calm" );
}
/// <summary>His nav agent's acceleration (the rage's, or back to the zombie's own). THE HOST.</summary>
void SetAccel( float accel )
{
var agent = Components.Get<NavMeshAgent>( FindMode.EverythingInSelfAndDescendants );
if ( agent.IsValid() ) agent.Acceleration = MathF.Max( 100f, accel );
}
/// <summary>A stagger — upstream's only flinch, for a hit while he cannot rage yet. THE HOST.</summary>
void React()
{
_nextReact = Time.Now + ReactGap;
if ( PlayShared( "nz_boss_nuke_react", ReactClip ) ) Queue( 88f / 30f, Beat.Yell );
}
void TickBuffs()
{
if ( _ai.State != ZombieState.Chasing || Time.Now < _nextThink ) return;
_nextThink = Time.Now + 0.5f;
var now = Time.Now;
// upstream's roll: 3 in 4 the speed buff, else the electric one
if ( Game.Random.Float( 0f, 1f ) < 0.75f )
{
if ( now >= _nextSpeedBuff && Near( BuffQualify, z => z.SpeedRating > 70f && z.SpeedRating < 200f ).Count >= 2 )
{
_nextSpeedBuff = now + SpeedBuffGap;
if ( PlayShared( "nz_boss_enrage_start_scream", ScreamClip ) ) Queue( 2f / 30f, Beat.SpeedYell );
}
}
else if ( now >= _nextElecBuff && Near( BuffQualify, z => !Charged( z ) ).Count >= 2 )
{
_nextElecBuff = now + ElecBuffGap;
if ( PlayShared( "nz_boss_enrage_start_scream", ScreamClip ) ) Queue( 2f / 30f, Beat.ElecYell );
}
}
/// <summary>Ordinary zombies within <paramref name="radius"/> passing <paramref name="test"/> — never a boss.</summary>
List<ZombieAI> Near( float radius, Func<ZombieAI, bool> test )
{
var r = radius * Size;
var list = new List<ZombieAI>();
foreach ( var z in Scene.GetAllComponents<ZombieAI>() )
{
if ( !z.IsValid() || z == _ai || z.State is ZombieState.Dead or ZombieState.Spawning ) continue;
if ( (z.Variant?.IsBoss ?? false) || z.WorldPosition.Distance( WorldPosition ) > r ) continue;
if ( test( z ) ) list.Add( z );
}
return list;
}
static bool Charged( ZombieAI z ) => z.Components.Get<DirectorCharge>( FindMode.EverythingInSelf ).IsValid();
/// <summary>The speed buff: everyone within reach sprints at the top tier (upstream's `SetRunSpeed(300)`). THE HOST.</summary>
void SpeedBuff()
{
var n = 0;
foreach ( var z in Near( BuffReach, _ => true ) )
if ( z.RaiseSpeedRating( 300f ) ) n++;
Log.Info( $"[nz-director] SPEED BUFF — {n} zombie(s) sprinting" );
}
/// <summary>
/// The electric buff: everyone within reach not yet charged gets <see cref="ElecHealth"/> zombies' health of this round,
/// and electricity on every machine (upstream's `SetHealth(GetZombieHealth() * 3)` + `SetWaterBuff`). THE HOST.
/// </summary>
void ElectricBuff( float reach )
{
int round = Math.Max( 1, RoundManager.Instance?.Round ?? 1 );
var health = MathF.Max( 1f, ZombieStats.HealthForRound( round ) * ElecHealth * Difficulty.ZombieHealth );
var n = 0;
foreach ( var z in Near( reach, z => !Charged( z ) ) )
{
var hp = z.Components.Get<Health>( FindMode.EverythingInSelfAndAncestors );
if ( !hp.IsValid() || hp.IsDead ) continue;
hp.Reset( MathF.Max( health, hp.Max ) );
z.Components.GetOrCreate<DirectorCharge>();
NZNet.DirectorCharged( z.GameObject.Id );
n++;
}
Log.Info( $"[nz-director] ELECTRIC BUFF — {n} zombie(s) charged to {health:0} hp" );
}
/// <summary>
/// His lines that the AI's passive voice does not cover: the entrance (upstream's `start_0` at 7 s, `start_2` at 10 s), and
/// "behind you" when his target is close and looking away (upstream's first priority, within 300).
/// </summary>
void TickVoice()
{
var now = Time.Now;
if ( !_start0 && now >= _born + 7f ) { _start0 = true; NZSound.PlayShared( "nz.director.start", Voice ); return; }
if ( !_start2 && now >= _born + 10f ) { _start2 = true; NZSound.PlayShared( "nz.director.start", Voice ); return; }
if ( now < _nextBehind || _ai.State != ZombieState.Chasing ) return;
var p = _ai.Target.IsValid() ? _ai.Target.Components.Get<NZPlayer>( FindMode.EverythingInSelfAndAncestors ) : null;
if ( !p.IsValid() || (p.IsDown || p.DownedNet) || p.WorldPosition.Distance( WorldPosition ) > 300f * Size ) return;
var pc = p.Components.Get<PlayerController>( FindMode.EverythingInSelfAndDescendants );
var look = (pc.IsValid() ? pc.EyeAngles.ToRotation() : p.WorldRotation).Forward.WithZ( 0 ).Normal;
var toHim = (WorldPosition - p.WorldPosition).WithZ( 0 ).Normal;
if ( look.Dot( toHim ) > 0f ) return; // looking at him
_nextBehind = now + Game.Random.Float( 8f, 15f );
NZSound.PlayShared( "nz.director.behind", Voice );
}
void Queue( float after, Beat what ) => _pending.Add( (Time.Now + after, what) );
void TickPending()
{
for ( int i = _pending.Count - 1; i >= 0; i-- )
{
if ( i >= _pending.Count ) continue;
var p = _pending[i];
if ( Time.Now < p.At ) continue;
_pending.RemoveAt( i );
Run( p.What );
}
}
void Run( Beat what )
{
switch ( what )
{
case Beat.Roar:
// upstream's `boss_vox_enrage`: the biggest shake it has, the angered vox, everyone within 575 dazed 1 s
NZSound.PlayShared( "nz.director.angered", Voice );
NZNet.DirectorRoar( WorldPosition, RoarRadius * Size, RoarDaze, 0.8f );
break;
case Beat.Fall:
NZSound.PlayShared( "nz.director.rubble", WorldPosition );
break;
case Beat.Slam:
Slam( true );
break;
case Beat.EntranceSlam:
Slam( false );
break;
case Beat.React:
NZSound.PlayShared( "nz.director.react", Voice );
break;
case Beat.Boom:
NZSound.PlayShared( "nz.director.beam", WorldPosition );
AvogadroFx.Flash( WorldPosition + Vector3.Up * 50f * Size, 1f );
break;
case Beat.Weak:
NZSound.PlayShared( "nz.director.weak", Voice );
break;
case Beat.Yell:
// upstream's `boss_vox_yell` with no buff pending: the pain yell, the shake, everyone within 575 dazed 1 s
NZSound.PlayShared( "nz.director.painyell", Voice );
NZNet.DirectorRoar( WorldPosition, RoarRadius * Size, RoarDaze, 0.8f );
break;
case Beat.SpeedYell:
NZSound.PlayShared( "nz.director.speedbuff", Voice );
SpeedBuff();
break;
case Beat.ElecYell:
NZSound.PlayShared( "nz.director.elecbuff", Voice );
ElectricBuff( BuffReach );
break;
}
}
/// <summary>
/// His slam, upstream's `boss_slam`: the zap and the rubble, a ring and a shake; in a rage also the heavy hit within 105, the
/// 3 s daze within 175 and the zombies within 100 charged. His entrance slams for show. THE HOST.
/// </summary>
void Slam( bool hurts )
{
var at = WorldPosition + Facing * 60f * Size;
NZSound.PlayShared( "nz.director.zap", at );
NZSound.PlayShared( "nz.director.rubble", at );
ShockRing.FireShared( at, 220f * Size, DirectorFx.LampBlue );
NZNet.ShakeAt( at, 0.6f, 1000f );
if ( !hurts ) return;
NZNet.DirectorRoar( WorldPosition, SlamDazeRadius * Size, SlamDaze, 0f );
int round = Math.Max( 1, RoundManager.Instance?.Round ?? 1 );
var swing = ZombieStats.AttackDamageForRound( round ) * (_ai.Variant?.DamageMultiplier ?? 1.8f) * Difficulty.ZombieDamage;
var damage = MathF.Min( swing * SlamWeight, MaxHit );
foreach ( var p in Scene.GetAllComponents<NZPlayer>().ToList() )
{
if ( !p.IsValid() || (p.IsDown || p.DownedNet) ) continue;
var hp = p.Components.Get<Health>( FindMode.EverythingInSelfAndDescendants );
if ( !hp.IsValid() || hp.IsDead ) continue;
if ( p.WorldPosition.Distance( WorldPosition ) > SlamRadius * Size ) continue;
hp.Apply( damage, false, GameObject, blast: true, blastAt: at );
}
ElectricBuff( 100f );
}
/// <summary>
/// His killer gets a perk they do not have yet, at random, through the slot cap — upstream's perk bottle, which this
/// project has no power-up for. Nobody to credit (a nuke): the nearest player. THE HOST picks; that player's machine
/// grants it (`DirectorReward.Grant`).
/// </summary>
void Reward()
{
if ( _rewarded ) return;
_rewarded = true;
var killer = _hp.IsValid() && _hp.LastAttacker.IsValid()
? _hp.LastAttacker.Components.Get<NZPlayer>( FindMode.EverythingInSelfAndAncestors )
: null;
var who = killer.IsValid()
? killer
: Scene.GetAllComponents<NZPlayer>().Where( p => p.IsValid() && !(p.IsDown || p.DownedNet) )
.OrderBy( p => p.WorldPosition.Distance( WorldPosition ) ).FirstOrDefault();
if ( !who.IsValid() ) { Log.Info( "[nz-director] dead — nobody to take his perk" ); return; }
// ⛔ ON THE KILLER'S OWN MACHINE (2026-10-06 fix): perks are bought, kept and lost there and synced to no one, so only it
// knows its slots and what it owns — granted here, a client's perk landed in a copy nobody reads
NZNet.DirectorPerk( who.GameObject.Id );
Log.Info( $"[nz-director] dead — his perk goes to {who.GameObject.Name}" );
}
// ── clips ──
/// <summary>`ZombieAI.PlaySpecial`, and the same clip on every watching machine — `PlaySpecial` does not relay.</summary>
bool PlayShared( string clip, float seconds )
{
if ( !_ai.PlaySpecial( clip, seconds ) ) return false;
if ( Networking.IsActive ) NZNet.ZombieClip( GameObject.Id, clip, 1f );
return true;
}
// ══ the damage table ═════════════════════════════════════════════════════
/// <summary>
/// The multiplier for one hit: ×0.095. A player's hit also asks for his rage or a stagger (decided in `TickHit`). THE HOST.
/// </summary>
public float ScaleFor( GameObject attacker )
{
if ( _deadHandled ) return 1f;
if ( _shielded ) return 0f;
if ( attacker.IsValid() && attacker.Components.Get<NZPlayer>( FindMode.EverythingInSelfAndAncestors ).IsValid() )
_hitPending = true;
return MathF.Max( 0f, DamageScale );
}
/// <summary>The scale for a hit on this object: the Director's, or 1 when it is not him (`Health.OnDamage`).</summary>
public static float ScaleOn( GameObject victim, GameObject attacker )
{
if ( !victim.IsValid() ) return 1f;
var d = victim.Components.Get<DirectorBoss>( FindMode.EverythingInSelfAndAncestors );
return d.IsValid() ? d.ScaleFor( attacker ) : 1f;
}
// ══ diagnostics ══════════════════════════════════════════════════════════
static IEnumerable<DirectorBoss> All
=> Game.ActiveScene?.GetAllComponents<DirectorBoss>() ?? Enumerable.Empty<DirectorBoss>();
/// <summary>`nz_director` — every Director alive.</summary>
[ConCmd( "nz_director" )]
public static void Report()
{
Log.Info( $"[nz-director] x{DamageScale:0.###} · rage {RageSeconds:0}s (or until he downs someone within {DownReach:0}u)"
+ $" at {RageSpeed:0} u/s (x{RageSpeedScale:0.#} his sprint), again after {RageCooldown:0}s"
+ $" · roar {RoarRadius:0}u/{RoarDaze:0.#}s · slam {SlamRadius:0}u x{SlamWeight:0.##}, daze {SlamDazeRadius:0}u/{SlamDaze:0.#}s"
+ $" · buffs: speed every {SpeedBuffGap:0}s, electric every {ElecBuffGap:0}s (x{ElecHealth:0.#} hp) · hit cap {MaxHit:0}" );
var list = All.ToList();
Log.Info( $"[nz-director] {list.Count} alive" );
foreach ( var d in list )
Log.Info( $"[nz-director] hp {d._hp?.Current ?? 0f:0}/{d._hp?.Max ?? 0f:0} · state {d._ai?.State} · speed {d._ai?.MoveSpeed ?? 0f:0}"
+ $" u/s{(d._raging ? $" · RAGING {MathF.Max( 0f, d._rageUntil - Time.Now ):0}s left" : "")} · hitboxes {d._ai?.HitboxCount ?? 0}" );
}
/// <summary>`nz_director_rage` — every Director rages now.</summary>
[ConCmd( "nz_director_rage" )]
public static void RageCmd()
{
if ( NZGame.IsClient ) { Log.Info( "[nz-director] the host decides" ); return; }
foreach ( var d in All.Where( d => d._ai.IsValid() && !d._raging ).ToList() ) d.StartRage();
}
/// <summary>`nz_director_calm` — every Director calms down now.</summary>
[ConCmd( "nz_director_calm" )]
public static void CalmCmd()
{
if ( NZGame.IsClient ) { Log.Info( "[nz-director] the host decides" ); return; }
foreach ( var d in All.Where( d => d._ai.IsValid() && d._raging ).ToList() ) d.CalmDown();
}
/// <summary>`nz_director_buff <speed|electric>` — every Director screams that buff now, at whoever is in reach.</summary>
[ConCmd( "nz_director_buff" )]
public static void BuffCmd( string kind = "speed" )
{
if ( NZGame.IsClient ) { Log.Info( "[nz-director] the host decides" ); return; }
var electric = kind.StartsWith( "e", StringComparison.OrdinalIgnoreCase );
foreach ( var d in All.Where( d => d._ai.IsValid() ).ToList() )
if ( d.PlayShared( "nz_boss_enrage_start_scream", ScreamClip ) )
d.Queue( 2f / 30f, electric ? Beat.ElecYell : Beat.SpeedYell );
}
/// <summary>`nz_director_set <key> <value>` — retune him live.</summary>
[ConCmd( "nz_director_set" )]
public static void SetCmd( string key = "", float value = 0f )
{
switch ( key.ToLowerInvariant() )
{
case "damage": DamageScale = value; break;
case "rage": RageSeconds = value; break;
case "cooldown": RageCooldown = value; break;
case "react": ReactGap = value; break;
case "sprint": SprintSpeed = value; break;
case "ragespeed": RageSpeedScale = value; break;
case "rageanim": RageAnimRate = value; break;
case "rageaccel": RageAccel = value; break;
case "downreach": DownReach = value; break;
case "roar": RoarRadius = value; break;
case "roardaze": RoarDaze = value; break;
case "slam": SlamWeight = value; break;
case "slamdaze": SlamDaze = value; break;
case "speedgap": SpeedBuffGap = value; break;
case "elecgap": ElecBuffGap = value; break;
case "elechp": ElecHealth = value; break;
case "hitcap": HitCap = value; break;
default:
Log.Info( "[nz-director] nz_director_set <damage|rage|cooldown|react|sprint|ragespeed|rageanim|rageaccel|downreach|roar"
+ "|roardaze|slam|slamdaze|speedgap|elecgap"
+ "|elechp|hitcap> <value>" );
return;
}
Log.Info( $"[nz-director] {key} = {value:0.###}" );
Report();
}
}