UNPKG

@rpgjs/physic

Version:

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

472 lines (471 loc) 12.7 kB
import { World } from "./index23.js"; import { Entity } from "./index7.js"; import { RegionManager } from "./index26.js"; import { assignPolygonCollider } from "./index19.js"; import { raycast } from "./index20.js"; import { sweepEntities } from "./index22.js"; import { MovementManager } from "./index30.js"; import { ZoneManager } from "./index29.js"; class PhysicsEngine { /** * Creates a new physics engine * * @param config - Engine configuration */ constructor(config = {}) { this.regionManager = null; this.movementManager = null; this.zoneManager = null; this.tick = 0; this.useRegions = config.enableRegions ?? false; if (this.useRegions) { if (!config.regionConfig) { throw new Error("Region configuration is required when enableRegions is true"); } this.regionManager = new RegionManager(config.regionConfig); this.world = new World({ ...config, enableSleep: false // Regions handle sleep }); } else { this.world = new World(config); } } /** * Gets the movement manager bound to this engine. * * The manager is lazily created and reused. * * @returns Movement manager instance */ getMovementManager() { if (!this.movementManager) { this.movementManager = MovementManager.forEngine(this); } return this.movementManager; } /** * Gets the zone manager bound to this engine. * * The manager is lazily created and reused. Zones allow detecting entities * within circular or cone-shaped areas without physical collisions (useful * for vision, skill ranges, explosions, etc.). * * **Important:** Call `zoneManager.update()` after each physics step to * keep zones synchronized: * * ```typescript * engine.step(); * engine.getZoneManager().update(); * ``` * * @returns Zone manager instance * * @example * ```typescript * const zones = engine.getZoneManager(); * const visionZone = zones.createAttachedZone(player, { * radius: 100, * angle: 90, * direction: 'right', * }, { * onEnter: (entities) => console.log('Player sees:', entities), * }); * * engine.step(); * zones.update(); * ``` */ getZoneManager() { if (!this.zoneManager) { this.zoneManager = new ZoneManager(this); } return this.zoneManager; } /** * Updates all registered movement strategies. * * @param dt - Time delta in seconds (defaults to the world's time step) */ updateMovements(dt) { const manager = this.getMovementManager(); const delta = dt ?? this.world.getTimeStep(); manager.update(delta); } /** * Updates movements and then steps the simulation. * * @param dt - Time delta in seconds (defaults to the world's time step) */ stepWithMovements(dt) { this.updateMovements(dt); this.step(); } /** * Advances the simulation by exactly one fixed tick. * * This helper is equivalent to {@link step} but returns the tick index after the step, * making it convenient for client-side prediction loops. * * @returns Current tick index after stepping */ stepOneTick() { this.step(); return this.tick; } /** * Advances the simulation by a fixed number of ticks. * * @param ticks - Number of ticks to simulate (>= 1) * @returns Current tick index after stepping */ stepTicks(ticks) { if (!Number.isFinite(ticks) || ticks <= 0) { return this.tick; } const total = Math.floor(ticks); for (let i = 0; i < total; i += 1) { this.step(); } return this.tick; } /** * Creates a new entity * * @param config - Entity configuration * @returns Created entity * * @example * ```typescript * const entity = engine.createEntity({ * position: { x: 100, y: 100 }, * radius: 15, * mass: 1, * velocity: { x: 5, y: 0 } * }); * ``` */ createEntity(config) { const entity = new Entity(config); if (this.useRegions && this.regionManager) { this.regionManager.addEntity(entity); } else { this.world.addEntity(entity); } return entity; } /** * Adds an existing entity to the engine * * @param entity - Entity to add * @returns The added entity */ addEntity(entity) { if (this.useRegions && this.regionManager) { this.regionManager.addEntity(entity); } else { this.world.addEntity(entity); } return entity; } /** * Removes an entity from the engine * * @param entity - Entity to remove */ removeEntity(entity) { if (this.useRegions && this.regionManager) { this.regionManager.removeEntity(entity); } else { this.world.removeEntity(entity); } } /** * Gets all entities * * @returns Array of all entities */ getEntities() { if (this.useRegions && this.regionManager) { const entities = []; for (const region of this.regionManager.getRegions()) { entities.push(...region.getEntities()); } return entities; } return this.world.getEntities(); } /** * Gets an entity by UUID * * @param uuid - Entity UUID * @returns Entity or undefined */ getEntityByUUID(uuid) { if (this.useRegions && this.regionManager) { for (const region of this.regionManager.getRegions()) { const entity = region.getWorld().getEntityByUUID(uuid); if (entity) { return entity; } } return void 0; } return this.world.getEntityByUUID(uuid); } /** * Steps the physics simulation forward * * Updates all entities, detects and resolves collisions. */ step() { if (this.useRegions && this.regionManager) { this.regionManager.step(); } else { this.world.step(); } this.tick += 1; } /** * Gets the event system * * @returns Event system instance */ getEvents() { return this.world.getEvents(); } /** * Applies a force to an entity * * @param entity - Entity to apply force to * @param force - Force vector */ applyForce(entity, force) { entity.applyForce(force); } /** * Applies an impulse to an entity * * @param entity - Entity to apply impulse to * @param impulse - Impulse vector */ applyImpulse(entity, impulse) { entity.applyImpulse(impulse); } /** * Teleports an entity to a new position * * @param entity - Entity to teleport * @param position - New position */ teleport(entity, position) { entity.teleport(position); if (this.useRegions && this.regionManager) { this.regionManager.updateEntities(); } } /** * Freezes an entity (makes it static) * * @param entity - Entity to freeze */ freeze(entity) { entity.freeze(); } /** * Unfreezes an entity (makes it dynamic) * * @param entity - Entity to unfreeze */ unfreeze(entity) { entity.unfreeze(); } /** * Queries entities in an AABB region * * @param bounds - AABB to query * @returns Array of entities in the region */ queryAABB(bounds) { if (this.useRegions && this.regionManager) { const entities = []; const regions = this.regionManager.getRegionsInBounds(bounds); for (const region of regions) { const world2 = region.getWorld(); const worldEntities = world2.getEntities(); for (const entity of worldEntities) { if (bounds.contains(entity.position)) { entities.push(entity); } } } return entities; } const world = this.world; if (world.spatialPartition) { return Array.from(world.spatialPartition.queryAABB(bounds)); } return this.world.getEntities().filter((e) => bounds.contains(e.position)); } /** * Clears all entities from the engine */ clear() { if (this.useRegions && this.regionManager) { this.regionManager.clear(); } else { this.world.clear(); } this.tick = 0; } /** * Assigns a polygon collider to an entity (supports convex or concave via convex parts). * * Design: the collider is attached via a registry and used by the detector on demand. * This keeps entities lightweight and preserves the separation of detection/resolution. * * @param entity - Target entity * @param config - Polygon configuration * @example * ```typescript * engine.assignPolygonCollider(entity, { vertices: [new Vector2(-1,-1), new Vector2(1,-1), new Vector2(1,1), new Vector2(-1,1)], isConvex: true }); * ``` */ assignPolygonCollider(entity, config) { assignPolygonCollider(entity, config); } /** * Casts a ray in the physics world and returns the nearest hit, if any. * Uses the world's spatial partition for broad-phase and shape-specific narrow-phase tests. * * @param origin - Ray origin * @param direction - Ray direction (any length) * @param maxDistance - Maximum cast length * @param mask - Optional collision mask (layer) * @param filter - Optional filter function (return true to include entity) * @returns Raycast hit or null * @example * ```typescript * const hit = engine.raycast(new Vector2(0,0), new Vector2(1,0), 1000); * ``` */ raycast(origin, direction, maxDistance, mask, filter) { const world = this.world; const partition = world.spatialPartition; if (!partition) return null; return raycast(partition, origin, direction, maxDistance, mask, filter); } /** * Computes continuous collision detection (sweep test) time-of-impact between two entities * over the next step of duration `dt`, using relative motion. * * @param a - First entity * @param b - Second entity * @param dt - Time step duration * @returns Sweep result or null if no hit in [0,1] * @example * ```typescript * const toi = engine.sweep(entityA, entityB, 1/60); * if (toi) { * // pre-resolve or clamp motion * } * ``` */ sweep(a, b, dt) { const rel = a.velocity.sub(b.velocity).mul(dt); return sweepEntities(a, b, rel); } /** * Gets statistics about the engine * * @returns Statistics object */ getStats() { if (this.useRegions && this.regionManager) { const regionStats = this.regionManager.getStats(); const worldStats = this.world.getStats(); return { ...worldStats, regions: { total: regionStats.totalRegions, active: regionStats.activeRegions } }; } return this.world.getStats(); } /** * Gets the underlying world instance * * @returns World instance */ getWorld() { return this.world; } /** * Gets the current simulation tick. * * @returns Tick counter (starts at 0 and increments after each {@link step}) */ getTick() { return this.tick; } /** * Captures a lightweight snapshot of the current world state. * * The snapshot only stores the minimum data required for client-side prediction: * position, velocity, rotation, angular velocity and sleeping flag per entity. * * @returns Snapshot object */ takeSnapshot() { return { tick: this.tick, entities: this.getEntities().map((entity) => ({ uuid: entity.uuid, position: { x: entity.position.x, y: entity.position.y }, velocity: { x: entity.velocity.x, y: entity.velocity.y }, rotation: entity.rotation, angularVelocity: entity.angularVelocity, sleeping: entity.isSleeping() })) }; } /** * Restores a snapshot previously produced by {@link takeSnapshot}. * * Entities that cannot be found in the current engine are skipped silently. * * @param snapshot - Snapshot to restore */ restoreSnapshot(snapshot) { const entities = new Map(this.getEntities().map((entity) => [entity.uuid, entity])); for (const state of snapshot.entities) { const entity = entities.get(state.uuid); if (!entity) continue; entity.position.set(state.position.x, state.position.y); entity.velocity.set(state.velocity.x, state.velocity.y); entity.rotation = state.rotation; entity.angularVelocity = state.angularVelocity; if (state.sleeping) { entity.sleep(); } else { entity.wakeUp(); } } this.tick = snapshot.tick; } /** * Gets the region manager (if regions are enabled) * * @returns Region manager or null */ getRegionManager() { return this.regionManager; } } export { PhysicsEngine }; //# sourceMappingURL=index28.js.map