perks/PerkBerserk.cs

A game perk class 'PerkBerserk' that gives the player a temporary berserk state after dashing. It modifies attack and reload speed, damage reduction, forces movement/aim toward nearest enemy, updates UI display and plays a nearby sound and overlay while active.

Networking
using Sandbox;
using System;

[Perk( Rarity.Mythic, locked: true, alwaysOfferDebug: false )]
public class PerkBerserk : Perk
{
	private enum Mod { Speed, Time, DamageReduction };

	private TimeSince _timeSinceStart;

	private TimeSince _timeSinceCheck;

	static PerkBerserk()
	{
		Register<PerkBerserk>(
			name: "Berserker",
			imagePath: "textures/icons/vector/berserk.png",
			description: level => $"On dash, lose control for {GetValue( level, Mod.Time ).ToString("0.#")}s with +{GetValue( level, Mod.Speed, true )}% attack/reload speed\nand [+]-{GetValue( level, Mod.DamageReduction, true )}%[/+] dmg taken",
			upgradeDescription: level => $"On dash, lose control for {GetValue( level - 1, Mod.Time ).ToString( "0.#" )}→{GetValue( level, Mod.Time ).ToString( "0.#" )}s with +{GetValue( level - 1, Mod.Speed, true )}→+{GetValue( level, Mod.Speed, true )}% attack/reload speed\nand [+]-{GetValue( level - 1, Mod.DamageReduction, true )}→-{GetValue( level, Mod.DamageReduction, true )}%[/+] dmg taken"
		);
	}

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

		ShouldUpdate = true;
		DisplayCooldownColor = new Color( 1f, 0.5f, 0.4f, 3f );

		HighlightColor = new Color( 1f, 1f, 1f );
		HighlightDuration = 0.15f;
		HighlightOpacity = 1f;
	}

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

	}

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

		DisplayCooldown = Utils.Map( _timeSinceStart, 0f, GetValue( Level, Mod.Time ), 1f, 0f );

		// todo: vfx
		//foreach ( var renderer in Renderers )
		//	renderer.Tint = Color.Lerp( Color.White, Color.Red, Utils.Map( Utils.FastSin( Time.Now * 30f ), -1f, 1f, 0f, 1f ) );

		if ( _timeSinceStart > GetValue( Level, Mod.Time ) )
		{
			DisableBerserk();
		}
		else
		{
			if( _timeSinceCheck > 0.2f )
			{
				var closestEnemy = Manager.Instance.GetClosestEnemy( Player.Position2D );
				var dir = closestEnemy.IsValid() ? ( closestEnemy.Position2D - Player.Position2D).Normal : Utils.GetRandomVector();

				Player.MoveVector = dir;
				Player.AimDir = dir;

				_timeSinceCheck = 0f;
			}

			//Player.MoveInputPercent = 1f;
			Player.MoveInputPercent = Input.AnalogMove.Length;

			DisplayTextColor = Color.Lerp( Color.White, Color.Red, 0.5f + Utils.FastSin( _timeSinceStart * 32f ) * 0.5f );

			IconScale = 1f + (Utils.FastSin( RealTime.Now * 26 ) * 0.05f);
			IconAngleOffset = (Utils.FastSin( RealTime.Now * 32f ) * 2f);
		}
	}

	public override void OnDashStarted( Vector2 dir )
	{
		base.OnDashStarted( dir );

		ShouldUpdate = true;
		_timeSinceStart = 0f;

		EnableBerserk();
	}

	void EnableBerserk()
	{
		float speed = 1f + GetValue( Level, Mod.Speed );
		Player.Modify( this, PlayerStat.AttackSpeed, speed, ModifierType.Mult );
		Player.Modify( this, PlayerStat.ReloadSpeed, speed, ModifierType.Mult );
		Player.Modify( this, PlayerStat.DamageReductionPercent, GetValue( Level, Mod.DamageReduction ), ModifierType.Add );
		Player.Modify( this, PlayerStat.IsBerserk, 1f, ModifierType.Add );

		DisplayText = $"+{GetValue( Level, Mod.Speed, true )}%";

		Highlight();

		Manager.Instance.PlaySfxNearbyRpc( "berserk", Player.Position2D, pitch: Game.Random.Float( 1.2f, 1.5f ), volume: 0.9f, maxDist: 320f );

		Manager.Instance.OverlayEffects[OverlayEffectsType.Berserk].Enabled = true;
	}

	void DisableBerserk()
	{
		ShouldUpdate = false;

		Player.StopModifying( this, PlayerStat.AttackSpeed );
		Player.StopModifying( this, PlayerStat.ReloadSpeed );
		Player.StopModifying( this, PlayerStat.DamageReductionPercent );
		Player.StopModifying( this, PlayerStat.IsBerserk );

		DisplayText = " ";

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

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.Speed:
			default:
				return isPercent
					? 60f + 20f * level
					: 0.6f + 0.2f * level;
			case Mod.Time:
				return 1f + 1f * level;
				// todo: might be too long of time, leading to perma-berserk and being unkillable?
			case Mod.DamageReduction:
				return isPercent
					? 50f + 10f * level
					: 0.50f + 0.10f * level;
		}
	}

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

		if ( Player.Stats[PlayerStat.IsBerserk] > 0f )
			DisableBerserk();

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

	// todo: make sure other perks remove things that they need to
	public override void Remove( bool restart = false )
	{
		base.Remove( restart );

		if ( Player.Stats[PlayerStat.IsBerserk] > 0f )
			DisableBerserk();
	}
}