The Anatomy of Weight — Arcade Movement and Jumping
Since I left out a physics engine, I wrote movement and jumping directly as pure functions.
The core idea is to represent the player's position in polar coordinates of the tunnel cross-section.
The angle (where you are around the tunnel's circumference) and the height (the distance lifted off the wall surface toward the center) — those two were all I needed.

Why no engine
Part of it is bundle size, but the bigger reason was determinism.
The game has a daily run where everyone gets the same layout, and if the result depends on an engine's internal integration or accumulated floating-point drift, "the same map" gets hard to guarantee.
What I needed wasn't realistic collision response, it was predictable feel, and a few functions covered that.
Polar coordinates came out of the same thinking.
The tunnel really does bend left and right, but the game logic knows nothing about that curve.
Everything is computed from angle and height, so the movement and collision math is identical no matter how the burrow winds.
The curve only gets applied when it's drawn.
Weight comes from the damping
For left-right movement, the game feel comes from it not being instantaneous.
The old version snapped the angle the moment you pressed a key.
This time a key press applies angular acceleration, and when you release the key the angular velocity bleeds off through damping.
So even after you let go, the rabbit doesn't stop dead — it slides.
That "extra beat where it won't stop" is exactly what weight is.
The values are 14 rad/s² of angular acceleration, a damping coefficient of 7, and a top angular speed of 3.2 rad/s.
A damping of 7 works out to a time constant of about 0.14 seconds, so for roughly that long after you release, there's still noticeable speed left.
Make that number smaller and it feels like ice; make it bigger and you're back to snapping.
That one constant ended up defining the character of the controls.
The cap on angular speed is there so circling the wall can't accelerate forever.
Without it, holding the key longer just keeps getting faster, and the game stops being about seeing an obstacle and dodging it and starts being about memorizing the layout.
The jump is artificial gravity
The jump is built with artificial gravity.
When you jump, you get an upward velocity, and every frame gravity shaves that velocity down.
It rises, naturally hits an apex, and falls.
let { height, radialVel, grounded } = p;
if (input.jump && grounded) { radialVel = PLAYER.JUMP_VELOCITY; grounded = false; }
if (!grounded) {
height += radialVel * dt;
radialVel -= PLAYER.GRAVITY * dt;
if (height <= 0) { height = 0; radialVel = 0; grounded = true; }
}
I set the values to JUMP_VELOCITY = 9 and GRAVITY = 24.
That puts the natural apex at v²/2g ≈ 1.69m.
I added an upper clamp (3.2m) to guard against any runaway, but a normal jump never reaches it.
There's a reason the clamp sits at 3.2m specifically.
Hitting a trampoline raises the jump velocity to 12, which under the same gravity peaks at 144/48 = 3.0m.
A clamp below that would chop the super-jump off like it hit a ceiling.
Keep the safety net, but put it above the highest value normal play can produce.
The root obstacles' clearance height of 1.1m is tied to these numbers too.
With a normal apex of 1.69m there's about 0.6m of headroom, and that margin is exactly the band where a jump counts as barely cleared.
Speed ramps with depth
The running speed isn't fixed.
It starts at 12 m/s, gains 0.012 for every meter of descent, and stops at 30 m/s.
Working it out, you hit top speed around 1,500m down.
That curve is the backbone of the difficulty.
Early on it's slow enough to learn the controls; deep down it pushes against the limits of reaction time.
Difficulty rises from speed alone without touching obstacle density, which keeps the balancing knobs simple.
Pure functions mean I can test it
There's a reason I wrote it as pure functions.
Since it's not tied to Three.js, I can verify the jump trajectory directly with Jest.
I feed in the input and dt and unit-test whether the height rises and returns to 0 — catching game feel with tests instead of with my eyes.
That takes the fear out of touching the constants.
Change gravity or jump velocity and the tests still check whether the apex clears the root height, and whether landing returns the height to exactly 0.
Instead of tweaking a value and replaying the game ten times, I save the file and get an answer.
Next time: the collision story of judging impacts on top of these coordinates.