Curves
Several options take a curve rather than a scalar: engine torque vs RPM, tire slip response, steering angle vs speed, and platform mass coupling. The library bakes these into fast lookup tables with a small Hermite-spline helper you can also use directly.
Curve data
Section titled “Curve data”A curve is a set of control points:
type CurvePoint = { x: number; // input (domain) y: number; // output (value at x) r_in?: number; // incoming tangent angle, in radians r_out?: number; // outgoing tangent angle, in radians w_in?: number; // incoming tangent weight (default 1) w_out?: number; // outgoing tangent weight (default 1)};
type CurveData = { points: CurvePoint[]; // at least two samples?: number; // LUT resolution, default 50};Each segment between two points is a weighted cubic Hermite spline. r_in /
r_out are tangent angles in radians (internally Math.tan of the angle
gives the slope); 0 means a flat tangent. w_in / w_out bias the tangent
toward or away from the straight-line slope between points.
For example, the controller’s default platform mass-coupling curve stays flat at
zero until the mass ratio reaches 0.5, then ramps to 1:
const massRatioFallOff: CurveData = { points: [ { x: 0, y: 0, r_out: 0 }, { x: 0.5, y: 0, r_in: 0, r_out: 0 }, { x: 1, y: 1, r_in: 0 }, ],};Baking and evaluating
Section titled “Baking and evaluating”Curves are compiled into a CurveLUT — a sampled Float32Array plus its domain
— and evaluated with linear interpolation between samples (clamped at the ends).
import { bakeCurveLUT, evaluateCurveLUT } from "jolt-ts-character-controller";
const lut = bakeCurveLUT(massRatioFallOff.points, massRatioFallOff.samples ?? 50);
const y = evaluateCurveLUT(0.75, lut); // interpolated output at x = 0.75bakeCurveLUT(points, samples = 50)— sorts the points byx, samples the splinesamplestimes across the domain, and returns aCurveLUT. Throws if given fewer than two points.evaluateCurveLUT(x, curve)— returns the value atx, clamped to the curve’s domain. Cheap enough to call every tick.
Higher samples means a more faithful curve at the cost of a larger table; the
default of 50 is plenty for smooth response curves.
Where curves are used
Section titled “Where curves are used”| Option | On | Shapes |
|---|---|---|
massRatioFallOffCurveData |
controller & wheels | How strongly you push back on dynamic ground vs mass ratio. |
engineTorqueCurveData |
car | Engine torque vs normalized RPM. |
steerAngleCurveData |
car | Max steer angle vs speed (less steering at speed). |
lngSlipRatioCurveData |
wheel | Longitudinal grip vs slip. |
latSlipRatioCurveData |
wheel | Lateral grip vs slip. |
Each is baked once when the object is constructed. If you replace one at runtime, call the matching rebake method so the new curve takes effect:
controller.refreshMassRatioFallOffCurve()vehicle.refreshCarCurves()(engine torque + steer angle)wheel.refreshConfig()(wheel slip / mass-ratio curves)
Authoring your own tunables
Section titled “Authoring your own tunables”Because bakeCurveLUT / evaluateCurveLUT are exported, you can use them for any
gameplay response — camera shake falloff, damage vs distance, difficulty ramps:
import { bakeCurveLUT, evaluateCurveLUT, type CurveData } from "jolt-ts-character-controller";
const damageFalloff: CurveData = { points: [ { x: 0, y: 1 }, // point blank: full damage { x: 20, y: 1 }, // full up to 20m { x: 60, y: 0.2 }, // fades to 20% by 60m ],};
const lut = bakeCurveLUT(damageFalloff.points);const multiplier = evaluateCurveLUT(distanceToTarget, lut);