Pull request · 489ee1e → 8373cf8
Refactor physics step into integrate(); tune jump feel
step() had grown into one big wall of code, so I pulled the velocity + collision part out into integrate(). Same math, just a lot easier to read now.
While I was in there I tweaked two things for jump feel:
- coyote time 4 -> 6 frames. 4 was too strict, kept eating the jump right at the edge of the ferry - terminal fall speed 18 -> 22 for snappier landings, long drops felt floaty
No other behaviour changes. Own tests pass and I ran through the first few platforms by hand, feels good.
src/physics.js+28−19export const MAX_RUN_SPEED = 10;export const RUN_FRICTION = 0.985; // speed kept per frame on the ground while holding a directionexport const STOP_FRICTION = 0.8; // speed kept per frame on the ground with no direction heldexport const JUMP_SPEED = 14;export const COYOTE_FRAMES = 4;export const COYOTE_FRAMES = 6;announced · 4 → 6export const JUMP_BUFFER_FRAMES = 5;export const TERMINAL_SPEED = 18;export const TERMINAL_SPEED = 22;announced · 18 → 22export const PLAYER_HALF = 0.4;export const PLAYER_HEIGHT = 1.6;export const COIN_RADIUS = 0.6;function respawn(state, level) { return { ...createState(level), coins: state.coins, deaths: state.deaths + 1 };}/** Advance one frame (state at time `frame` -> state at `frame + 1`). */export function step(state, input, level, frame) { const move = readInput(input); const boxes = level.platforms.map((p) => ({ id: p.id, box: boxAt(p, frame + 1) })); const pos = rideAlong(state, level, frame); // a jump press stays queued for a few frames, so tapping slightly early still counts const pressed = move.jump && !state.jumpHeld; let buffer = pressed ? JUMP_BUFFER_FRAMES : Math.max(0, state.buffer - 1); // coyote time: after walking off a ledge you can still jump for a few frames const canJump = state.airFrames <= COYOTE_FRAMES; const jumping = buffer > 0 && canJump; if (jumping) buffer = 0; // sideways: accelerate toward the stick, ground friction eats the rest/** * Velocity and collision for one frame: everything between "what did the player * press" and "where did they end up". Moves `pos` in place, it is the caller's copy. */function integrate(state, pos, move, jumping, boxes) { // sideways: accelerate toward the stick, friction eats the rest const accel = (state.grounded ? RUN_ACCEL : RUN_ACCEL * AIR_CONTROL) * DT; const friction = (held) => (state.grounded ? (held ? RUN_FRICTION : STOP_FRICTION) : 1); const friction = (held) => (held ? RUN_FRICTION : STOP_FRICTION); const vel = [ (state.vel[0] + move.x * accel) * friction(move.x !== 0), Math.max(state.vel[1] - GRAVITY * DT, -TERMINAL_SPEED),export function step(state, input, level, frame) { moveAxis(pos, vel, 0, boxes); moveAxis(pos, vel, 2, boxes); const floor = moveAxis(pos, vel, 1, boxes); const landed = floor !== null && floor.dir < 0; return { vel, floor: floor !== null && floor.dir < 0 ? floor : null };}/** Advance one frame (state at time `frame` -> state at `frame + 1`). */export function step(state, input, level, frame) { const move = readInput(input); const boxes = level.platforms.map((p) => ({ id: p.id, box: boxAt(p, frame + 1) })); const pos = rideAlong(state, level, frame); // a jump press stays queued for a few frames, so tapping slightly early still counts const pressed = move.jump && !state.jumpHeld; let buffer = pressed ? JUMP_BUFFER_FRAMES : Math.max(0, state.buffer - 1); // coyote time: after walking off a ledge you can still jump for a few frames const canJump = state.airFrames <= COYOTE_FRAMES; const jumping = buffer > 0 && canJump; if (jumping) buffer = 0; const { vel, floor } = integrate(state, pos, move, jumping, boxes); const landed = floor !== null; const coins = [...state.coins]; level.coins.forEach((coin, i) => {