The run that died on the third barrier of Neon Dash was clean until the frame it stopped being clean. The jump key went down while the runner was in the air, and the update loop threw the press away, because all it knows is whether the jump flag is set on the frame it reads it. From the game's side, nothing was dropped.
That distinction is invisible on screen. A press that arrived late and a press that never registered both end with a runner face-down on a spike, and players forgive the first while they blame the game for the second. The blame matters. Somebody who decides the controls lied to them stops trusting every jump afterwards, and a platformer whose jumps cannot be trusted is finished.
Two countdown timers cover both halves of it. Together they are about ten lines, and neither one makes a level easier in any way the player can point at.
A dropped press and a late press are two different accidents
The late press is the one everybody names. The player has run past the lip of the platform but their picture of where they are is still a frame or two behind the game's, so they press jump at a moment when the character is, strictly, falling. A game that reads its grounded flag directly kills them for a decision they made in time.
The dropped press is the quieter one, and it happens at the other end of the same arc. Landing is visible before it happens, so players press early: a frame or two before contact, which is exactly when the game has no jump available. The browser delivers a key event when it gets to it, not at the top of the frame, so the handler runs between two updates and the flag it sets has to survive until the next one.
Both accidents live inside a band of roughly 200 milliseconds around the landing frame. Spend some of that budget on each side and the band stops being where runs end.
Coyote time has to be set when the feet leave, not when they land
Coyote time is a countdown that starts the moment the character stops being grounded. While it is above zero, a jump press still produces a jump. The name comes from the cartoon animal who keeps running for a step after the cliff ends, and the mechanic is that joke taken seriously for six frames.
Set the timer in the wrong branch and it still ticks, still shows up in a debug overlay, and never does the only job it exists for. Neon Dash sets its coyote value on landing, where it drains over the next 100 milliseconds and doubles as the grace period for a buffered jump. That is not wrong code, but it is a different mechanic wearing the name: the runner never falls off anything, so the timer is never asked to forgive a jump into thin air.
The jump buffer is the mirror image and has no such trap. A press while airborne is stored for 120 milliseconds, and the landing code spends it immediately rather than waiting for the next frame. That is the whole idea: the input arrives when the player makes it, and the game acts on it when it can.
Six frames of coyote time is 36 pixels of road
A window written in seconds is not how the player experiences it. At 60 Hz a frame is 16.67 ms, so a 100 ms window is six frames, and at the 360 pixels per second Neon Dash starts a run with, six frames is 36 pixels of road going past. The runner is 30 pixels wide.
| Window | Frames at 60 Hz | Road covered | How it reads |
|---|---|---|---|
| 50 ms | 3.0 | 18 px | Solid, close to literal |
| 100 ms | 6.0 | 36 px | The jump the player is sure they made |
| 120 ms | 7.2 | 43 px | Still inside what a press deserves |
| 200 ms | 12.0 | 72 px | Reads as a bug, because it is one |
The last column is the whole argument. The difference between the second row and the fourth is whether the player's feet are still on something when the jump fires. Past about 150 ms the character is visibly over the gap, and a jump from there looks like the game taking a second run at the physics.
Frames are the useful unit here, not milliseconds. A window of six frames is six opportunities for the player to be right, and it is also six frames in which the horizontal position has moved on. The reason to write the timer in seconds is that the update is not guaranteed 60 Hz; the reason to check it in frames is that the player is.
The buffer only works if the landing frame spends it
Three details decide whether the buffer does anything at all. It has to be set in the key handler rather than sampled in the update, because the handler is the only place that sees a press made between two frames. It has to count down by delta time rather than a fixed step, or the window shrinks on a 144 Hz machine, the same trap the timestep note describes for movement. And it has to be cleared the instant it is used, or one press pays for two jumps and a forgiveness feature becomes a free double jump.
Both timers also want clearing on a restart and on a resume from pause: a buffer left over from the death animation fires the moment the new run begins, and the runner leaves the ground before the player has touched anything.
A held jump key can spend the double jump on its own
Key events repeat. A keydown handler fires again and again while a key is held down, and the repeats are marked by event.repeat, which the initial press does not set. Neon Dash's handler never looks at that flag, so a held jump key re-enters the jump function on every repeat. The first call spends the grounded jump; a later repeat finds the runner airborne with one jump used, takes the double-jump branch, and the second jump is gone unasked.
The fix is one line at the top of the handler: return early when event.repeat is true. The alternative shape, a held-flag object that the update reads once per frame, has the same effect and no repeat handling at all, which is why the frame budget note argues for it.
Forgiveness hides the bugs it does not fix
Every window buys a tolerance and spends a little honesty. A wide buffer will happily fire a jump the player asked for half a second ago, from a position they have moved on from, and a jump that arrives out of nowhere is worse than a jump that never arrives. A wide coyote window turns the character into something that can jump off nothing.
The two also stack. A press stored during the descent and a coyote window after taking off can put a jump 300 ms after the button went down, which is long enough to be noticeable as a delay rather than as help.
There is a cost on the other side too, and it lands on whoever maintains the game. With a seven-frame buffer in place, a handler that quietly loses three frames of input looks completely healthy. The forgiveness masks the fault until the day the frame time changes and the masking stops working. The other half of the same trade is that a level tuned around a tight window gets softer the moment the window widens, which is why the values belong next to the physics constants.
Pros and cons
Pros
- Two floats and two comparisons per frame, which is cheaper than any other change to how a jump feels.
- The player stops blaming the controls, which is the only reason a hard platformer stays playable at all.
- Nothing about the level geometry has to move, so the gains arrive without a single new tile being placed.
- The same two timers work for dashes, slides and ledge grabs, because the shape of the problem does not change.
Cons
- Past roughly 150 ms the jump fires from visibly mid-air and the game looks broken rather than generous.
- It hides real input bugs, because a handler that loses three frames of presses behaves normally behind a wide buffer.
- Both windows stack, so a single press can produce a jump a third of a second after the player gave up on it.
- Existing levels quietly get easier, and the people who had already learned them are the ones who notice.
Sources
- Celeste & Forgiveness — Maddy Thorson's list of the ten places Celeste fudges in the player's favour, including the 2-pixel wall-jump window on a 320x180 screen, which is where this whole idea comes from.
- MDN: KeyboardEvent.repeat — the flag that separates the first press from the auto-repeats that follow it, and the reason a held jump key can spend a double jump.
- MDN: Element keydown event — when the event fires, and that it bubbles far enough to reach
window, which is where the game on this site listens for it.