Player/JuggAugments.cs

Static utility class implementing Juggernog perk augments for players. It stores numeric tuning fields, checks augment ownership, updates player max health, modifies damage taken, applies on-hit effects (adrenal window, stun), handles on-kill healing/armor, refills armor on round start, exposes helpers for armor-related scales, and debug console commands.

NetworkingFile Access
using Sandbox;
using System;
using System.Linq;

namespace NZombies;

/// <summary>
/// Juggernog's nine augments. Base perk: max health +100.
///
/// Every number here is from `perks/sh_augment_effects.lua` and
/// `extra/sv_nz_armor_system.lua`, not from the augment descriptions — the descriptions
/// are what the UI shows the player, and twice already in this port a blurb has promised
/// something the code did not do.
///
/// | id | effect | source |
/// |----|--------|--------|
/// | M1 | max health +100 more | `RefreshJuggHealth` |
/// | M2 | armor to full at round start | `nzAug_JuggPlatedUp` |
/// | M3 | damage x0.75 | `nzAug_JuggBulwark` |
/// | M4 | kills heal +10 | `nzAug_JuggBloodthirst` |
/// | m1 | +10 armor per headshot kill | `nzAug_JuggHardplate` |
/// | m2 | damage through armor x0.5 | ⚠ CHANGED — see ArmorBleedScale |
/// | m3 | +5 hits per armor bar | ⚠ CHANGED — see DensePlatingBarHits |
/// | m4 | speed x1.25 for 2s when hit | `nzAug_JuggAdrenal` |
/// | m5 | melee attacker stunned 5s | ⚠ 5s, not the original's 1 |
///
/// ⚠️ FIVE OF THE NINE TOUCH ARMOR, which is why the armor system had to exist first.
/// Three of them (m1, m2, m3) do nothing at all at armor tier 0 — the default — so
/// "the augment does nothing" is the EXPECTED result until `nz_armor_tier 1` is run.
/// <see cref="Report"/> says so out loud rather than leaving that to be rediscovered.
/// </summary>
public static class JuggAugments
{
	const string Perk = "jugg";

	// ── tuning ───────────────────────────────────────────────────────────────
	// ⚠️ Statics with their defaults INLINE, which survives a hotload. A static that
	// starts empty or zero does not (INSTRUCTIONS.md §1).

	/// <summary>M1 Overhealth — extra max health on top of Juggernog's own bonus.</summary>
	public static float OverhealthBonus { get; set; } = 100f;

	/// <summary>M3 Bulwark — incoming damage scale. 0.75 = 25% less.</summary>
	public static float BulwarkScale { get; set; } = 0.75f;

	/// <summary>M4 Bloodthirst — health per kill.</summary>
	public static float BloodthirstHeal { get; set; } = 10f;

	/// <summary>m1 Hardplate — armor per headshot kill.</summary>
	public static float HardplateArmor { get; set; } = 10f;

	/// <summary>
	/// m2 Efficient Weave — scale on the damage that gets THROUGH armor. 0.8 = 20% less.
	///
	/// ⚠️ 0.75 FOR AN HOUR, THEN 0.8 (2026-10-03, the user: *"efficient weave reduces it by 20%"*).
	///
	/// ⛔ WAS 0.5 UNTIL 2026-10-03. With Dense Plating and a tier-3 vest it was "90% of" why late game was easy, in the user's
	/// words after a round-88 game: *"zombies dealing little to no damage when a player has juggernog with the minor upgrades
	/// … + tier 3 armor"*. A round-60+ hit (90) cost 13.5 health and the worst 10 seconds of the late game cost 156 of 350.
	/// Changed together with Dense Plating (+5 → +2) and armor letting more through late (`Armor.BleedThroughNow`).
	///
	/// ⛔ A DELIBERATE DEPARTURE FROM THE ORIGINAL, BY REQUEST. `sv_nz_armor_system:124`
	/// halves the armor DEPLETION, so a vest soaks twice as much before breaking and the
	/// damage taken is unchanged. This halves the damage taken instead, and armor depletes
	/// at the normal rate.
	///
	/// ⚠️ WHY THAT IS NOT A SECOND BULWARK. M3 scales ALL incoming damage, always. This
	/// scales only the fraction that survives armor, so it does nothing at tier 0, nothing
	/// with an empty vest, and stops the moment armor breaks. The two stack when both are
	/// equipped and armored, which is intended — that is a major plus a minor spent on the
	/// same axis.
	/// </summary>
	public static float WeaveScale { get; set; } = 0.8f;

