@rpgjs/physic
v5.0.0-alpha.22
Published
A deterministic 2D top-down physics library for RPG, sandbox and MMO games
Downloads
1,230
Maintainers
Readme
RPG Physic
A deterministic 2D top-down physics library for RPG, sandbox and MMO games.
Features
- Deterministic: Same inputs produce same results across platforms
- Cross-platform: Works in Node.js, browsers, Deno, and Web Workers
- Modular: Extensible architecture with plugin support
- Performant: Optimized for 1000+ dynamic entities at 60 FPS
- Zero dependencies: No external runtime dependencies
- Region-based: Support for distributed simulation across regions
- Collision detection: Circle and AABB colliders with spatial optimization
- Forces & Constraints: Springs, anchors, attractions, explosions
- Event system: Collision events, sleep/wake notifications
Installation
npm install @rpgjs/physicQuick Start
import { PhysicsEngine } from '@rpgjs/physic';
// Create physics engine
const engine = new PhysicsEngine({
timeStep: 1 / 60, // 60 FPS
});
// Create entities
const ball = engine.createEntity({
position: { x: 0, y: 0 },
radius: 10,
mass: 1,
velocity: { x: 5, y: 0 },
});
const ground = engine.createEntity({
position: { x: 0, y: 100 },
width: 200,
height: 10,
mass: 0, // Static
});
// Listen to collisions
engine.getEvents().onCollisionEnter((collision) => {
console.log('Collision!', collision.entityA.uuid, collision.entityB.uuid);
});
// Simulation loop
function gameLoop() {
engine.step();
// Render entities at engine.getEntities()
requestAnimationFrame(gameLoop);
}
gameLoop();Determinism & Networking
@rpgjs/physic is deterministic as long as every peer advances the simulation by whole ticks.
Use the new stepOneTick / stepTicks helpers to drive the engine with an integer tick counter, and keep a copy of that counter for reconciliation purposes.
const engine = new PhysicsEngine({ timeStep: 1 / 60 });
const fixedDt = engine.getWorld().getTimeStep();
function predictionLoop(collectedInputs: InputBuffer) {
// Apply buffered inputs for this tick (movement, abilities, etc.)
applyInputs(collectedInputs.peek());
engine.updateMovements(fixedDt);
const tick = engine.stepOneTick(); // identical tick index on every machine
renderAtTick(tick);
}Snapshots & Reconciliation
For client-side prediction, take a snapshot when the server acknowledges a tick, rewind to it, then replay the unconfirmed inputs:
const confirmed = engine.takeSnapshot();
pendingInputs = []; // clear inputs up to confirmed tick
// ...later (server correction)
engine.restoreSnapshot(serverSnapshot);
for (const input of pendingInputs) {
applyInput(input);
engine.stepOneTick();
}Snapshots only store the minimal per-entity state (position, velocity, rotation, sleeping flag) to keep payloads small.
Quantization
To eliminate floating-point drift across platforms, you can quantize positions/velocities every tick:
const engine = new PhysicsEngine({
timeStep: 1 / 60,
positionQuantizationStep: 1 / 16, // 1/16th of a pixel
velocityQuantizationStep: 1 / 256, // optional velocity clamp
});Quantization is optional but strongly recommended for authoritative MMO servers.
Prediction & Reconciliation Helpers
Networking a top-down RPG now relies on dedicated utilities:
PredictionController(client-side) buffers local inputs, queues server snapshots, and reconciles the physics body once authoritative data arrives.DeterministicInputBuffer(server-side) stores per-player inputs in order, deduplicates frames, and lets you consume the queue deterministically each tick.
// Client
const engine = new PhysicsEngine({ timeStep: 1 / 60 });
const hero = engine.createEntity({ /* ... */ });
const prediction = new PredictionController({
correctionThreshold: 5,
getPhysicsTick: () => engine.getTick(),
getCurrentState: () => ({
x: hero.position.x,
y: hero.position.y,
direction: hero.velocity
}),
setAuthoritativeState: (state) => {
hero.position.set(state.x, state.y);
hero.velocity.set(state.direction.x, state.direction.y);
},
});
// Server
const buffer = new DeterministicInputBuffer<Direction>();
buffer.enqueue(playerId, { frame, tick, timestamp, payload: direction });
const orderedInputs = buffer.consume(playerId);Activate prediction only when you need it; otherwise the controller can be skipped and everything falls back to the authoritative server position.
Integration with @rpgjs/common
@rpgjs/common now delegates all simulation to this package. The legacy Matter.js wrapper has been removed in favour of the shared deterministic PhysicsEngine that lives directly in @rpgjs/physic. Every hitbox, zone and movement strategy is backed by the deterministic core exposed here, ensuring the same behaviour on both client and server without third-party physics engines.
Using PhysicsEngine for RPG Games
For RPG-style games, use PhysicsEngine directly instead of the deprecated TopDownPhysics class. This section shows how to create characters, manage collisions, zones, and movements using the core engine.
Creating Characters
Characters in RPG games are typically circular entities with a radius. Create them using createEntity:
import { PhysicsEngine, Vector2, EntityState } from '@rpgjs/physic';
const engine = new PhysicsEngine({
timeStep: 1 / 60,
gravity: new Vector2(0, 0), // No gravity for top-down games
enableSleep: false,
});
// Create a hero character
const hero = engine.createEntity({
uuid: 'hero-1',
position: { x: 128, y: 96 },
radius: 24,
mass: 1,
friction: 0.4,
linearDamping: 0.2,
maxLinearVelocity: 200, // pixels per second
});
// Create an NPC
const npc = engine.createEntity({
uuid: 'npc-1',
position: { x: 200, y: 150 },
radius: 20,
mass: 100,
friction: 0.4,
linearDamping: 0.2,
maxLinearVelocity: 150,
});Character Movement
Use the MovementManager to apply movement strategies to characters:
import { MovementManager, LinearMove, Dash } from '@rpgjs/physic';
const movement = engine.getMovementManager();
// Apply linear movement to hero (e.g., from keyboard input)
const moveSpeed = 200; // pixels per second
const direction = new Vector2(1, 0).normalize(); // normalized direction
movement.add(hero, new LinearMove(direction, moveSpeed));
// Apply a dash ability
movement.add(hero, new Dash(300, { x: 1, y: 0 }, 0.2)); // speed, direction, duration
// Update movements and step simulation
function gameLoop() {
engine.stepWithMovements(); // Updates movements and advances physics
// Render entities...
requestAnimationFrame(gameLoop);
}Handling Input for Character Control
For player-controlled characters, set velocity directly based on input:
const moveSpeed = 200; // pixels per second
function updateHeroMovement(keys: { [key: string]: boolean }) {
const move = new Vector2(0, 0);
if (keys['w'] || keys['arrowup']) move.y -= 1;
if (keys['s'] || keys['arrowdown']) move.y += 1;
if (keys['a'] || keys['arrowleft']) move.x -= 1;
if (keys['d'] || keys['arrowright']) move.x += 1;
if (move.length() > 0) {
move.normalizeInPlace().mulInPlace(moveSpeed);
hero.setVelocity({ x: move.x, y: move.y });
} else {
hero.setVelocity({ x: 0, y: 0 });
}
}
// In your game loop
function gameLoop() {
updateHeroMovement(keyboardState);
engine.step();
// Render...
}Character Collisions
By default, all entities with mass > 0 will collide with each other and with static obstacles. To control collision behavior:
// Make an entity static (won't be pushed, but will block others)
const wall = engine.createEntity({
position: { x: 100, y: 0 },
width: 20,
height: 100,
mass: Infinity, // or mass: 0
state: EntityState.Static,
});
wall.freeze(); // Ensure it's frozen
// Listen to collisions
hero.onCollisionEnter(({ other }) => {
console.log(`Hero collided with ${other.uuid}`);
});
// Temporarily disable collisions for a character (e.g., for phasing ability)
// Note: This requires managing collision groups or using custom collision filtering
// For now, you can teleport the entity or use movement strategies to pass throughZones for Vision and Detection
Use ZoneManager to create vision cones, skill ranges, and area-of-effect detection:
const zones = engine.getZoneManager();
// Create a vision zone attached to the hero
const visionZoneId = zones.createAttachedZone(hero, {
radius: 150,
angle: 120, // 120-degree cone
direction: 'right', // Initial direction
offset: { x: 0, y: 0 },
}, {
onEnter: (entities) => {
console.log('Hero sees entities:', entities.map(e => e.uuid));
},
onExit: (entities) => {
console.log('Hero lost sight of entities:', entities.map(e => e.uuid));
},
});
// Update zone direction based on hero movement
function updateVisionZone() {
const velocity = hero.velocity;
if (velocity.length() > 1) {
// Determine direction from velocity
const angle = Math.atan2(velocity.y, velocity.x);
let direction: 'up' | 'down' | 'left' | 'right' = 'right';
if (angle > -Math.PI / 4 && angle < Math.PI / 4) direction = 'right';
else if (angle > Math.PI / 4 && angle < 3 * Math.PI / 4) direction = 'down';
else if (angle > -3 * Math.PI / 4 && angle < -Math.PI / 4) direction = 'up';
else direction = 'left';
zones.updateZone(visionZoneId, { direction });
}
}
// In game loop
function gameLoop() {
engine.step();
zones.update(); // Important: update zones after physics step
updateVisionZone();
// Render...
}Deterministic Tick-Based Simulation
For networked games, use stepOneTick to ensure deterministic simulation:
const engine = new PhysicsEngine({ timeStep: 1 / 60 });
const fixedDt = engine.getWorld().getTimeStep();
function gameLoop() {
// Gather inputs for this tick
const input = collectInputs();
// Apply inputs
applyInputToHero(hero, input);
// Advance exactly one tick
const tick = engine.stepOneTick();
// Update zones
zones.update();
// Render at this tick
render();
requestAnimationFrame(gameLoop);
}Complete RPG Example
Here's a complete example combining all concepts:
import {
PhysicsEngine,
Vector2,
MovementManager,
ZoneManager,
LinearMove,
SeekAvoid,
} from '@rpgjs/physic';
const engine = new PhysicsEngine({
timeStep: 1 / 60,
gravity: new Vector2(0, 0),
enableSleep: false,
});
const movement = engine.getMovementManager();
const zones = engine.getZoneManager();
// Create hero
const hero = engine.createEntity({
uuid: 'hero',
position: { x: 300, y: 300 },
radius: 25,
mass: 1,
friction: 0.4,
linearDamping: 0.2,
maxLinearVelocity: 200,
});
// Create NPCs
const npc = engine.createEntity({
uuid: 'npc-1',
position: { x: 500, y: 400 },
radius: 20,
mass: 100,
friction: 0.4,
linearDamping: 0.2,
maxLinearVelocity: 150,
});
// Create static obstacles (walls)
const wall = engine.createEntity({
uuid: 'wall-1',
position: { x: 400, y: 300 },
width: 20,
height: 100,
mass: Infinity,
});
wall.freeze();
// Create vision zone for hero
const visionZoneId = zones.createAttachedZone(hero, {
radius: 150,
angle: 120,
direction: 'right',
}, {
onEnter: (entities) => console.log('Hero sees:', entities),
});
// Apply movement strategy to NPC (e.g., seek and avoid hero)
movement.add(npc, new SeekAvoid(engine, () => hero, 180, 140, 80, 48));
// Game loop
function gameLoop() {
// Update hero movement from input
updateHeroFromInput(hero);
// Step simulation
engine.stepWithMovements();
// Update zones
zones.update();
// Render
render();
requestAnimationFrame(gameLoop);
}Recommended Input Flow for Networked Games
- Gather inputs for the next tick (direction, dash, attack, ...).
- Apply them locally through
PhysicsEngine(client-side prediction). - Send the input packet
{ tick, payload }to the server. - When the authoritative snapshot comes back, restore it and replay any unconfirmed inputs using
stepOneTick.
The included RPG example under packages/physic/examples/rpg demonstrates this loop with keyboard controls, NPC strategies, and debug UI using PhysicsEngine directly.
Deprecated: TopDownPhysics
Note:
TopDownPhysicsis deprecated. UsePhysicsEnginedirectly as shown above. TheTopDownPhysicsclass was a convenience wrapper that is no longer recommended for new code.
Zones
Zones allow detecting entities within circular or cone-shaped areas without physical collisions. This is useful for vision systems, skill ranges, explosions, area-of-effect abilities, and other gameplay mechanics that need to detect presence without triggering collision responses.
Zones can be:
- Static: Fixed position in the world
- Attached: Follow an entity's position (with optional offset)
Each zone can have:
- A circular or cone-shaped detection area (angle < 360° creates a cone)
- Optional line-of-sight checking (blocks through static entities)
- Event callbacks for entities entering/exiting the zone
Basic Usage
import { PhysicsEngine, ZoneManager } from '@rpgjs/physic';
const engine = new PhysicsEngine({ timeStep: 1/60 });
const zones = engine.getZoneManager();
// Create a static zone
const staticZone = zones.createZone({
position: { x: 100, y: 100 },
radius: 50,
}, {
onEnter: (entities) => console.log('Entities entered:', entities),
onExit: (entities) => console.log('Entities exited:', entities),
});
// Create a zone attached to an entity
const player = engine.createEntity({
position: { x: 0, y: 0 },
radius: 10,
mass: 1,
});
const visionZone = zones.createAttachedZone(player, {
radius: 100,
angle: 90, // 90-degree cone
direction: 'right',
offset: { x: 0, y: 0 }, // Optional offset from entity position
}, {
onEnter: (entities) => console.log('Player sees:', entities),
onExit: (entities) => console.log('Player lost sight of:', entities),
});
// Update zones after each physics step
function gameLoop() {
engine.step();
zones.update(); // Important: call update after step
// ... render entities
}Zone Configuration
radius: Detection radius in world unitsangle: Cone angle in degrees (360 = full circle, < 360 = cone)direction: Direction for cone-shaped zones ('up' | 'down' | 'left' | 'right')limitedByWalls: If true, line-of-sight is required (static entities block detection)offset: For attached zones, offset from entity positionmetadata: Optional custom data attached to the zone
Updating Zones
Important: Always call zones.update() after each physics step to keep zones synchronized:
engine.step();
zones.update(); // Zones are calculated on post-step stateThis ensures zones detect entities based on their positions after physics simulation, maintaining determinism.
Querying Zones
// Get all entities currently in a zone
const entities = zones.getEntitiesInZone(visionZoneId);
// Update zone configuration
zones.updateZone(visionZoneId, { radius: 150, angle: 120 });
// Remove a zone
zones.removeZone(visionZoneId);Using Zones with PhysicsEngine
The ZoneManager exposed by PhysicsEngine is a generic system that works with any Entity and can be used independently for vision, skills, explosions, and other gameplay mechanics on both client and server. This is the recommended approach for all zone-based detection in RPG games.
Tile Grid System
The engine includes a built-in tile grid system for grid-based logic, such as tile-based movement, triggers, or blocking specific areas (e.g., water, lava).
Configuration
Configure the tile size in the PhysicsEngine constructor:
const engine = new PhysicsEngine({
timeStep: 1 / 60,
tileWidth: 32, // Default: 32
tileHeight: 32, // Default: 32
});Tile Hooks
Entities have hooks to react to tile changes:
// Triggered when entering a new tile
entity.onEnterTile(({ x, y }) => {
console.log(`Entered tile [${x}, ${y}]`);
});
// Triggered when leaving a tile
entity.onLeaveTile(({ x, y }) => {
console.log(`Left tile [${x}, ${y}]`);
});
// Check if entity can enter a tile (return false to block movement)
entity.canEnterTile(({ x, y }) => {
if (isWater(x, y)) {
return false; // Block movement
}
return true;
});The currentTile property on the entity stores the current tile coordinates:
console.log(entity.currentTile); // Vector2(10, 5)Vision Blocking (Raycasting)
The engine supports raycasting for vision blocking and line-of-sight checks.
Raycasting API
You can perform raycasts directly via the PhysicsEngine or World:
import { Ray } from '@rpgjs/physic';
const hit = engine.raycast(
startPosition,
direction,
maxDistance,
collisionMask, // Optional mask
(entity) => entity !== self // Optional filter
);
if (hit) {
console.log('Hit entity:', hit.entity.uuid);
console.log('Hit point:', hit.point);
console.log('Hit normal:', hit.normal);
console.log('Distance:', hit.distance);
}Vision Zones with Line of Sight
Zones can be configured to respect walls using limitedByWalls: true. This uses raycasting internally to check if entities are visible.
const visionZone = zones.createAttachedZone(hero, {
radius: 150,
angle: 120,
limitedByWalls: true, // Enable line-of-sight checks
}, {
onEnter: (entities) => console.log('Seen:', entities),
});Static entities (mass = 0 or Infinity) act as blockers for line-of-sight.
Examples
- Canvas Example - Interactive HTML5 Canvas demo (run with
npm run example) - Basic Usage - Simple physics simulation
- Static Obstacles - Creating immovable obstacles for RPG games
- Regions - Distributed simulation with regions
- Forces - Applying forces and constraints
Architecture
The library is organized in layers:
- Core Math Layer: Vectors, matrices, AABB, geometric utilities
- Physics Layer: Entities, integrators, forces, constraints
- Collision Layer: Colliders, detection, resolution, spatial hash
- World Layer: World management, events, spatial partitioning
- Region Layer: Multi-region simulation, entity migration
- API Layer: High-level gameplay-oriented API
API Reference
PhysicsEngine
Main entry point for physics simulation.
const engine = new PhysicsEngine({
timeStep: 1 / 60,
enableRegions: false,
gravity: new Vector2(0, 0),
});
// Create entities
const entity = engine.createEntity({
position: { x: 0, y: 0 },
radius: 10,
mass: 1,
});
// Step simulation
engine.step();
// Apply forces
engine.applyForce(entity, new Vector2(10, 0));
// Teleport entity
engine.teleport(entity, new Vector2(100, 200));Entity
Physical entities in the world.
const entity = new Entity({
position: { x: 0, y: 0 },
velocity: { x: 5, y: 0 },
radius: 10,
mass: 1,
restitution: 0.8, // Bounciness
friction: 0.3,
});
// Apply forces
entity.applyForce(new Vector2(10, 0));
entity.applyImpulse(new Vector2(5, 0));
// Control state
entity.freeze(); // Make static
entity.sleep(); // Put to sleep
entity.wakeUp(); // Wake up
entity.stopMovement(); // Stop all movement immediately (keeps entity dynamic)Per-entity Hooks
Entity exposes local hooks so you can react to collisions, position changes, direction changes, and movement state without diving into the global event bus.
onCollisionEnterandonCollisionExitfire when the entity starts or stops colliding with another body.onPositionChangefires whenever the entity's position (x, y) changes. Useful for synchronizing rendering, network updates, or logging.onDirectionChangefires when the entity's direction changes, providing both the normalized direction vector and a simplified cardinal direction (CardinalDirection:'left','right','up','down', or'idle').onMovementChangefires when the entity starts or stops moving (based on velocity threshold). ProvidesisMovingboolean andintensity(speed magnitude in pixels/second) for fine-grained animation control. Useful for animations, gameplay reactions, or network sync.
You can also manually trigger these hooks using notifyPositionChange(), notifyDirectionChange(), and notifyMovementChange() when modifying position or velocity directly.
const player = engine.createEntity({ position: { x: 0, y: 0 }, radius: 12, mass: 1 });
const stopWatchingCollision = player.onCollisionEnter(({ other }) => {
console.log(`Player collided with ${other.uuid}`);
});
// Sync position changes for rendering or network updates
player.onPositionChange(({ x, y }) => {
console.log(`Position changed to (${x}, ${y})`);
// Update rendering, sync network, etc.
});
player.onDirectionChange(({ cardinalDirection, direction }) => {
console.log(`Heading: ${cardinalDirection}`, direction);
// Update sprite direction, sync network, etc.
});
// Detect when player starts or stops moving
player.onMovementChange(({ isMoving, intensity }) => {
console.log(`Player is ${isMoving ? 'moving' : 'stopped'} at speed ${intensity.toFixed(1)} px/s`);
// Update animations based on intensity
if (isMoving && intensity > 100) {
// Fast movement - use run animation
playerAnimation = 'run';
} else if (isMoving && intensity < 10) {
// Slow movement - use walk animation (avoid flicker on micro-movements)
playerAnimation = 'walk';
} else if (!isMoving) {
// Stopped - use idle animation
playerAnimation = 'idle';
}
// Sync network, etc.
});
// Manually trigger position sync after direct modification
player.position.set(100, 200);
player.notifyPositionChange(); // Trigger sync hooks
// Manually trigger movement state sync after velocity modification
player.velocity.set(5, 0);
player.notifyMovementChange(); // Trigger sync hooks if state changedUse the returned unsubscribe function to detach listeners when they are no longer needed.
Movement System
The movement module provides reusable strategies and a manager that plugs into the physics engine.
import {
PhysicsEngine,
MovementManager,
Dash,
LinearMove,
} from '@rpgjs/physic';
const engine = new PhysicsEngine({ timeStep: 1 / 60 });
const player = engine.createEntity({
position: { x: 0, y: 0 },
radius: 10,
mass: 1,
});
const movement = engine.getMovementManager();
movement.add(player, new Dash(8, { x: 1, y: 0 }, 0.2));
movement.add(player, new LinearMove({ x: 0, y: 3 }, 1.5));
function loop() {
engine.stepWithMovements();
requestAnimationFrame(loop);
}
loop();MovementManageraccepts entities directly or can be instantiated with a resolver (MovementManager.forEngine(engine)is used internally byPhysicsEngine).- Strategies consume the generic
MovementBodyinterface so you can wrap custom bodies;@rpgjs/commonexposes an adapter for Matter.js hitboxes. - Call
movement.update(dt)manually when you need custom timing, or useengine.stepWithMovements(dt)to update movements and advance the simulation in one call. - Use
movement.stopMovement(entity)to completely stop an entity's movement, clearing all strategies and stopping velocity (useful when changing maps or teleporting).
Static Obstacles
Create immovable obstacles (walls, trees, decorations) by setting mass to 0 or Infinity.
Static entities will block other entities without being pushed.
// Dynamic player character
const player = engine.createEntity({
position: { x: 0, y: 0 },
radius: 10,
mass: 1, // Normal mass for dynamic entity
});
// Static wall obstacle (cannot be pushed)
const wall = engine.createEntity({
position: { x: 100, y: 0 },
width: 20,
height: 100,
mass: Infinity, // Immovable obstacle
});
// Alternative: use mass = 0
const tree = engine.createEntity({
position: { x: 200, y: 0 },
radius: 15,
mass: 0, // Also makes it immovable
});
// Player will be blocked by obstacles, but obstacles won't moveForces
Apply various forces to entities.
import { applyAttraction, applyRepulsion, applyExplosion } from '@rpgjs/physic';
// Attract entity to point
applyAttraction(entity, targetPoint, strength, maxDistance);
// Repel entity from point
applyRepulsion(entity, sourcePoint, strength, maxDistance);
// Explosion force
applyExplosion(entity, center, strength, radius, falloff);Constraints
Connect entities with constraints.
import { SpringConstraint, DistanceConstraint, AnchorConstraint } from '@rpgjs/physic';
// Spring between two entities
const spring = new SpringConstraint(entity1, entity2, restLength, stiffness, damping);
spring.update(deltaTime);
// Distance constraint
const distance = new DistanceConstraint(entity1, entity2, targetDistance, stiffness);
distance.update(deltaTime);
// Anchor entity to point
const anchor = new AnchorConstraint(entity, anchorPoint, stiffness);
anchor.update(deltaTime);Regions
Distributed simulation across regions.
const engine = new PhysicsEngine({
enableRegions: true,
regionConfig: {
worldBounds: new AABB(0, 0, 1000, 1000),
regionSize: 200,
overlap: 20,
autoActivate: true,
},
});
// Entities automatically migrate between regions
const entity = engine.createEntity({ position: { x: 100, y: 100 }, radius: 10 });Performance
The library is optimized for:
- 1000 dynamic entities at 60 FPS
- 10000 static entities supported
- Spatial hash for O(n) collision detection
- Sleep system for inactive entities
- Object pooling ready (utilities provided)
Determinism
All physics operations are deterministic. Same inputs will produce same outputs across platforms, making it suitable for:
- Network synchronization
- Replay systems
- Testing and debugging
Testing
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverage
# Run benchmarks (separate from tests)
npm run benchmark # Run all benchmarks
npm run benchmark:1000 # 1000 entities benchmark
npm run benchmark:10000 # 10000 static entities benchmark
npm run benchmark:collisions # Collision detection benchmark
npm run benchmark:regions # Region-based simulation benchmarkBuilding
# Build library
npm run build
# Type check
npm run typecheck
# Generate documentation
npm run docsExamples
# Run interactive canvas example
npm run exampleThis will start a Vite dev server and open the canvas example in your browser.
Documentation
Full API documentation is available after building:
npm run docsDocumentation will be generated in the docs/ directory.
License
MIT
