things/player/Player.Dashing.cs

Player partial class implementing dashing, dash cooldown/recharge, dash effects, teleport/blink and related state. Handles input to start dashes, applies dash velocity, invulnerability timers, perks/loadout callbacks, SFX/particle spawning, and RPC broadcasts for start/finish/recharge.

NetworkingFile Access
using System;
using System.IO;

public partial class Player
{
	public bool IsDashing { get; private set; }
	public float DashTimer { get; private set; }
	public Vector2 DashVelocity { get; private set; }
	public float DashInvulnTimer { get; private set; }
	[Sync] public float DashRechargeProgress { get; set; }
	[Sync] public int NumDashesAvailable { get; set; }
	[Sync] public int NumTempDashesAvailable { get; set; }
	public TimeSince TimeSinceDash { get; private set; }
	public bool HasEverDashed { get; set; }

	private TimeSince _timeSinceDashCloud;

	void HandleDashClouds()
	{
		if ( !IsDashing || IsInTheAir || IsDead )
			return;

		if ( _timeSinceDashCloud > 0.05f )
		{
			var pos = WorldPosition.WithZ( Game.Random.Float( 10f, 20f ) );
			GameObject.Clone( "prefabs/effects/cloud.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( pos ) } );
			_timeSinceDashCloud = 0f;
		}
	}

	void HandleDashing()
	{
		if ( IsDead )// || IsInTheAir )
			return;

		int numDashes = Math.Max( (int)MathF.Round( Stats[PlayerStat.NumDashes] ), 0 );
		if ( NumDashesAvailable < numDashes )
		{
			DashTimer -= Time.Delta * TimeScale;
			DashRechargeProgress = Utils.Map( DashTimer, Stats[PlayerStat.DashCooldown], 0f, 0f, 1f );
			if ( DashTimer <= 0f )
				DashRecharged();
		}

		if ( IsDashing )
		{
			DashInvulnTimer -= Time.Delta;

			if ( !IsInTheAir )
			{
				//DashProgress = Utils.Map(DashInvulnTimer, Stats[PlayerStat.DashInvulnTime], 0f, 0f, 1f);
				if ( DashInvulnTimer <= 0f )
				{
					DashFinished();
				}
				else
				{
					if ( Stats[PlayerStat.CanChangeDashDirection] > 0f )
					{
						if ( MoveVector.LengthSquared > 0f )
							DashVelocity = MoveVector.Normal * DashVelocity.Length;
					}
				}
			}
		}

		if ( !Manager.Instance.IsPaused && !Manager.Instance.IsPausedForChoosing && !IsDashing && !IsInTheAir )
		{
			var dashPressed = (!Input.UsingController && Input.Pressed( "Dash" )) || Input.Pressed( "RB" ) || Input.Pressed( "RT" );
			var dashHeld = (!Input.UsingController && Input.Down( "Dash" )) || Input.Down( "RB" ) || Input.Down( "RT" );
			if ( dashPressed || (dashHeld && (NumDashesAvailable > 0 || NumTempDashesAvailable > 0) && !IsInTheAir && !IsStunned && TimeSinceDash > 0.25f) )
			{
				Vector2 dashDir = MoveVector.LengthSquared > 0f ? MoveVector : (Velocity.LengthSquared > 0.1f ? Velocity.Normal : FacingDir);
				Dash( dashDir );
			}
		}
	}

	public void Dash( Vector2 dashDir, bool isFree = false )
	{
		if ( IsInTheAir || IsStunned || (NumDashesAvailable <= 0 && NumTempDashesAvailable <= 0 && !isFree) )
		{
			var numDashes = Math.Max( (int)MathF.Round( Stats[PlayerStat.NumDashes] ), 0 );
			float pitch = numDashes > 0
				? Utils.Map( DashRechargeProgress, 0f, 1f, 0.6f, 0.8f )
				: 0.6f;

			Manager.Instance.PlaySfxUI( "error2", pitch, volume: 0.7f );

			if ( (NumDashesAvailable <= 0 && !isFree) && Stats[PlayerStat.DashFailSelfDmg] > 0f )
			{
				Damage( Stats[PlayerStat.DashFailSelfDmg], DamageType.Self, Position2D, dashDir, upwardAmount: Game.Random.Float( 0f, 0.2f ), force: 0f, ragdollForce: 1f, enemySource: null, enemyType: EnemyType.None );
				HighlightPerk( TypeLibrary.GetType( typeof( CurseDashFailHurt ) ) );
				// todo: sfx
			}

			return;
		}

		bool invuln = !(Stats[PlayerStat.NoDashInvuln] > 0f);
		DashStarted();

		if ( Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.DashRandomDirChance] )
			dashDir = Utils.GetRandomVector();

		ForEachPerk( perk => perk.OnDashStartedEarly( dashDir ) );
		ForEachLoadoutItem( item => item.OnDashStartedEarly( dashDir ) );

		if ( Stats[PlayerStat.Blink] > 0 )
		{
			var targetPos = Stats[PlayerStat.BlinkToCursor] > 0f
				? (Input.UsingController ? Position2D + AimDir * Stats[PlayerStat.DashStrength] * 1.25f : Manager.Instance.MouseWorldPos)
				: Position2D + dashDir * Stats[PlayerStat.DashStrength] * 0.75f;

			Teleport( targetPos );

			if ( Stats[PlayerStat.JumpNotDash] > 0 )
				Jump( targetPos: Position2D, height: Game.Random.Float( 120f, 140f ), lifetime: Stats[PlayerStat.DashInvulnTime] * 1.3f );

			DashInvulnTimer = 0.05f;
			if ( invuln ) BecomeInvincible( 0.15f );
		}
		else if ( Stats[PlayerStat.JumpNotDash] > 0 )
		{
			var targetPos = Position2D + dashDir * Stats[PlayerStat.DashStrength] * PerkDashToJump.DASH_DISTANCE_FACTOR;
			var height = Game.Random.Float( 160f, 220f );
			var lifetime = Stats[PlayerStat.DashInvulnTime] * 3f;
			Jump( targetPos, height, lifetime );

			Manager.Instance.PlaySfxNearbyRpc( "player.dash", Position2D + dashDir * 10f, pitch: Utils.Map( NumDashesAvailable, 0, 5, 1f, 0.85f ), volume: 0.75f, maxDist: 400f );

			DashInvulnTimer = lifetime;
			if ( invuln ) BecomeInvincible( lifetime );
		}
		else
		{
			DashVelocity = dashDir * Stats[PlayerStat.DashStrength];
			//TempWeight = 2f;

			//var _pdasheffect = SS2Game.CreateParticle( "particles/gameplay/dashcloud/dashcloud.vpcf", this, destroyOnRestart: true );

			//if ( _pdasheffect != null )
			//	_pdasheffect.Set( "Length", Stats[PlayerStat.DashStrength] / 1000 );

			Manager.Instance.PlaySfxNearbyRpc( "player.dash", Position2D + dashDir * 10f, pitch: Utils.Map( NumDashesAvailable, 0, 5, 1f, 0.9f ), volume: 0.75f, maxDist: 400f );

			//////if(IsInvulnerable)
			//////	SetMaterial(DashInvulnMaterial);
			///
			DashInvulnTimer = Stats[PlayerStat.DashInvulnTime];
			if ( invuln ) BecomeInvincible( Stats[PlayerStat.DashInvulnTime] );
		}

		if ( !isFree && NumTempDashesAvailable <= 0 )
		{
			if ( NumDashesAvailable == (int)Stats[PlayerStat.NumDashes] )
			{
				DashTimer = Stats[PlayerStat.DashCooldown];
				DashRechargeProgress = 0f;
			}
			NumDashesAvailable--;
		}
		else if ( isFree )
		{
			NumDashesAvailable--;
		}
		// else: temp dash — perk decrements via OnDashStarted → Modify → stat handler

		TimeSinceDash = 0f;
		HasEverDashed = true;

		AddResultsStat( ResultStat.TimesDashed, 1 );
		ProgressManager.IncrementStat( ProgressStat.TimesDashed, 1 );

		ForEachPerk( perk => perk.OnDashStarted( dashDir ) );
		ForEachLoadoutItem( item => item.OnDashStarted( dashDir ) );
	}

