Augment/perk logic for the PhD Flopper perk. It tracks fall state, handles ground-slam and double-jump inputs, computes blast damage/radius from the held weapon and augments, triggers explosions (including chained detonations), applies damage to zombies via Health.OnDamage, and provides console commands to report and retune values.
using Sandbox;
using System;
using System.Linq;
namespace NZombies;
/// <summary>
/// PhD Flopper's augments. Base perk: explosive/fall damage negation, **plus a fall blast**.
///
/// | id | effect | status |
/// |----|--------|--------|
/// | base | falling far enough detonates for **×5 the held weapon's damage** | ⚠️ NEW — replaced the dive |
/// | M1 Bigger Boom | that blast deals ×3 damage over a +50% radius | as the original |
/// | M2 Chain Blast | every blast this perk causes fires **3 times, 0.2s apart** | ⚠️ NEW — replaced Double Jump |
/// | M3 Kinetic Burst | a zombie hitting you **while sprinting** detonates | as the original |
/// | M4 Reactive Blast | taking damage **below 30% HP** detonates | as the original |
/// | m1 Ground Slam | crouch mid-air to **drop fast** and detonate on impact | ⚠️ retargeted — was "dive without the double jump" |
/// | m2 Trap Immunity | **(not implemented)** — no traps exist yet | ⛔ description only, by request |
/// | m3 Slide Bomb | sliding detonates | as the original |
/// | m4 Hops | **+30% jump height** | ⚠️ was m5's effect, retuned |
/// | m5 Double Jump | a second jump in mid-air | ⚠️ was M2, demoted to a minor |
///
/// ⛔ THE DIVE DOES NOT EXIST IN THIS PORT AND IS NOT BEING BUILT. Four of the original nine
/// augments scaled, triggered or extended a dive-slam that was never ported, so the pool was
/// four-ninths dead on arrival. The base effect is now a FALL blast — you already fall, so the
/// trigger exists for free — and every augment that referenced the dive was retargeted onto it.
///
/// ⚠️ ONE BLAST PRIMITIVE, FIVE TRIGGERS. The base fall, M3's sprint hit, M4's low-HP hit,
/// m1's slam and m3's slide all call <see cref="Blast"/>. That is deliberate: M1's scaling and
/// M2's chaining then apply to all five without being written five times, and "the explosion"
/// means one thing everywhere. Five bespoke explosions is the §3 shape, and here it would show
/// up as M1 scaling some of them.
///
/// ⚠️ DAMAGE IS READ OFF THE HELD WEAPON, so the blast scales with the gun, Pack-a-Punch and
/// every tech node — a late-round player is not detonating for pistol damage. It uses the same
/// `Damage × Bullets` product the stats card's Damage row uses, so the two agree about what
/// "the weapon's damage" means.
/// </summary>
public static class PhdAugments
{
const string Perk = "phd";
// ══ tuning ════════════════════════════════════════════════════════════════
//
// ⚠️ EVERY DEFAULT LIVES IN A GETTER OVER A NULLABLE FIELD, NOT AN INITIALISER. A field
// initialiser runs once per session and hotload carries the VALUE forward, so changing a
// default here would have no effect until a restart — which is exactly how Vigor Rush spent
// an evening dealing ×2 while its source read `1.2f` (§1). These are numbers that will be
// retuned repeatedly, so they are built to be retunable.
static float? _fallThreshold;
/// <summary>How far you must fall before it detonates, in units. 220 ≈ three storeys.</summary>
public static float FallThreshold { get => _fallThreshold ?? 220f; set => _fallThreshold = value; }
static float? _damageMultiple;
/// <summary>Blast damage as a multiple of the held weapon's damage. ×5.</summary>
public static float DamageMultiple { get => _damageMultiple ?? 5f; set => _damageMultiple = value; }
static float? _radius;
/// <summary>Blast radius in units. 260, matching Cranial Detonation's.</summary>
public static float Radius { get => _radius ?? 260f; set => _radius = value; }
static float? _noWeaponDamage;
/// <summary>
/// Blast damage when nothing is held, before the ×5.
///
/// ⚠️ NOT ZERO. An empty-handed player is rare but reachable — mid-swap, or a fresh
/// Creative spawn — and a blast that silently does nothing reads as the perk being broken
/// rather than as an edge case.
/// </summary>
public static float NoWeaponDamage { get => _noWeaponDamage ?? 40f; set => _noWeaponDamage = value; }
static float? _biggerBoomDamage;
/// <summary>M1 Bigger Boom — damage multiplier on the blast. ×3.</summary>
public static float BiggerBoomDamage { get => _biggerBoomDamage ?? 3f; set => _biggerBoomDamage = value; }
static float? _biggerBoomRadius;
/// <summary>M1 Bigger Boom — radius multiplier. +50%.</summary>
public static float BiggerBoomRadius { get => _biggerBoomRadius ?? 1.5f; set => _biggerBoomRadius = value; }
static int? _chainCount;
/// <summary>M2 Chain Blast — how many detonations per trigger. 3.</summary>
public static int ChainCount { get => _chainCount ?? 3; set => _chainCount = value; }
static float? _chainDelay;
/// <summary>M2 Chain Blast — seconds between them. 0.2.</summary>
public static float ChainDelay { get => _chainDelay ?? 0.2f; set => _chainDelay = value; }
static float? _sprintCooldown;
/// <summary>M3 Kinetic Burst — seconds between sprint detonations. 10.</summary>
public static float SprintCooldown { get => _sprintCooldown ?? 10f; set => _sprintCooldown = value; }
static float? _reactiveCooldown;
/// <summary>M4 Reactive Blast — seconds between low-HP detonations. 15.</summary>
public static float ReactiveCooldown { get => _reactiveCooldown ?? 15f; set => _reactiveCooldown = value; }
static float? _reactiveThreshold;
/// <summary>M4 Reactive Blast — the HP fraction below which a hit detonates. 30%.</summary>
public static float ReactiveThreshold { get => _reactiveThreshold ?? 0.30f; set => _reactiveThreshold = value; }
static float? _slamSpeed;
/// <summary>m1 Ground Slam — downward speed the slam forces, units/sec. 1400.</summary>
public static float SlamSpeed { get => _slamSpeed ?? 1400f; set => _slamSpeed = value; }
static float? _jumpScale;
/// <summary>m4 Hops — jump height multiplier. +30%.</summary>
public static float JumpScale { get => _jumpScale ?? 1.3f; set => _jumpScale = value; }
static float? _doubleJumpScale;
/// <summary>
/// m5 Double Jump — the second jump's strength, relative to the first.
///
/// ⚠️ 0.9, NOT 1.0. A mid-air jump with no ground contact to slow you reads as floaty at
/// full strength; slightly weaker keeps it a recovery move rather than a flight mode.
/// </summary>
public static float DoubleJumpScale { get => _doubleJumpScale ?? 0.9f; set => _doubleJumpScale = value; }
static bool Has( NZPlayer p, string augId )
=> p.IsValid() && p.HasPerk( Perk ) && PerkAugments.Has( p, Perk, augId );
// ══ the blast ═════════════════════════════════════════════════════════════
/// <summary>
/// Damage one detonation deals. ×5 the held weapon, ×3 again with M1.
///
/// ⚠️ `Damage × Bullets`, THE SAME PRODUCT THE STATS CARD'S DAMAGE ROW USES. A shotgun's
/// per-pellet figure would make it detonate for a tenth of what the card says the gun does,
/// and "the weapon's damage" has to mean one thing in both places.
/// </summary>
public static float BlastDamage( NZPlayer player )
{
if ( !player.IsValid() ) return 0f;
var wep = VultureAugments.HeldWeapon( player );
var si = wep.IsValid() ? wep.Primary : null;
var weaponDamage = si is not null && si.Damage > 0f
? si.Damage * Math.Max( 1, si.Bullets )
: NoWeaponDamage;
var mult = DamageMultiple;
if ( Has( player, "M1" ) ) mult *= BiggerBoomDamage;
return weaponDamage * MathF.Max( 0f, mult );
}
/// <summary>Blast radius, +50% with M1.</summary>
public static float BlastRadius( NZPlayer player )
=> Radius * (Has( player, "M1" ) ? BiggerBoomRadius : 1f);
/// <summary>How many detonations one trigger produces. 3 with M2.</summary>
public static int BlastCount( NZPlayer player )
=> Has( player, "M2" ) ? Math.Max( 1, ChainCount ) : 1;
/// <summary>
/// Detonate at a position. THE one entry point for all five triggers.
///
/// ⚠️ M2's CHAIN IS HANDLED HERE, not at the call sites, so every trigger chains. Putting
/// it in the triggers would mean adding it five times and forgetting it once.
///
/// ⚠️ THE CHAIN RE-READS `BlastDamage` PER DETONATION rather than computing it once. A
/// player who swaps weapons or packs one mid-chain should have the later blasts match the
/// gun in hand — and more importantly the first blast can KILL the zombie whose death would
/// change nothing, so re-reading costs nothing and cannot go stale.
/// </summary>
public static void Blast( NZPlayer player, Vector3 at, string reason )
{
if ( !player.IsValid() || !player.HasPerk( Perk ) ) return;
var count = BlastCount( player );
Detonate( player, at, reason, 1, count );
if ( count > 1 ) ChainRest( player, at, reason, count );
}
/// <summary>
/// The 2nd..Nth detonations of an M2 chain.
///
/// ⚠️ `GameTask.Delay( ms )`, THIS PROJECT'S DELAY IDIOM. `Task.DelaySeconds` does not
/// exist here and adding `using System.Threading.Tasks` shadows `Sandbox.Task` and makes it
/// worse — that is written down because it cost two build failures.
///
/// ⚠️ RE-VALIDATES THE PLAYER EVERY STEP. 0.4s is long enough to go down, lose the perk, or
/// end the round, and an async continuation holding a stale reference is how this project
/// got `NullReferenceException at Weapon.OnUpdate` spam once already.
/// </summary>
static async void ChainRest( NZPlayer player, Vector3 at, string reason, int count )
{
var ms = (int)(MathF.Max( 0.02f, ChainDelay ) * 1000f);
for ( var i = 2; i <= count; i++ )
{
await GameTask.Delay( ms );
if ( !player.IsValid() || !player.HasPerk( Perk ) ) return;
Detonate( player, at, reason, i, count );
}
}
/// <summary>
/// One detonation.
///
/// ⚠️ THE SAME SHAPE AS `DeadshotAugments.Detonate` — walk `ZombieAI.All`, measure to a
/// point 32u up from the feet, damage through `Health.OnDamage` with the player as attacker.
/// Going through `OnDamage` rather than subtracting HP is what makes the kill count, the
/// points pay and every on-kill augment fire.
///
/// ⛔ IT DOES NOT DAMAGE THE PLAYER. PhD's whole identity is immunity to your own
/// explosions, and the base perk already blocks blast damage in `Health.Apply` — but
/// relying on that would mean a player who somehow lost the perk mid-chain taking their own
/// blast. Never adding them as a target is one less thing to be conditional about.
/// </summary>
static void Detonate( NZPlayer player, Vector3 at, string reason, int index, int total )
{
var damage = BlastDamage( player );
var radius = BlastRadius( player );
if ( damage <= 0f || radius <= 0f ) return;
var hit = 0;
foreach ( var z in ZombieAI.All )
{
if ( !z.IsValid() || !z.GameObject.IsValid() ) continue;
var target = z.WorldPosition + Vector3.Up * 32f;
if ( at.Distance( target ) > radius ) continue;
var hp = z.Components.Get<Health>( FindMode.EverythingInSelfAndAncestors );
if ( !hp.IsValid() || hp.IsDead ) continue;
hp.OnDamage( new DamageInfo
{
Damage = damage,
Attacker = player.GameObject,
Position = target,
Tags = new TagSet(),
} );
hit++;
}
Effect( at, radius );
var step = total > 1 ? $" [{index}/{total}]" : "";
Log.Info( $"[nz-aug] phd blast{step} — {reason} — {damage:0} to {hit} zombie(s)"
+ $" within {radius:0}u" );
}
/// <summary>
/// The visible half.
///
/// ⛔ `prefabs/engine/explosion_med.prefab`, THE SAME ONE THE GRENADE USES, AND ITS OWN
/// NOTE EXPLAINS WHY: it is the ONLY explosion prefab that ships, and searching the whole
/// asset system returns exactly one. My first attempt at this method invented
/// `prefabs/effects/nz_explosion.prefab`, which does not exist — an unverified asset path
/// fails silently at LOAD time with a green compile (§18), so it would have shipped as
/// "the blast does damage but you cannot see it".
///
/// ⚠ A LIGHT AS WELL AS THE PARTICLE, for the reason `Grenade.Effect` records: most of a
/// blast's impact in a dark map is the flash on the walls, and a fireball that leaves the
/// geometry unlit reads as a sprite pasted over the scene.
/// </summary>
static void Effect( Vector3 at, float radius )
=> BlastEffect.Spawn( at, radius );
// ══ base — the fall ═══════════════════════════════════════════════════════
/// <summary>
/// Per-frame: track the fall, and run m1's slam and m5's double jump.
///
/// ⛔ CALLED FROM `NZPlayer.OnUpdate`, NOT FROM A COMPONENT OF ITS OWN. A component created
/// at runtime is destroyed by a hotload and does not come back — `Slide` carries that exact
/// warning and the fix there was `Components.GetOrCreate` from OnUpdate. A static called
/// from the player's own tick has nothing to lose in the first place.
///
/// ⚠️ THE PEAK IS TRACKED WHILE AIRBORNE, NOT THE TAKEOFF HEIGHT. Jumping up off a ledge
/// and falling should measure from the top of the arc, which is what the player sees as
/// "how far I fell". Takeoff height would under-count every fall that started with a jump.
/// </summary>
public static void Tick( NZPlayer player )
{
if ( !player.IsValid() ) return;
var c = player.Components.Get<PlayerController>();
if ( !c.IsValid() ) return;
var onGround = c.IsOnGround;
var z = player.WorldPosition.z;
if ( onGround )
{
// ⚠️ THE LANDING IS RESOLVED BEFORE THE STATE IS CLEARED, and the order matters:
// clearing first would lose the peak this frame's blast is measured against.
if ( player.PhdAirborne )
{
var fell = player.PhdFallPeak - z;
if ( player.HasPerk( Perk )
&& (player.PhdSlamming || fell >= FallThreshold) )
{
Blast( player, player.WorldPosition,
player.PhdSlamming ? "ground slam" : $"fell {fell:0}u" );
}
// ⚠ BASALT SEAL 1: a Ground Slam that lands on one of this round's picked tiles lights it. Only the
// slam counts, not a long fall. `HexPlatforms` reports the tile and the host decides.
if ( player.PhdSlamming )
HexPlatforms.OnSlamLanded( player, player.WorldPosition );
}
player.PhdAirborne = false;
player.PhdSlamming = false;
player.PhdFallPeak = z;
player.PhdJumps = 0;
return;
}
// ── airborne ──
if ( !player.PhdAirborne )
{
player.PhdAirborne = true;
player.PhdFallPeak = z;
}
if ( z > player.PhdFallPeak ) player.PhdFallPeak = z;
TickSlam( player, c );
TickDoubleJump( player, c );
}
/// <summary>
/// m1 Ground Slam — crouch in mid-air to drop hard.
///
/// ⚠️ IT DOES NOT DETONATE IN THE AIR. The blast lands with the player, on impact, which is
/// both what a slam is and what makes it aimable — detonating at the moment of the keypress
/// would put the explosion wherever you happened to be, usually nowhere near the zombies
/// you dived at.
///
/// ⚠️ IT SETS A DOWNWARD SPEED, IT DOES NOT ADD ONE. Adding would make a second crouch
/// mid-slam twice as fast, and a third faster still.
///
/// ⚠️ THIS SHARES ITS INPUT WITH BANANA COLADA'S SLIDE CHAIN, and both firing is correct:
/// the slam drops you, the blast goes off on impact, and Banana's chain resumes the slide
/// from the landing. They compose rather than compete, so neither is gated on the other.
/// </summary>
static void TickSlam( NZPlayer player, PlayerController c )
{
if ( player.PhdSlamming ) return;
if ( !Has( player, "m1" ) ) return;
if ( !Input.Pressed( "Duck" ) ) return;
player.PhdSlamming = true;
// ⚠ THROUGH `c.Body`, NOT `c.Velocity`. `PlayerController.Velocity` is READ-ONLY;
// `Slide` writes `_c.Body.Velocity` for the same reason, and that is the one place in
// this project that already moves the player by hand.
if ( c.Body.IsValid() )
c.Body.Velocity = c.Body.Velocity.WithZ( -MathF.Abs( SlamSpeed ) );
Log.Info( $"[nz-aug] phd m1 Ground Slam — dropping at {SlamSpeed:0}u/s" );
}
/// <summary>
/// m5 Double Jump — one extra jump per airborne period.
///
/// ⚠️ COUNTS FROM 1, NOT 0, because leaving the ground already spent the first jump — and a
/// player who WALKED off a ledge never jumped at all, so they get the mid-air one. Counting
/// presses instead would silently grant a free extra jump to anyone stepping off a kerb.
/// </summary>
static void TickDoubleJump( NZPlayer player, PlayerController c )
{
if ( !Has( player, "m5" ) ) return;
if ( player.PhdJumps >= 1 ) return;
if ( !Input.Pressed( "Jump" ) ) return;
player.PhdJumps++;
// ⚠ SETS the upward speed rather than adding to it, so a double jump taken while still
// rising is not a rocket, and one taken while falling fast is not swallowed by the
// existing downward velocity.
//
// ⚠ `c.Body.Velocity`, because `PlayerController.Velocity` is read-only — see the note
// in TickSlam.
if ( c.Body.IsValid() )
c.Body.Velocity = c.Body.Velocity
.WithZ( ActiveConfig.Player.JumpPower * DoubleJumpScale );
Log.Info( "[nz-aug] phd m5 Double Jump" );
}
/// <summary>
/// m4 Hops — the jump-height multiplier, read at `NZPlayer`'s JumpSpeed write.
///
/// ⛔ A MULTIPLIER RETURNED, NOT A WRITE. `NZPlayer` recomputes `JumpSpeed` from the config
/// every frame, so anything written here would be undone on the next tick — the same reason
/// its own comment gives for zeroing jump while downed rather than swallowing the key.
/// </summary>
public static float JumpMultiplier( NZPlayer player )
=> Has( player, "m4" ) ? MathF.Max( 0.1f, JumpScale ) : 1f;
// ══ M3 / M4 — being hit ═══════════════════════════════════════════════════
/// <summary>
/// A zombie damaged this player. Runs M3 Kinetic Burst and M4 Reactive Blast.
///
/// ⚠️ RETURNS NOTHING AND CHANGES NOTHING ABOUT THE DAMAGE. Both augments are side effects
/// of being hit, not reductions of it — Juggernog owns that axis.
///
/// ⚠️ BOTH CAN FIRE ON ONE HIT, and they should: sprinting into a hit at low HP is exactly
/// the moment the player bought both augments for. Each has its own cooldown, so a build
/// with both gets two blasts and then two independent waits.
///
/// ⚠️ THE HP TEST USES THE PRE-HIT FRACTION, because it is what the augment text describes
/// ("taking damage below 30%") and because the post-hit value may be 0 — a killing blow
/// would otherwise always qualify, which turns a defensive augment into a death rattle.
/// </summary>
public static void OnPlayerDamaged( NZPlayer victim, float amount )
{
if ( !victim.IsValid() || !victim.HasPerk( Perk ) || amount <= 0f ) return;
var at = victim.WorldPosition;
if ( Has( victim, "M3" )
&& victim.IsRunning
&& victim.PhdSprintReady <= 0f )
{
// ⚠ THROUGH `TimeAugments.Cooldown`, so Timeslip Tonic's m4 Time Warp shortens it.
// That helper returns its input unchanged without the augment, which is what lets
// every cooldown site wrap unconditionally instead of branching — and it makes an
// UNWRAPPED site a visible omission rather than a silent one.
victim.PhdSprintReady = TimeAugments.Cooldown( victim, SprintCooldown );
Blast( victim, at, "kinetic burst (sprinting)" );
}
if ( !Has( victim, "M4" ) || victim.PhdReactiveReady > 0f ) return;
var hp = victim.Components.Get<Health>( FindMode.EverythingInSelfAndAncestors );
if ( !hp.IsValid() || hp.Max <= 0f ) return;
if ( hp.Current / hp.Max > ReactiveThreshold ) return;
victim.PhdReactiveReady = TimeAugments.Cooldown( victim, ReactiveCooldown );
Blast( victim, at, $"reactive blast ({hp.Current / hp.Max * 100f:0}% HP)" );
}
// ══ m3 — sliding ══════════════════════════════════════════════════════════
/// <summary>
/// m3 Slide Bomb — a slide started.
///
/// ⚠️ ON THE START, NOT PER FRAME. Banana Colada makes slides long and chainable, so a
/// per-frame blast would be a continuous explosion for as long as the player held crouch.
/// </summary>
public static void OnSlideStarted( NZPlayer player )
{
if ( !Has( player, "m3" ) ) return;
Blast( player, player.WorldPosition, "slide bomb" );
}
// ══ diagnostics ═══════════════════════════════════════════════════════════
public static void Report( NZPlayer player )
{
if ( !player.IsValid() ) { Log.Warning( "[nz-aug] no player" ); return; }
var has = player.HasPerk( Perk );
var equipped = PerkAugments.EquippedOn( player, Perk );
Log.Info( $"[nz-aug] PhD FLOPPER {(has ? "owned" : "NOT OWNED — every line below is inert")}"
+ $" · equipped [{(equipped.Length == 0 ? "none" : string.Join( "+", equipped ))}]" );
var wep = VultureAugments.HeldWeapon( player );
var si = wep.IsValid() ? wep.Primary : null;
// ⚠️ RESOLVED NUMBERS, NOT MULTIPLIERS. "×5 of ×3" cannot be checked against anything;
// "1350 damage over 390u" can.
Log.Info( $"[nz-aug] base blast {BlastDamage( player ):0} damage"
+ $" over {BlastRadius( player ):0}u"
+ $" · after a {FallThreshold:0}u fall"
+ $" · weapon {(si is null ? $"none (assumes {NoWeaponDamage:0})" : $"{si.Damage:0}x{Math.Max( 1, si.Bullets )}")}" );
Log.Info( $"[nz-aug] M1 Bigger Boom {(Has( player, "M1" ) ? $"x{BiggerBoomDamage:0.##} damage, x{BiggerBoomRadius:0.##} radius" : "-")}" );
Log.Info( $"[nz-aug] M2 Chain Blast {(Has( player, "M2" ) ? $"{ChainCount} blasts {ChainDelay:0.##}s apart" : "-")}"
+ $" this trigger fires {BlastCount( player )}x" );
Log.Info( $"[nz-aug] M3 Kinetic {(Has( player, "M3" ) ? $"on a hit while sprinting, {SprintCooldown:0.#}s cd" : "-")}"
+ $" sprinting now {player.IsRunning}"
+ $" · ready in {MathF.Max( 0f, player.PhdSprintReady ):0.0}s" );
Log.Info( $"[nz-aug] M4 Reactive {(Has( player, "M4" ) ? $"on a hit below {ReactiveThreshold * 100f:0}% HP, {ReactiveCooldown:0.#}s cd" : "-")}"
+ $" ready in {MathF.Max( 0f, player.PhdReactiveReady ):0.0}s" );
Log.Info( $"[nz-aug] m1 Ground Slam {(Has( player, "m1" ) ? $"crouch mid-air, {SlamSpeed:0}u/s" : "-")}"
+ $" airborne {player.PhdAirborne}"
+ $" · slamming {player.PhdSlamming}" );
Log.Info( "[nz-aug] m2 Trap Immunity NOT IMPLEMENTED — no traps exist yet" );
Log.Info( $"[nz-aug] m3 Slide Bomb {(Has( player, "m3" ) ? "on" : "-")}" );
Log.Info( $"[nz-aug] m4 Hops jump x{JumpMultiplier( player ):0.##}" );
Log.Info( $"[nz-aug] m5 Double Jump {(Has( player, "m5" ) ? $"x{DoubleJumpScale:0.##} of a normal jump" : "-")}"
+ $" used {player.PhdJumps}/1 this airborne" );
}
/// <summary>`nz_aug_phd` — the resolved state of all nine.</summary>
[ConCmd( "nz_aug_phd" )]
public static void ReportCmd()
=> Report( NZPlayer.Local );
/// <summary>
/// `nz_phd_blast` — detonate at the player, bypassing every trigger.
///
/// ⚠️ EXISTS BECAUSE ALL FIVE TRIGGERS NEED SOMETHING TO HAPPEN FIRST — a long fall, a
/// zombie hit while sprinting, low HP. Testing M1's scaling or M2's chain through them means
/// setting up the trigger every time, and a failure could be either half.
/// </summary>
[ConCmd( "nz_phd_blast" )]
public static void BlastCmd()
{
var p = NZPlayer.Local;
if ( !p.IsValid() ) { Log.Warning( "[nz-aug] no player" ); return; }
if ( !p.HasPerk( Perk ) )
{
Log.Warning( "[nz-aug] PhD Flopper not owned — nz_perk_give phd" );
return;
}
Blast( p, p.WorldPosition, "nz_phd_blast" );
}
/// <summary>`nz_phd_set` — retune live. Negative or omitted leaves a value alone.</summary>
[ConCmd( "nz_phd_set" )]
public static void SetCmd( float fall = -1f, float multiple = -1f, float radius = -1f,
float boomDamage = -1f, float boomRadius = -1f, int chain = -1, float chainDelay = -1f,
float sprintCd = -1f, float reactiveCd = -1f, float reactiveHp = -1f,
float slam = -1f, float jump = -1f, float doubleJump = -1f )
{
if ( fall >= 0f ) FallThreshold = fall;
if ( multiple >= 0f ) DamageMultiple = multiple;
if ( radius >= 0f ) Radius = radius;
if ( boomDamage >= 0f ) BiggerBoomDamage = boomDamage;
if ( boomRadius >= 0f ) BiggerBoomRadius = boomRadius;
if ( chain >= 1 ) ChainCount = chain;
if ( chainDelay >= 0f ) ChainDelay = chainDelay;
if ( sprintCd >= 0f ) SprintCooldown = sprintCd;
if ( reactiveCd >= 0f ) ReactiveCooldown = reactiveCd;
if ( reactiveHp >= 0f ) ReactiveThreshold = reactiveHp;
if ( slam >= 0f ) SlamSpeed = slam;
if ( jump >= 0f ) JumpScale = jump;
if ( doubleJump >= 0f ) DoubleJumpScale = doubleJump;
ReportCmd();
}
}