	/// <summary>
	/// m3 Dense Plating — EXTRA HITS PER ARMOR BAR. 2.
	///
	/// ⛔ WAS 5 UNTIL 2026-10-03 — see Efficient Weave above. A tier-3 vest held 3,750 with it (42 hits at round 31+); at +2 it
	/// holds 3,300 (37 hits).
	///
	/// ⛔️ WAS A x1.5 ON THE CAP. Durability is authored in hits now (ArmorSettings.BarHits), and
	/// a multiplier on the total would have been worth a flat 50% on every tier. Hits per BAR
	/// compounds with the bar count the way the tiers themselves do: +5 on tier 1 is +5 hits of
	/// vest, +5 on tier 3 is +15.
	///
	/// ⚠️ RENAMED, NOT RETYPED IN PLACE. A hotload copies statics forward by NAME and skips the
	/// initialiser, so leaving this as DensePlatingScale would have kept the running editor on
	/// 1.5 while the source read 5 (INSTRUCTIONS.md).
	/// </summary>
	public static int DensePlatingBarHits { get; set; } = 2;

	/// <summary>m4 Adrenal Surge — speed multiplier while the window is open.</summary>
	public static float AdrenalSpeed { get; set; } = 1.25f;

	/// <summary>m4 Adrenal Surge — how long the window lasts, seconds.</summary>
	public static float AdrenalSeconds { get; set; } = 2f;

	/// <summary>
	/// m5 Retaliate — how long the attacker is stunned. **5s**, not the original's 1.
	///
	/// ⛔ A REAL FIELD AGAIN, AND `StatusEffects.Apply` GREW A `seconds` OVERRIDE FOR IT.
	/// It was briefly a read-through of the `stun` rule, because Apply took no duration —
	/// which made the value honest but unchangeable. Editing the shared rule to 5s was the
	/// wrong fix: Elemental Pop's discharge uses the same `stun` and its 1s is correct, so
	/// one value serving two callers with different needs is §3 waiting to happen.
	///
	/// ⚠️ The override follows the 0-means-the-rule's convention `speedScale` already
	/// established there, rather than inventing a second one.
	///
	/// ⚠️ 5s IS A LONG TIME on a zombie — it is most of a walk across a room. This is a
	/// judged value from the user, not the original's, and it makes m5 substantially
	/// stronger than the port it came from.
	/// </summary>
	public static float RetaliateStun { get; set; } = 5f;

	// ── helpers ──────────────────────────────────────────────────────────────

	/// <summary>
	/// Does this player have Juggernog AND this augment.
	///
	/// ⛔ BOTH HALVES, ALWAYS, AND THE PERK HALF IS NOW LOAD-BEARING RATHER THAN DEFENSIVE. This
	/// used to say the augment "should already be gone" because `PerkAugments.ClearFor` ran on perk
	/// loss. It no longer does — by request, going down costs the perk and keeps the augments — so an
	/// augment surviving its perk is now the NORMAL state between going down and re-buying, and
	/// `HasPerk` is the only thing making it inert. Dropping it would turn every augment the player
	/// ever bought into a permanent free buff.
	/// </summary>
	static bool Has( NZPlayer p, string augId )
		=> p.IsValid() && p.HasPerk( Perk ) && PerkAugments.Has( p, Perk, augId );

	// ── M1 Overhealth · stat-based ───────────────────────────────────────────