	[Rpc.Broadcast]
	public void DashStarted()
	{
		IsDashing = true;
		DashVelocity = Vector2.Zero;

		AnimationHelper.Target.Set( "skid_x", 1f );
		AnimationHelper.Target.Set( "skid_y", 1f );
		//AnimationHelper.Sitting = CitizenAnimationHelper.SittingStyle.Chair;

		if ( IsProxy )
			return;

	}

	[Rpc.Broadcast]
	public void DashFinished()
	{
		//if ( IsInvulnerable )
		//	ClearMaterial();

		IsDashing = false;

		AnimationHelper.Target.Set( "skid_x", 0f );
		AnimationHelper.Target.Set( "skid_y", 0f );
		//AnimationHelper.Sitting = CitizenAnimationHelper.SittingStyle.None;

		ResetMaterials();

		if ( IsProxy )
			return;

		ForEachPerk( perk => perk.OnDashFinished( DashVelocity.Normal ) );
		ForEachLoadoutItem( item => item.OnDashFinished( DashVelocity.Normal ) );

		DashVelocity = Vector2.Zero;
	}

	public void DashRecharged()
	{
		NumDashesAvailable++;
		var numDashes = Math.Max( (int)MathF.Round( Stats[PlayerStat.NumDashes] ), 0 );
		if ( NumDashesAvailable > numDashes )
			NumDashesAvailable = numDashes;

		if ( NumDashesAvailable < numDashes )
		{
			DashTimer = Stats[PlayerStat.DashCooldown];
			DashRechargeProgress = 0f;
		}
		else
		{
			DashRechargeProgress = 1f;
		}

		Manager.Instance.PlaySfxUI( "player.dash.recharge", pitch: Utils.Map( NumDashesAvailable, 0, numDashes, 1f, 0.8f ), volume: 0.25f );

		ScaleHeight( amount: 1.5f, time: 0.04f );

		ForEachPerk( perk => perk.OnDashRecharged() );
		ForEachLoadoutItem( item => item.OnDashRecharged() );
	}

