perks/CurseMoveDrainHealth.cs

A unique curse perk called "Bloody Feet" that drains the players health when they move. It accumulates distance traveled and when a distance threshold is passed it applies self-damage equal to a configured amount, then resets distance and damage counters and triggers UI highlights.

NetworkingFile Access
using System;
using Sandbox;

[Perk( Rarity.Unique, curse: true, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.SelfDmg })]
public class CurseMoveDrainHealth : Perk
{
	private enum Mod { Distance, MaxHpLoss };


	private float _currDamage;
	private float _currDist;

	static CurseMoveDrainHealth()
	{
		Register<CurseMoveDrainHealth>(
			name: "Bloody Feet",
			imagePath: "textures/icons/vector/curse_move_drain_health.png",
			description: level => $"-{(int)GetValue( level, Mod.MaxHpLoss )} hp when you move [-]{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();

	}

	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;

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

			if ( _currDist > distReq )
			{
				if ( !(Player.IsInvincible && Player.Stats[PlayerStat.IgnoreSelfDamageWhenInvuln] > 0f) )
				{
					_currDamage += GetValue( Level, Mod.MaxHpLoss );

					if ( _currDamage > 0.85f )
					{
						Player.Damage( _currDamage, DamageType.Self, Player.Position2D, Utils.GetRandomVector(), upwardAmount: 0f, force: 0f, ragdollForce: 0f, 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();

						IconScale = Game.Random.Float( 1.1f, 1.2f );
						IconAngleOffset = Game.Random.Float( 10f, 15f ) * (Game.Random.Int( 0, 1 ) == 0 ? -1f : 1f);
					}
				}

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

			//Player.Modify( this, PlayerStat.HpRegenDisplay, -GetValue( Level, Mod.MaxHpLoss ) * vel * 0.02f, ModifierType.Add );
			//isCurrentlyDraining = true;
		}

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

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.Distance:
			default:
				return 2.8f * (isPercent ? 1f : Utils.Meter2Unit);
			case Mod.MaxHpLoss:
				return 2;
		}
	}
}