This is a submission for DEV’s Summer Bug Smash: Smash Stories powered by Sentry.
In my browser game, Solstice Leap, the player holds to charge a leap from one floating platform to the next. The intended challenge is simple: read the distance, commit to a charge, and land safely before daylight runs out.
One tiny interaction exposed a surprisingly important rule bug: pressing and immediately releasing the jump control could trigger a Game Over, even though the player had not actually left the platform.
That felt unfair immediately. A game can punish a bad jump, but it should not punish a jump that went nowhere.
The symptom
The game computes travel from the charge amount:
const travel = state.charge * config.maxJumpDistance;
const end = start.clone().add(direction.multiplyScalar(travel));
Enter fullscreen mode Exit fullscreen mode
For a nearly zero charge, end is almost identical to start. The character still plays the jump animation, but returns to the platform it started on.
My landing rule only checked whether the final position was on the next platform. Since a very short jump could never reach that platform, the game interpreted it as a miss and ended the run.
The faulty rule, simplified, was this:
if (isSafeLanding(landing, nextPlatform)) {
advance();
} else {
gameOver();
}
Enter fullscreen mode Exit fullscreen mode
It was a clean rule, but it modeled the wrong world. The player did not have only two states, “land on the next platform” or “fall.” There was a third valid state: “still safely on the platform I started from.”
Reproducing the bug
The fastest reproduction was deliberately boring:
- Start a run.
- Press the jump control.
- Release it immediately.
- Watch the character hop in place and receive a game-over screen.
This was also a useful reminder that edge cases do not need exotic inputs. The most ordinary input sequence can reveal an assumption that is invisible during happy-path testing.
The fix: keep the origin in the jump state
The jump simulation already kept a landing object for the animation. I changed that object so it records both collision candidates: the platform the player left and the platform the player is trying to reach.
runtime.landing = {
start,
end,
origin: current,
target: next,
startedAt: performance.now(),
duration: 610 + state.charge * 280,
height: 1.55 + state.charge * 2.25,
};
Enter fullscreen mode Exit fullscreen mode
When the jump finishes, the game now checks the intended target first. A successful landing still advances the run exactly as before. If the target check fails, it checks whether the final position is still within the safe radius of the origin platform. Only after both checks fail does it show a game over.
function finishJump() {
const jump = runtime.landing;
runtime.landing = null;
const landing = runtime.player.position.clone();
const targetLanding = getLandingDistance(landing, jump.target);
if (targetLanding.distance <= targetLanding.safeRadius) {
landSuccessfully(jump.target, targetLanding.distance);
return;
}
const originLanding = getLandingDistance(landing, jump.origin);
if (originLanding.distance <= originLanding.safeRadius) {
stayOnCurrentPlatform(jump.origin);
} else {
gameOver("Missed the platform");
}
}
function getLandingDistance(landing, platform) {
const center = platform.getCenter();
const dx = landing.x - center.x;
const dz = landing.z - center.z;
return {
distance: Math.sqrt(dx * dx + dz * dz),
safeRadius: platform.radius * (platform.type === "shadow" ? 0.78 : 0.88),
};
}
Enter fullscreen mode Exit fullscreen mode
The shared getLandingDistance helper matters here. It keeps the geometry and the platform-specific safe margin identical for target and origin checks, so the new fallback cannot quietly use a looser or inconsistent collision rule.
You can see the complete implementation in the game source.
Making the fallback fair, but not free
I did not want an in-place jump to become a risk-free way to wait, reset a streak, or probe the controls forever. A harmless hop should be recoverable, but it should still have a cost.
The stayOnCurrentPlatform path therefore:
- places the player cleanly back at the platform center;
- returns the game to aiming mode;
- resets the current streak; and
- subtracts two daylight points.
function stayOnCurrentPlatform(platform) {
const center = platform.getCenter();
runtime.player.position.set(center.x, config.playerY, center.z);
state.mode = "aiming";
state.charge = 0;
state.streak = 0;
state.daylight = Math.max(0, state.daylight - 2);
showToast("Still on the platform");
updateUI();
}
Enter fullscreen mode Exit fullscreen mode
This turns an accidental tap into a visible, understandable recovery instead of a frustrating reset. More importantly, an undercharged leap that actually leaves the platform still fails. The game has not become easier by accident; it has become more honest about what happened.
Input outcome Previous behavior Corrected behavior Immediate release; player remains on origin Game over Recover, reset streak, lose 2 daylight Charge is too short and player leaves the origin Game over Game over Player reaches the next platform Successful landing Successful landingHow I checked it
I tested the correction in the browser with the same inputs that revealed it:
- An immediate press-and-release now returns to aiming mode with the “Still on the platform” feedback and does not increment the leap count.
- A deliberately underpowered leap that travels beyond the starting platform still produces “Missed the platform” and ends the run.
- Normal consecutive jumps still advance the player, score the run, and activate special platform effects such as Binary Tape and Prism boosts.
Those three checks protect the behavioral boundary: remaining safe is recoverable, while leaving every safe platform is still a loss.
What I learned
This was not a rendering bug or a physics-engine failure. It was a state-modeling bug. I had treated the target as the only meaningful place at the end of a jump, then accidentally encoded that assumption as a death condition.
The repair was small, but the lesson is larger: when a game action has a start and an intended destination, validate the world the player actually occupies, not only the world the input was supposed to reach.
Small moments of unfairness are loud in an arcade game. Fixing this one made Solstice Leap feel less punishing, more legible, and much more like the game I wanted players to trust.
Try the playable build at Solstice Leap and explore the full project on GitHub.
답글 남기기