perks/CurseHurtTurnCounterclockwise.cs

A unique curse perk for a player that deals self-damage when the player turns counterclockwise a certain accumulated amount. It tracks yaw changes, accumulates positive (counterclockwise) rotation, and applies damage and visual feedback when a threshold is crossed.

Networking
using System;
using Sandbox;

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


	// Track player rotation over time
	private float _lastYaw;
	private float _accumulatedCCW;
	private TimeSince _timeSinceHurt;

	static CurseHurtTurnCounterclockwise()
	{
		Register<CurseHurtTurnCounterclockwise>(
			name: "Forbidden Glance",
			imagePath: "textures/icons/vector/curse_hurt_turn_counterclockwise.png",
			description: level => $"-{(int)GetValue( level, Mod.HpLoss )} hp while turning counterclockwise"
		);
	}

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

		ShouldUpdate = true;

		_lastYaw = Player.Model.LocalRotation.Yaw();
		_accumulatedCCW = 0f;

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

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

	}

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

		// Track turning
		float currentYaw = Player.Model.LocalRotation.Yaw();
		float deltaYaw = AngleDelta( _lastYaw, currentYaw );
		_lastYaw = currentYaw;

		// Counterclockwise is positive deltaYaw
		if ( deltaYaw > 0f )
			_accumulatedCCW += deltaYaw;

		var turnThreshold = 45f; // degrees to trigger damage
		if ( _accumulatedCCW > turnThreshold && _timeSinceHurt > 0.25f )
		{
			var dir = Utils.GetPerpendicularVector( Player.FacingDir );
			Player.Damage( GetValue( Level, Mod.HpLoss ), DamageType.Self, Player.Position2D, dir, upwardAmount: 0f, force: 0f, ragdollForce: 1f, enemySource: null, enemyType: EnemyType.None );

			Highlight();

			_accumulatedCCW -= turnThreshold;
			_timeSinceHurt = 0f;

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

		_accumulatedCCW *= (1f - Time.Delta * 1.5f);
	}

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

	private static float AngleDelta( float from, float to )
	{
		float delta = (to - from) % 360f;
		if ( delta > 180f ) delta -= 360f;
		if ( delta < -180f ) delta += 360f;
		return delta;
	}
}