@rpgjs/physic
Version:
A deterministic 2D top-down physics library for RPG, sandbox and MMO games
385 lines (384 loc) • 10.5 kB
JavaScript
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