	/// <summary>
	/// Recompute max health from what is owned, and heal into the new ceiling.
	///
	/// ⛔ A RECOMPUTE, NOT `+=`, AND THAT IS A FIX RATHER THAN A STYLE CHOICE.
	/// `PerkEffects.OnPerkGained` did `hp.Max += JuggBonusHealth`, which is correct
	/// exactly once and wrong every other time it runs. An augment bought AFTER the perk
	/// needs the total recomputed, and there is no additive form of that which does not
	/// stack on a second call. The original recomputes for the same reason
	/// (`RefreshJuggHealth`), from settings, every time.
	///
	/// ⚠️ HEALS UP BUT NEVER DOWN. Buying Juggernog should fill you to the new maximum —
	/// that is why health is the one thing the perk system handles as an event at all.
	/// Losing the perk clamps down, and that lives in `NZPlayer.LosePerksOnDown`, which
	/// sets Max absolutely and then trims Current.
	///
	/// ⚠️ SAFE TO CALL FOR A PLAYER WITHOUT THE PERK, and it must be: this runs on every
	/// refresh, and returning early would leave a stale Max behind after a perk loss that
	/// went through some other path.
	/// </summary>
	public static void RefreshHealth( NZPlayer player )
	{
		if ( !player.IsValid() ) return;

		var hp = player.Components.Get<Health>( FindMode.EverythingInSelf );
		if ( !hp.IsValid() ) return;

		// ⚠️ FROM THE MATCH'S MAX HEALTH (the lobby's Difficulty, 2026-10-05)
		var max = Difficulty.MaxHealth;

		if ( player.HasPerk( Perk ) )
		{
			max += PerkEffects.JuggBonusHealth;
			if ( Has( player, "M1" ) ) max += OverhealthBonus;
		}

		// ⚠️ LIFELINE (handgun tier 5, 2026-10-04): +150 while its gun is in hand. Its share is remembered so a switch can tell
		// it from Juggernog's below.
		var lifeline = ClassTech.LifelineBonus( player );
		var lifelineRise = lifeline - player.LifelineApplied;
		player.LifelineApplied = lifeline;
		max += lifeline;

		// ⚠️ GUARDED, so the common case — a refresh that changes nothing — does not
		// touch Health at all. Without this, every round start would reassign Max and
		// heal the player to full, which is Plated Up's effect applied to health by
		// accident.
		if ( MathF.Abs( hp.Max - max ) < 0.01f ) return;

		var before = hp.Max;
		hp.Max = max;

		// ⚠️ A RISE HEALS INTO THE NEW CEILING; A FALL ONLY TRIMS. Taking Overhealth off lowers
		// the ceiling through here, and the single line this was healed the player to full on
		// the way down — a free heal, with half the augment's price back — or, above the new
		// ceiling, left them over it.
		// ⛔ A RISE THAT IS ONLY LIFELINE'S HEALS NOTHING: drawing the pistol must not be a free full heal on every swap. Its
		// fall trims like any other (the user: health above your normal maximum is lost).
		if ( max > before && max - before <= lifelineRise + 0.01f ) { /* the ceiling alone */ }
		else if ( max > before ) { if ( hp.Current < max ) hp.Reset( max ); }
		else if ( hp.Current > max ) hp.Reset( max );

		Log.Info( $"[nz-aug] jugg health {before:0} → {max:0}"
			+ $" (base {Difficulty.MaxHealth:0}"
			+ $"{(player.HasPerk( Perk ) ? $" + jugg {PerkEffects.JuggBonusHealth:0}" : "")}"
			+ $"{(Has( player, "M1" ) ? $" + M1 {OverhealthBonus:0}" : "")})" );
	}

	// ── M3 Bulwark · m4 Adrenal Surge · m5 Retaliate · on damage ─────────────

	/// <summary>
	/// The three damage-time augments. Returns the possibly-reduced amount.
	///
	/// ⚠️ ALL THREE READ THE SAME EVENT, so they share one entry point rather than three
	/// hooks into `Health.Apply`. They are independent: Bulwark scales, Adrenal stamps a
	/// window, Retaliate hits back — no ordering between them matters, which is exactly
	/// why one function is safe here.
	/// </summary>
	public static float OnPlayerDamaged( NZPlayer victim, GameObject from, float amount, bool clawed = true )
	{
		if ( !victim.IsValid() || !victim.HasPerk( Perk ) ) return amount;

		// m4 Adrenal Surge — a window, not an impulse.
		//
		// ⚠️ STAMPED ON THE PLAYER, read by the speed multiplier. The original does the
		// same via a networked float rather than by writing a speed, because writing one
		// means owning the job of writing it back — and a missed write-back is a
		// permanent speed buff.
		if ( Has( victim, "m4" ) )
			victim.AdrenalUntil = AdrenalSeconds;

		// m5 Retaliate — the attacker eats a stun.
		//
		// ⚠️ ZOMBIES ONLY. The original asks `attacker:IsValidZombie()`; without that
		// check a trap, a teammate's grenade or the player's own fall damage would all
		// try to stun whatever object was named as the attacker.
		//
		// ⚠️ AND ONLY A CLAW (`clawed`): an area hit — Oberon's bombs, his leap, his black hole — is no melee to answer, and
		// would have stunned him for five seconds with each of his own bombs.
		if ( clawed && Has( victim, "m5" ) && from.IsValid() )
		{
			var zombie = from.Components.Get<ZombieAI>( FindMode.EverythingInSelfAndAncestors );

			// ⚠️ The stun is applied to the ZOMBIE'S OWN GameObject, not to `from`.
			// `from` may be a hitbox child, and a status on a child is a status nothing
			// reads — `StatusEffects.IsDisarmed` is asked about the AI's object.
			if ( zombie.IsValid() )
				StatusEffects.Apply( zombie.GameObject, "stun", victim.GameObject,
					seconds: RetaliateStun );
		}

		// M3 Bulwark — last, so the returned figure is the final one.
		if ( Has( victim, "M3" ) )
			amount *= BulwarkScale;

		return amount;
	}

