A game component implementing the Krasny boss and its fire bomb for an NZombies mode. It defines tuning parameters, state, AI behaviors (flamethrower, bomb lob, jet lunge), armour piece mechanics, networked visuals/effects, and console commands for diagnostics and tuning.
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
namespace NZombies;
/// <summary>
/// The Krasny Soldat — Black Ops Cold War's pyro mech, ported 2026-10-07 from GMod nZombies' `nz_zombie_boss_krasny` (GhostlyMoo's
/// port), the third batch. Spec: `Sbox nzombies/Docs/boss_specs/krasny.md`. His clips are the Panzer's mech animation base.
///
/// HIS ARMOUR IS THE FIGHT, the Panzer's table (upstream's `PostTookDamage`: distances to two points, not hit groups). A hit within
/// <see cref="PlateReach"/> of his eyes is the FACEPLATE's (HP half his max), within <see cref="CoreReach"/> of the core plate the
/// POWER CORE's (a quarter): the piece takes it raw and he x0.015. Every hit x0.95, x0.75 more while the plate is on, x0.25 while
/// the core is (x0.178 with both on). A piece off pays its shooter 90 and staggers him; THE FACEPLATE OFF ENRAGES HIM once he is
/// free (`berserk_1`, then the sprint, 204.4, for good) and puts his headlamp out.
/// ⚠️ His own faceplate only: upstream's `angering` is a global shared with the Panzer, the Panzerhund and the Viper.
/// ⚠️ The core's sphere sits on the core plate (`j_spine4`, measured from its mesh): upstream's `tag_powersupply` is 11 off it, and
/// a frontal shot meets the torso's hitbox 13.4 from that point, outside its 12.
/// ⚠️ A piece breaks on the hit that empties it, the Panzer's way (upstream waits for one more).
///
/// HIS MOVES (the host, while he chases: upstream's `AI()`, here four times a second). He runs at 119.1 (`run_pyro`, upstream's tier
/// 36), and after a lunge he could not take at 162.8 (`run`, upstream's `MoveFaster`) until one goes.
/// • FLAMETHROWER at <see cref="FlameMin"/>-<see cref="FlameMax"/> in sight: he keeps coming with the jet on (`ft_run`, 162.9), and
/// every 0.125 s the nearest player in it takes x<see cref="FlameWeight"/> of his swing (upstream's 6 against 50). ⚠️ `BossFlame`
/// aims it at you and walls stop it (upstream's box was world-aligned and went through walls). ⚠️ He walks while he flames:
/// upstream plants him for 4.1 s (`flamethrower_stand_aim`) at 150+, but the Panzer, whose jet this is, walks since the user
/// asked (2026-10-06); <see cref="StandToFlame"/> brings it back. ⚠️ A stagger or his roar puts it out (upstream meant to).
/// • FIRE BOMB at <see cref="BombMin"/>-<see cref="BombMax"/> in sight, every 3-7 s: aimed (`weapon_intro_to_aim`) and lobbed from
/// his gun arm on a 1.2 s arc with a 1.2 s lead (`KrasnyBomb`), it bursts on a floor into a fire pit, 100 wide for 6 s: a tick
/// every 0.75-0.9 s (upstream's 15) and 2 s alight. ⚠️ Solved from the gun (upstream's, solved from his eyes, fell short), and
/// needing no addon (upstream's needed TFA Base to exist and the BO4 wonder weapons to burn). ⚠️ A flaming ball with a trail
/// in flight (`BossShotLook`, 2026-10-07: the user could not see it).
/// • JETPACK LUNGE at anyone in sight <see cref="LungeMin"/> or more away, 10 s after his last landing, when the flight is clear:
/// `jump_start`, the flight (`launch_pad_move_loop`) on `ZombieAI.BeginArc` — 1.24 s, up to 130 over the line — down 70 short
/// of you, and his own `jump_land`. ⚠️ The clearance is the arc itself, sampled (upstream swept two world-aligned boxes), and
/// the landing has a clip (upstream's stumble is not on his model). ⚠️ NEW (2026-10-07): the landing is a burst of fire
/// <see cref="LandRadius"/> wide that sets you alight (upstream's did no damage).
/// • HIS SWING within 115: upstream's 50, a walker's.
/// • AN EXPLOSION KNOCKS HIM DOWN (`knockgetup_blend`, 7.2 s) one time in four once the core is off, then not for 8 s — upstream's
/// damage force over 50,000, which no hit here carries.
/// • ⚠️ THE USER'S REVIEW (2026-10-07): THE WEAKER HE IS, THE WIDER AND HOTTER HIS FIRE — the pit and the landing burst up to
/// x<see cref="AreaMax"/> as wide, every fire of his up to x<see cref="FireMax"/> as hot, by the health he has lost
/// (`nz_krasny` shows each one's now).
/// HE ARRIVES from the sky (`arrive_fast`), boosters burning, to a roar heard map-wide and his alarm (under a roof the short arrival
/// and a purple portal), untouchable for <see cref="SpawnProtection"/> s.
/// LEFT OUT: the dissolve stun, the DG-4 lift and the game-over taunt (their clips are not on his model), the back stack's smoke,
/// and his flame lighting ordinary zombies.
///
/// ⚠️ HOST AUTHORITY, EVERY MACHINE DRAWS: `BossFlame` (`NZNet.BossFlameOn`), `NZNet.BossPart`, the network-spawned `KrasnyBomb`;
/// the headlamp follows the faceplate's bodygroup and the jetpack the clip, on each machine.
/// ⛔ NO HIT OF HIS DOWNS ANYONE FROM FULL HEALTH (`BossBase.MaxHit`), Oberon's rule — the flame, the pit, its burn and the landing
/// burst included, however weak he is.
/// </summary>
public sealed class KrasnyBoss : BossBase
{
protected override string Tag => "[nz-krasny]";
protected override float VoiceHeight => 76f; // his eyes (upstream's `eyes`, 76.5 up)
// ══ tuning ═══════════════════════════════════════════════════════════════
//
// ⛔ NULLABLE-BACKED, INSTRUCTIONS.md §1.
static float? _plateShare, _coreShare, _plateReach, _coreReach, _pieceScale, _allScale, _plateScale, _coreScale;
static int? _piecePoints;
/// <summary>
/// The pieces' health, a share of his max (0.5 the plate, 0.25 the core); how near a hit must land, at size 1 (15 from his eyes,
/// 12 from the core plate); the table (x0.015 on a piece, x0.95 on all, x0.75 / x0.25 while each is on); 90 points a piece.
/// Upstream's, every one.
/// </summary>
public static float PlateShare { get => _plateShare ?? 0.5f; set => _plateShare = value; }
public static float CoreShare { get => _coreShare ?? 0.25f; set => _coreShare = value; }
public static float PlateReach { get => _plateReach ?? 15f; set => _plateReach = value; }
public static float CoreReach { get => _coreReach ?? 12f; set => _coreReach = value; }
public static float PieceScale { get => _pieceScale ?? 0.015f; set => _pieceScale = value; }
public static float AllScale { get => _allScale ?? 0.95f; set => _allScale = value; }
public static float PlateScale { get => _plateScale ?? 0.75f; set => _plateScale = value; }
public static float CoreScale { get => _coreScale ?? 0.25f; set => _coreScale = value; }
public static int PiecePoints { get => _piecePoints ?? 90; set => _piecePoints = value; }
static float? _flameMin, _flameMax, _flameReach, _flameHalf, _flameWeight;
static bool? _stand;
/// <summary>The jet: on for a target in sight 115-300 away (upstream's `AttackRange` to 300), 325 long and 14 to each side
/// (upstream's box, −8..20 wide), a tick every 0.125 s of x0.12 his swing (upstream's 6 against 50) — at full health, hotter as
/// he weakens (<see cref="FireMax"/>, 2026-10-07).</summary>
public static float FlameMin { get => _flameMin ?? 115f; set => _flameMin = value; }
public static float FlameMax { get => _flameMax ?? 300f; set => _flameMax = value; }
public static float FlameReach { get => _flameReach ?? 325f; set => _flameReach = value; }
public static float FlameHalfWidth { get => _flameHalf ?? 14f; set => _flameHalf = value; }
public static float FlameWeight { get => _flameWeight ?? 0.12f; set => _flameWeight = value; }
/// <summary>
/// ⚠️ OFF: upstream plants him for the whole 4.1 s `flamethrower_stand_aim` when his target is 150 or more away and within 90° of
/// his front; the Panzer, whose jet this is, walks since 2026-10-06 (the user: *"it can keep moving while using the
/// flamethrower"*). `nz_krasny_set stand 1` brings upstream's back.
/// </summary>
public static bool StandToFlame { get => _stand ?? false; set => _stand = value; }
static float? _bombMin, _bombMax, _bombGapMin, _bombGapMax, _bombFlight, _pitRadius, _pitSeconds, _pitWeight, _burnSeconds, _burnWeight;
/// <summary>
/// The fire bomb: at a target in sight 400-1250 away, every 3-7 s (upstream rolls whole seconds), a 1.2 s flight that leads a
/// moving target by as much; its pit 100 wide for 6 s, a tick of x0.3 his swing (upstream's 15 against 50); and a tick sets you
/// alight (upstream's `Ignite(2, 0)`) — 2 s of the Fire Margwa's burn (no regen, never the kill) at x0.1 his swing a second.
/// ⚠️ All at full health: wider and hotter as he weakens (<see cref="AreaMax"/>, <see cref="FireMax"/>; 2026-10-07).
/// </summary>
public static float BombMin { get => _bombMin ?? 400f; set => _bombMin = value; }
public static float BombMax { get => _bombMax ?? 1250f; set => _bombMax = value; }
public static float BombGapMin { get => _bombGapMin ?? 3f; set => _bombGapMin = value; }
public static float BombGapMax { get => _bombGapMax ?? 7f; set => _bombGapMax = value; }
public static float BombFlight { get => _bombFlight ?? 1.2f; set => _bombFlight = value; }
public static float PitRadius { get => _pitRadius ?? 100f; set => _pitRadius = value; }
public static float PitSeconds { get => _pitSeconds ?? 6f; set => _pitSeconds = value; }
public static float PitWeight { get => _pitWeight ?? 0.3f; set => _pitWeight = value; }
public static float BurnSeconds { get => _burnSeconds ?? 2f; set => _burnSeconds = value; }
public static float BurnWeight { get => _burnWeight ?? 0.1f; set => _burnWeight = value; }
static float? _lungeMin, _lungeMax, _lungeGap, _lungeSeconds, _lungeLift, _knockChance, _protect;
/// <summary>
/// The lunge: at a target in sight 500 or more away (upstream sets no limit; <see cref="LungeMax"/> 0 is none), 10 s after he
/// lands; 1.24 s in the air (upstream's 0.2 s hop and 1.04 s arc at his gravity, 950), up to 130 over the line (495² / 1900).
/// </summary>
public static float LungeMin { get => _lungeMin ?? 500f; set => _lungeMin = value; }
public static float LungeMax { get => _lungeMax ?? 0f; set => _lungeMax = value; }
public static float LungeGap { get => _lungeGap ?? 10f; set => _lungeGap = value; }
public static float LungeSeconds { get => _lungeSeconds ?? 1.24f; set => _lungeSeconds = value; }
public static float LungeLift { get => _lungeLift ?? 130f; set => _lungeLift = value; }
static float? _areaMax, _fireMax, _landRadius, _landWeight;
/// <summary>
/// THE WEAKER HE IS, THE WIDER AND HOTTER HIS FIRE (2026-10-07, the user's review: *"for kransy / the less hp it has, the bigger
/// the area of the leap and fire bomb attack should be / and the higher the damage from the fire"*). By the share of his health
/// he has lost (<see cref="Weakness"/>: 0 at full, 1 at none — his own health, not the armour pools), the fire bomb's pit and
/// the lunge's landing burst are x lerp(1, <see cref="AreaMax"/>) as wide, and every fire of his — the jet's ticks, the pit's,
/// the burn they leave, the landing burst — x lerp(1, <see cref="FireMax"/>) as hot: both x1.5 at half health, x1.9 at a tenth.
/// Read as each starts: a bomb as it leaves his gun (its pit and burn keep what it was thrown with), the jet tick by tick, the
/// lunge as he lands. ⚠️ `BossBase.MaxHit` STILL CAPS EVERY HIT AT 100.
/// </summary>
public static float AreaMax { get => _areaMax ?? 2f; set => _areaMax = value; }
public static float FireMax { get => _fireMax ?? 2f; set => _fireMax = value; }
/// <summary>
/// ⚠️ NEW, THE LANDING BURST (2026-10-07 — upstream's landing did no damage; the user asked for the leap's area to grow, so it
/// has one): fire where he comes down, <see cref="LandRadius"/> (120) round him at full health, times the area above; x<see
/// cref="LandWeight"/> (1.0) of his swing at the middle down to 0.3 of that at the rim, times the fire above; and it sets you
/// alight as his pit does (<see cref="BurnSeconds"/> of the Fire Margwa's burn).
/// </summary>
public static float LandRadius { get => _landRadius ?? 120f; set => _landRadius = value; }
public static float LandWeight { get => _landWeight ?? 1f; set => _landWeight = value; }
/// <summary>An explosion's chance to knock him down once the core is off (0.25), and how long he is untouchable after he
/// arrives (5 s) — upstream's.</summary>
public static float KnockChance { get => _knockChance ?? 0.25f; set => _knockChance = value; }
public static float SpawnProtection { get => _protect ?? 5f; set => _protect = value; }
// his clips (30 fps) and their events — ⛔ `const`, never `static readonly` (INSTRUCTIONS.md §1)
const string ClipArrive = "nz_soldat_arrive_fast", ClipArriveRoof = "nz_soldat_arrive_tomb_short", ClipMantle = "nz_soldat_mantle_48",
ClipStand = "nz_soldat_flamethrower_stand_aim", ClipAim = "nz_soldat_weapon_intro_to_aim", ClipFire = "nz_soldat_weapon_fire",
ClipJump = "nz_soldat_jump_start", ClipFly = "nz_soldat_launch_pad_move_loop", ClipLand = "nz_soldat_jump_land",
ClipBerserk = "nz_soldat_berserk_1", ClipPainPlate = "nz_soldat_pain_faceplate", ClipPainCore = "nz_soldat_powercore_pain",
ClipKnock = "nz_soldat_knockgetup_blend";
const float ArriveLand = 28f / 30f, ArriveJetOff = 27f / 30f, RoofLand = 5f / 30f, RoofJetOff = 3f / 30f, StandLen = 123f / 30f,
AimLen = 35f / 30f, FireLen = 27f / 30f, JumpLen = 21f / 30f, LandLen = 16f / 30f, BerserkLen = 41f / 30f,
BerserkYell = 21f / 30f, PainPlateLen = 59f / 30f, PainCoreLen = 65f / 30f, KnockLen = 216f / 30f, KnockThud = 17f / 30f;
/// <summary>His clips' own speeds — `krasny.zvar`'s tiers are picked by them (the flame walk a hair over the run, the Panzer's way).</summary>
const float RunSpeed = 119.1f, FastSpeed = 162.8f, FlameSpeed = 162.9f, SprintSpeed = 204.4f;
const float FlameTick = 0.125f, StandMin = 150f, LandShort = 70f, StunGap = 8f;
/// <summary>The landing burst's rim against its middle, and how long its rim stays drawn on the floor (2026-10-07).</summary>
const float LandRim = 0.3f, LandRingSeconds = 0.5f;
/// <summary>
/// From the QC, each in its bone's frame: `eyes` (`j_head`), `tag_headlamp_fx` (`j_spine4`, its sprite a unit out), the
/// `tag_gun_spin` attachment, `tag_flamethrower_fx` (`j_elbow_ri`) — and the core plate's own middle (`j_spine4`, from its mesh).
/// </summary>
static Vector3 EyesOffset => new( 3.8f, 3.6f, 0f );
static Vector3 LampOffset => new( 33.4f, 0.5f, -4.2f );
static Vector3 MuzzleOffset => new( 19f, -1.6f, 1.2f );
static Vector3 NozzleOffset => new( -34f, -0.3f, -8.4f );
static Vector3 CoreOffset => new( 5f, 10f, 0f );
static Color Dust => new( 0.75f, 0.65f, 0.5f );
// ══ state ════════════════════════════════════════════════════════════════
BossFlame _flame;
BossGlow _lamp;
readonly List<BossGlow> _boosters = new();
bool _armed, _plateOn = true, _coreOn = true, _enraged, _enrageDue, _flaming, _standing, _hurry, _lunging, _jetSounding;
float _plateHp, _coreHp, _protectedUntil, _nextThink, _nextBurn, _nextBomb, _nextLunge, _nextStun, _nextSpark, _flameForced;
float _nextThrust, _clipSince, _trackedAt;
string _lastClip = "";
GameObject _bombTarget, _tracked;
Vector3 _trackedPos, _targetVel;
SoundHandle _jetLoop;
enum Beat { Unprotect, Arrive, BombFire, Fly, Land, Yell, Thud }
public bool Enraged => _enraged;
public bool PlateOn => _plateOn;
public bool CoreOn => _coreOn;
/// <summary>The share of his health he has lost: 0 at full, 1 at none — his own, not the armour pools' (2026-10-07).</summary>
public float Weakness => Hp.IsValid() && Hp.Max > 1f ? Math.Clamp( 1f - Hp.Current / Hp.Max, 0f, 1f ) : 0f;
/// <summary>How much wider his pit and landing burst are now, and how much hotter his fire: lerp(1, <see cref="AreaMax"/>) and
/// lerp(1, <see cref="FireMax"/>) by <see cref="Weakness"/>.</summary>
public float AreaFactor => 1f + (AreaMax - 1f) * Weakness;
public float FireFactor => 1f + (FireMax - 1f) * Weakness;
/// <summary>One Krasny in 50 (by his id, so every machine agrees) flies to the joyride, and every one on 1 April — upstream's roll.</summary>
bool Joyride => (uint)GameObject.Id.GetHashCode() % 50u == 0u || DateTime.Now is { Month: 4, Day: 1 };
protected override void BossStart()
{
// ⚠️ EVERY MACHINE: the jet (the Panzer's own nozzle), his headlamp (upstream's sprite, while the faceplate is on) and the
// boosters' flames (`bo3_panzer_engine`: the drop, the lunge)
_flame = Components.GetOrCreate<BossFlame>();
_flame.Bone = "j_elbow_ri";
_flame.Offset = NozzleOffset;
_flame.Reach = FlameReach;
_lamp = Components.Create<BossGlow>();
_lamp.Bone = "j_spine4";
_lamp.Offset = LampOffset;
_lamp.Colour = new Color( 1f, 1f, 0.85f );
_lamp.Radius = 280f;
_lamp.Brightness = 2.5f;
foreach ( var bone in new[] { "tag_booster_le_fx", "tag_booster_ri_fx" } )
{
var g = Components.Create<BossGlow>();
g.Bone = bone;
g.Colour = new Color( 1f, 0.55f, 0.2f );
g.Radius = 140f;
g.Brightness = 2.4f;
g.SparkEvery = 0.05f;
g.SparkReach = 26f;
g.SparkColour = PanzerFx.SparkColour;
g.Lit = false;
_boosters.Add( g );
}
}
protected override void HostStart()
{
Ai.MaxHitDamage = MaxHit;
// ⛔ NONE OF THE WALKER'S CLIMB CLIPS ON HIS MODEL (Oberon's note), nor upstream's `mantle_36`: the Panzer's mantle, in place
Ai.ClimbClipOverride = ClipMantle;
// untouchable while he arrives and a little after: his table takes nothing, and his health refuses the rest
_protectedUntil = Time.Now + SpawnProtection;
if ( Hp.IsValid() ) Hp.Invulnerable = true;
Queue( SpawnProtection, (int)Beat.Unprotect );
_nextBomb = Time.Now + 3f;
_nextLunge = Time.Now + 5f;
_nextStun = Time.Now + StunGap;
// the boss round's arrival, to everyone (upstream's `spawnfunc`)
NZSound.PlayShared( "nz.krasny.intro" );
// ⚠️ A ROOF ABOVE: the drop would come through it — the short arrival (upstream's hull up 750, the Panzer's rule; upstream's
// also counted a zombie in that column)
var roof = Scene.Trace.Ray( WorldPosition + Vector3.Up * 16f, WorldPosition + Vector3.Up * 750f )
.WithoutTags( "zombie", "player", "trigger" ).Run();
var landAt = ArriveLand;
if ( roof.Hit && Body.IsValid() )
{
Body.Sequence.Name = ClipArriveRoof;
Body.Sequence.Time = 0f;
if ( Networking.IsActive ) NZNet.ZombieClip( GameObject.Id, ClipArriveRoof, 1f );
// upstream's `panzer_spawn_tp`: a purple implosion 80 up
BossFx.BurstShared( WorldPosition + Vector3.Up * 80f, 140f * Size, new Color( 0.62f, 0.3f, 1f ), 10 );
landAt = RoofLand;
}
Queue( landAt, (int)Beat.Arrive );
Say( $"spawned — {Hp?.Max ?? 0f:0} hp · {(roof.Hit ? "short arrival (roof)" : "drop")} · untouchable {SpawnProtection:0.#}s"
+ $" · flame {FlameMin:0}-{FlameMax:0}u · bomb {BombMin:0}-{BombMax:0}u · lunge from {LungeMin:0}u"
+ $"{(Joyride ? " · JOYRIDE" : "")} · hitboxes {Ai.HitboxCount}" );
}
protected override void EveryFrame()
{
// the headlamp: lit while the faceplate is on — the bodygroup every machine was told (`NZNet.BossPart`)
if ( _lamp.IsValid() && Body.IsValid() ) _lamp.Lit = Body.GetBodyGroup( "faceplate" ) == 0;
// the boosters: for the drop (`arrive_fast` frames 1-27, the roof's 1-3) and a lunge — read off the clip this machine plays
var clip = Ai.CurrentClip;
if ( clip != _lastClip )
{
_lastClip = clip;
_clipSince = Time.Now;
}
var since = Time.Now - _clipSince;
var lunge = clip is ClipJump or ClipFly;
var jet = lunge || (clip == ClipArrive && since < ArriveJetOff) || (clip == ClipArriveRoof && since < RoofJetOff);
foreach ( var g in _boosters )
if ( g.IsValid() ) g.Lit = jet;
if ( jet && Time.Now >= _nextThrust )
{
_nextThrust = Time.Now + 0.06f;
var down = (Vector3.Down - Facing * 0.35f).Normal;
foreach ( var g in _boosters )
if ( g.IsValid() ) PanzerFlamePuff.Throw( g.Point, down );
}
// ⚠️ THE JET'S ROAR IS EACH MACHINE'S OWN, started and stopped by the clip it sees (`BossFlame`'s way): a looping sound sent
// through `NZSound.PlayShared` could never be stopped on the others
if ( lunge != _jetSounding )
{
_jetSounding = lunge;
if ( lunge ) _jetLoop = NZSound.Play( Joyride ? "nz.krasny.jet.joyride" : "nz.krasny.jet", Voice );
else if ( _jetLoop.IsValid() ) _jetLoop.Stop();
}
if ( _jetSounding && _jetLoop.IsValid() ) _jetLoop.Position = Voice;
}
protected override void HostFrame()
{
Ai.MaxHitDamage = MaxHit;
Arm();
TrackTarget();
if ( Ai.State == ZombieState.Spawning ) return;
// the jet is out the moment he does anything but chase (or stand to flame): a swing, a stagger, a lunge, a throw
if ( _standing && Ai.State != ZombieState.Special ) StopFlame();
if ( _flaming && !_standing && Ai.State != ZombieState.Chasing ) StopFlame();
// ⚠️ ENRAGED BY HIS OWN FACEPLATE once he is free: after its stagger, or the lunge or the throw it came off during
if ( _enrageDue && Ai.State == ZombieState.Chasing ) Enrage();
if ( Time.Now >= _nextThink )
{
_nextThink = Time.Now + 0.25f;
Think();
}
TickPace();
if ( !_flaming || Time.Now < _nextBurn ) return;
_nextBurn = Time.Now + FlameTick;
// ⚠️ HOTTER AS HE WEAKENS, read tick by tick (the user's review, 2026-10-07: `FireMax`)
BossFlame.Burn( GameObject, _flame.Nozzle, _flame.Aim, FlameReach * Size, FlameHalfWidth * Size,
SwingDamage( FlameWeight ) * FireFactor, "krasny flame" );
}
protected override void Died( bool host )
{
if ( _jetLoop.IsValid() ) _jetLoop.Stop();
_jetSounding = false;
if ( !host ) return;
_lunging = false;
StopFlame();
NZSound.PlayShared( "nz.panzer.killshot", Voice );
Say( "killed" );
}
protected override void OnDestroy()
{
if ( _jetLoop.IsValid() ) _jetLoop.Stop();
}
protected override void OnBeat( int beat )
{
switch ( (Beat)beat )
{
case Beat.Unprotect:
if ( Hp.IsValid() ) Hp.Invulnerable = false;
break;
case Beat.Arrive:
// `mech_land` + `mech_alarm` (the stomps are `WalkerFootsteps`'): the thud, his alarm, the shake, the dust
NZSound.PlayShared( "nz.krasny.land", WorldPosition );
NZSound.PlayShared( "nz.panzer.alarm2", Voice );
NZNet.ShakeAt( WorldPosition, 0.6f, 1200f );
ShockRing.FireShared( WorldPosition + Vector3.Up * 4f, 180f * Size, Dust );
break;
case Beat.BombFire:
FireBomb();
break;
case Beat.Fly:
if ( _lunging ) SwapClip( ClipFly );
break;
case Beat.Land:
if ( !_lunging ) break;
_lunging = false;
// ⚠️ HIS OWN `jump_land`: upstream looked for a stumble this model does not have, and landed straight into his run
SwapClip( ClipLand );
Ai.HoldSpecialFor( LandLen );
NZSound.PlayShared( "nz.krasny.jet.stop", Voice );
NZSound.PlayShared( "nz.krasny.land", WorldPosition );
NZNet.ShakeAt( WorldPosition, 0.5f, 1000f );
ShockRing.FireShared( WorldPosition + Vector3.Up * 4f, 160f * Size, Dust );
LandBurst();
break;
case Beat.Yell:
NZSound.PlayShared( "nz.krasny.rage", Voice ); // `mech_yell`
break;
case Beat.Thud:
NZSound.PlayShared( "nz.krasny.land", WorldPosition ); // the knockdown's `mech_land`
NZNet.ShakeAt( WorldPosition, 0.5f, 1000f );
break;
}
}
// ══ the armour ═══════════════════════════════════════════════════════════
/// <summary>His table (see the header): the piece a hit lands on takes it raw; nothing while he arrives; with the core off an
/// explosion may knock him down. THE HOST, ⛔ once a hit — it counts.</summary>
protected override float ScaleDamage( float amount, in DamageInfo damage )
{
if ( Time.Now < _protectedUntil ) return 0f;
Arm();
var scale = 1f;
var at = damage.Position;
if ( _armed && !at.IsNearlyZero() )
{
if ( _plateOn && at.Distance( PlatePoint ) <= PlateReach * Size )
{
_plateHp -= MathF.Max( 0f, amount );
ArmourHit( at );
scale = PieceScale;
if ( _plateHp <= 0f ) BreakPiece( 0, damage.Attacker );
}
else if ( _coreOn && at.Distance( CorePoint ) <= CoreReach * Size )
{
_coreHp -= MathF.Max( 0f, amount );
ArmourHit( at );
scale = PieceScale;
if ( _coreHp <= 0f ) BreakPiece( 1, damage.Attacker );
}
}
TryKnockdown( damage );
return scale * AllScale * (_plateOn ? PlateScale : 1f) * (_coreOn ? CoreScale : 1f);
}
/// <summary>The pieces' health, once his own is known (`ZombieAI` may size it after this component starts — the Panzer's note).</summary>
void Arm()
{
if ( _armed || !Hp.IsValid() || Hp.Max <= 1f ) return;
_armed = true;
_plateHp = Hp.Max * PlateShare;
_coreHp = Hp.Max * CoreShare;
}
Vector3 PlatePoint => Body.IsValid() && Body.TryGetBoneTransform( "j_head", out var head ) ? head.PointToWorld( EyesOffset ) : Voice;
Vector3 CorePoint => Body.IsValid() && Body.TryGetBoneTransform( "j_spine4", out var spine ) ? spine.PointToWorld( CoreOffset )
: WorldPosition + Vector3.Up * 60f * Size;
/// <summary>A clank and sparks off a piece, throttled per bullet (upstream's metal impact and `npcarmor_hit`).</summary>
void ArmourHit( Vector3 at )
{
if ( Time.Now < _nextSpark ) return;
_nextSpark = Time.Now + 0.08f;
NZSound.PlayShared( "nz.panzer.armor.hit", at );
NZNet.PanzerSparks( at );
}
/// <summary>A piece off: everyone sees it go, its shooter gets 90, he staggers — and the faceplate enrages him once he is free. THE HOST.</summary>
void BreakPiece( int piece, GameObject attacker )
{
if ( piece == 0 ) { if ( !_plateOn ) return; _plateOn = false; }
else { if ( !_coreOn ) return; _coreOn = false; }
var at = piece == 0 ? PlatePoint : CorePoint;
SetPart( piece == 0 ? "faceplate" : "powercap", 1 );
BossFx.BurstShared( at, 20f * Size, PanzerFx.SparkColour, 12 );
NZSound.PlayShared( "nz.panzer.armor.break", at );
NZSound.PlayShared( "nz.krasny.pain", Voice );
// ⚠️ THE PAY GOES THROUGH `AddPoints`, which relays to a client's own machine — and only to a player (upstream's
// `attacker:GivePoints` threw on anything else, and that hit then landed unscaled)
var shooter = attacker.IsValid() ? attacker.Components.Get<NZPlayer>( FindMode.EverythingInSelfAndAncestors ) : null;
if ( shooter.IsValid() && PiecePoints > 0 ) shooter.AddPoints( PiecePoints );
StopFlame();
_nextStun = Time.Now + StunGap;
PlayShared( piece == 0 ? ClipPainPlate : ClipPainCore, piece == 0 ? PainPlateLen : PainCoreLen );
if ( piece == 0 ) _enrageDue = true;
Say( $"{(piece == 0 ? "FACEPLATE" : "POWER CORE")} OFF · {(shooter.IsValid() ? $"+{PiecePoints} to {shooter.GameObject.Name}" : "no shooter")}"
+ $" · now x{AllScale * (_plateOn ? PlateScale : 1f) * (_coreOn ? CoreScale : 1f):0.###}" );
}
/// <summary>With the core off, an explosion has a <see cref="KnockChance"/> chance to put him down, then not for 8 s. THE HOST.</summary>
void TryKnockdown( in DamageInfo damage )
{
if ( _coreOn || KnockChance <= 0f || Time.Now < _nextStun ) return;
if ( Ai.State is not (ZombieState.Chasing or ZombieState.Attacking or ZombieState.Idle) ) return;
// an explosion is its tags (the grenade's, a tech blast's — `Health.GorePartOf`'s reading; `DamageInfo.IsExplosion` is obsolete)
var tags = damage.Tags;
var blast = (tags?.Has( Health.ExplosionTag ) ?? false) || (tags?.Has( TechBlast.BlastTag ) ?? false);
if ( blast && Game.Random.Float( 0f, 1f ) < KnockChance ) Knockdown();
}
/// <summary>Down and up again (`knockgetup_blend`, 7.2 s), the jet out. THE HOST.</summary>
void Knockdown()
{
StopFlame();
if ( !PlayShared( ClipKnock, KnockLen ) ) return;
_nextStun = Time.Now + StunGap;
Queue( KnockThud, (int)Beat.Thud );
Say( "knocked down" );
}
/// <summary>Enraged, once: `berserk_1` and his yell, then the sprint for good (upstream's `SetRunSpeed(71)`). THE HOST.</summary>
void Enrage()
{
if ( _enraged ) return;
_enraged = true;
_enrageDue = false;
_hurry = false;
StopFlame();
if ( PlayShared( ClipBerserk, BerserkLen ) ) Queue( BerserkYell, (int)Beat.Yell );
else NZSound.PlayShared( "nz.krasny.rage", Voice );
Say( "ENRAGED — the sprint from now on" );
}
// ══ the host's moves ═════════════════════════════════════════════════════
/// <summary>Upstream's `AI()` while he chases, in its order: the flamethrower, the lunge, the fire bomb. THE HOST.</summary>
void Think()
{
if ( Ai.State != ZombieState.Chasing ) return;
var p = TargetPlayer;
if ( !IsUp( p ) )
{
StopFlame();
return;
}
var d = WorldPosition.Distance( p.WorldPosition );
var seen = CanSee( p.GameObject );
// ── the flamethrower: on in its band and in sight, off out of it ──
var flame = Time.Now < _flameForced || (seen && d >= FlameMin * Size && d < FlameMax * Size);
if ( flame && !_flaming )
{
if ( StandToFlame && d >= StandMin * Size && FacingWithin( p.WorldPosition, 90f ) && PlayShared( ClipStand, StandLen ) )
_standing = true;
StartFlame();
if ( _standing ) return;
}
else if ( !flame && _flaming ) StopFlame();
// ── the lunge: a clear flight lifts him; a blocked one hurries him on foot and he looks again soon ──
if ( Time.Now >= _nextLunge && seen && d >= LungeMin * Size && (LungeMax <= 0f || d <= LungeMax * Size) && OnGround( p ) )
{
if ( FlightClear( p, out var landing ) )
{
Lunge( p, landing );
return;
}
_nextLunge = Time.Now + 0.5f;
if ( !_enraged ) _hurry = true;
}
// ── the fire bomb ──
if ( Time.Now >= _nextBomb && seen && !_flaming && d >= BombMin * Size && d < BombMax * Size ) Lob( p );
}
/// <summary>
/// His pace for what he is doing: the flame walk, the sprint (enraged), the hurry, his run. ⚠️ NEVER MID-MOVE: a re-pick pushes the
/// agent's speed back, where a special roots him by zeroing it. THE HOST.
/// </summary>
void TickPace()
{
if ( Ai.State is ZombieState.Special or ZombieState.Spawning ) return;
var speed = _flaming ? FlameSpeed : _enraged ? SprintSpeed : _hurry ? FastSpeed : RunSpeed;
if ( MathF.Abs( Ai.SpeedOverride - MathF.Max( 1f, speed * Size ) ) > 0.01f ) SetSpeed( speed );
}
void StartFlame()
{
if ( _flaming ) return;
_flaming = true;
_nextBurn = Time.Now + 0.2f;
NZNet.BossFlameOn( GameObject.Id, true );
}
void StopFlame()
{
_standing = false;
if ( !_flaming ) return;
_flaming = false;
NZNet.BossFlameOn( GameObject.Id, false );
}
/// <summary>Aim (`weapon_intro_to_aim`); the bomb leaves his gun arm as `weapon_fire` plays (<see cref="FireBomb"/>). THE HOST.</summary>
bool Lob( NZPlayer p )
{
Face( p.WorldPosition );
if ( !PlayShared( ClipAim, AimLen + FireLen ) ) return false;
_bombTarget = p.GameObject;
_nextBomb = Time.Now + AimLen + FireLen + Game.Random.Float( BombGapMin, BombGapMax );
Queue( AimLen, (int)Beat.BombFire );
return true;
}
/// <summary>
/// The bomb leaves his gun (`tag_gun_spin`) for where its target will be in <see cref="BombFlight"/> s, by its measured pace
/// (<see cref="TrackTarget"/>). ⚠️ SOLVED FROM THE GUN: upstream solved it from his eyes and launched it from his arm, short and
/// low. THE HOST.
/// </summary>
void FireBomb()
{
var target = _bombTarget.IsValid() ? _bombTarget : Ai.Target;
if ( Ai.State != ZombieState.Special || !target.IsValid() ) return;
Face( target.WorldPosition );
SwapClip( ClipFire );
var from = Muzzle;
var lead = target == _tracked ? _targetVel.WithZ( 0f ) * BombFlight : Vector3.Zero;
// ⚠️ AS WEAK AS HE IS AS IT LEAVES HIS GUN (the user's review, 2026-10-07): its pit that much wider, its ticks and its burn
// that much hotter, for the pit's whole life
var fire = FireFactor;
KrasnyBomb.Launch( Scene, from, KrasnyBomb.VelocityFor( from, target.WorldPosition + lead, BombFlight ), GameObject,
SwingDamage( PitWeight ) * fire, SwingDamage( BurnWeight ) * fire, PitRadius * Size * AreaFactor, PitSeconds );
// upstream's AR2 alt-fire is in no archive: the Panzer's arm launcher instead, and his attack vox
NZSound.PlayShared( "nz.panzer.claw.fire", from );
NZSound.PlayShared( "nz.krasny.attack", Voice );
NZNet.PanzerSparks( from );
}
Vector3 Muzzle => Body.IsValid() && Body.TryGetBoneTransform( "tag_gun_spin", out var gun ) ? gun.PointToWorld( MuzzleOffset )
: WorldPosition + (Vector3.Up * 50f + Facing * 30f) * Size;
/// <summary>
/// His target player's pace, measured from where they have been every 0.1 s — the player's own object, the one a throw aims at
/// (`Ai.Target` may be a part of it). ⚠️ MEASURED: on the host a client's controller is a copy whose velocity nothing promises.
/// THE HOST.
/// </summary>
void TrackTarget()
{
if ( Time.Now < _trackedAt + 0.1f ) return;
var p = TargetPlayer;
var t = p.IsValid() ? p.GameObject : null;
if ( t != _tracked )
{
_tracked = t;
_targetVel = Vector3.Zero;
_trackedAt = 0f;
}
if ( !t.IsValid() ) return;
if ( _trackedAt > 0f ) _targetVel = Vector3.Lerp( _targetVel, (t.WorldPosition - _trackedPos) / (Time.Now - _trackedAt), 0.5f );
_trackedPos = t.WorldPosition;
_trackedAt = Time.Now;
}
/// <summary>Up on the boosters and down 70 short of them (`ZombieAI.BeginArc`), `jump_start` into the flight. THE HOST.</summary>
void Lunge( NZPlayer p, Vector3 landing )
{
Face( p.WorldPosition );
if ( !PlayShared( ClipJump, LungeSeconds + LandLen + 0.1f ) ) return;
if ( !Ai.BeginArc( landing, LungeSeconds, LungeLift * Size ) )
{
Ai.CancelSpecial();
return;
}
_lunging = true;
_hurry = false;
_nextLunge = Time.Now + LungeSeconds + LungeGap;
NZSound.PlayShared( "nz.krasny.jet.start", Voice );
NZNet.ShakeAt( WorldPosition, 0.3f, 800f );
Queue( JumpLen, (int)Beat.Fly );
Queue( LungeSeconds, (int)Beat.Land );
Say( $"LUNGE — {WorldPosition.Distance( landing ):0}u in {LungeSeconds:0.##}s" );
}
/// <summary>
/// ⚠️ NEW (2026-10-07, the user's review — upstream's landing did no damage): fire where he comes down, as wide and as hot as he
/// is weak as he lands (<see cref="LandRadius"/>). Every player up within it, from the feet as his pit, in its line, takes
/// x<see cref="LandWeight"/> of his swing at the middle down to 0.3 of that at the rim (never more than `BossBase.MaxHit`: an
/// area hit, Oberon's leap's way) and is set alight as the pit sets them (`NZNet.MargwaBurn`, never the kill). Everyone sees
/// it: a fire burst the size of it, its rim on the floor for half a second, the bomb's own blast — and the landing's thud and
/// shake. THE HOST.
/// </summary>
void LandBurst()
{
if ( LandRadius <= 0f ) return; // `nz_krasny_set landradius 0`: upstream's harmless landing
var fire = FireFactor;
var radius = MathF.Max( 16f, LandRadius * Size * AreaFactor );
var at = WorldPosition;
var middle = at + Vector3.Up * 40f * Size;
var hit = SwingDamage( LandWeight ) * fire;
var burn = SwingDamage( BurnWeight ) * fire;
var burned = 0;
foreach ( var p in Scene.GetAllComponents<NZPlayer>().ToList() )
{
if ( !IsUp( p ) ) continue;
var d = p.WorldPosition.Distance( at );
if ( d > radius ) continue;
var chest = p.WorldPosition + Vector3.Up * 40f;
var tr = Scene.Trace.Ray( middle, chest ).IgnoreGameObjectHierarchy( GameObject )
.WithoutTags( "zombie", "player", "trigger", "ragdoll" ).Run();
if ( tr.Hit && tr.HitPosition.Distance( chest ) > 24f ) continue;
var share = 1f - (1f - LandRim) * (d / radius);
if ( !HurtPlayer( p, hit * share, blast: true, at: at, source: "krasny landing" ) ) continue;
if ( burn > 0f && BurnSeconds > 0f ) NZNet.MargwaBurn( p.GameObject.Id, BurnSeconds, burn );
burned++;
}
var c = PanzerFx.FireColour;
BossFx.BurstShared( at + Vector3.Up * 8f, radius, c, 14 );
NZNet.BossTelegraph( at, radius, LandRingSeconds, c.r, c.g, c.b );
NZSound.PlayShared( "nz.krasny.bomb.explode", at );
Say( $"landing burst — {radius:0}u (x{AreaFactor:0.##}), {MathF.Min( hit, MaxHit ):0.#} at the middle (x{fire:0.##})"
+ $" · {burned} burned" );
}
/// <summary>
/// Is the flight clear, and where it comes down: 70 short of them, on the navmesh. ⚠️ THE ARC ITSELF, sampled — four legs at his
/// knees and his head, lifted as `BeginArc` will lift him — where upstream swept two boxes aligned to the WORLD (one 200 along
/// world +X from him, one above the target): a different volume for every way he faced. THE HOST.
/// </summary>
bool FlightClear( NZPlayer p, out Vector3 landing )
{
var from = WorldPosition;
var course = (p.WorldPosition - from).WithZ( 0f );
var dist = course.Length;
var reach = MathF.Max( 0f, dist - LandShort * Size );
landing = ZombieAI.NavGround( Scene, (from + course / MathF.Max( 1f, dist ) * reach).WithZ( p.WorldPosition.z ) );
var lift = LungeLift * Size;
var prev = from;
for ( int i = 1; i <= 4; i++ )
{
var t = i / 4f;
var at = Vector3.Lerp( from, landing, t ) + Vector3.Up * MathF.Sin( t * MathF.PI ) * lift;
if ( Blocked( prev, at, 24f * Size ) || Blocked( prev, at, 88f * Size ) ) return false;
prev = at;
}
return true;
}
bool Blocked( Vector3 a, Vector3 b, float height ) => Scene.Trace.Ray( a + Vector3.Up * height, b + Vector3.Up * height )
.IgnoreGameObjectHierarchy( GameObject ).WithoutTags( "zombie", "player", "trigger", "ragdoll" ).Run().Hit;
/// <summary>Is the player on the floor (upstream's `IsOnGround`): a trace, which a client's copy on the host answers as well.</summary>
bool OnGround( NZPlayer p ) => Scene.Trace.Ray( p.WorldPosition + Vector3.Up * 4f, p.WorldPosition + Vector3.Down * 24f )
.WithoutTags( "player", "zombie", "trigger", "ragdoll" ).Run().Hit;
// ══ diagnostics ══════════════════════════════════════════════════════════
static IEnumerable<KrasnyBoss> All
=> Game.ActiveScene?.GetAllComponents<KrasnyBoss>() ?? Enumerable.Empty<KrasnyBoss>();
/// <summary>Every Krasny alive, for a command that makes one act — on the host; a client is told the host decides.</summary>
static List<KrasnyBoss> Acting()
{
if ( NZGame.IsClient ) { Log.Info( "[nz-krasny] the host decides" ); return new(); }
return All.Where( k => k.Ai.IsValid() && !k.Dead ).ToList();
}
/// <summary>`nz_krasny` — every Krasny alive, and the tuning.</summary>
[ConCmd( "nz_krasny" )]
public static void Report()
{
Log.Info( $"[nz-krasny] plate {PlateShare:0.##} of max within {PlateReach:0} of his eyes, core {CoreShare:0.##} within {CoreReach:0}"
+ $" of its plate · piece x{PieceScale:0.###}, all x{AllScale:0.##}, plate on x{PlateScale:0.##}, core on x{CoreScale:0.##}"
+ $" · +{PiecePoints} a piece · knockdown {KnockChance * 100f:0}% · untouchable {SpawnProtection:0.#}s · hit cap {MaxHit:0}" );
Log.Info( $"[nz-krasny] flame {FlameMin:0}-{FlameMax:0}u x{FlameWeight:0.##} every {FlameTick:0.###}s{(StandToFlame ? ", standing" : ", walking")}"
+ $" · bomb {BombMin:0}-{BombMax:0}u every {BombGapMin:0.#}-{BombGapMax:0.#}s, pit {PitRadius:0}u {PitSeconds:0.#}s x{PitWeight:0.##}"
+ $" + {BurnSeconds:0.#}s alight · lunge from {LungeMin:0}{(LungeMax > 0f ? $" to {LungeMax:0}" : "")} every {LungeGap:0}s,"
+ $" {LungeSeconds:0.##}s up to {LungeLift:0}" );
Log.Info( $"[nz-krasny] weaker = wider and hotter: area to x{AreaMax:0.##}, fire to x{FireMax:0.##} at no health"
+ $" · landing burst {(LandRadius > 0f ? $"{LandRadius:0}u, x{LandWeight:0.##} at the middle to x{LandWeight * LandRim:0.##} at the rim" : "OFF")}" );
var list = All.ToList();
Log.Info( $"[nz-krasny] {list.Count} alive" );
foreach ( var k in list )
Log.Info( $"[nz-krasny] hp {k.Hp?.Current ?? 0f:0}/{k.Hp?.Max ?? 0f:0} · plate {(k._plateOn ? $"{k._plateHp:0}" : "OFF")}"
+ $" · core {(k._coreOn ? $"{k._coreHp:0}" : "OFF")} · state {k.Ai?.State} · speed {k.Ai?.MoveSpeed ?? 0f:0}"
+ $"{(k._enraged ? " · ENRAGED" : "")}{(k._flaming ? " · FLAMING" : "")}{(k._hurry ? " · hurrying" : "")}{(k._lunging ? " · LUNGING" : "")}"
+ $" · bomb in {MathF.Max( 0f, k._nextBomb - Time.Now ):0.#}s · lunge in {MathF.Max( 0f, k._nextLunge - Time.Now ):0.#}s"
+ $" · hitboxes {k.Ai?.HitboxCount ?? 0}"
+ $" · lost {k.Weakness * 100f:0}%: area x{k.AreaFactor:0.##} (pit {PitRadius * k.Size * k.AreaFactor:0}u,"
+ $" landing {LandRadius * k.Size * k.AreaFactor:0}u), fire x{k.FireFactor:0.##}" );
}
/// <summary>`nz_krasny_break <0|1>` — shoot the faceplate (0: he enrages) or the power core (1) off every Krasny.</summary>
[ConCmd( "nz_krasny_break" )]
public static void BreakCmd( int piece = 0 )
{
foreach ( var k in Acting() )
{
k.Arm();
k.BreakPiece( Math.Clamp( piece, 0, 1 ), null );
}
}
/// <summary>`nz_krasny_flame` — every Krasny lights up for 3 s, walking, whoever is in front.</summary>
[ConCmd( "nz_krasny_flame" )]
public static void FlameCmd()
{
foreach ( var k in Acting() )
{
k._flameForced = Time.Now + 3f;
k.StartFlame();
}
}
/// <summary>`nz_krasny_bomb` — every Krasny lobs a fire bomb at its target now, whatever the range.</summary>
[ConCmd( "nz_krasny_bomb" )]
public static void BombCmd()
{
foreach ( var k in Acting() )
if ( IsUp( k.TargetPlayer ) ) k.Lob( k.TargetPlayer );
}
/// <summary>`nz_krasny_lunge` — every Krasny lunges at its target now, whatever the range, if the flight is clear (and bursts
/// into fire where it lands, 2026-10-07).</summary>
[ConCmd( "nz_krasny_lunge" )]
public static void LungeCmd()
{
foreach ( var k in Acting() )
{
var p = k.TargetPlayer;
if ( !IsUp( p ) ) continue;
if ( k.FlightClear( p, out var landing ) ) k.Lunge( p, landing );
else Log.Info( "[nz-krasny] no clear flight to its target" );
}
}
/// <summary>`nz_krasny_knock` — every Krasny is knocked down now (`knockgetup_blend`).</summary>
[ConCmd( "nz_krasny_knock" )]
public static void KnockCmd()
{
foreach ( var k in Acting() ) k.Knockdown();
}
/// <summary>`nz_krasny_set <key> <value>` — retune him live.</summary>
[ConCmd( "nz_krasny_set" )]
public static void SetCmd( string key = "", float value = 0f )
{
switch ( key.ToLowerInvariant() )
{
case "plate": PlateShare = value; break;
case "core": CoreShare = value; break;
case "platereach": PlateReach = value; break;
case "corereach": CoreReach = value; break;
case "piece": PieceScale = value; break;
case "points": PiecePoints = (int)value; break;
case "flame": FlameWeight = value; break;
case "flamemin": FlameMin = value; break;
case "flamemax": FlameMax = value; break;
case "stand": StandToFlame = value > 0f; break;
case "bombmin": BombMin = value; break;
case "bombmax": BombMax = value; break;
case "gapmin": BombGapMin = value; break;
case "gapmax": BombGapMax = value; break;
case "pit": PitWeight = value; break;
case "pitradius": PitRadius = value; break;
case "pitseconds": PitSeconds = value; break;
case "burn": BurnWeight = value; break;
case "lungemin": LungeMin = value; break;
case "lungemax": LungeMax = value; break;
case "lungegap": LungeGap = value; break;
case "lungetime": LungeSeconds = value; break;
case "lift": LungeLift = value; break;
case "knock": KnockChance = value; break;
case "protect": SpawnProtection = value; break;
case "areamax": AreaMax = value; break;
case "firemax": FireMax = value; break;
case "landradius": LandRadius = value; break;
case "landweight": LandWeight = value; break;
default:
Log.Info( "[nz-krasny] nz_krasny_set <plate|core|platereach|corereach|piece|points|flame|flamemin|flamemax|stand|bombmin"
+ "|bombmax|gapmin|gapmax|pit|pitradius|pitseconds|burn|lungemin|lungemax|lungegap|lungetime|lift|knock|protect"
+ "|areamax|firemax|landradius|landweight> <value>" );
return;
}
Log.Info( $"[nz-krasny] {key} = {value:0.###}" );
Report();
}
}
/// <summary>
/// The Krasny Soldat's fire bomb (2026-10-07), upstream's `nz_glowstick_fire`: lobbed on an arc at GMod's gravity, thrown back by a
/// wall, a ceiling or a steep slope, it bursts on the first floor it meets into a fire pit for <see cref="Seconds"/>. Every 0.75-0.9 s
/// (the first at once) each player up whose feet are within <see cref="Radius"/>, in its line, takes a tick (never more than
/// `BossBase.MaxHit`) and is set alight (`NZNet.MargwaBurn`). The direct hit does nothing, upstream's.
///
/// ⚠️ THE HOST FLIES IT AND BURNS WITH IT; EVERYONE ELSE WATCHES: `Launch` network-spawns it (<see cref="Radius"/> and
/// <see cref="Seconds"/> travel with it); the look — a ball of the burning zombie's flame, a flame head with a trail you can follow
/// (`BossShotLook`, 2026-10-07), its light, its hiss — is built on every
/// machine, and <see cref="LandedAt"/> tells each where to put the pit (`PitVisual`'s Fire, whose light is GMod's fire pit's own).
/// </summary>
public sealed class KrasnyBomb : Component
{
/// <summary>GMod's `sv_gravity`: the arc upstream's `getvel` solves for.</summary>
public const float Gravity = 600f;
const float TickMin = 0.75f, TickMax = 0.9f, MaxFlight = 5f;
[Property] public float Radius { get; set; } = 100f;
[Property] public float Seconds { get; set; } = 6f;
/// <summary>Where it burst — zero until it has. Set by the host, read by every machine.</summary>
[Sync( SyncFlags.FromHost )] public Vector3 LandedAt { get; set; }
// ── the host's ──
public GameObject Owner { get; set; }
public float Tick { get; set; }
public float BurnDps { get; set; }
Vector3 _vel;
float _born, _landedAt, _nextTick;
bool _landed, _done;
// ── every machine's ──
GameObject _ball, _lightGo, _pitGo;
BossShotLook _look;
SoundHandle _hiss;
bool _pitShown;
/// <summary>The flaming ball in flight (2026-10-07): a flame sprite this wide, its halo, and a trail of fire (`BossShotLook`).</summary>
const string HeadSprite = "sprites/nz/napalm_flame.sprite";
const float LookSize = 32f, TrailWidth = 14f, TrailSeconds = 0.3f, LookBrightness = 4f;
/// <summary>The velocity that lobs it from <paramref name="from"/> to <paramref name="to"/> in <paramref name="seconds"/> (upstream's `getvel`).</summary>
public static Vector3 VelocityFor( Vector3 from, Vector3 to, float seconds )
{
var t = MathF.Max( 0.1f, seconds );
var d = to - from;
return new Vector3( d.x / t, d.y / t, (d.z + 0.5f * Gravity * t * t) / t );
}
/// <summary>Throw one. THE HOST; a client only watches.</summary>
public static KrasnyBomb Launch( Scene scene, Vector3 from, Vector3 velocity, GameObject owner, float tick, float burnDps, float radius,
float seconds )
{
if ( !scene.IsValid() || NZGame.IsClient ) return null;
var go = new GameObject( true, "nz_krasny_bomb" );
go.Flags |= GameObjectFlags.NotSaved;
go.WorldPosition = from;
var b = go.Components.Create<KrasnyBomb>();
b.Radius = radius;
b.Seconds = seconds;
b.Owner = owner;
b.Tick = tick;
b.BurnDps = burnDps;
b._vel = velocity;
go.NetworkSpawn();
return b;
}
protected override void OnStart()
{
_born = Time.Now;
// ── the look, on every machine: a ball of fire, its light (upstream's trail colour, 255/25/0) and its hiss ──
try { _ball = GameObject.Clone( PanzerFx.FlamePrefab, new Transform( WorldPosition ) ); }
catch ( Exception ) { }
if ( _ball.IsValid() )
{
_ball.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.NotNetworked;
_ball.NetworkMode = NetworkMode.Never;
_ball.SetParent( GameObject );
_ball.WorldScale = 0.5f;
}
_lightGo = new GameObject { Parent = GameObject, Name = "light" };
_lightGo.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.NotNetworked;
var light = _lightGo.Components.Create<PointLight>();
light.LightColor = new Color( 1f, 25f / 255f, 0f ) * 3f;
light.Radius = 160f;
light.Shadows = false;
// ⚠️ A BALL YOU CAN SEE (2026-10-07). The user, after Avogadro's bolt: *"same for kransy, i cannot see the projectile"*. The
// ball above is half a burning zombie's flame: at most 10 particles 4-16 across, each fading in over its first fifth, dim and
// brown at the tail, and no trail (upstream's was 35 wide) — at 400-1250 away what showed was its red light on the walls. A
// flame ~32 across with its halo and a trail of fire, on every machine; it goes with the ball when the pit shows
_look = BossShotLook.On( GameObject, PanzerFx.FireColour, LookSize, trailWidth: TrailWidth, trailSeconds: TrailSeconds,
brightness: LookBrightness, headSprite: HeadSprite );
_hiss = NZSound.Play( "nz.krasny.bomb.loop", WorldPosition );
}
protected override void OnUpdate()
{
// ── every machine: the hiss follows it, and once it has burst, the pit ──
if ( _hiss.IsValid() ) _hiss.Position = WorldPosition;
if ( !_pitShown && !LandedAt.IsNearlyZero() ) ShowPit();
if ( NZGame.IsClient || _done ) return;
if ( _landed ) Burn();
else Fly();
}
/// <summary>One frame of the lob, and whatever it meets. THE HOST.</summary>
void Fly()
{
// off the map, or bouncing for good
if ( Time.Now - _born > MaxFlight )
{
_done = true;
GameObject.Destroy();
return;
}
var dt = Time.Delta;
_vel += Vector3.Down * Gravity * dt;
var to = WorldPosition + _vel * dt;
var tr = Scene.Trace.Ray( WorldPosition, to ).IgnoreGameObjectHierarchy( GameObject )
.WithoutTags( "zombie", "player", "trigger", "ragdoll" ).Run();
if ( !tr.Hit )
{
WorldPosition = to;
return;
}
// ⚠️ A WALL, A CEILING OR A SLOPE STEEPER THAN ~41° THROWS IT BACK (upstream's `HitNormal·down < 0.75`, there with an odd extra
// impulse — here a plain bounce at half its speed); a floor is where it bursts
if ( tr.Normal.z < 0.75f )
{
WorldPosition = tr.HitPosition + tr.Normal * 2f;
_vel = (_vel - 2f * _vel.Dot( tr.Normal ) * tr.Normal) * 0.5f;
return;
}
_landed = true;
_landedAt = Time.Now;
_nextTick = Time.Now; // upstream's first tick, on the next frame
WorldPosition = tr.HitPosition;
LandedAt = tr.HitPosition;
NZSound.PlayShared( "nz.krasny.bomb.explode", tr.HitPosition );
BossFx.BurstShared( tr.HitPosition + Vector3.Up * 8f, Radius, PanzerFx.FireColour, 10 );
}
/// <summary>The pit's ticks, then gone. THE HOST; destroying a network object ends it everywhere.</summary>
void Burn()
{
if ( Time.Now >= _landedAt + Seconds )
{
_done = true;
GameObject.Destroy();
return;
}
if ( Time.Now < _nextTick ) return;
_nextTick = Time.Now + Game.Random.Float( TickMin, TickMax );
var at = WorldPosition;
var by = Owner.IsValid() ? Owner : GameObject;
foreach ( var p in Scene.GetAllComponents<NZPlayer>().ToList() )
{
// ⚠️ FROM THE FEET, as the napalm zombie's pool: standing in it is what burns — and upstream's line, nothing solid between
if ( !BossBase.IsUp( p ) || p.WorldPosition.Distance( at ) > Radius ) continue;
var chest = p.WorldPosition + Vector3.Up * 40f;
var tr = Scene.Trace.Ray( at + Vector3.Up * 8f, chest ).IgnoreGameObjectHierarchy( GameObject )
.WithoutTags( "zombie", "player", "trigger", "ragdoll" ).Run();
if ( tr.Hit && tr.HitPosition.Distance( chest ) > 24f ) continue;
var hp = p.Components.Get<Health>( FindMode.EverythingInSelfAndDescendants );
if ( !hp.IsValid() || hp.IsDead ) continue;
hp.Apply( MathF.Min( Tick, BossBase.MaxHit ), false, by, tick: true, source: "krasny fire bomb" );
if ( BurnDps > 0f && KrasnyBoss.BurnSeconds > 0f ) NZNet.MargwaBurn( p.GameObject.Id, KrasnyBoss.BurnSeconds, BurnDps );
}
}
/// <summary>The pit, on THIS machine: `PitVisual`'s Fire where it burst; the ball, its look, its light and its hiss gone.</summary>
void ShowPit()
{
_pitShown = true;
if ( _hiss.IsValid() ) _hiss.Stop();
if ( _ball.IsValid() ) _ball.Destroy();
if ( _look.IsValid() ) _look.GameObject.Destroy(); // ⛔ else the ball would sit burning on the pit for its 6 s
if ( _lightGo.IsValid() ) _lightGo.Destroy();
// ⛔ THIS MACHINE'S OWN AND NOT A CHILD: `PitVisual` drops its object to the floor, and a child would then ride the bomb's
// interpolation down past it; out of a joiner's snapshot (INSTRUCTIONS.md §39), and gone with the bomb (`OnDestroy`)
_pitGo = new GameObject( true, "nz_krasny_pit" );
_pitGo.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.NotNetworked;
_pitGo.NetworkMode = NetworkMode.Never;
_pitGo.WorldPosition = LandedAt;
PitVisual.Attach( _pitGo, Radius, PitVisual.Style.Fire, Seconds );
_pitGo.Components.Create<MargwaExpire>().At = Time.Now + Seconds + 1f;
}
protected override void OnDestroy()
{
if ( _hiss.IsValid() ) _hiss.Stop();
if ( _pitGo.IsValid() ) _pitGo.Destroy();
}
}