perks/PerkDashSlash.cs

Perk component that grants a Dash Slash ability. It modifies player stats for dash damage, length and invulnerability, plays a sound when dashing, makes the player temporarily ignore physics while dashing, and damages enemies the player passes through exactly once per enemy.

Networking
using System;
using Sandbox;

[Perk( Rarity.Uncommon, alwaysOfferDebug: false )]
public class PerkDashSlash : Perk
{
	private enum Mod { DamagePercent, DashLength };

	private List<Enemy> _hitEnemies = new List<Enemy>();

	private bool _ignorePhysicsActive;

	static PerkDashSlash()
	{
		Register<PerkDashSlash>(
			name: "Dash Slash",
			imagePath: "textures/icons/vector/dash_slash.png",
			description: level => $"Dash through enemies\nfor {GetValue( level, Mod.DamagePercent, true )}% of bullet dmg\n+{GetValue( level, Mod.DashLength, true )}% dash distance",
			upgradeDescription: level => $"Dash through enemies\nfor {GetValue( level - 1, Mod.DamagePercent, true )}%→{GetValue( level, Mod.DamagePercent, true )}% of bullet dmg\n+{GetValue( level - 1, Mod.DashLength, true )}%→{GetValue( level, Mod.DashLength, true )}% dash distance"
		);
	}

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

	}

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

		Player.Modify( this, PlayerStat.DashSlashBulletDamagePercent, GetValue( Level, Mod.DamagePercent ), ModifierType.Add );
		Player.Modify( this, PlayerStat.DashStrength, GetValue( Level, Mod.DashLength ), ModifierType.Mult );
		Player.Modify( this, PlayerStat.DashInvulnTime, GetValue( Level, Mod.DashLength ), ModifierType.Mult );
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.DamagePercent:
			default:
				return isPercent
					? 70f + 50f * level + (level == 6 ? 20f : 0f)
					: 0.7f + 0.5f * level + (level == 6 ? 0.2f : 0f);
			case Mod.DashLength:
				return isPercent
					? 10f * level
					: 1f + 0.10f * level;
		}
	}

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

		Manager.Instance.PlaySfxNearbyRpc( "player.dash.slash", Player.Position2D, pitch: Game.Random.Float( 1.05f, 1.1f ), volume: 0.7f, maxDist: 200f );
		_hitEnemies.Clear();

		if( !_ignorePhysicsActive )
		{
			Player.IgnorePhysicsAmount++;
			_ignorePhysicsActive = true;
		}
	}

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

		if ( _ignorePhysicsActive )
		{
			Player.IgnorePhysicsAmount--;
			_ignorePhysicsActive = false;
		}
	}

	public override void Colliding( Thing other, float percent, float dt )
	{
		base.Colliding( other, percent, dt );

		if ( Player.IsDashing && other is Enemy enemy && !enemy.IsDying && !_hitEnemies.Contains( enemy ) )
		{
			float damage = Player.GetBulletDamage( isFromClip: false, isLastAmmo: false ) * GetValue( Level, Mod.DamagePercent );

			Vector2 dir = (other.Position2D - Player.Position2D).LengthSquared > Manager.TOUCH_DIST_REQUIRED_SQR
				? (other.Position2D - Player.Position2D).Normal
				: Utils.GetRandomVector();

			var hitPos = enemy.Position2D - dir * enemy.Radius;

			enemy.DamageRpc( damage, Player, DamageType.DashSlash, new Vector3( hitPos.x, hitPos.y, enemy.Height ), force: dir * 50f, isCrit: false, shouldFlinch: true );

			_hitEnemies.Add( enemy );
		}
	}

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

		if ( _ignorePhysicsActive )
			Player.IgnorePhysicsAmount--;
	}
}