-
Notifications
You must be signed in to change notification settings - Fork 29
Superdash
Note
This is a raw port of the Superdash document by Nina/Wojowu.
Superdash is an optional game mechanic in Celeste which can be toggled via the Variants menu. It fundamentally changes some of the dash mechanics, extending the dash duration and allowing for changing the direction of the dash after it begins. Notably, the dash cooldown timer is not changed, allowing one to initiate another dash before a given one ends, letting one preserve speed. This, together with generally improved maneuverability and ultra mechanics allow one to build up large amounts of speed both horizontally and vertically.
This document will explain from the technical point of view the superdash mechanics, intended as a reference for TASers.
Note: the timer variables are stored in-game as float values which are decremented by the current value of Engine.DeltaTime, which depends on the current game speed and equals 0.0166667 at default game speed. For simplicity, I have translated those values to frame counts at default speed, leaving the float values in the brackets if required.
The variable controlling whether to apply superdash mechanics is a booleanSaveData.Instance.Assists.SuperDashing, which is normally usually set via the Variants menu, but might be enabled in different ways in custom maps. In Celeste Studio it can be enabled via the command Set,Superdashing,true All other relevant variables are stored inside the Player entity and are as follows:
- Speed - a vector storing the current vertical and horizontal speed of the player
- DashDir - normalized vector storing the current dash direction. This is computed differently with superdash active than without as explained below.
- dashAttackTimer - a timer used to determine whether certain dash-related actions should occur, like entering a dream block. Lasts 18 frames without superdash (0.3f) and 27 frames with superdash (0.45f). Due to the order of operations it might not be usable for some actions on the last frame.
- dashCooldownTimer - a timer which prevents the player from dashing again. Lasts 12 frames (0.2f) regardless of whether superdash is on.
- canCurveDash - boolean which determines whether the dash can be curved. It is set to true when the dash begins and is set to false on both horizontal and vertical collisions.
Unlike the other timers mentioned above, the remaining dash time is not stored inside a variable, but rather is determined by DashCoroutine. This coroutine performs the following steps:
- After pressing the dash input, the player is put into StDash and then spends 1 frame stationary. This frame is (usually) followed by 3 freeze frames.
- Afterwards, the dash is initialized, which includes setting the dash direction and speed.
- Next, the coroutine waits a hard-coded number of frames: 10 for a normal dash (0.15f) or 19 for a superdash (0.3f) - the exact number is given by 1+Ceil(floatValue/deltaTime). During those frames the player will move with constant speed, unless superdash is active and the dash is curved, or some other interaction (like a wall collision) occurs.
- After this time, the player’s state will be set to StNormal and the horizontal speed will be reset to 160 in the current horizontal direction, unless the player has nonzero downwards speed, in which case it will be preserved. In total, including the initial, final, and freeze frames, normal dashes therefore last 15 frames, while superdash lasts 24 frames.
When a dash is started, the dashCooldownTimer is set to 12 frames (0.3f; 15 frames including the freeze frames) which prevents the player from beginning another dash before the current one ends. However, with superdash active, the timer keeps its value, while the dash itself lasts longer. As such, another dash can be initiated before the current one ends, which effectively resets the coroutine described above. This can be repeated for as long as the player has dashes available, which in particular lets one delay resetting the horizontal speed.
It has to be noted that while in StDash, the crouch dash button does not make the player crouch. This can still be accomplished using a “manual demo”, by redirecting a dash, for instance using 4DX 1R. Because of dash curving, it is generally more optimal to instead do 4LDX 1R. The reasons for that are explained in the Order of operations section. (thanks to Hane and Vasher for explaining this point)
DashDir is a Vector2 variable which stores the current dash direction. At the beginning of the dash, it is always set to the lastAim value, which is: the direction the player is facing, if there is no directional input; player’s input, suitably rounded to one of the 8 principal directions. As this is nearly never relevant in superdash, let us omit a detailed explanation of this rounding. When dashing normally, DashDir is then preserved until the next dash begins, or the player lands while having down-diagonal DashDir, in which case it is reset to the horizontal direction. The conditions for the last point will be discussed in more detail in the Ultras section.
When superdashing, on every frame that some directional input is held, DashDir is updated to a unit vector which is the normalization of the player’s speed. If one of the coordinates of the speed is smaller than 0.001f in absolute value, then it will be rounded to the principal direction. This may occur for instance when dashing up-diagonally at extremely high (>100000) speeds. After the dash ends, the dash direction is similarly preserved until the next dash or landing with down-diagonal DashDir.
Upon collision with a dream block (space jam) while dashAttackTimer is positive, the player is put in StDreamDash state, and their speed is set to a vector of magnitude 240 in the direction of DashDir. This direction cannot be further changed, but as explained in the next section, superdash allows for very fine control of the angle of entry, often permitting unintended skips, for instance by entering long vertical blocks from a side at near-vertical angles.
Upon exiting a dream block, the player is put back in StNormal, and is granted coyote frames (jumpGraceTimer) if and only if the horizontal component of DashDir is nonzero. Therefore one can also exploit dream block entries at near-vertical angles to perform dream jumps or dream supers/hypers when exiting the dream block from above or below, which would not be possible with a perfectly vertical entry angle.
A wallbounce (SuperWallJump) is performed if a jump is activated during dashAttackTimer and the boolean variables WallJumpCheck and SuperWallJumpAngleCheck evaluate to true.
The first of these variables determines whether the player is close enough to the wall to perform a wall jump. It depends on a direction variable, and checks for walls to the right of the player (Player.WallJumpCheck(1)) is always evaluated before walls to the left (Player.WallJumpCheck(-1)). This means that when within range of two walls, the right wall will always take priority. Evaluation of WallJumpCheck proceeds as follows:
- If the player is DashAttacking, DashDir is perfectly vertical, and there are no spikes between the player and the wall pointing away from the wall, let num = 5. Otherwise let num = 3.
- If the player is less than num pixels away from the wall, and is not being currently prevented from climbing (e.g. by a ceiling or an invisible barrier) return true, otherwise return false. The condition in the first bullet point is commonly referred to as the “wallbounce leniency”.
SuperWallJumpAngleCheck is a simple boolean which evaluates to true precisely when the X component of DashDir is at most 0.2f in absolute value and the Y component is at most -0.75f. This translates to angles which are at most approximately 11.5° away from vertical.
The discrepancy between the angle checks in the two variables means that when approaching a wall from a near-vertical angle, then wallbounce is going to be possible, but the leniency will not apply in most cases.
When beginning a dash, the canCurveDash variable is set to true, which means that for the duration of the dash and until a collision occurs, the player can change the direction of their dash. This change preserves the magnitude of the Speed vector, and only changes its orientation. The change in orientation depends on the lastAim vector described above, and in particular only depends on the input rounded to the 8 principal directions, not the precise analog angle. Below, “input angle” will refer to this rounded angle, and “current angle” will refer to the normalization of the player’s Speed vector.
For the curving to occur, the absolute difference between input angle and current angle must be between approximately 8° and 95° (more precisely, their dot product must lie between -0.1f and 0.99f). If this occurs, then the Speed vector will be rotated towards the input angle, by 4° every frame at default game speed. In general, the vector will be rotated by at most 4.1887903f * DeltaTime radians, and exactly that value if the angle difference exceeds this maximum. After speed is modified, DashDir is updated with the new angle as described in the section above.
Every frame a large number of operations relating to the player entity are computed, and the order in which those operations are performed can have a subtle effect on player interactions. Dash curving, and the successive DashDir computation, occur relatively early during a frame, which means the change in speed and direction can occur before some other actions which would cancel this ability. Below is a non-exhaustive list of such interactions, with an explanation of their effects:
- Climb jump/corner boost: as those preserve horizontal speed, one can use curving to affect the exact speed one gets out of the climb jump, by inputting one of RGJ, RUGJ, or RDGJ.
- Grabbing a holdable: any horizontal or upwards speed is preserved, so those can be manipulated.
- Dashing: horizontal speed is preserved when dashing in directions other than vertical (except when dashing down-diagonally, as explained in the next section). This in particular can happen unintentionally when trying to turn a dash into a crouched dash as explained in the Chaining dashes section. To keep maximum speed, one wants to not curve the dash, which can be done by inputting a directional input opposite to the current speed, for instance 4LDX 1R for a dash going right.
In Celeste, if you perform a down-diagonal dash and subsequently collide with the ground, then your vertical speed will be set to 0, while your horizontal speed will be multiplied by a factor of 1.2f. The precise condition for this to occur is that the current DashDir has nonzero horizontal component and positive vertical component, corresponding to downwards speed. After colliding with the floor, DashDir will then be set to the horizontal vector pointing in the player’s current direction. There are, in fact, two different pieces of code responsible for this behavior to occur. The first of them takes place at the beginning of DashCoroutine and applies if the player dashes down-diagonally while on the ground. This sets the horizontal speed to 1.2f times the speed the player had before initiating the dash. The second of these occurs in the OnCollideV routine, and thus occurs if the player dashes down-diagonally towards the ground, or curves the dash towards the ground, and then collides with it.
In normal play, a good way to gain a short burst of higher speed is to dash down-diagonally into the ground, usually out of a hyper jump. In normal play, this will usually cause the player to lose the gained speed at the end of the dash, unless for instance some entity interrupts the dash.
With superdash, there is a way to preserve this speed even without an entity to help. If this operation is performed on or right above the ground, the speed boost will be handled by the DashCoroutine, which in particular doesn’t set canCurveDash to false. Therefore when using superdash, one can preserve the speed by curving down towards the end of the dash. As DashDir then gains a positive vertical component, horizontal speed will be preserved.
Note that in order to preserve the speed this way, one must not collide with the ground after curving down, as otherwise DashDir will be reset to a horizontal vector. This may be impossible starting on the ground at high speeds, but can be averted by first curving up before curving back down. This maneuver has an additional benefit of giving another 1.2x speed boost on the next landing. Alternatively, in some cases it might be beneficial to curve a dash towards a corner in order to retain speed through a corner boost.
Alternatively, one can intentionally curve the dash towards the floor immediately after dashing down-diagonally on the ground. This will grant the player two 1.2x speed boosts for a total of approximately 1.44x boost from a single dash. (Due to horizontal speed loss when curving down, the speed increase will not be quite equal to 1.44x).
The collision with the floor removes the ability to curve the current dash, however, as it is possible to begin another dash before this one ends, one can still keep all of the built up speed for as long as the dash can be refreshed, or interrupted without losing speed. Chaining those speed boosts is the best way to accelerate on long stretches of flat ground.
Another way these mechanics can be used is in gaining a lot of speed from a standstill, using a variation of the normal hyper bunnyhop. Performing a hyper dash does not reset DashDir, meaning that after starting a crouched dash, one can curve it down, and then jump out of it to gain the 1.2x speed boost upon landing. Naively, the optimal way to perform those inputs (when going right) would be 5RZ 1RD 1RJ 8R. However, the order of operations in a frame makes it so that the dash is curved before a hyper would be performed, so the curve and jump can be combined into a single input, resulting in 5RZ 1RDJ 8R 1RJ. The second jump occurs on the same frame as dash cooldown ends, allowing one to chain this directly into another dash.
A variant of the above can be used with a reverse hyper dash. However, inputs 5LZ 1RDJ would not work as the angle difference between the current and input angle, 135°, lies outside the angle range discussed in the Curving dashes section. This can however be averted with a clever use of an analog angle: if on the frame we jump we input an angle with value approximately 160°, then for the purposes of curving the dash, this angle will be rounded to the downwards pointing vector, giving us the angle difference of 90° which is small enough to curve the dash down. Simultaneously, this angle is pointing right, meaning the player will face right and perform a hyper jump right. Combining those inputs gives 5LZ 1JF160 8R 1RJ (where F160 is the “analog angle 160” input).
One very unique interaction which can be abused with help of superdash is the fact screens with a Theo crystal prevent the player from transitioning right unless Theo has already left the screen or is being held. Unlike regular walls however, those transitions do not reset the player's speed, which with superdash allows one to build arbitrarily high amounts of speed while remaining in place by repeatedly dashing down-diagonally into the transition. Note that due to screen transition mechanics, it is possible to clip into the floor when curving the dash.
If the Theo crystal is placed in a position which can be grabbed while dashing, the dash can be interrupted and all of the built up speed can be carried into the next room.