A Boss component for the Megaton enemy and its two halves (Blaster and Bomber). Implements spawn effects, movement pacing, enraging, ranged attacks (acid shots and bombs with gas clouds), pain reactions, knockback of nearby walkers, death split into two halves, and console commands for diagnostics and live tuning.
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
namespace NZombies;
/// <summary>
/// The Megaton — BOCW's radioactive brute — and the two halves he splits into, ported 2026-10-07 from GMod nZombies'
/// `nz_zombie_boss_steiner`, `…_steiner_right` (the Megaton Blaster) and `…_steiner_left` (the Megaton Bomber), GhostlyMoo's
/// ports, the third batch. Spec: `Sbox nzombies/Docs/boss_specs/megaton.md`. ONE COMPONENT FOR ALL THREE, told apart by the
/// model (`megaton.vmdl`, `megaton_blaster.vmdl`, `megaton_bomber.vmdl`): upstream's halves are line-for-line diffs of him.
///
/// • HE ARRIVES in a purple implosion (upstream's `panzer_spawn_tp`, the Panzerhund's reading), his music to everyone and a
/// blast heard far, and stands two seconds (`TimeOut(2)`).
/// • HE WALKS (92.1 u/s) — a half walks (94.9 / 74.0) while its target is past 250 and sprints (249.1 / 233.0) inside it —
/// and UNDER <see cref="EnrageAt"/> OF ITS HEALTH each sprints for good (his 255.5). Upstream's own rules.
/// • THE BLAST (his and the Blaster's), at a target in sight <see cref="BlastMin"/>-<see cref="RangedMax"/> away, every
/// <see cref="BlastGapMin"/>-<see cref="BlastGapMax"/> s (the first 6 s after it arrives): three acid shots from the mouth
/// (`MegatonShot`: 1750 u/s, straight) at the target's feet + 25-65, led by 0.35 s of its pace — each a 50-unit blast,
/// <see cref="ShotDamage"/> at the middle (upstream's number, as a share of this one's swing) to nothing at the rim, a
/// <see cref="Daze"/> s daze and a fireball (upstream's `util.Effect("Explosion")`).
/// • THE BOMB (his and the Bomber's), at <see cref="BombMin"/>-<see cref="RangedMax"/>, every <see cref="BombGapMin"/>-<see
/// cref="BombGapMax"/> s (the first 12 s after): one from the left hand at the feet + 10, led 0.3 s — the same blast, then
/// its GAS CLOUD (`MegatonGas`: 8 s and 120 wide, <see cref="GasTick"/> every 0.25 s but never the last point of health,
/// after a burst of <see cref="GasBurst"/> 150 wide as it forms). ⚠️ THE BOMBER'S HITS HARDER (<see cref="BomberBombHit"/>,
/// <see cref="BomberGasTick"/>, <see cref="BomberGasBurst"/>: the user's review, 2026-10-07); his keeps upstream's.
/// • ITS SWING (the variant's attacks): his 75, a half's 50, against a walker's 50.
/// • A HIT ON THE HEAD has a <see cref="PainChance"/> chance to hurt it into a pain clip (upstream's 25% on `HITGROUP_HEAD`).
/// • ON THE MOVE HE KNOCKS WALKERS ASIDE within <see cref="KnockRange"/> (a half only while it sprints): Shockwave's shove.
/// • HE TAKES x<see cref="MegatonResist"/> FROM EVERYTHING; a half x<see cref="HalfResist"/>.
/// • HE DIES BY SPLITTING: at frame 6 of his death his chest burns blue, at frame 100 he bursts and is gone, and the Blaster
/// and the Bomber stand where he stood, each tearing itself free (`com_pain_split_01`, 5.93 s) and stepping 62 units to
/// its own side (its clip's root motion, which the port strips, replayed). A half a boss round sends alone does the same.
/// Both are bosses (`IsBoss`), as he is: the round neither waits for them nor counts their kills (`RoundManager`).
///
/// ⚠️ ONE ATTACK AT A TIME: upstream's `AI()` could queue the bomb AND the blast in one call (`TempBehaveThread`) and fire them
/// back to back; here the first one ready goes, and both timers re-roll as it starts, as upstream's own threads do.
/// ⚠️ THE BLASTER'S FIRE VOX ON ITS OWN EVENT (frame 40): upstream listened for the Megaton's and never played it.
/// ⚠️ PAIN BY THE BULLET'S OWN HITBOX (upstream's head test traced from the hit point to twice it: near random), and
/// <see cref="PainGap"/> s of rest after a pain clip — sustained head fire would otherwise hold it in one.
/// ⚠️ LEFT OUT: the melee shove (no hook on a landed swing, and `ShoveGuard` caps a zombie's throw at 560), the Nuke's stun (a
/// boss takes its variant's `NukeDamageFraction` here), the game-over summon, the mantles' jump and land vox, the tear's
/// blood, and `NZNovaGas` (nothing in the addon reads it).
///
/// ⚠️ HOST AUTHORITY, EVERY MACHINE DRAWS: the shots are network-spawned `MegatonShot`s, the cloud's look `NZNet.MargwaPortal`,
/// the bursts `NZNet.BossBurstFx`; the split's glow and blast run on each machine off its own copy of the death (`DeadFrame`).
/// ⛔ NO HIT OF THEIRS DOWNS ANYONE FROM FULL HEALTH (`BossBase.MaxHit`), Oberon's rule — the shots and the cloud included.
/// </summary>
public sealed class MegatonBoss : BossBase
{
protected override string Tag => "[nz-megaton]";
protected override float VoiceHeight => _kind == Kind.Megaton ? 80f : 76f;
// ══ tuning ═══════════════════════════════════════════════════════════════
//
// ⛔ NULLABLE-BACKED, INSTRUCTIONS.md §1.
static float? _blastMin, _bombMin, _rangedMax, _blastGapMin, _blastGapMax, _bombGapMin, _bombGapMax;
/// <summary>The blast at a target in sight 250-750 away, the bomb at 350-750 (upstream's `TargetInRange`, feet to feet).</summary>
public static float BlastMin { get => _blastMin ?? 250f; set => _blastMin = value; }
public static float BombMin { get => _bombMin ?? 350f; set => _bombMin = value; }
public static float RangedMax { get => _rangedMax ?? 750f; set => _rangedMax = value; }
/// <summary>The gaps: the blast 8.24-19.54 s, the bomb 12.71-29.13 s — both re-rolled as either starts (upstream's).</summary>
public static float BlastGapMin { get => _blastGapMin ?? 8.24f; set => _blastGapMin = value; }
public static float BlastGapMax { get => _blastGapMax ?? 19.54f; set => _blastGapMax = value; }
public static float BombGapMin { get => _bombGapMin ?? 12.71f; set => _bombGapMin = value; }
public static float BombGapMax { get => _bombGapMax ?? 29.13f; set => _bombGapMax = value; }
static float? _shot, _gasTick, _gasBurst, _daze;
/// <summary>
/// A shot or the bomb at its middle (50), a tick of the cloud (1, every 0.25 s) and its burst as it forms (15) — UPSTREAM'S
/// NUMBERS, each turned into a weight of this one's swing (<see cref="Weighted"/>: ÷ 75 on him, ÷ 50 on a half), so they
/// grow with the round, the variant and the difficulty as the swing does.
/// </summary>
public static float ShotDamage { get => _shot ?? 50f; set => _shot = value; }
public static float GasTick { get => _gasTick ?? 1f; set => _gasTick = value; }
public static float GasBurst { get => _gasBurst ?? 15f; set => _gasBurst = value; }
/// <summary>How long a shot or the bomb dazes whoever is in its blast: 0.75 s (upstream's `NZSonicBlind(0.75)`).</summary>
public static float Daze { get => _daze ?? 0.75f; set => _daze = value; }
static float? _bomberBomb, _bomberGasTick, _bomberGasBurst;
/// <summary>
/// THE BOMBER'S OWN, RAISED OVER HIS (2026-10-07). The user: *"megaton bomber's bomb should deal more damage, and the damage
/// field should deal far more damage per tick"*. Its bomb at the middle 100 (his 50), its cloud's burst 30 (his 15) and a tick 5
/// (his 1: 20 a second at round 12, not 4). The same weights of its swing as his, and the same caps: `BossBase.MaxHit` a hit,
/// and the cloud never takes the last point. ⚠️ THE BOMBER'S ALONE: the Megaton's own bomb keeps upstream's numbers (the user,
/// the same review: *"megaton is good"*).
/// </summary>
public static float BomberBombHit { get => _bomberBomb ?? 100f; set => _bomberBomb = value; }
public static float BomberGasTick { get => _bomberGasTick ?? 5f; set => _bomberGasTick = value; }
public static float BomberGasBurst { get => _bomberGasBurst ?? 30f; set => _bomberGasBurst = value; }
static float? _resist, _halfResist, _enrageAt, _pain, _painGap, _knock;
/// <summary>What he takes from anything: 0.15; a half 0.2 (upstream's `ScaleDamage` in `OnInjured`).</summary>
public static float MegatonResist { get => _resist ?? 0.15f; set => _resist = value; }
public static float HalfResist { get => _halfResist ?? 0.2f; set => _halfResist = value; }
/// <summary>The share of its health under which each sprints for good: 0.5, upstream's.</summary>
public static float EnrageAt { get => _enrageAt ?? 0.5f; set => _enrageAt = value; }
/// <summary>A head hit's chance of a pain clip: 0.24 (upstream's `math.random(100) < 25`); then 6 s of rest after it.</summary>
public static float PainChance { get => _pain ?? 0.24f; set => _pain = value; }
public static float PainGap { get => _painGap ?? 6f; set => _painGap = value; }
/// <summary>Walkers this near, feet to feet, are knocked aside: 49 (upstream's `7^2`, a plain distance).</summary>
public static float KnockRange { get => _knock ?? 49f; set => _knock = value; }
/// <summary>Upstream's fixed sizes: a shot's blast (50), the cloud (120 wide for 8 s) and its burst as it forms (150).</summary>
public const float ShotRadius = 50f, GasRadius = 120f, GasSeconds = 8f, GasBurstRadius = 150f;
/// <summary>How near a half sprints (250), how far it steps aside as it tears free (62, its clip's root travel), how far
/// ahead a shot (0.35 s) and the bomb (0.3 s) lead, a knocked walker's shove (30 over 1 s), a half's fall (0.7 s in).</summary>
const float SprintRange = 250f, SplitStep = 62f, ShotLead = 0.35f, BombLead = 0.3f, KnockPush = 30f, KnockSeconds = 1f,
BodyfallAt = 0.7f;
/// <summary>Their clips' own ground speeds — the `.zvar`s' tiers are picked by them.</summary>
const float MegatonWalk = 92.1f, MegatonSprint = 255.5f, BlasterWalk = 94.9f, BlasterSprint = 249.1f, BomberWalk = 74f,
BomberSprint = 233f;
// their clips (30 fps): lengths (frames − 1) / 30, events frame / 30, from `Staging/megaton*/prepare_report.json`
// ⛔ `const`, never `static readonly` (INSTRUCTIONS.md §1)
const string MegatonIdle = "nz_ai_steiner_idle_01", MegatonIdle2 = "nz_ai_steiner_idle_02", MegatonIdle3 = "nz_ai_steiner_idle_03",
MegatonBlast = "nz_ai_steiner_com_stn_atk_blast_01_quick_charging",
MegatonBomb = "nz_ai_steiner_com_stn_atk_bomb_01_quick_charging",
MegatonMantle = "nz_ai_steiner_com_stn_walk_trv_mantle_48";
const float Idle2Len = 100f / 30f, Idle3Len = 85f / 30f, MegatonBlastLen = 120f / 30f, MegatonBombLen = 117f / 30f,
SplitStart = 6f / 30f, SplitEnd = 100f / 30f;
const string BlasterBlast = "nz_ai_steiner_right_com_atk_blast_01", BlasterTear = "nz_ai_steiner_right_com_pain_split_01",
BlasterMantle = "nz_ai_steiner_right_com_stn_walk_trv_mantle_48";
const string BomberBomb = "nz_ai_steiner_left_com_atk_bomb_01", BomberTear = "nz_ai_steiner_left_com_pain_split_01",
BomberMantle = "nz_ai_steiner_left_com_stn_walk_trv_mantle_48";
const float BlasterBlastLen = 112f / 30f, BomberBombLen = 122f / 30f, TearLen = 178f / 30f;
/// <summary>
/// His acid green (the shots: upstream's `acidbug` glow (184, 216, 108) and trail (45, 255, 45)), the gas's paler one
/// (`zmb_gas_explo`), his eyes' lime (`Color(170, 255, 0)`) and the split's blue-white (`zmb_radz_split`: (173, 208, 255)).
/// </summary>
public static Color Acid => new( 0.55f, 1f, 0.25f );
public static Color Gas => new( 0.7f, 1f, 0.45f );
static Color Lime => new( 0.67f, 1f, 0f );
static Color SplitBlue => new( 0.55f, 0.8f, 1f );
// ══ state ════════════════════════════════════════════════════════════════
enum Kind { Megaton, Blaster, Bomber }
enum Beat { Charge, Fire, Exhale, Shot, BombCharge, BombFlare, Throw, Bomb, SplitVox, Tear }
Kind _kind;
bool _enraged, _sprinting, _tearing, _fell;
int _split;
float _nextBlast, _nextBomb, _nextThink, _nextPain, _nextFlourish, _tearStart, _diedAt;
Vector3 _slideFrom, _slideTo, _trackedAt, _trackedVel;
Rotation _tearRotation;
GameObject _tracked, _glowGo;
PointLight _glow;
NavMeshAgent _agent;
bool HasBlast => _kind != Kind.Bomber;
bool HasBomb => _kind != Kind.Blaster;
float Resist => _kind == Kind.Megaton ? MegatonResist : HalfResist;
float WalkSpeed => _kind switch { Kind.Blaster => BlasterWalk, Kind.Bomber => BomberWalk, _ => MegatonWalk };
float SprintSpeed => _kind switch { Kind.Blaster => BlasterSprint, Kind.Bomber => BomberSprint, _ => MegatonSprint };
/// <summary>Upstream's damage <paramref name="upstream"/> as this one's swing deals it: upstream's swing is his 75, a half's 50.</summary>
float Weighted( float upstream ) => SwingDamage( upstream / (_kind == Kind.Megaton ? 75f : 50f) );
/// <summary>Its mouth (`jaw_fx_tag`), left hand (`lhand_fx_tag`) and chest (`chest_fx_tag`) now: the QC's attachments, offsets
/// in their bones' frames.</summary>
Vector3 Jaw => _kind == Kind.Megaton ? At( "j_jaw_radz", new Vector3( 0f, -7f, -0.8f ) ) : At( "j_head", new Vector3( -1f, 7f, 0f ) );
Vector3 Hand => At( "j_wrist_le", new Vector3( 6f, 0f, -4f ) );
Vector3 Chest => At( "j_spineupper", Vector3.Zero );
Vector3 At( string bone, Vector3 offset )
=> Body.IsValid() && Body.TryGetBoneTransform( bone, out var t ) ? t.PointToWorld( offset ) : Voice;
protected override void BossStart()
{
var path = Body.IsValid() ? Body.Model?.ResourcePath ?? "" : "";
_kind = path.Contains( "megaton_blaster" ) ? Kind.Blaster : path.Contains( "megaton_bomber" ) ? Kind.Bomber : Kind.Megaton;
// ⚠️ EVERY MACHINE: the radiation on the body (upstream's `zmb_radz_aura`: green smoke, goo flecks and embers) and the
// eyes (`DrawEyeGlow`'s lime sprites, on the QC's `eyes`)
var aura = Components.Create<BossGlow>();
aura.Bone = "j_spineupper";
aura.Colour = Acid;
aura.Radius = _kind == Kind.Megaton ? 170f : 130f;
aura.Brightness = 1.4f;
aura.SparkEvery = 0.16f;
aura.SparkReach = 20f;
aura.SparkColour = new Color( 0.75f, 1f, 0.35f );
var eyes = Components.Create<BossGlow>();
eyes.Bone = "j_head";
eyes.Offset = _kind == Kind.Megaton ? new Vector3( 3.1f, 5.5f, 0f ) : new Vector3( 2.3f, 4.9f, 0f );
eyes.Colour = Lime;
eyes.Radius = 50f;
eyes.Brightness = 2.2f;
}
protected override void HostStart()
{
Ai.MaxHitDamage = MaxHit;
// ⛔ NONE OF THE WALKER'S CLIMB CLIPS ON THESE MODELS: each one's own 48-unit mantle (upstream's `NormalMantleOver48`)
Ai.ClimbClipOverride = _kind switch { Kind.Blaster => BlasterMantle, Kind.Bomber => BomberMantle, _ => MegatonMantle };
SetSpeed( WalkSpeed );
// upstream's first cooldowns: the blast 6 s after it arrives (a Blaster's, just after it tears free), the bomb 12 s
_nextBlast = Time.Now + 6f;
_nextBomb = Time.Now + 12f;
_nextFlourish = Time.Now + 5f;
if ( _kind == Kind.Megaton )
{
// his arrival: upstream's `panzer_spawn_tp` (a purple implosion), the music to everyone and the blast heard far — the
// sweetener is his variant's spawn sound
BossFx.BurstShared( WorldPosition + Vector3.Up * 50f * Size, 140f * Size, new Color( 0.62f, 0.3f, 1f ), 10 );
NZSound.PlayShared( "nz.megaton.intro" );
NZSound.PlayShared( "nz.megaton.spawn.explo", WorldPosition );
// `TimeOut(2)`: he stands two seconds before he comes
PlayShared( MegatonIdle, 2f );
}
else
BeginTear();
Say( $"spawned — the {_kind} · {Hp?.Max ?? 0f:0} hp (x{Resist:0.##} from everything)"
+ (HasBlast ? $" · blast {BlastMin:0}-{RangedMax:0}u" : "") + (HasBomb ? $" · bomb {BombMin:0}-{RangedMax:0}u" : "")
+ $" · sprints for good under {EnrageAt * 100f:0}% · hitboxes {Ai.HitboxCount}" );
}
protected override void HostFrame()
{
Ai.MaxHitDamage = MaxHit;
if ( _tearing ) TickTear();
if ( Ai.State == ZombieState.Spawning ) return;
TrackTarget();
TickEnrage();
if ( Time.Now < _nextThink ) return;
_nextThink = Time.Now + 0.25f;
TickPace();
TickKnock();
TickFlourish();
TickRanged();
}
protected override void Died( bool host )
{
_diedAt = Time.Now;
if ( !host ) return;
// ⚠️ HIS CORPSE IS GONE A SECOND AFTER THE SPLIT (hidden at its frame 100): the halves stand where he stood
if ( _kind == Kind.Megaton ) Ai.CorpseLinger = 1f;
Say( _kind == Kind.Megaton ? "killed — splitting" : "killed" );
}
/// <summary>
/// After death, on EVERY machine off its own copy of it: a half's fall (the base's `bodyfall_heavy`, about frame 21 of each
/// of its deaths); his split — the chest burning blue from frame 6 (`steiner_split_start`, `zmb_radz_split`), and at frame 100
/// (`steiner_split_end`) the blast, the body gone and, on the host, the two halves.
/// </summary>
protected override void DeadFrame()
{
var t = Time.Now - _diedAt;
if ( _kind != Kind.Megaton )
{
if ( _fell || t < BodyfallAt ) return;
_fell = true;
NZSound.Play( "nz.megaton.bodyfall", WorldPosition );
return;
}
if ( _split == 0 && t >= SplitStart )
{
_split = 1;
NZSound.Play( "nz.megaton.split.rise", Chest );
_glowGo = new GameObject { Parent = GameObject, Name = "nz_megaton_split" };
_glowGo.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.NotNetworked;
_glow = _glowGo.Components.Create<PointLight>();
_glow.LightColor = SplitBlue;
_glow.Radius = 240f * Size;
_glow.Shadows = false;
}
if ( _split != 1 ) return;
if ( t < SplitEnd )
{
// brighter as it nears
if ( !_glow.IsValid() ) return;
_glowGo.WorldPosition = Chest + Facing * 8f * Size;
var k = (t - SplitStart) / (SplitEnd - SplitStart);
_glow.LightColor = SplitBlue * (1.5f + 5f * k) * Game.Random.Float( 0.85f, 1.1f );
return;
}
_split = 2;
if ( _glowGo.IsValid() ) _glowGo.Destroy();
// `zmb_radz_split_explo` (blue flares, a shockwave, teal goo) with `zmb_monster_explosion`, on this machine; the body goes
// (`MargwaFx.SetHidden`), the halves taking its place
var at = WorldPosition + Vector3.Up * 40f * Size;
BossFx.Burst( at, 160f * Size, SplitBlue, 18 );
NZSound.Play( "nz.megaton.split.explode", at );
MargwaFx.SetHidden( GameObject, true );
if ( IsHost ) Split();
}
protected override void OnDestroy()
{
if ( _glowGo.IsValid() ) _glowGo.Destroy();
}
/// <summary>His table: x<see cref="MegatonResist"/> from everything (a half x<see cref="HalfResist"/>), and a hit on the head
/// may hurt it into a pain clip. THE HOST, once a hit.</summary>
protected override float ScaleDamage( float amount, in DamageInfo damage )
{
if ( Health.IsHeadshot( damage ) ) TryPain();
return Resist;
}
protected override void OnBeat( int beat )
{
switch ( (Beat)beat )
{
case Beat.Charge:
NZSound.PlayShared( "nz.megaton.blast.charge", Jaw );
break;
case Beat.Fire:
NZSound.PlayShared( "nz.megaton.blast.fire", Voice );
break;
case Beat.Exhale:
// `zmb_radz_blast_exhale` at the mouth
BossFx.BurstShared( Jaw, 18f * Size, Acid, 6 );
break;
case Beat.Shot:
if ( Ai.State == ZombieState.Special ) Shoot( false );
break;
case Beat.BombCharge:
// `zmb_radz_bomb_charge` on the left hand, and the charge — vox and sound
BossFx.BurstShared( Hand, 22f * Size, Acid, 10 );
NZSound.PlayShared( "nz.megaton.bomb.vox", Voice );
NZSound.PlayShared( "nz.megaton.bomb.charge", Hand );
break;
case Beat.BombFlare:
BossFx.BurstShared( Hand, 22f * Size, Acid, 10 );
break;
case Beat.Throw:
NZSound.PlayShared( "nz.megaton.bomb.throw.vox", Voice );
NZSound.PlayShared( "nz.megaton.bomb.throw", Hand );
break;
case Beat.Bomb:
if ( Ai.State == ZombieState.Special ) Shoot( true );
break;
case Beat.SplitVox:
NZSound.PlayShared( "nz.megaton.split.vox", Voice );
break;
case Beat.Tear:
NZSound.PlayShared( "nz.megaton.tear", Chest );
break;
}
}
// ══ the host ═════════════════════════════════════════════════════════════
/// <summary>A half sprints while its target is within 250 and walks while it is further (upstream's, on every think) — until
/// it sprints for good. THE HOST.</summary>
void TickPace()
{
if ( _kind == Kind.Megaton || _enraged || Ai.State is not (ZombieState.Chasing or ZombieState.Attacking) ) return;
var p = TargetPlayer;
if ( !IsUp( p ) ) return;
var near = WorldPosition.Distance( p.WorldPosition ) < SprintRange * Size;
if ( near == _sprinting ) return;
_sprinting = near;
SetSpeed( near ? SprintSpeed : WalkSpeed );
}
/// <summary>Under <see cref="EnrageAt"/> of its health: the sprint, for good (upstream's enrage — nothing else). THE HOST.</summary>
void TickEnrage()
{
if ( _enraged || !Hp.IsValid() || Hp.Max <= 1f || Hp.Current > Hp.Max * EnrageAt ) return;
if ( Ai.State is not (ZombieState.Chasing or ZombieState.Attacking) ) return;
_enraged = true;
_sprinting = true;
SetSpeed( SprintSpeed );
Say( $"ENRAGED at {Hp.Current:0}/{Hp.Max:0} — sprinting at {Ai.SpeedOverride:0} u/s for good" );
}
/// <summary>
/// Upstream's "knock normal zombies aside": every walker within <see cref="KnockRange"/> of him on the move — of a half only
/// while it sprints — is shoved aside and held a moment (`ZombieAI.Knockback`, Shockwave's: no fall). ⚠️ Upstream played their
/// pain clip in place, which the port's walkers cannot play mid-stride. Bosses and specials are left alone, as upstream's
/// `IsMooSpecial` were. THE HOST.
/// </summary>
void TickKnock()
{
if ( Ai.State is not (ZombieState.Chasing or ZombieState.Attacking) ) return;
if ( _kind != Kind.Megaton && !_sprinting ) return;
var r = KnockRange * Size;
foreach ( var z in ZombieAI.All.ToList() )
{
if ( !z.IsValid() || z == Ai || z.State is ZombieState.Dead or ZombieState.Spawning or ZombieState.Special ) continue;
if ( (z.Variant?.IsBoss ?? false) || SpecialEnemies.IsSpecial( z.Variant ) ) continue;
if ( z.WorldPosition.Distance( WorldPosition ) > r ) continue;
z.Knockback( WorldPosition, KnockPush * Size, KnockSeconds, 0.3f, tip: false );
}
}
/// <summary>No one to chase: now and then his idle flourish (upstream's `idle_02` / `idle_03`, the first 5 s in, then every
/// 8-15 s). His alone — the halves' code has none. THE HOST.</summary>
void TickFlourish()
{
if ( _kind != Kind.Megaton || Ai.Target.IsValid() || Time.Now < _nextFlourish ) return;
if ( Ai.State is not (ZombieState.Chasing or ZombieState.Idle) ) return;
_nextFlourish = Time.Now + Game.Random.Int( 8, 15 );
var second = Game.Random.Int( 0, 1 ) == 0;
PlayShared( second ? MegatonIdle2 : MegatonIdle3, second ? Idle2Len : Idle3Len );
}
/// <summary>The blast, else the bomb — upstream's `AI()` order — at a target in sight, while it chases. THE HOST.</summary>
void TickRanged()
{
if ( Ai.State != ZombieState.Chasing ) return;
var p = TargetPlayer;
if ( !IsUp( p ) ) return;
var d = WorldPosition.Distance( p.WorldPosition );
if ( d >= RangedMax * Size ) return;
var blast = HasBlast && Time.Now >= _nextBlast && d >= BlastMin * Size;
var bomb = HasBomb && Time.Now >= _nextBomb && d >= BombMin * Size;
if ( !blast && !bomb ) return;
// upstream's `IsAttackBlocked`: an eye to eye trace — anything between, no throw
if ( !CanSee( p.GameObject ) ) return;
if ( blast ) StartBlast( p );
else StartBomb( p );
}
/// <summary>Both timers, as either attack starts (upstream's threads re-roll both).</summary>
void ReRoll()
{
_nextBlast = Time.Now + Game.Random.Float( BlastGapMin, BlastGapMax );
_nextBomb = Time.Now + Game.Random.Float( BombGapMin, BombGapMax );
}
void QueueAt( Beat beat, params float[] frames )
{
foreach ( var f in frames ) Queue( Frames( f ), (int)beat );
}
/// <summary>
/// The blast: face them, the inhale and the long charge vox as it starts (upstream's, in code), the charge, the fire vox and
/// three shots on the clip's own events — turning after its target all the while (`ZombieAI.FaceMovement`, upstream's
/// `FaceEnemy`). THE HOST.
/// </summary>
void StartBlast( NZPlayer p )
{
if ( IsUp( p ) ) Face( p.WorldPosition );
var blaster = _kind == Kind.Blaster;
if ( !PlayShared( blaster ? BlasterBlast : MegatonBlast, blaster ? BlasterBlastLen : MegatonBlastLen ) ) return;
ReRoll();
BossFx.BurstShared( Jaw, 26f * Size, Acid, 8 );
NZSound.PlayShared( "nz.megaton.blast.vox", Voice );
if ( blaster )
{
QueueAt( Beat.Charge, 1f, 28f );
// ⚠️ THE FIRE VOX ON THE BLASTER'S OWN EVENT (`sndnt#51a552145a932c7`, frame 40), which upstream never listened for
QueueAt( Beat.Fire, 40f );
QueueAt( Beat.Exhale, 44f );
QueueAt( Beat.Shot, 46f, 54f, 61f );
}
else
{
QueueAt( Beat.Charge, 34f );
QueueAt( Beat.Fire, 47f );
QueueAt( Beat.Shot, 48f, 56f, 64f );
QueueAt( Beat.Exhale, 49f );
}
}
/// <summary>The bomb: face them, the charge on the left hand (twice), the throw, the bomb on the clip's own events. THE HOST.</summary>
void StartBomb( NZPlayer p )
{
if ( IsUp( p ) ) Face( p.WorldPosition );
var bomber = _kind == Kind.Bomber;
if ( !PlayShared( bomber ? BomberBomb : MegatonBomb, bomber ? BomberBombLen : MegatonBombLen ) ) return;
ReRoll();
QueueAt( Beat.BombCharge, bomber ? 7f : 8f );
QueueAt( Beat.BombFlare, 44f );
QueueAt( Beat.Throw, bomber ? 70f : 69f );
QueueAt( Beat.Bomb, bomber ? 72f : 81f );
}
/// <summary>
/// One shot from the mouth or the bomb from the left hand — upstream's `Retarget()` on each: its target if still up, else
/// the nearest player — at their feet + 25-65 (a shot) or + 10 (the bomb), led by 0.35 s or 0.3 s of their pace (upstream's
/// `GetVelocity() * clamp(speed, 0, 0.35)`: all of it, for anyone moving). THE HOST.
/// </summary>
void Shoot( bool bomb )
{
var from = bomb ? Hand : Jaw;
var target = TargetPlayer;
var p = IsUp( target ) ? target : NearestPlayer( RangedMax * 2f * Size );
var to = from + Facing * 500f;
if ( p.IsValid() )
{
to = p.WorldPosition + Vector3.Up * (bomb ? 10f : Game.Random.Int( 25, 65 ));
if ( p == target && _tracked.IsValid() ) to += _trackedVel.WithZ( 0 ) * (bomb ? BombLead : ShotLead);
}
if ( !bomb ) NZSound.PlayShared( "nz.megaton.blast.shot", from );
// ⚠️ THE BOMBER'S BOMB AND CLOUD HIT HARDER THAN HIS (`BomberBombHit`, 2026-10-07)
var bomber = _kind == Kind.Bomber;
MegatonShot.Launch( Scene, from, to - from, GameObject, Weighted( bomb && bomber ? BomberBombHit : ShotDamage ), bomb,
Weighted( bomber ? BomberGasTick : GasTick ), Weighted( bomber ? BomberGasBurst : GasBurst ) );
}
/// <summary>Its target's ground speed, frame to frame, smoothed — what a shot leads by (Avogadro's: a client's controller
/// velocity is not the host's to read). THE HOST.</summary>
void TrackTarget()
{
var t = Ai.Target;
if ( !t.IsValid() ) { _tracked = null; return; }
var pos = t.WorldPosition;
if ( t != _tracked )
{
_tracked = t;
_trackedAt = pos;
_trackedVel = Vector3.Zero;
return;
}
var dt = MathF.Max( 0.001f, Time.Delta );
var v = (pos - _trackedAt) / dt;
_trackedAt = pos;
if ( v.Length > 1200f ) return; // a teleport or a respawn, not a run
_trackedVel = Vector3.Lerp( _trackedVel, v, 1f - MathF.Exp( -8f * dt ) );
}
/// <summary>
/// A head hit's pain clip, one of upstream's `PainSequences`, at <see cref="PainChance"/>, and then not again for its length
/// and <see cref="PainGap"/> — never over a special (upstream's `!GetSpecialAnimation()`). THE HOST, from the hit.
/// </summary>
void TryPain()
{
if ( Ai.State is not (ZombieState.Chasing or ZombieState.Attacking or ZombieState.Idle) ) return;
if ( Time.Now < _nextPain || Game.Random.Float( 0f, 1f ) >= PainChance ) return;
var (clip, len) = PainClip();
if ( !PlayShared( clip, len ) ) return;
_nextPain = Time.Now + len + PainGap;
NZSound.PlayShared( "nz.megaton.pain", Voice );
}
(string Clip, float Len) PainClip() => _kind switch
{
Kind.Blaster => Game.Random.Int( 0, 2 ) switch
{
0 => ("nz_ai_steiner_right_com_stn_walk_pain_face_01", 78f / 30f),
1 => ("nz_ai_steiner_right_com_stn_walk_pain_face_02", 64f / 30f),
_ => ("nz_ai_steiner_right_com_stn_walk_pain_hvy_01", 81f / 30f),
},
Kind.Bomber => Game.Random.Int( 0, 3 ) switch
{
0 => ("nz_ai_steiner_left_com_stn_walk_pain_arm_l_01", 161f / 30f),
1 => ("nz_ai_steiner_left_com_walk_pain_hvy_01", 195f / 30f),
2 => ("nz_ai_steiner_left_com_walk_pain_hvy_02", 160f / 30f),
_ => ("nz_ai_steiner_left_com_walk_pain_hvy_03", 151f / 30f),
},
_ => Game.Random.Int( 0, 3 ) switch
{
0 => ("nz_ai_steiner_com_stn_walk_pain_arm_l_01", 112f / 30f),
1 => ("nz_ai_steiner_com_stn_walk_pain_face_01", 131f / 30f),
2 => ("nz_ai_steiner_com_stn_walk_pain_face_02", 100f / 30f),
_ => ("nz_ai_steiner_com_stn_walk_pain_hvy_01", 108f / 30f),
},
};
// ══ the split ════════════════════════════════════════════════════════════
/// <summary>
/// The two halves where he stood, facing his way — the Blaster, then the Bomber (`steiner_split_end`). Through
/// `ZombieCommands.SpawnAt` with each one's variant, as `ShrekBoss` summons his Helldonkeys: every machine gets them, and their
/// `.zvar`s say `IsBoss`, so the round neither waits for them nor counts them. THE HOST.
/// </summary>
void Split()
{
NZNet.ShakeAt( WorldPosition, 0.6f, 1000f );
var yaw = Rotation.LookAt( Facing ).Yaw();
var came = 0;
foreach ( var name in new[] { SpecialEnemies.MegatonBlaster, SpecialEnemies.MegatonBomber } )
{
var variant = SpecialEnemies.VariantFor( name );
if ( variant is null )
{
Log.Warning( $"{Tag} no {name} to split into — zombies/{name}.zvar did not load" );
continue;
}
var z = ZombieCommands.SpawnAt( Scene, WorldPosition, variant );
if ( z is null ) continue;
// ⚠️ THE YAW ALONE: its own `ApplyVariantBody` lays its rig's turn (90) on top. Shrek hands his donkeys his whole
// rotation, which on Brutus's rig (no turn) is the same thing — here it would turn them a quarter
z.WorldRotation = Rotation.FromYaw( yaw );
z.GameObject.Name = name;
came++;
}
Say( $"split — {came} of 2 halves" );
}
/// <summary>
/// A half tearing itself free (`com_pain_split_01`, 5.93 s): the vox and the tearing on the clip's own events, and the step
/// to its side — its clip's root travel, which the port strips — replayed over the clip. Split or sent alone. THE HOST.
/// </summary>
void BeginTear()
{
var blaster = _kind == Kind.Blaster;
if ( !PlayShared( blaster ? BlasterTear : BomberTear, TearLen ) ) return;
_tearing = true;
_tearStart = Time.Now;
_tearRotation = WorldRotation;
_slideFrom = WorldPosition;
_slideTo = SideSpot();
if ( blaster )
{
QueueAt( Beat.SplitVox, 15f, 45f, 79f );
QueueAt( Beat.Tear, 19f, 40f, 67f, 87f );
}
else
{
QueueAt( Beat.Tear, 4f, 25f, 63f, 105f );
QueueAt( Beat.SplitVox, 140f );
}
}
/// <summary>
/// Where a half's tear-apart carries it: 62 units to its own side (the Blaster to its right, the Bomber to its left: the root
/// in each clip goes 62.5 along −X / +X of a rig whose art faces −Y), short of any wall, on the navmesh of its own level —
/// `ZombieAI.KnockbackEnd`'s rules. No room, where it stands.
/// </summary>
Vector3 SideSpot()
{
var right = Rotation.LookAt( Facing ).Right;
var side = _kind == Kind.Blaster ? right : -right;
var start = WorldPosition;
var eye = start + Vector3.Up * 40f * Size;
var reach = SplitStep * Size;
var wall = Scene.Trace.Ray( eye, eye + side * reach ).IgnoreGameObjectHierarchy( GameObject )
.WithoutTags( "zombie", "player", "trigger", "ragdoll" ).Run();
if ( wall.Hit ) reach = MathF.Max( 0f, wall.Distance - 20f * Size );
if ( reach < 4f ) return start;
var end = ZombieAI.NavGround( Scene, start + side * reach );
if ( MathF.Abs( end.z - start.z ) > 24f || (end - start).WithZ( 0 ).Length > reach + 16f ) return start;
return end;
}
/// <summary>
/// The step aside, frame by frame along the clip's own curve — the agent told, as `ZombieAI.KeepWithin` moves a body — and its
/// facing held (`ZombieAI.FaceMovement` would turn a still zombie to its target). Done when the clip is. THE HOST.
/// </summary>
void TickTear()
{
if ( Ai.State != ZombieState.Special )
{
_tearing = false;
return;
}
var at = Vector3.Lerp( _slideFrom, _slideTo, SlideAt( _kind == Kind.Blaster, (Time.Now - _tearStart) / TearLen ) );
WorldPosition = at;
if ( !_agent.IsValid() ) _agent = Components.Get<NavMeshAgent>( FindMode.EverythingInSelfAndDescendants );
if ( _agent.IsValid() ) _agent.SetAgentPosition( at );
WorldRotation = _tearRotation;
}
/// <summary>
/// How far through its step aside a half is, <paramref name="t"/> of the way through its tear-apart: the root's own travel in
/// the SMD, eleven samples (t = 0, 0.1 … 1) — the Blaster's mostly 2.8-4.2 s in, the Bomber's 1.8-4.3 s.
/// </summary>
static float SlideAt( bool blaster, float t )
{
t = Math.Clamp( t, 0f, 1f ) * 10f;
var i = Math.Min( 9, (int)t );
return MathX.Lerp( Sample( blaster, i ), Sample( blaster, i + 1 ), t - i );
}
static float Sample( bool blaster, int i ) => blaster
? i switch { 0 => 0f, 1 => 0.048f, 2 => 0.097f, 3 => 0.116f, 4 => 0.118f, 5 => 0.213f, 6 => 0.674f, 7 => 0.971f, _ => 1f }
: i switch { 0 or 1 or 2 => 0f, 3 => 0.011f, 4 => 0.153f, 5 => 0.26f, 6 => 0.479f, 7 => 0.961f, _ => 1f };
// ══ diagnostics ══════════════════════════════════════════════════════════
static IEnumerable<MegatonBoss> All
=> Game.ActiveScene?.GetAllComponents<MegatonBoss>() ?? Enumerable.Empty<MegatonBoss>();
/// <summary>`nz_megaton` — every Megaton and half alive, and the tuning.</summary>
[ConCmd( "nz_megaton" )]
public static void Report()
{
Log.Info( $"[nz-megaton] blast {BlastMin:0}-{RangedMax:0}u every {BlastGapMin:0.##}-{BlastGapMax:0.##}s · bomb {BombMin:0}-{RangedMax:0}u"
+ $" every {BombGapMin:0.##}-{BombGapMax:0.##}s · {ShotDamage:0.#} at a blast's middle ({ShotRadius:0}u, daze {Daze:0.##}s)"
+ $" · gas {GasTick:0.##} a tick for {GasSeconds:0}s ({GasRadius:0}u), {GasBurst:0.#} as it forms ({GasBurstRadius:0}u) — upstream's numbers"
+ $" · THE BOMBER'S: bomb {BomberBombHit:0.#}, gas {BomberGasTick:0.##} a tick, {BomberGasBurst:0.#} as it forms"
+ $" · x{MegatonResist:0.##} him, x{HalfResist:0.##} a half · sprints under {EnrageAt * 100f:0}%"
+ $" · pain {PainChance * 100f:0}% on the head, {PainGap:0.#}s apart · knocks walkers within {KnockRange:0} · hit cap {MaxHit:0}" );
var list = All.ToList();
Log.Info( $"[nz-megaton] {list.Count} alive" );
foreach ( var m in list )
Log.Info( $"[nz-megaton] the {m._kind} · 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._sprinting ? " · sprinting" : "")}"
+ $"{(m._tearing ? " · TEARING FREE" : "")}"
+ $"{(m.HasBlast ? $" · blast in {MathF.Max( 0f, m._nextBlast - Time.Now ):0.#}s" : "")}"
+ $"{(m.HasBomb ? $" · bomb in {MathF.Max( 0f, m._nextBomb - Time.Now ):0.#}s" : "")} · hitboxes {m.Ai?.HitboxCount ?? 0}" );
}
/// <summary>`nz_megaton_blast` — every Megaton and Blaster blasts now, whatever the range; `nz_megaton_bomb` — every Megaton
/// and Bomber throws.</summary>
[ConCmd( "nz_megaton_blast" )]
public static void BlastCmd()
{
if ( NZGame.IsClient ) { Log.Info( "[nz-megaton] the host decides" ); return; }
foreach ( var m in All.Where( m => m.Ai.IsValid() && !m.Dead && m.HasBlast ).ToList() )
m.StartBlast( m.TargetPlayer ?? m.NearestPlayer() );
}
[ConCmd( "nz_megaton_bomb" )]
public static void BombCmd()
{
if ( NZGame.IsClient ) { Log.Info( "[nz-megaton] the host decides" ); return; }
foreach ( var m in All.Where( m => m.Ai.IsValid() && !m.Dead && m.HasBomb ).ToList() )
m.StartBomb( m.TargetPlayer ?? m.NearestPlayer() );
}
/// <summary>`nz_megaton_split` — every Megaton alive dies now, and splits.</summary>
[ConCmd( "nz_megaton_split" )]
public static void SplitCmd()
{
if ( NZGame.IsClient ) { Log.Info( "[nz-megaton] the host decides" ); return; }
foreach ( var m in All.Where( m => m._kind == Kind.Megaton && m.Ai.IsValid() && !m.Dead && m.Hp.IsValid() ).ToList() )
m.Hp.Apply( m.Hp.Current + 1f );
}
/// <summary>`nz_megaton_set <key> <value>` — retune them live.</summary>
[ConCmd( "nz_megaton_set" )]
public static void SetCmd( string key = "", float value = 0f )
{
switch ( key.ToLowerInvariant() )
{
case "blastmin": BlastMin = value; break;
case "bombmin": BombMin = value; break;
case "range": RangedMax = value; break;
case "blastgapmin": BlastGapMin = value; break;
case "blastgapmax": BlastGapMax = value; break;
case "bombgapmin": BombGapMin = value; break;
case "bombgapmax": BombGapMax = value; break;
case "shot": ShotDamage = value; break;
case "gas": GasTick = value; break;
case "gasburst": GasBurst = value; break;
case "daze": Daze = value; break;
case "bomberbomb": BomberBombHit = value; break;
case "bombergas": BomberGasTick = value; break;
case "bombergasburst": BomberGasBurst = value; break;
case "resist": MegatonResist = value; break;
case "halfresist": HalfResist = value; break;
case "enrage": EnrageAt = value; break;
case "pain": PainChance = value; break;
case "paingap": PainGap = value; break;
case "knock": KnockRange = value; break;
default:
Log.Info( "[nz-megaton] nz_megaton_set <blastmin|bombmin|range|blastgapmin|blastgapmax|bombgapmin|bombgapmax|shot|gas"
+ "|gasburst|daze|bomberbomb|bombergas|bombergasburst|resist|halfresist|enrage|pain|paingap|knock> <value>" );
return;
}
Log.Info( $"[nz-megaton] {key} = {value:0.###}" );
Report();
}
}