	/// <summary>
	/// m4 Adrenal Surge's speed contribution. 1 when the window is shut.
	///
	/// ⚠️ ASKED PER FRAME, so it must be cheap and must not log. Folded into
	/// `PerkEffects.SpeedMultiplier` so both consumers — NZPlayer's walk/run speeds and
	/// Stamina's sprint speed — get it from the one place. Multiplying it in at only one
	/// of the two would give a boost that vanished the moment you sprinted.
	/// </summary>
	public static float SpeedMultiplier( NZPlayer player )
	{
		if ( !player.IsValid() || player.AdrenalUntil <= 0f ) return 1f;
		if ( !Has( player, "m4" ) ) return 1f;

		return AdrenalSpeed;
	}

	// ── M4 Bloodthirst · m1 Hardplate · on kill ──────────────────────────────

	/// <summary>
	/// The two on-kill augments.
	///
	/// ⚠️ `headshot` GATES ONLY HARDPLATE. Bloodthirst pays on any kill; the original's
	/// two hooks differ in exactly that and nothing else.
	/// </summary>
	public static void OnZombieKilled( NZPlayer player, bool headshot )
	{
		if ( !player.IsValid() || !player.HasPerk( Perk ) ) return;

		// M4 Bloodthirst
		if ( Has( player, "M4" ) )
		{
			var hp = player.Components.Get<Health>( FindMode.EverythingInSelf );

			// ⛔ `Heal`, NOT A WRITE TO `Current` — which is private-set anyway, and the
			// method exists precisely so healing does not go through the damage path.
			// `Apply` with a negative amount clamps to zero and also fires OnDamaged,
			// which HealthRegen reads as "was just hit" — healing that way would reset
			// the regen delay forever. Heal clamps to Max itself.
			if ( hp.IsValid() )
				hp.Heal( BloodthirstHeal );
		}

		// m1 Hardplate
		//
		// ⚠️ REQUIRES AN ARMOR TIER, silently. Armor.CapFor is 0 at tier 0, so the
		// MathF.Min below would put armor at 0 and the augment would appear broken —
		// which it is not, it is unequippable-into. Report() states this.
		if ( headshot && Has( player, "m1" ) )
		{
			var cap = Armor.CapFor( player );
			if ( cap > 0f )
				player.Armor = MathF.Min( cap, player.Armor + HardplateArmor );
		}
	}

	// ── M2 Plated Up · on round start ────────────────────────────────────────

	/// <summary>
	/// M2 Plated Up — armor to full at the start of a round.
	///
	/// ⚠️ NO PLATE IS SPENT and none is required. That is the whole augment: it is a
	/// free refill once a round, which is why it is a major.
	/// </summary>
	public static void OnRoundStart( NZPlayer player )
	{
		if ( !Has( player, "M2" ) ) return;

		var cap = Armor.CapFor( player );
		if ( cap <= 0f ) return;   // no tier owned — nothing to fill

		player.Armor = cap;
		Log.Info( $"[nz-aug] jugg M2 Plated Up — armor refilled to {cap:0}" );
	}

	// ── m2 Efficient Weave · m3 Dense Plating · armor seams ──────────────────

