Two components for the Megaton weapon: MegatonShot implements the projectile (movement, collision with walls/players, spawning visual effects, applying blast damage and daze, playing sounds). MegatonGas implements the gas cloud left by bombs (spawns visuals/sounds, ticks periodic damage to players while avoiding killing them, and destroys itself after a duration).
using Sandbox;
using System;
using System.Linq;
namespace NZombies;
/// <summary>
/// The Megaton's shots (2026-10-07) — upstream's `nz_proj_steiner_blast` (a green ball trailing glow, three to a blast) and
/// `nz_proj_steiner_bomb` (the same ball unseen inside its sparks): straight at <see cref="Speed"/> u/s, no gravity, no
/// homing, through zombies until a wall or a player. Then a <see cref="MegatonBoss.ShotRadius"/>-unit blast: its damage at the
/// middle falling to nothing at the rim (upstream's `50 × (1 − d / 50)`, to the body), and everyone in it dazed (upstream's
/// `NZSonicBlind`, `BossFx.Burst`'s daze); a shot's also goes off in a fireball (upstream's `util.Effect("Explosion")`), a
/// bomb's leaves its gas cloud (<see cref="MegatonGas"/>).
///
/// ⚠️ THE HOST FLIES IT; EVERYONE ELSE WATCHES — `AvogadroBolt`'s pattern: `Launch` network-spawns it with its kind a
/// `[Property]` (the spawn carries the look), every machine builds the light and the sparks, the host moves it and settles it.
/// ⚠️ A <see cref="Life"/> s LIFE: upstream's has none and flies until it meets something. ⚠️ WALLS, DOORS AND PROPS STOP IT:
/// upstream's trigger met only the world's brushes and a player, and went through props and doors.
/// ⛔ NEVER MORE THAN `BossBase.MaxHit` A HIT.
/// </summary>
public sealed class MegatonShot : Component
{
/// <summary>1750 u/s (upstream's `SetLocalVelocity(dir * 1750)`), for 3 s; a player within 20 of its path is hit.</summary>
public const float Speed = 1750f, Life = 3f, Touch = 20f;
/// <summary>The bomb (sparks, the cloud) rather than a shot of the blast (a ball of light, the fireball).</summary>
[Property] public bool Bomb { get; set; }
// ── the host's ──
public GameObject Owner { get; set; }
/// <summary>Its blast at the middle, and its cloud's tick and burst — sized on the host at launch, so its thrower's death
/// mid-flight (his split) changes nothing.</summary>
public float Damage { get; set; }
public float GasTick { get; set; }
public float GasBurst { get; set; }
Vector3 _dir;
float _dieAt, _nextSpark;
bool _done;
/// <summary>Throw one. THE HOST; a client only watches.</summary>
public static MegatonShot Launch( Scene scene, Vector3 from, Vector3 dir, GameObject owner, float damage, bool bomb, float gasTick,
float gasBurst )
{
if ( !scene.IsValid() || NZGame.IsClient ) return null;
var go = new GameObject( true, bomb ? "nz_megaton_bomb" : "nz_megaton_shot" );
go.Flags |= GameObjectFlags.NotSaved;
go.WorldPosition = from;
go.WorldRotation = Rotation.LookAt( dir.Length > 0.01f ? dir.Normal : Vector3.Forward, Vector3.Up );
var s = go.Components.Create<MegatonShot>();
s.Bomb = bomb;
s.Owner = owner;
s.Damage = damage;
s.GasTick = gasTick;
s.GasBurst = gasBurst;
// ⚠️ UNCONDITIONAL (a no-op solo), and after the component is configured, since the spawn is serialised now
go.NetworkSpawn();
return s;
}
protected override void OnStart()
{
_dir = WorldRotation.Forward;
_dieAt = Time.Now + Life;
// the look, on every machine — a child of a networked object is each machine's own
var lightGo = new GameObject { Parent = GameObject, Name = "light" };
lightGo.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.NotNetworked;
var light = lightGo.Components.Create<PointLight>();
light.LightColor = MegatonBoss.Acid * (Bomb ? 3f : 4.5f);
light.Radius = Bomb ? 200f : 150f;
light.Shadows = false;
}
protected override void OnUpdate()
{
// ── every machine: sparks in its wake (the blast's trail, upstream's `acidbug_spit_trail_glow`; the bomb's
// `zmb_radz_bomb_aura`, thicker) ──
if ( !_done && Time.Now >= _nextSpark )
{
_nextSpark = Time.Now + (Bomb ? 0.02f : 0.035f);
var c = WorldPosition;
AvogadroSpark.Between( c + Vector3.Random * 4f, c + Vector3.Random.Normal * Game.Random.Float( Bomb ? 10f : 6f, Bomb ? 20f : 12f ),
Game.Random.Float( 0.06f, 0.14f ), Bomb ? 0.2f : 0.14f, MegatonBoss.Acid );
}
if ( _done || NZGame.IsClient ) return;
var from = WorldPosition;
var next = from + _dir * Speed * Time.Delta;
// ── a wall ends it; zombies it passes through, as upstream's trigger does ──
var trace = Scene.Trace.Ray( from, next ).IgnoreGameObjectHierarchy( GameObject ).WithoutTags( "zombie", "player", "trigger", "ragdoll" );
if ( Owner.IsValid() ) trace = trace.IgnoreGameObjectHierarchy( Owner );
var wall = trace.Run();
if ( wall.Hit )
{
End( wall.HitPosition - _dir * 4f );
return;
}
// ── a player it passes ──
foreach ( var p in Scene.GetAllComponents<NZPlayer>() )
{
if ( !BossBase.IsUp( p ) || ToBody( p, from, next ) > Touch ) continue;
End( next );
return;
}
WorldPosition = next;
if ( Time.Now >= _dieAt ) End( next );
}
/// <summary>How near the segment <paramref name="a"/>-<paramref name="b"/> passes a player's body (a line from shin to head).</summary>
static float ToBody( NZPlayer p, Vector3 a, Vector3 b )
{
var foot = p.WorldPosition;
var best = float.MaxValue;
for ( int i = 0; i <= 4; i++ )
{
var q = Vector3.Lerp( a, b, i / 4f );
var z = Math.Clamp( q.z, foot.z + 12f, foot.z + 64f );
best = MathF.Min( best, q.Distance( new Vector3( foot.x, foot.y, z ) ) );
}
return best;
}
/// <summary>The blast. THE HOST; destroying a network object ends it everywhere.</summary>
void End( Vector3 at )
{
if ( _done ) return;
_done = true;
var radius = MegatonBoss.ShotRadius;
var from = Owner.IsValid() ? Owner : GameObject;
foreach ( var p in Scene.GetAllComponents<NZPlayer>().ToList() )
{
if ( !BossBase.IsUp( p ) ) continue;
var d = ToBody( p, at, at );
if ( d > radius ) continue;
var hp = p.Components.Get<Health>( FindMode.EverythingInSelfAndDescendants );
if ( !hp.IsValid() || hp.IsDead ) continue;
hp.Apply( MathF.Min( Damage * (1f - d / radius), BossBase.MaxHit ), false, from, blast: true, blastAt: at );
}
// on every machine: the splash (upstream's `acidbug_spit_impact`) and the daze, and the shake (20 within 100)
BossFx.BurstShared( at, radius, MegatonBoss.Acid, Bomb ? 12 : 8, MegatonBoss.Daze );
NZNet.ShakeAt( at, 0.3f, 350f );
if ( Bomb )
{
NZSound.PlayShared( "nz.megaton.bomb.explode", at );
MegatonGas.Lay( Scene, at, Owner, GasTick, GasBurst );
}
else
{
BlastEffect.Spawn( at, radius );
NZSound.PlayShared( "nz.megaton.blast.impact", at );
NZSound.PlayShared( "nz.megaton.blast.cloud", at );
}
GameObject.Destroy();
}
}
/// <summary>
/// The Megaton's gas cloud, where a bomb bursts (2026-10-07) — upstream's `nova_gas_cloud`: for <see cref="MegatonBoss.GasSeconds"/>
/// s everyone up within <see cref="MegatonBoss.GasRadius"/> takes a tick every quarter second (upstream's 1 — 4 a second, 32 at
/// most) but never their last point of health (upstream's `Health() > 1`: the cloud alone downs no one), after a burst as it
/// forms (upstream's `util.BlastDamage(150, 15)`, full at the middle).
///
/// ⚠️ THE HOST'S ALONE AND NOT NETWORKED: its look is every machine's own — the Shadows of Evil Margwa's portal, `PitVisual`'s green
/// fallout pit (`NZNet.MargwaPortal`: a glow, rising gas and a ring at the cloud's reach, gone with it) — and this object only
/// ticks the damage. ⚠️ Upstream's `NZNovaGas` is left out: nothing in the addon reads it.
/// ⛔ NEVER MORE THAN `BossBase.MaxHit` A HIT.
/// </summary>
public sealed class MegatonGas : Component
{
public GameObject Owner { get; set; }
public float Tick { get; set; }
public float Until { get; set; }
float _next;
/// <summary>A cloud under <paramref name="at"/>. THE HOST.</summary>
public static void Lay( Scene scene, Vector3 at, GameObject owner, float tick, float burst )
{
if ( !scene.IsValid() || NZGame.IsClient ) return;
var ground = PitVisual.GroundAt( at );
// every machine: the cloud, the burst of it (`zmb_gas_explo`'s pale green flash) and its bang
NZNet.MargwaPortal( ground, MegatonBoss.GasRadius, MegatonBoss.GasSeconds );
BossFx.BurstShared( ground + Vector3.Up * 6f, MegatonBoss.GasBurstRadius, MegatonBoss.Gas, 14 );
NZSound.PlayShared( "nz.megaton.gas", ground );
NZNet.ShakeAt( ground, 0.2f, 450f );
// the burst as it forms, full at the middle
var from = owner.IsValid() ? owner : null;
foreach ( var p in scene.GetAllComponents<NZPlayer>().ToList() )
{
if ( !BossBase.IsUp( p ) ) continue;
var d = (p.WorldPosition + Vector3.Up * 40f).Distance( ground );
if ( d > MegatonBoss.GasBurstRadius ) continue;
var hp = p.Components.Get<Health>( FindMode.EverythingInSelfAndDescendants );
if ( !hp.IsValid() || hp.IsDead ) continue;
hp.Apply( MathF.Min( burst * (1f - d / MegatonBoss.GasBurstRadius), BossBase.MaxHit ), false, from, blast: true,
blastAt: ground );
}
var go = new GameObject( true, "nz_megaton_gas" );
go.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.NotNetworked;
go.NetworkMode = NetworkMode.Never; // ⛔ THE HOST'S OWN: its look went out above
go.WorldPosition = ground;
var g = go.Components.Create<MegatonGas>();
g.Owner = owner;
g.Tick = tick;
g.Until = Time.Now + MegatonBoss.GasSeconds;
}
protected override void OnUpdate()
{
var now = Time.Now;
if ( now >= Until )
{
GameObject.Destroy();
return;
}
if ( now < _next ) return;
_next = now + 0.25f;
foreach ( var p in Scene.GetAllComponents<NZPlayer>().ToList() )
{
if ( !BossBase.IsUp( p ) ) continue;
var off = p.WorldPosition - WorldPosition;
if ( off.WithZ( 0 ).Length > MegatonBoss.GasRadius || MathF.Abs( off.z ) > 64f ) continue;
var hp = p.Components.Get<Health>( FindMode.EverythingInSelfAndDescendants );
if ( !hp.IsValid() || hp.IsDead ) continue;
// ⚠️ NEVER THE LAST POINT (upstream's `Health() > 1`)
var dmg = MathF.Min( MathF.Min( Tick, BossBase.MaxHit ), hp.Current - 1f );
if ( dmg <= 0f ) continue;
hp.Apply( dmg, false, Owner.IsValid() ? Owner : null, tick: true, source: "megaton gas" );
}
}
}