# Game Quantization Helpers _(no submodules)_ ## Purpose Coordinate-rounding helpers for game-state vectors and rotations. The functions in this module trade absolute precision for compact wire representations: `quantizePosition({ x: 1.234, y: 5.678, z: 9.012 }, 0.01)` rounds each component to two decimal places before packing, halving the bit-width versus a raw float. This is the standard tactic for bandwidth-constrained multiplayer games — a typical ±1024 m world quantized to 0.01 m precision packs into ~17 bits per axis instead of 32 bits. The 2-D and 3-D variants exist because some games (top-down, side-scrolling) work in `Vector2` and would otherwise pay the cost of a wasted z component. `quantizeRotation` separately handles yaw and pitch with their own precisions; `interpolateAngle` handles the 360°/0° wrap so a transition from 350° to 10° interpolates through the short arc, not the long one. `validateRange` is a defensive check used at API boundaries. ## Features - `quantizePosition(position, precision = 0.01)` — round a `Vector3` to a precision divisor - `quantizePosition2D(position, precision = 0.01)` — round a `Vector2` - `quantizeRotation(rotation, yawPrecision = 1, pitchPrecision = 1)` — round yaw + pitch - `interpolateAngle(a, b, t)` — interpolate angles in degrees, taking the short arc through the wrap - `validateRange(value, min, max, name?)` — throw if out of range, with an optional field name for the error ## Usage: Basic Example ```ts import { quantizePosition, quantizeRotation } from "@suaveplan/bitpacker-game"; const pos = quantizePosition({ x: 1.234, y: 5.678, z: 9.012 }, 0.01); // { x: 1.23, y: 5.68, z: 9.01 } const rot = quantizeRotation({ yaw: 90.55, pitch: -45.4 }, 1, 1); // { yaw: 91, pitch: -45 } ``` ## Usage: Advanced Example ```ts import { quantizePosition, quantizeRotation, interpolateAngle, validateRange, } from "@suaveplan/bitpacker-game"; function tick(prev: { yaw: number }, target: { yaw: number }, t: number) { validateRange(t, 0, 1, "interpolation t"); return { yaw: interpolateAngle(prev.yaw, target.yaw, t) }; } // Quantize before pack to match the schema's fixed-point precision exactly: function preparePacket(position: { x: number; y: number; z: number }) { const q = quantizePosition(position, 0.01); validateRange(q.x, -1024, 1024, "x"); validateRange(q.y, -1024, 1024, "y"); validateRange(q.z, -128, 128, "z"); return q; } ``` ## API Reference ### `quantizePosition(position: Vector3, precision = 0.01): Vector3` Returns a new `Vector3` with each component rounded to `precision` (e.g. `0.01` for two decimal places, `1` for whole units). The result is the nearest grid point on a precision-divisor lattice. ### `quantizePosition2D(position: Vector2, precision = 0.01): Vector2` Same as `quantizePosition` for a 2-D vector. No z component. ### `quantizeRotation(rotation: Rotation, yawPrecision = 1, pitchPrecision = 1): Rotation` Returns a new `Rotation` with `yaw` and `pitch` rounded to their respective precisions. The two axes have separate precisions because yaw typically needs less granularity than pitch (or vice versa). ### `interpolateAngle(a: number, b: number, t: number): number` Returns the interpolation between `a` and `b` taking the short arc through the 360°/0° wrap. `t ∈ [0, 1]`. Equivalent to slerp on the unit circle for two-axis cases. ### `validateRange(value: number, min: number, max: number, name?: string): void` Throws if `value` is outside `[min, max]`. The optional `name` is included in the error message to identify the offending field. ## Implementation Notes - **Quantization is round-to-nearest-grid.** `Math.round(value / precision) * precision` produces banker's rounding behaviour at the exact half. Consumers that need round-half-up should pre-add a half-precision offset before quantizing. - **No clamp.** These helpers do not clamp out-of-range values; pair them with `validateRange` if your wire format has a hard cap. Quantizing without clamping can produce values outside the schema's bit-width and cause pack failures. - **`interpolateAngle` is not unwrap-aware.** It takes the short arc per pair; chains of three or more interpolated angles can drift unless the consumer normalizes between calls. - **Defensive-but-cheap.** `validateRange` is `O(1)` and emits a clear error message including the field name; it's appropriate at every API boundary, not just hot-path entries.