UNPKG

@rpgjs/physic

Version:

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

385 lines (384 loc) 10.5 kB
import { Vector2 } from "./index2.js"; import { AABB } from "./index4.js"; import { generateUUID } from "./index43.js"; class ZoneManager { /** * Creates a new zone manager * * @param engine - Physics engine instance */ constructor(engine) { this.zones = /* @__PURE__ */ new Map(); this.engine = engine; } /** * Creates a new zone * * @param config - Zone configuration * @param callbacks - Optional event callbacks * @returns Zone identifier * * @example * ```typescript * const zoneId = zones.createZone({ * position: { x: 100, y: 100 }, * radius: 50, * angle: 180, * direction: 'right', * }, { * onEnter: (entities) => console.log('Entered:', entities), * onExit: (entities) => console.log('Exited:', entities), * }); * ``` */ createZone(config, callbacks) { const id = generateUUID(); const radius = config.radius; if (typeof radius !== "number" || radius <= 0) { throw new Error("Zone radius must be a positive number"); } const angle = config.angle ?? 360; const direction = config.direction ?? "down"; const limitedByWalls = config.limitedByWalls ?? false; let position; let type; let attachedEntity; let offset; if ("entity" in config) { type = "attached"; attachedEntity = config.entity; const entityPos = attachedEntity.position; const offsetValue = config.offset ?? { x: 0, y: 0 }; if (offsetValue instanceof Vector2) { offset = offsetValue.clone(); } else { offset = new Vector2(offsetValue.x, offsetValue.y); } position = new Vector2(entityPos.x + offset.x, entityPos.y + offset.y); } else { type = "static"; const pos = config.position; if (pos instanceof Vector2) { position = pos.clone(); } else { position = new Vector2(pos.x, pos.y); } } const record = { id, type, position, radius, angle, direction, limitedByWalls, metadata: config.metadata, attachedEntity, offset, callbacks, inside: /* @__PURE__ */ new Set() }; this.zones.set(id, record); return id; } /** * Creates a zone attached to an entity (convenience method) * * @param entity - Entity to attach the zone to * @param config - Zone configuration * @param callbacks - Optional event callbacks * @returns Zone identifier * * @example * ```typescript * const visionZone = zones.createAttachedZone(player, { * radius: 100, * angle: 90, * direction: 'right', * offset: { x: 0, y: -10 }, * }, { * onEnter: (entities) => console.log('Player sees:', entities), * }); * ``` */ createAttachedZone(entity, config, callbacks) { return this.createZone({ ...config, entity }, callbacks); } /** * Updates a zone's configuration * * @param id - Zone identifier * @param updates - Partial configuration updates * @returns True if the zone was found and updated * * @example * ```typescript * zones.updateZone(zoneId, { radius: 75, angle: 120 }); * ``` */ updateZone(id, updates) { const zone = this.zones.get(id); if (!zone) return false; if (updates.radius !== void 0) { if (typeof updates.radius !== "number" || updates.radius <= 0) { throw new Error("Zone radius must be a positive number"); } zone.radius = updates.radius; } if (updates.angle !== void 0) { zone.angle = updates.angle; } if (updates.direction !== void 0) { zone.direction = updates.direction; } if (updates.limitedByWalls !== void 0) { zone.limitedByWalls = updates.limitedByWalls; } if (updates.metadata !== void 0) { zone.metadata = updates.metadata; } if ("offset" in updates && updates.offset !== void 0 && zone.type === "attached") { const offsetValue = updates.offset; if (offsetValue instanceof Vector2) { zone.offset = offsetValue.clone(); } else { zone.offset = new Vector2(offsetValue.x, offsetValue.y); } } return true; } /** * Registers or updates callbacks for a zone * * @param id - Zone identifier * @param callbacks - Event callbacks * @returns True if the zone was found * * @example * ```typescript * zones.registerCallbacks(zoneId, { * onEnter: (entities) => console.log('Entered:', entities), * onExit: (entities) => console.log('Exited:', entities), * }); * ``` */ registerCallbacks(id, callbacks) { const zone = this.zones.get(id); if (!zone) return false; zone.callbacks = callbacks; return true; } /** * Removes a zone * * @param id - Zone identifier * @returns True if the zone was found and removed */ removeZone(id) { return this.zones.delete(id); } /** * Gets zone information * * @param id - Zone identifier * @returns Zone information or undefined */ getZone(id) { const zone = this.zones.get(id); if (!zone) return void 0; return { id: zone.id, type: zone.type, position: zone.position.clone(), radius: zone.radius, angle: zone.angle, direction: zone.direction, limitedByWalls: zone.limitedByWalls, metadata: zone.metadata }; } /** * Gets all entities currently inside a zone * * @param id - Zone identifier * @returns Array of entities inside the zone */ getEntitiesInZone(id) { const zone = this.zones.get(id); if (!zone) return []; const entities = []; for (const uuid of zone.inside) { const entity = this.engine.getEntityByUUID(uuid); if (entity) { entities.push(entity); } } return entities; } /** * Gets all zone identifiers * * @returns Array of zone IDs */ getAllZoneIds() { return Array.from(this.zones.keys()); } /** * Clears all zones */ clear() { this.zones.clear(); } /** * Updates all zones, detecting entities entering/exiting * * This should be called after each physics step to keep zones synchronized. * * @param _deltaTime - Optional delta time (not used currently, but kept for future use) * * @example * ```typescript * engine.step(); * zones.update(); * ``` */ update(_deltaTime) { for (const zone of this.zones.values()) { if (zone.type === "attached" && zone.attachedEntity) { const entityPos = zone.attachedEntity.position; const offset = zone.offset ?? new Vector2(0, 0); zone.position.set(entityPos.x + offset.x, entityPos.y + offset.y); } const aabb = new AABB( zone.position.x - zone.radius, zone.position.y - zone.radius, zone.position.x + zone.radius, zone.position.y + zone.radius ); const candidates = this.engine.queryAABB(aabb); const hits = /* @__PURE__ */ new Set(); for (const entity of candidates) { if (zone.attachedEntity && entity.uuid === zone.attachedEntity.uuid) { continue; } if (this.isEntityInsideZone(zone, entity)) { hits.add(entity.uuid); } } const previous = zone.inside; const entered = []; const exited = []; for (const uuid of hits) { if (!previous.has(uuid)) { const entity = this.engine.getEntityByUUID(uuid); if (entity) { entered.push(entity); } } } for (const uuid of previous) { if (!hits.has(uuid)) { const entity = this.engine.getEntityByUUID(uuid); if (entity) { exited.push(entity); } } } zone.inside = hits; if (entered.length > 0 && zone.callbacks?.onEnter) { zone.callbacks.onEnter(entered); } if (exited.length > 0 && zone.callbacks?.onExit) { zone.callbacks.onExit(exited); } } } /** * Checks if an entity is inside a zone * * @param zone - Zone record * @param entity - Entity to check * @returns True if the entity is inside the zone */ isEntityInsideZone(zone, entity) { const zonePos = zone.position; const entityPos = entity.position; const dx = entityPos.x - zonePos.x; const dy = entityPos.y - zonePos.y; const distance = Math.hypot(dx, dy); const entityRadius = entity.radius > 0 ? entity.radius : Math.max(entity.width, entity.height) / 2; if (distance - entityRadius > zone.radius) { return false; } if (zone.angle < 360) { const facing = this.directionToAngle(zone.direction); const angle = Math.atan2(dy, dx); const delta = this.normalizeAngle(angle - facing); const halfAperture = zone.angle * Math.PI / 360; if (Math.abs(delta) > halfAperture) { return false; } } if (zone.limitedByWalls) { if (!this.hasLineOfSight(zonePos, entityPos, entity.uuid)) { return false; } } return true; } /** * Checks if there's a clear line of sight between two points * * @param start - Start position * @param end - End position * @param ignoreEntityId - Entity UUID to ignore (usually the target entity) * @returns True if line of sight is clear */ hasLineOfSight(start, end, ignoreEntityId) { const direction = end.sub(start); const distance = direction.length(); if (distance < 1e-5) return true; direction.normalizeInPlace(); const hit = this.engine.raycast(start, direction, distance, void 0, (entity) => { if (!entity.isStatic()) return false; if (ignoreEntityId && entity.uuid === ignoreEntityId) return false; return true; }); return hit === null; } /** * Converts direction string to angle in radians * * @param dir - Direction * @returns Angle in radians */ directionToAngle(dir) { switch (dir) { case "up": return -Math.PI / 2; case "down": return Math.PI / 2; case "left": return Math.PI; case "right": default: return 0; } } /** * Normalizes angle to [-π, π] * * @param angle - Angle in radians * @returns Normalized angle */ normalizeAngle(angle) { let a = angle; while (a > Math.PI) a -= Math.PI * 2; while (a < -Math.PI) a += Math.PI * 2; return a; } } export { ZoneManager }; //# sourceMappingURL=index29.js.map