	/// <summary>m3 Dense Plating's extra hits per armor bar. 0 when not equipped. (Its old cap multiplier is gone, and so is its
	/// doc block, which was still sitting here on this member until 2026-10-03.)</summary>
	public static int ArmorBarHits( NZPlayer player )
		=> Has( player, "m3" ) ? DensePlatingBarHits : 0;

	/// <summary>
	/// m2 Efficient Weave's multiplier on the damage that gets THROUGH armor. 1 when not
	/// equipped.
	///
	/// ⛔ THIS REPLACED AN `ArmorLossScale` THAT SCALED DEPLETION INSTEAD. The original
	/// halves what the vest spends; the requested behaviour is to halve what the player
	/// takes. Armor now depletes at the normal rate and the bleed-through is cut, so a
	/// 30%-bleed vest passes 24% (15% while this was 0.5, until 2026-10-03).
	///
	/// ⚠️ MULTIPLIES THE BLEED-THROUGH, so it inherits armor's own preconditions for
	/// free: no tier, no armor left, or armor-bypassing damage all skip `Absorb` entirely
	/// and the augment does nothing. That is what keeps it distinct from M3 Bulwark, which
	/// applies to every hit unconditionally.
	/// </summary>
	public static float ArmorBleedScale( NZPlayer player )
		=> Has( player, "m2" ) ? WeaveScale : 1f;

	// ── diagnostics ──────────────────────────────────────────────────────────