	[Rpc.Broadcast]
	public void RechargeDashInstantly()
	{
		//SS2Game.PlaySfx("reload.start", Position, pitch: 1.2f, volume: 1.2f);

		if ( IsProxy )
			return;

		int numDashes = Math.Max( (int)MathF.Round( Stats[PlayerStat.NumDashes] ), 0 );
		if ( NumDashesAvailable >= numDashes )
			return;

		DashRecharged();
	}

	[Rpc.Broadcast]
	public void Teleport( Vector2 targetPos, bool effectUseRealTime = false )
	{
		CancelJump();

		targetPos = Manager.Instance.ClampPosToBounds( targetPos );

		CreateTeleportParticle( Position2D, Game.Random.Float( 30f, 35f ), new Color( 0.1f, 0.1f, 1f ), 0.6f, effectUseRealTime );
		CreateTeleportParticle( targetPos, Game.Random.Float( 50f, 55f ), new Color( 0.1f, 0.1f, 1f ), 0.7f, effectUseRealTime );

		//SS2Game.CreateParticle( "particles/gameplay/blink/blink_start.vpcf", this ).SetPosition( 1, Position2D );

		DodgeDuck( dir: Vector2.Zero, time: Game.Random.Float( 0.1f, 0.15f ) );

		if ( IsProxy )
			return;

		var from = Position2D;

		Manager.Instance.PlaySfxNearbyRpc( "blink.start", Position2D, pitch: Utils.Map( NumDashesAvailable, 0, 5, 1f, 0.9f ), volume: 0.8f, maxDist: 400f );
		Manager.Instance.PlaySfxNearbyRpc( "blink.end", targetPos, pitch: 1f, volume: 0.8f, maxDist: 400f );

		Position2D = targetPos;

		TimeSinceTeleport = 0f;
		Transform.ClearInterpolation();

		ForEachPerk( perk => perk.OnTeleport( from, targetPos ) );
		ForEachLoadoutItem( item => item.OnTeleport( from, targetPos ) );
	}
}