UNPKG

@rpgjs/physic

Version:

A deterministic 2D top-down physics library for RPG, sandbox and MMO games

955 lines (724 loc) 27.1 kB
# 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 ```bash npm install @rpgjs/physic ``` ## Quick Start ```typescript 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. ```ts 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: ```ts 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: ```ts 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. ```ts // 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`: ```ts 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: ```ts 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: ```ts 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: ```ts // 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 through ``` ### Zones for Vision and Detection Use `ZoneManager` to create vision cones, skill ranges, and area-of-effect detection: ```ts 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: ```ts 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: ```ts 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 1. Gather inputs for the next tick (direction, dash, attack, ...). 2. Apply them locally through `PhysicsEngine` (client-side prediction). 3. Send the input packet `{ tick, payload }` to the server. 4. 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:** `TopDownPhysics` is deprecated. Use `PhysicsEngine` directly as shown above. The `TopDownPhysics` class 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 ```typescript 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 units - `angle`: 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 position - `metadata`: Optional custom data attached to the zone ### Updating Zones **Important:** Always call `zones.update()` after each physics step to keep zones synchronized: ```typescript engine.step(); zones.update(); // Zones are calculated on post-step state ``` This ensures zones detect entities based on their positions after physics simulation, maintaining determinism. ### Querying Zones ```typescript // 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: ```typescript 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: ```typescript // 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: ```typescript 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`: ```typescript 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. ```typescript 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](./examples/canvas/) - Interactive HTML5 Canvas demo (run with `npm run example`) - [Basic Usage](./examples/basic.ts) - Simple physics simulation - [Static Obstacles](./examples/static-obstacles.ts) - Creating immovable obstacles for RPG games - [Regions](./examples/regions.ts) - Distributed simulation with regions - [Forces](./examples/forces.ts) - Applying forces and constraints ## Architecture The library is organized in layers: 1. **Core Math Layer**: Vectors, matrices, AABB, geometric utilities 2. **Physics Layer**: Entities, integrators, forces, constraints 3. **Collision Layer**: Colliders, detection, resolution, spatial hash 4. **World Layer**: World management, events, spatial partitioning 5. **Region Layer**: Multi-region simulation, entity migration 6. **API Layer**: High-level gameplay-oriented API ## API Reference ### PhysicsEngine Main entry point for physics simulation. ```typescript 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. ```typescript 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. - `onCollisionEnter` and `onCollisionExit` fire when the entity starts or stops colliding with another body. - `onPositionChange` fires whenever the entity's position (x, y) changes. Useful for synchronizing rendering, network updates, or logging. - `onDirectionChange` fires when the entity's direction changes, providing both the normalized direction vector and a simplified cardinal direction (`CardinalDirection`: `'left'`, `'right'`, `'up'`, `'down'`, or `'idle'`). - `onMovementChange` fires when the entity starts or stops moving (based on velocity threshold). Provides `isMoving` boolean and `intensity` (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. ```typescript 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 changed ``` Use 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. ```typescript 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(); ``` - `MovementManager` accepts entities directly or can be instantiated with a resolver (`MovementManager.forEngine(engine)` is used internally by `PhysicsEngine`). - Strategies consume the generic `MovementBody` interface so you can wrap custom bodies; `@rpgjs/common` exposes an adapter for Matter.js hitboxes. - Call `movement.update(dt)` manually when you need custom timing, or use `engine.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. ```typescript // 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 move ``` ### Forces Apply various forces to entities. ```typescript 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. ```typescript 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. ```typescript 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 ```bash # 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 benchmark ``` ## Building ```bash # Build library npm run build # Type check npm run typecheck # Generate documentation npm run docs ``` ## Examples ```bash # Run interactive canvas example npm run example ``` This will start a Vite dev server and open the canvas example in your browser. ## Documentation Full API documentation is available after building: ```bash npm run docs ``` Documentation will be generated in the `docs/` directory. ## License MIT