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.
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 ) );
}
}