perks/CurseReverseControls.cs

A unique curse perk that periodically reverses the player's movement and aim controls. It toggles state on a timer, applies/removes player stat modifiers, updates UI highlight/icon properties, plays a sound, and enables/disables a reverse-controls overlay effect.

NetworkingFile Access
using System;
using Sandbox;

[Perk( Rarity.Unique, curse: true, alwaysOfferDebug: false )]
public class CurseReverseControls : Perk
{
	private enum Mod { ReverseTime, Delay, };

	private bool _areControlsReversed;
	private TimeSince _timeSince;
	private float _currDelayTime;

	static CurseReverseControls()
	{
		Register<CurseReverseControls>(
			name: "Backwards Man",
			imagePath: "textures/icons/vector/curse_reverse_controls.png",
			description: level => $"Your controls are\nsometimes reversed"
		);
	}

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

		ShouldUpdate = true;

		DisplayCooldownColor = new Color( 0.75f, 0.25f, 0.45f );

		_timeSince = 0f;
		_currDelayTime = GetValue( Level, Mod.Delay );
	}

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

	}

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

		if ( _timeSince > _currDelayTime )
		{
			_areControlsReversed = !_areControlsReversed;

			if(_areControlsReversed)
			{
				Player.Modify( this, PlayerStat.ReverseMoveControls, 1f, ModifierType.Add );
				Player.Modify( this, PlayerStat.ReverseAimControls, 1f, ModifierType.Add );

				HighlightColor = new Color( 0.5f, 0f, 0f );
				HighlightDuration = 0.25f;
				HighlightOpacity = 3.5f;
				Highlight();

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

				DisplayCooldownColor = new Color( 0.75f, 0.25f, 0.45f );

				Manager.Instance.OverlayEffects[OverlayEffectsType.ReverseControls].Enabled = true;
			}
			else
			{
				Player.StopModifying( this, PlayerStat.ReverseMoveControls );
				Player.StopModifying( this, PlayerStat.ReverseAimControls );

				HighlightColor = new Color( 0f, 0f, 0.5f );
				HighlightDuration = 0.25f;
				HighlightOpacity = 2.5f;
				Highlight();

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

				DisplayCooldownColor = new Color( 0.55f, 0.25f, 0.65f );

				Manager.Instance.OverlayEffects[OverlayEffectsType.ReverseControls].Enabled = false;
			}

			_timeSince = 0f;
			_currDelayTime = _areControlsReversed 
				? MathF.Min( GetValue( Level, Mod.ReverseTime ), GetValue( Level, Mod.ReverseTime ) ) // roll twice and pick the lower value
				: GetValue( Level, Mod.Delay );

			Manager.Instance.PlaySfxNearby( "player.dash", Player.Position2D, pitch: Game.Random.Float( 2.1f, 2.2f ), volume: 0.5f, maxDist: 200f );
		}

		DisplayText = $"{MathX.CeilToInt( _currDelayTime - _timeSince )}";

		DisplayCooldown = _areControlsReversed
			? Utils.Map( _timeSince, 0f, _currDelayTime, 1f, 0f )
			: Utils.Map( _timeSince, 0f, _currDelayTime, 0f, 1f );
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.ReverseTime:
			default:
				return Game.Random.Float( 3f, 13f );
				//return level == 1
				//	? Game.Random.Float( 2f, 8f )
				//	: Game.Random.Float( 5f, 20f );
			case Mod.Delay:
				return Game.Random.Float( 7f, 40f );
				//return level == 1
				//	? Game.Random.Float( 10f, 35f )
				//	: Game.Random.Float( 2f, 15f );
		}
	}

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

		Manager.Instance.OverlayEffects[OverlayEffectsType.ReverseControls].Enabled = false;
	}

	public override void Remove( bool restart = false )
	{
		base.Remove( restart );

		Manager.Instance.OverlayEffects[OverlayEffectsType.ReverseControls].Enabled = false;
	}
}