perks/CurseHurtMoveDiagonal.cs

Perk component that damages the player when they move diagonally for a short accumulated time. It tracks analog movement, accumulates diagonal time, and applies self-damage and a visual highlight when a threshold is exceeded.

Networking
using System;
using Sandbox;

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


	// Track diagonal movement over time
	private float _accumulatedDiagonalTime;
	private TimeSince _timeSinceHurt;

	static CurseHurtMoveDiagonal()
	{
		Register<CurseHurtMoveDiagonal>(
			name: "Angular Strain",
			imagePath: "textures/icons/vector/curse_hurt_move_diagonal.png",
			description: level => $"-{(int)GetValue( level, Mod.HpLoss )} hp while moving diagonally"
		);
	}

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

		ShouldUpdate = true;

		_accumulatedDiagonalTime = 0f;

		HighlightColor = new Color( 1f, 0.3f, 0.6f );
		HighlightDuration = 0.75f;
		HighlightOpacity = 4f;
	}

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

	}

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

		// Check if player is moving diagonally
		var moveVector = Player.MoveVector;
		var isMoving = moveVector.Length > 0.1f;
		
		if ( isMoving )
		{
			// Normalize to check direction
			var normalizedMove = Input.AnalogMove.Normal;
			
			// Calculate how "diagonal" the movement is
			// Pure cardinal directions (up/down/left/right) have abs(x) or abs(y) close to 1 and the other close to 0
			// Diagonal movement has both abs(x) and abs(y) away from 0
			var absX = MathF.Abs( normalizedMove.x );
			var absY = MathF.Abs( normalizedMove.y );
			
			// Diagonalness is high when both components are significant (away from pure cardinal)
			// Using min of the two components - this is 0 for cardinal, ~0.707 for perfect diagonal
			var diagonalness = MathF.Min( absX, absY );
			
			// Consider it diagonal if the smaller component is > 0.3 (roughly 30+ degrees from cardinal)
			var isDiagonal = diagonalness > 0.25f;
			
			if ( isDiagonal )
			{
				_accumulatedDiagonalTime += dt;
			}
			else
			{
				// Decay when moving in cardinal directions
				_accumulatedDiagonalTime *= (1f - dt * 2f);
			}
		}
		else
		{
			// Decay when not moving
			_accumulatedDiagonalTime *= (1f - dt * 2f);
		}

		var timeThreshold = 0.35f; // seconds of diagonal movement to trigger damage
		if ( _accumulatedDiagonalTime > timeThreshold && _timeSinceHurt > 0.25f )
		{
			Player.Damage( GetValue( Level, Mod.HpLoss ), DamageType.Self, Player.Position2D, Player.MoveVector, upwardAmount: 0f, force: 0f, ragdollForce: 1f, enemySource: null, enemyType: EnemyType.None );

			Highlight();

			_accumulatedDiagonalTime -= timeThreshold;
			_timeSinceHurt = 0f;

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

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.HpLoss:
			default:
				return 1f;
				//switch( level )
				//{
				//	case 1: default: return 1;
				//	case 2: return 5;
				//	case 3: return 10;
				//	case 4: return 25;
				//}
		}
	}
}