perks/CurseHurtChangeDirection.cs

A unique curse perk class that hurts the player when they quickly reverse movement direction. It tracks recent movement, checks dot product against previous movement, and applies self-damage plus a visual highlight and icon effects.

Networking
using System;
using Sandbox;

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


	private TimeSince _timeSinceHurt;

	private Vector2 _movementVector;

	static CurseHurtChangeDirection()
	{
		Register<CurseHurtChangeDirection>(
			name: "Shin Splints",
			imagePath: "textures/icons/vector/curse_hurt_change_direction.png",
			description: level => $"-{(int)GetValue( level, Mod.HpLoss )} hp when quickly moving\nin the opposite direction"
		);
	}

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

		ShouldUpdate = true;

		HighlightColor = new Color( 1f, 0.2f, 0.3f );
		HighlightDuration = 0.55f;
		HighlightOpacity = 6f;
	}

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

	}

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

		if ( _movementVector.Length > 0.05f && Player.MoveInputPercent > 0f && _timeSinceHurt > 0.4f )
		{
			var dot = Vector2.Dot( _movementVector.Normal, Player.MoveVector );
			if( dot < -0.7f )
			{
				Player.Damage( GetValue( Level, Mod.HpLoss ), DamageType.Self, Player.Position2D, _movementVector, upwardAmount: 0f, ragdollForce: 1f, force: 0f, enemySource: null, enemyType: EnemyType.None );
				
				Highlight();

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

				_timeSinceHurt = 0f;
			}
		}
		_movementVector = Vector2.Lerp( _movementVector, Player.MoveVector, dt * 25f );
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.HpLoss:
			default:
				return 7;
				//switch( level )
				//{
				//	case 1: default: return 4;
				//	case 2: return 8;
				//	case 3: return 16;
				//	case 4: return 30;
				//}
		}
	}
}