perks/PerkMoveDrainHealth.cs

A Mythic perk class 'PerkMoveDrainHealth' named "Death Pace" that damages the player when they move a certain distance and increases overall damage multiplier. It tracks distance moved, applies periodic self-damage when thresholds are passed, updates a display cooldown and HP regen display modifier, and sets highlight/visual properties.

Networking
using System;
using Sandbox;

[Perk( Rarity.Mythic, locked: true, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.SelfDmg })]
public class PerkMoveDrainHealth : Perk
{
	private enum Mod { Distance, OverallDamageMultiplier };

	private float _currDamage;
	private float _currDist;


	static PerkMoveDrainHealth()
	{
		Register<PerkMoveDrainHealth>(
			name: "Death Pace",
			imagePath: "textures/icons/vector/move_drain_health.png",
			description: level => $"+{GetValue( level, Mod.OverallDamageMultiplier, true )}% dmg\n-1 hp when you move {GetValue( level, Mod.Distance, true ).ToString( "0.#" )}m",
			upgradeDescription: level => $"+{GetValue( level - 1, Mod.OverallDamageMultiplier, true )}%→{GetValue( level, Mod.OverallDamageMultiplier, true )}% dmg\n-1 hp when you move [-]{GetValue( level - 1, Mod.Distance, true ).ToString( "0.#" )}→{GetValue( level, Mod.Distance, true ).ToString( "0.#" )}m[/-]"
		);
	}

	public override void Start()
	{
		base.Start();

		ShouldUpdate = true;

		DisplayCooldownColor = new Color( 1f, 0f, 0f, 0.5f );

		HighlightColor = new Color( 1f, 0f, 0f );
		HighlightDuration = 0.25f;
		HighlightOpacity = 3f;
	}

	public override void Refresh()
	{
		base.Refresh();

		Player.Modify( this, PlayerStat.OverallDamageMultiplier, GetValue( Level, Mod.OverallDamageMultiplier ), ModifierType.Mult );
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.Distance:
			default:
				return (1.5f - 0.3f * level) * (isPercent ? 1f : Utils.Meter2Unit);
			case Mod.OverallDamageMultiplier:
				return isPercent
					? 7f + 7f * level
					: 1f + (0.07f + 0.07f * level);
		}
	}

	public override void Update( float dt )
	{
		base.Update( dt );

		bool isCurrentlyDraining = false;

		if ( Player.TotalVelocity.LengthSquared > 0.1f )
		{
			float vel = Player.TotalVelocity.Length;
			_currDist += vel * dt;

			float damage = 1;
			var distReq = GetValue( Level, Mod.Distance );

			if ( _currDist > distReq )
			{
				if ( !(Player.IsInvincible && Player.Stats[PlayerStat.IgnoreSelfDamageWhenInvuln] > 0f) )
				{
					_currDamage += damage;

					if ( _currDamage > 0.85f )
					{
						Player.Damage( _currDamage, DamageType.Self, Player.Position2D, Utils.GetRandomVector(), upwardAmount: 0f, force: 0f, ragdollForce: 1f, enemySource: null, enemyType: EnemyType.None );
						float percent = Utils.Map( Player.Health, Player.Stats[PlayerStat.MaxHp], 0f, 0f, 1f );
						//Manager.Instance.PlaySfxNearbyRpc( "zombie.attack.player", Player.Position2D, pitch: Utils.Map( percent, 0f, 1f, 1.4f, 1.75f, EasingType.QuadIn ), volume: Utils.Map( percent, 0f, 1f, 0.25f, 0.5f, EasingType.QuadIn ), maxDist: 180f );

						_currDamage = 0f;

						Highlight();
					}
				}

				_currDist -= distReq;
				DisplayCooldown = 0f;
			}
			else
			{
				DisplayCooldown = Utils.Map( _currDist, 0f, distReq, 0f, 1f );
			}

			// todo: average this out, so the value doesn't change so rapidly
			Player.Modify( this, PlayerStat.HpRegenDisplay, -damage * vel * 0.02f, ModifierType.Add );
			isCurrentlyDraining = true;
		}

		if ( !isCurrentlyDraining || Player.TotalVelocity.LengthSquared < 0.25f )
			Player.StopModifying( this, PlayerStat.HpRegenDisplay );
	}
}