The pack
rider3d
function rider3d(): Pack<{ rider: Rider3dApi }>
The rider-3d pack: bodies that ride a height field without jitter.
It owns the VERTICAL — ride height, orientation, and the grounded-versus-airborne state. The GAME still
owns HORIZONTAL motion, because that genuinely varies: a board carves, a ball rolls, a kart grips. Each
frame the game moves x and z its own way, then calls step(dt, x, z, yaw) and reads back y, pitch,
roll, grounded, landed and airTime.
Headless and pure — it holds no objects and draws nothing.
Functions
buildFootprint
function buildFootprint( contact: ContactShape, length: number, width: number, radius: number, ringTaps: number, ): FootTap[]
Build the contact footprint for a shape (exported for the tests). A PLANK/point is a symmetric plus of fore×side taps; a SPHERE is the centre + two rings sampling the ball's contact disc (averaged).
footprintTaps
function footprintTaps(L: number): number[]
Fore-aft sample offsets across a footprint of length L (a symmetric 5-tap line, or a single centre tap when L = 0). Exported for the tests.
Classes
TerrainRider
class TerrainRider
Rides a body on a height-field surface without jitter — owns ride height,
slope orientation and the grounded/airborne state. The game moves x/z its
own way, then calls step(dt, x, z, yaw) and reads the outputs back.
x: number— Horizontal position (the game writes these via step).z: number— Horizontal Z position, in world units (the game writes it via step).yaw: number— Heading the footprint is sampled along (radians).y: number— The smoothed ride height — set this on your mesh.vy: number— Vertical velocity (managed in the air; 0 while grounded).grounded: boolean— True while the rider is on the surface, false while airborne.airTime: number— Seconds since takeoff (frozen at flight length after landing — read it on thelandedframe to scale a stomp/impact).landed: boolean— True ONLY on the frame the rider touched down.pitch: number— Surface-following orientation for the body (radians). Add your own lean (carve, steer) on top.roll: number— Surface-following roll for the body, in radians — the cross-slope bank.slope: number— The fitted along-heading slope (dY/dForward), smoothed.groundY: number— This frame's raw support height and last frame's — the delta is the height traded for speed if you run an energy model (v² += 2g·Δh).lastGroundY: number— Last frame's raw support height, in world units — pair withgroundY.constructor(opts: TerrainRiderOptions)sample(x: number, z: number, yaw: number): { support: number; slopeFore: number; slopeSide: number }— Where the body rests on the terrain, and the surface slope under it. SPHERE: the average ground over the ball's contact disc (its centre and two rings), plusradius— averaging smooths the terrain's quad kinks, so a ball rolls over them and bridges notches without the hard support-switching a footprint max() gives. PLANK: a plane at the fitted slope, laid on the highest contact — bridges dips, pivots crests, banks to the slope. Pure (tests drive it).reset(x: number, z: number, yaw = 0): void— Place the rider on the ground at (x, z), at rest — call before the first step (and on respawn).launch(vy: number): void— Pop into the air with an initial upward velocity (a jump).step(dt: number, x: number, z: number, yaw: number, speed?: number): void— Advance one frame. The GAME has already moved the rider horizontally — pass the new (x, z, yaw).speed(horizontal, world u/s) tunes the suspension stiffness; omit it and it's derived from the move. Sets y, pitch, roll, grounded, landed, airTime, groundY/lastGroundY.
Types
ContactShape
type ContactShape = 'point' | 'plank' | 'sphere'
How the body meets the ground. point = a single sample (a marker, a
foot). sphere = a ball: the average ground under its contact disc
(rolls over quad kinks and small notches without the
hard support-switching a footprint max() gives — the right model for a
round thing). plank = a flat footprint (board/kart/box/GLB) that rests
on its highest contact and, if tilt, banks to the slope. Auto-picked:
radius → sphere, length → plank, else point.
Rider3dApi
interface Rider3dApi
game.rider.
add(opts: TerrainRiderOptions): TerrainRider— Make a rider over a surface.groundAtis the one required input.readonly all: readonly TerrainRider[]— Every rider made, so a game can step them together.clear(): void— Forget them all.
TerrainRiderOptions
interface TerrainRiderOptions
Options for the TerrainRider constructor — groundAt is the one required input.
groundAt: (x: number, z: number) => number— Surface height at world (x, z) — the ONE required input. Ride terrain with(x, z) => terrain.heightAt(x, z)(or the fresh field, or a seabed…). The rider adds the body's own thickness (radius/ the plank plane), so groundAt returns the bare surface. Keep it smooth (the fresh field, not the carved surface, if the rider carves — else it rides its own cut).contact?: ContactShape— Contact shape (default auto from radius/length — see ContactShape).radius?: number— SPHERE radius — a ball/wheel. The centre rides the spherical envelope, so it bridges notches narrower than R and never jitters on inclines.ringTaps?: number— Sphere envelope resolution — samples on a ring (default 12). More = smoother over rough ground, costs more groundAt calls.length?: number— Fore-aft footprint length (PLANK; default 0 = point). A board/kart/box BRIDGES dips and PIVOTS crests over this length — the biggest anti-jitter win for a flat-bottomed body.width?: number— Lateral footprint width (PLANK; default 0). Set it to bank to the cross-slope (a wide vehicle).tilt?: boolean— Bank/pitch the body to the surface slope (default: true for a plank, false for sphere/point). Set false for a box that stays level, resting on its highest corner.settle?: number— How far the contact settles INTO the surface (default 0). A board compacts the snow it rides; the trench opens up behind it.gravity?: number— Air gravity, world units/s² (default 30). Ignored while glued.detach?: number— Detach into the air when the surface drops more than this below the rider — a lip throws you (default 0.9).Infinity= GLUED (never airborne; e.g. a ball stuck to a seabed).followK?: number— Suspension stiffness =followK + speed·followKGain, capped atfollowKMax. Soft enough to filter terrain-quad steps, stiff enough not to float off a fast descent — but the cap matters: uncapped it goes near-rigid at speed and transmits every support switch raw. Defaults 25 / 0.5 / 55. Set followK = Infinity for a hard follow.followKGain?: number— Extra suspension stiffness per unit of speed. Default 0.5.followKMax?: number— The cap on the speed-scaled suspension stiffness. Default 55.slopeK?: number— Orientation smoothing: the fitted slope eases atslopeK, then the pitch/roll ease toward it atorientK(defaults 14 / 10).orientK?: number— Ease rate of pitch/roll toward the fitted slope, per second. Default 10.pitchGain?: number— Surface pitch/roll = atan(slope) × gain (defaults 0.9).rollGain?: number— Roll = atan(cross-slope) × this gain. Default 0.9.maxFall?: number— Terminal fall speed (default 200).