	/// <summary>
	/// Print each augment's live contribution.
	///
	/// ⛔ PRINTS THE RESULTING NUMBER, NOT "equipped: yes". Whether an augment is
	/// equipped is already answerable from `nz_augments jugg`; what that cannot tell you
	/// is whether it is REACHING the system it is meant to move. Five of these nine touch
	/// armor and three of them are inert at tier 0, so "equipped and doing nothing" is a
	/// legitimate state that needs saying rather than debugging.
	/// </summary>
	public static void Report( NZPlayer player )
	{
		if ( !player.IsValid() ) { Log.Warning( "[nz-aug] no player" ); return; }

		var has = player.HasPerk( Perk );
		var hp = player.Components.Get<Health>( FindMode.EverythingInSelf );
		var equipped = PerkAugments.EquippedOn( player, Perk );

		Log.Info( $"[nz-aug] JUGGERNOG {(has ? "owned" : "NOT OWNED — every line below is inert")}"
			+ $" · equipped [{(equipped.Length == 0 ? "none" : string.Join( "+", equipped ))}]" );

		Log.Info( $"[nz-aug]  M1 Overhealth   health {(hp.IsValid() ? $"{hp.Current:0}/{hp.Max:0}" : "?")}"
			+ $"   {(Has( player, "M1" ) ? $"+{OverhealthBonus:0} from M1" : "-")}" );

		Log.Info( $"[nz-aug]  M2 Plated Up    {(Has( player, "M2" ) ? "refills at round start" : "-")}" );

		Log.Info( $"[nz-aug]  M3 Bulwark      damage x{(Has( player, "M3" ) ? BulwarkScale : 1f):0.##}" );

		Log.Info( $"[nz-aug]  M4 Bloodthirst  {(Has( player, "M4" ) ? $"+{BloodthirstHeal:0} hp per kill" : "-")}" );

		Log.Info( $"[nz-aug]  m1 Hardplate    {(Has( player, "m1" ) ? $"+{HardplateArmor:0} armor per headshot kill" : "-")}" );

		// ⚠️ PRINTS THE RESOLVED BLEED, not just the multiplier. "x0.5" says nothing
		// about how much damage actually reaches health, and the base bleed-through is a
		// config value that can be edited.
		// ⚠️ THIS ROUND'S, which rises late (`Armor.BleedThroughNow`, 2026-10-03).
		var bleed = Armor.BleedThroughNow();
		Log.Info( $"[nz-aug]  m2 Weave        damage through armor x{ArmorBleedScale( player ):0.##}"
			+ $"   ({bleed * 100f:0.#}% → {bleed * ArmorBleedScale( player ) * 100f:0.#}% of a hit)" );

		var tier = player.ArmorTier;
		Log.Info( $"[nz-aug]  m3 Dense Plate  +{ArmorBarHits( player )} hits per bar"
			+ $"   armor {player.Armor:0}/{Armor.CapFor( player ):0}"
			+ (tier > 0
				? $"  ({tier} bar(s) x {Armor.BarHits( player, tier )} hits)"
				: "  (no tier — inert)") );

		Log.Info( $"[nz-aug]  m4 Adrenal      speed x{SpeedMultiplier( player ):0.##}"
			+ $"   window {(player.AdrenalUntil > 0f ? $"{(float)player.AdrenalUntil:0.##}s left" : "shut")}" );

		Log.Info( $"[nz-aug]  m5 Retaliate    {(Has( player, "m5" ) ? $"melee attacker stunned {RetaliateStun:0.##}s" : "-")}"
			+ $"   (rule default {(StatusEffects.Rules.TryGetValue( "stun", out var sr ) ? sr.Seconds : 0f):0.##}s)" );

		// ⚠️ THE TIER-0 WARNING IS THE POINT OF THIS COMMAND. Three augments here are
		// no-ops without a vest, and the default tier is 0 — so the most likely first
		// experience of m1/m2/m3 is "it does nothing".
		if ( player.ArmorTier <= 0 )
			Log.Warning( "[nz-aug]  ⚠ ARMOR TIER 0 — m1, m2 and m3 cannot do anything."
				+ " nz_armor_tier 1, then nz_armor_plate + nz_armor_use." );
	}

	// ── commands ─────────────────────────────────────────────────────────────

	static NZPlayer Me()
		=> NZPlayer.Local;

	/// <summary>`nz_aug_jugg` — the report on its own.</summary>
	[ConCmd( "nz_aug_jugg" )]
	public static void JuggCmd() => Report( Me() );

	/// <summary>
	/// `nz_aug_jugg_set [overhealth] [bulwark] [bloodthirst] [hardplate] [weave] [dense]`
	/// — retune the six numeric augments.
	///
	/// ⚠️ Adrenal and Retaliate are on the second command; six positional arguments is
	/// already more than anyone will remember, and the report prints every current value
	/// so the command is discoverable from there rather than from its signature.
	/// </summary>
	[ConCmd( "nz_aug_jugg_set" )]
	public static void SetCmd( float overhealth = -1f, float bulwark = -1f,
		float bloodthirst = -1f, float hardplate = -1f, float weave = -1f, int dense = -1 )
	{
		if ( overhealth >= 0f ) OverhealthBonus = overhealth;
		if ( bulwark >= 0f ) BulwarkScale = bulwark;
		if ( bloodthirst >= 0f ) BloodthirstHeal = bloodthirst;
		if ( hardplate >= 0f ) HardplateArmor = hardplate;
		if ( weave >= 0f ) WeaveScale = weave;
		if ( dense >= 0 ) DensePlatingBarHits = dense;

		// ⚠️ A health change has to be pushed, not just stored. OverhealthBonus is
		// stat-based, so without this the new value sits there until the next perk or
		// augment event — which reads as the command having no effect.
		RefreshHealth( Me() );

		Report( Me() );
	}

	/// <summary>
	/// `nz_aug_jugg_adrenal [speed] [seconds] [stun]` — m4's window and m5's stun.
	///
	/// ⚠️ THE STUN ARGUMENT IS BACK, and it is real now: `StatusEffects.Apply` takes a
	/// per-application `seconds`, so setting it here does not touch the shared `stun` rule
	/// and cannot retune Elemental Pop's discharge by accident.
	/// </summary>
	[ConCmd( "nz_aug_jugg_adrenal" )]
	public static void AdrenalCmd( float speed = -1f, float seconds = -1f, float stun = -1f )
	{
		if ( speed >= 0f ) AdrenalSpeed = speed;
		if ( seconds >= 0f ) AdrenalSeconds = seconds;
		if ( stun >= 0f ) RetaliateStun = stun;

		var p = Me();

		// ⚠️ OPENS THE WINDOW so the boost can be felt immediately. A speed change you
		// have to go and get hit to observe is a change nobody will verify.
		if ( p.IsValid() && Has( p, "m4" ) ) p.AdrenalUntil = AdrenalSeconds;

		Log.Info( $"[nz-aug] jugg m4 speed x{AdrenalSpeed:0.##} for {AdrenalSeconds:0.##}s"
			+ $" · m5 stun {RetaliateStun:0.##}s"
			+ $" · window {(p.IsValid() && p.AdrenalUntil > 0f ? "OPEN" : "shut")}" );
	}
}