@dill-pixel/plugin-crunch-physics
Version:
Crunch Physics
1,440 lines (1,439 loc) • 73.1 kB
JavaScript
import { resolvePointLike as m, resolveSizeLike as A, bindAllMethods as M, SignalConnections as R, Application as b, randomUUID as k, Plugin as T, version as G, isDev as O, Logger as E } from "dill-pixel";
import { Container as S, Graphics as B, Rectangle as L } from "pixi.js";
var C = /* @__PURE__ */ ((a) => (a[a.NONE = 0] = "NONE", a[a.DEFAULT = 1] = "DEFAULT", a[a.PLAYER = 2] = "PLAYER", a[a.ENEMY = 4] = "ENEMY", a[a.PROJECTILE = 8] = "PROJECTILE", a[a.PLATFORM = 16] = "PLATFORM", a[a.TRIGGER = 32] = "TRIGGER", a[a.ITEM = 64] = "ITEM", a[a.WALL = 128] = "WALL", a[a.FX = 256] = "FX", a[a.ALL = 4294967295] = "ALL", a))(C || {});
class _ {
constructor() {
this._layers = /* @__PURE__ */ new Map(), this._usedIndices = /* @__PURE__ */ new Set();
}
/**
* Get the singleton instance of the registry
*/
static get instance() {
return _._instance || (_._instance = new _()), _._instance;
}
/**
* Register a new collision layer
*
* @param name Name of the collision layer
* @param index Index from 0-15 representing which user bit to use (gets shifted to bits 16-31)
* @param description Optional description of the layer
* @returns The registered collision layer
*/
register(t, i, s) {
if (i < 0 || i > 15)
throw new Error("Custom collision layer index must be between 0 and 15");
if (this._usedIndices.has(i))
throw new Error(`Collision layer index ${i} is already in use`);
if (this._layers.has(t))
throw new Error(`Collision layer with name "${t}" already exists`);
const e = 1 << i + 16, o = { name: t, value: e, description: s };
return this._layers.set(t, o), this._usedIndices.add(i), o;
}
/**
* Get a registered collision layer by name
*
* @param name Name of the collision layer
* @returns The registered collision layer or undefined if not found
*/
get(t) {
return this._layers.get(t);
}
/**
* Get all registered collision layers
*
* @returns Array of all registered collision layers
*/
getAll() {
return Array.from(this._layers.values());
}
/**
* Check if a collision layer with the given name exists
*
* @param name Name of the collision layer
* @returns True if the layer exists
*/
has(t) {
return this._layers.has(t);
}
/**
* Remove a registered collision layer
*
* @param name Name of the collision layer to remove
* @returns True if the layer was removed, false if it didn't exist
*/
remove(t) {
const i = this._layers.get(t);
if (!i) return !1;
const s = i.value, e = Math.log2(s) - 16;
return this._usedIndices.delete(e), this._layers.delete(t);
}
/**
* Clear all registered collision layers
*/
clear() {
this._layers.clear(), this._usedIndices.clear();
}
/**
* Get the next available index for a custom collision layer
*
* @returns The next available index or -1 if all indices are used
*/
getNextAvailableIndex() {
for (let t = 0; t < 16; t++)
if (!this._usedIndices.has(t))
return t;
return -1;
}
}
const v = {
/**
* Creates a custom collision layer using bits 16-31 (user space)
*
* @param index Index from 0-15 representing which user bit to use (gets shifted to bits 16-31)
* @returns A unique collision layer value
*
* @example
* ```typescript
* // Create custom collision layers
* const WATER_LAYER = CollisionLayers.createLayer(0); // 1 << 16
* const LAVA_LAYER = CollisionLayers.createLayer(1); // 1 << 17
* const CLOUD_LAYER = CollisionLayers.createLayer(2); // 1 << 18
*
* // Use in entity creation
* const waterEntity = physics.createActor({
* type: 'Water',
* position: [100, 400],
* size: [800, 100],
* collisionLayer: WATER_LAYER,
* collisionMask: CollisionLayer.PLAYER | CollisionLayer.ENEMY
* });
* ```
*/
createLayer(a) {
if (a < 0 || a > 15)
throw new Error("Custom collision layer index must be between 0 and 15");
return 1 << a + 16;
},
/**
* Creates a collision mask from multiple layers
*
* @param layers Array of collision layers to combine
* @returns A combined collision mask
*
* @example
* ```typescript
* // Create a mask that collides with players, enemies and projectiles
* const mask = CollisionLayers.createMask([
* CollisionLayer.PLAYER,
* CollisionLayer.ENEMY,
* CollisionLayer.PROJECTILE
* ]);
* ```
*/
createMask(a) {
return a.reduce((t, i) => t | i, 0);
},
/**
* Checks if a layer is included in a mask
*
* @param layer The layer to check
* @param mask The mask to check against
* @returns True if the layer is included in the mask
*
* @example
* ```typescript
* // Check if player layer is in the mask
* if (CollisionLayers.isLayerInMask(CollisionLayer.PLAYER, entity.collisionMask)) {
* console.log('Entity can collide with players');
* }
* ```
*/
isLayerInMask(a, t) {
return (a & t) !== 0;
},
/**
* Get the registry for custom collision layers
*
* @returns The collision layer registry
*/
getRegistry() {
return _.instance;
}
};
function P(a) {
const { x: t, y: i } = a.position !== void 0 ? m(a.position) : { x: (a == null ? void 0 : a.x) ?? 0, y: (a == null ? void 0 : a.y) ?? 0 };
return { x: t, y: i };
}
function z(a) {
const { width: t, height: i } = a.size !== void 0 ? A(a.size) : { width: (a == null ? void 0 : a.width) ?? 32, height: (a == null ? void 0 : a.height) ?? 32 };
return { width: t, height: i };
}
class w {
/**
* Creates a new Entity instance.
*
* @param config - Optional configuration for the entity
*/
constructor(t) {
this.collisionLayer = C.NONE, this.collisionMask = C.NONE, this._active = !0, this.updatedFollowPosition = !1, this.updatedGroupPosition = !1, M(this), this._config = t, this.signalConnections = new R(), this.shouldRemoveOnCull = !1, this.width = 0, this.height = 0, this._data = {}, this._group = null, this._isCulled = !1, this._isDestroyed = !1, this._isInitialized = !1, this._xRemainder = 0, this._yRemainder = 0, this._x = 0, this._y = 0, t && this.init(t), this.initialize(), this.addView();
}
set active(t) {
this._active = t;
}
get active() {
return this._active;
}
set id(t) {
this._id = t;
}
get id() {
return this._id || this.type;
}
get make() {
return this.app.make;
}
/**
* Custom data associated with this entity
*/
set data(t) {
this._data = t;
}
get data() {
return this._data;
}
setFollowing(t, i = { x: 0, y: 0 }) {
this._followOffset = m(i), this._following && this.system.removeFollower(this), this._following = t, t && this.system.addFollower(t, this);
}
get followOffset() {
return this._followOffset || { x: 0, y: 0 };
}
get following() {
return this._following;
}
get followers() {
return this.system.getFollowersOf(this);
}
/**
* The group this entity belongs to, if any.
* Groups allow for collective movement and management of entities.
*/
get group() {
return this._group;
}
set groupOffset(t) {
this._groupOffset = m(t);
}
get groupOffset() {
return this._groupOffset || { x: 0, y: 0 };
}
setGroup(t, i = { x: 0, y: 0 }) {
this._groupOffset = m(i), this._group && (this.system.removeFromGroup(this), this.onRemovedFromGroup()), this._group = t, t && (this.system.addToGroup(t, this), this.onAddedToGroup());
}
set position(t) {
const { x: i, y: s } = m(t);
this.setPosition(i, s);
}
get position() {
return { x: this.x, y: this.y };
}
/**
* Entity's X position in world space.
* If the entity belongs to a group, returns position relative to group.
*/
set x(t) {
this._x = t;
}
get x() {
return this._group ? Math.round(this._x + this._group.getChildOffset(this).x) : Math.round(this._x);
}
/**
* Entity's Y position in world space.
* If the entity belongs to a group, returns position relative to group.
*/
set y(t) {
this._y = t;
}
get y() {
return this._group ? Math.round(this._y + this._group.getChildOffset(this).y) : Math.round(this._y);
}
/** Whether this entity is currently culled (out of bounds) */
get isCulled() {
return this._isCulled;
}
/** Whether this entity has been destroyed */
get isDestroyed() {
return this._isDestroyed;
}
/** Reference to the main application instance */
get app() {
return b.getInstance();
}
/** Reference to the physics plugin */
get physics() {
return this.app.getPlugin("crunch-physics");
}
/**
* Called after construction to perform additional initialization.
* Override this in subclasses to add custom initialization logic.
*/
initialize() {
}
/**
* Called before update to prepare for the next frame.
* Override this in subclasses to add pre-update logic.
*/
preUpdate() {
}
/**
* Called every frame to update the entity's state.
* Override this in subclasses to add update logic.
*
* @param dt - Delta time in seconds since last update
*/
update(t) {
}
/**
* Called after update to finalize the frame.
* Override this in subclasses to add post-update logic.
*/
postUpdate() {
}
/**
* Excludes collision types for this entity
* @deprecated Use setCollisionMask instead
*/
excludeCollisionType() {
console.warn("excludeCollisionType is deprecated. Use setCollisionMask instead.");
}
/**
* Includes collision types for this entity
* @deprecated Use setCollisionMask instead
*/
includeCollisionType() {
console.warn("includeCollisionType is deprecated. Use setCollisionMask instead.");
}
/**
* Removes collision types for this entity
* @deprecated Use removeCollisionMask instead
*/
removeCollisionType() {
console.warn("removeCollisionType is deprecated. Use removeCollisionMask instead.");
}
/**
* Adds collision types for this entity
* @deprecated Use addCollisionMask instead
*/
addCollisionType() {
console.warn("addCollisionType is deprecated. Use addCollisionMask instead.");
}
/**
* Checks if this entity can collide with a specific type
*/
canCollideWith() {
return !0;
}
/**
* Adds the entity's view to the physics container and updates its position.
*/
addView() {
this.view && (this.view.visible = !0, this.view.label = this.id || this.type, this.system.container && (this.system.container.addChild(this.view), this.updateView()));
}
/**
* Initializes or reinitializes the entity with new configuration.
* Used by object pools when recycling entities.
*
* @param config - New configuration to apply
*/
init(t) {
if (!t) return;
this._config = t, t.id ? this._id = t.id : this._id = k(), t.type && (this.type = t.type), t.data && (this._data = t.data);
const i = P(t);
this._x = i.x, this._y = i.y;
const s = z(t);
this.width = s.width, this.height = s.height, t.collisionLayer !== void 0 && this.setCollisionLayer(t.collisionLayer), t.collisionMask !== void 0 && this.setCollisionMask(t.collisionMask), this._xRemainder = 0, this._yRemainder = 0, t.view && this.setView(t.view), t.group && this.setGroup(t.group ?? null, t.groupOffset ? m(t.groupOffset) : { x: 0, y: 0 }), t.follows && this.setFollowing(
t.follows ?? null,
t.followOffset ? m(t.followOffset) : { x: 0, y: 0 }
), this.addView();
}
/**
* Resets the entity to its initial state for reuse in object pools.
* Override this to handle custom reset logic.
*/
reset() {
this._isCulled = !1, this._isDestroyed = !1, this._xRemainder = 0, this._yRemainder = 0, this._followOffset = { x: 0, y: 0 }, this._following = null, this._groupOffset = { x: 0, y: 0 }, this._group = null, this._data = {}, this._x = -Number.MAX_SAFE_INTEGER, this._y = -Number.MAX_SAFE_INTEGER, this.view && (this.view.visible = !1), this.system.removeEntity(this);
}
/** Reference to the physics system */
get system() {
return this.physics.system;
}
/**
* Called when the entity is added to a group.
* Override this to handle custom group addition logic.
*/
onAddedToGroup() {
}
/**
* Called when the entity is removed from a group.
* Override this to handle custom group removal logic.
*/
onRemovedFromGroup() {
}
/**
* Updates the entity's position and view.
*/
updatePosition() {
this.x = this._x, this.y = this._y, this.updateView();
}
/**
* Called when the entity is culled (goes out of bounds).
* Override this to handle culling differently.
*/
onCull() {
this._isCulled = !0, this.view && (this.view.visible = !1);
}
/**
* Called when the entity is brought back after being culled.
* Override this to handle unculling differently.
*/
onUncull() {
this._isCulled = !1, this.view && (this.view.visible = !0);
}
/**
* Prepares the entity for removal/recycling.
* Override this to handle custom cleanup.
*/
destroy() {
this._isDestroyed || (this._isDestroyed = !0, this._isCulled = !1, this.signalConnections.disconnectAll(), this.view && (this.view.visible = !1, this.view.removeFromParent()), this.system.removeFollower(this));
}
/**
* Called when the entity is removed from the physics system.
* Override this to handle custom removal logic.
*/
onRemoved() {
this._isDestroyed || this.destroy();
}
/**
* Sets a new view for the entity and updates its position.
*
* @param view - The new view to use
*/
setView(t) {
this.view = t, this.updateView();
}
/**
* Updates the view's position to match the entity's position.
*/
updateView() {
this.view && this.view.visible && this.view.position && this.view.position.set(this.x, this.y);
}
/**
* Gets the entity's bounding rectangle.
*
* @returns Rectangle representing the entity's bounds
*/
getBounds() {
return {
x: this.x,
y: this.y,
width: this.width,
height: this.height
};
}
/**
* Sets the entity's position, resetting any movement remainders.
*
* @param x - New X position
* @param y - New Y position
*/
setPosition(t, i) {
this._x = t, this._y = i, this._xRemainder = 0, this._yRemainder = 0, this.updateView();
}
/**
* Alias for setPosition.
*
* @param x - New X position
* @param y - New Y position
*/
moveTo(t, i) {
this.setPosition(t, i);
}
/**
* Adds signal connections to the entity.
*
* @param args - Signal connections to add
*/
addSignalConnection(...t) {
for (const i of t)
this.signalConnections.add(i);
}
/**
* Alias for addSignalConnection.
*
* @param args - Signal connections to add
*/
connectSignal(...t) {
for (const i of t)
this.signalConnections.add(i);
}
/**
* Alias for addSignalConnection, specifically for action signals.
*
* @param args - Action signal connections to add
*/
connectAction(...t) {
for (const i of t)
this.signalConnections.add(i);
}
/**
* Checks if this entity can collide with another entity
*/
canCollideWithEntity(t) {
return (this.collisionLayer & t.collisionMask) !== 0 && (t.collisionLayer & this.collisionMask) !== 0;
}
/**
* Sets the collision layer for this entity
*
* @param layer The collision layer or layers (can be combined with bitwise OR)
*/
setCollisionLayer(t) {
this.collisionLayer = t;
}
/**
* Adds the specified layers to this entity's collision layer
*
* @param layers The layers to add (can be combined with bitwise OR)
*/
addCollisionLayer(t) {
this.collisionLayer |= t;
}
/**
* Removes the specified layers from this entity's collision layer
*
* @param layers The layers to remove (can be combined with bitwise OR)
*/
removeCollisionLayer(t) {
this.collisionLayer &= ~t;
}
/**
* Sets the collision mask for this entity
*
* @param mask The collision mask (can be combined with bitwise OR)
*/
setCollisionMask(...t) {
this.collisionMask = this.physics.createCollisionMask(...t);
}
/**
* Adds the specified layers to this entity's collision mask
*
* @param layers The layers to add to the mask (can be combined with bitwise OR)
*/
addCollisionMask(t) {
this.collisionMask |= t;
}
/**
* Removes the specified layers from this entity's collision mask
*
* @param layers The layers to remove from the mask (can be combined with bitwise OR)
*/
removeCollisionMask(t) {
this.collisionMask &= ~t;
}
/**
* Checks if this entity belongs to a specific collision layer
*
* @param layer The layer to check
* @returns True if the entity belongs to the specified layer
*
* @example
* ```typescript
* // Check if entity is on the PLAYER layer
* if (entity.hasCollisionLayer(CollisionLayer.PLAYER)) {
* console.log('Entity is a player');
* }
*
* // Check if entity is on a custom layer
* const WATER_LAYER = CollisionLayers.createLayer(0);
* if (entity.hasCollisionLayer(WATER_LAYER)) {
* console.log('Entity is water');
* }
* ```
*/
hasCollisionLayer(t) {
return (this.collisionLayer & t) !== 0;
}
/**
* Checks if this entity can collide with a specific collision layer
*
* @param layer The layer to check
* @returns True if the entity can collide with the specified layer
*
* @example
* ```typescript
* // Check if entity can collide with players
* if (entity.canCollideWithLayer(CollisionLayer.PLAYER)) {
* console.log('Entity can collide with players');
* }
*
* // Check if entity can collide with a custom layer
* const WATER_LAYER = CollisionLayers.createLayer(0);
* if (entity.canCollideWithLayer(WATER_LAYER)) {
* console.log('Entity can collide with water');
* }
* ```
*/
canCollideWithLayer(t) {
return (this.collisionMask & t) !== 0;
}
}
class I extends w {
constructor() {
super(...arguments), this.entityType = "Actor", this.velocity = { x: 0, y: 0 }, this.disableActorCollisions = !1, this.shouldRemoveOnCull = !0, this.collisions = [], this.actorCollisions = [], this._isRidingSolidCache = null, this._carriedBy = null, this._carriedByOverlap = 0, this._currentGridCells = [];
}
/**
* Initialize or reinitialize the actor with new configuration.
*
* @param config - Configuration for the actor
*/
init(t) {
super.init(t), this.velocity = { x: 0, y: 0 }, this._isRidingSolidCache = null, this._carriedBy = null, this._carriedByOverlap = 0, this.actorCollisions = [], this._currentGridCells = [], t.disableActorCollisions !== void 0 && (this.disableActorCollisions = t.disableActorCollisions), this.system.enableActorCollisions && !this.disableActorCollisions && this.updateGridCells();
}
/**
* Called at the start of each update to prepare for collision checks.
*/
preUpdate() {
this.active && (this.collisions = [], this.actorCollisions = [], this._isRidingSolidCache = null, this._carriedBy = null, this._carriedByOverlap = 0);
}
/**
* Updates the actor's position based on velocity and handles collisions.
*
* @param dt - Delta time in seconds
*/
update(t) {
this.active && (this.isRidingSolid() || (this.velocity.y += this.system.gravity * t), this.velocity.x = Math.min(Math.max(this.velocity.x, -this.system.maxVelocity), this.system.maxVelocity), this.velocity.y = Math.min(Math.max(this.velocity.y, -this.system.maxVelocity), this.system.maxVelocity), this.velocity.x !== 0 && this.moveX(this.velocity.x * t), this.velocity.y !== 0 && this.moveY(this.velocity.y * t), this.system.enableActorCollisions && this.updateGridCells(), this.updateView());
}
/**
* Called after update to handle post-movement effects.
*/
postUpdate() {
this.active && this.isRidingSolid() && (this.velocity.y = 0);
}
/**
* Resets the actor to its initial state.
*/
reset() {
super.reset(), this._isRidingSolidCache = null, this._carriedBy = null, this._carriedByOverlap = 0, this.velocity = { x: 0, y: 0 }, this.updatePosition();
}
/**
* Called when the actor is culled (goes out of bounds).
* Override this to handle culling differently.
*/
onCull() {
var t;
(t = this.view) == null || t.destroy();
}
/**
* Called when this actor collides with a solid.
* Override this method to implement custom collision response.
*
* @param result - Information about the collision
*/
onCollide(t) {
}
/**
* Called when this actor collides with another actor.
* Override this method to implement custom actor-to-actor collision response.
*
* @param result - Information about the actor collision
*/
onActorCollide(t) {
}
/**
* Checks if this actor is riding the given solid.
* An actor is riding if it's directly above the solid.
*
* @param solid - The solid to check against
* @returns True if riding the solid
*/
isRiding(t) {
if (!t.collideable || !(this.collisionLayer & t.collisionMask) || !(t.collisionLayer & this.collisionMask) || this._carriedBy && this._carriedBy !== t)
return !1;
const i = this.y + this.height, s = Math.abs(i - t.y) <= 1, e = this.x + this.width > t.x && this.x < t.x + t.width, o = Math.min(this.x + this.width, t.x + t.width) - Math.max(this.x, t.x), h = s && e;
return h && o > this._carriedByOverlap && (this._carriedBy = t, this._carriedByOverlap = o), h;
}
/**
* Checks if this actor is riding any solid in the physics system.
* Uses caching to optimize multiple checks per frame.
*
* @returns True if riding any solid
*/
isRidingSolid() {
if (this._isRidingSolidCache !== null)
return this._isRidingSolidCache;
const t = this.getSolidsAt(this.x, this.y + 1);
return this._isRidingSolidCache = t.some((i) => this.isRiding(i)), this._isRidingSolidCache;
}
/**
* Called when the actor is squeezed between solids.
* Override this to handle squishing differently.
*/
squish(t) {
}
/**
* Updates the actor's grid cells in the spatial partitioning system.
* This is called when the actor moves or when its size changes.
*/
updateGridCells() {
this.disableActorCollisions || this.system.updateActorInGrid(this);
}
/**
* Gets the current grid cells this actor occupies
*/
get currentGridCells() {
return this._currentGridCells;
}
/**
* Sets the current grid cells this actor occupies
*/
set currentGridCells(t) {
this._currentGridCells = t;
}
/**
* Moves the actor horizontally, checking for collisions with solids.
*
* @param amount - Distance to move in pixels
* @param collisionHandler - Optional callback for handling collisions
* @returns Array of collision results
*/
moveX(t, i, s) {
if (!this.active || t === 0) return [];
this._xRemainder += t;
const e = Math.round(this._xRemainder);
if (e === 0) return [];
const o = [], h = this.collisionLayer, c = this.collisionMask;
if (c === 0)
return this._xRemainder -= e, this._x += e, this.updateView(), [];
this._xRemainder -= e;
const n = Math.sign(e);
let d = Math.abs(e);
const u = n;
for (s && (s.collideable = !1); d > 0; ) {
const p = this._x + u, r = this.getSolidsAt(p, this._y);
let y = !1;
for (const l of r) {
if (!l.canCollide || !(h & l.collisionMask) || !(l.collisionLayer & c))
continue;
const f = {
collided: !0,
solid: l,
normal: { x: -n, y: 0 },
penetration: u > 0 ? this.x + this.width - l.x : l.x + l.width - this.x,
pushingSolid: s
};
o.push(f), i && i(f), this.onCollide(f), y = !0;
}
if (y)
break;
this._x = p, d--, (d % 4 === 0 || d === 0) && this.updateView();
}
return s && (s.collideable = !0), Math.abs(e) - d > 0 && this.updateView(), o;
}
/**
* Moves the actor vertically, checking for collisions with solids.
*
* @param amount - Distance to move in pixels
* @param collisionHandler - Optional callback for handling collisions
* @returns Array of collision results
*/
moveY(t, i, s) {
if (!this.active || t === 0) return [];
this._yRemainder += t;
const e = Math.round(this._yRemainder);
if (e === 0) return [];
const o = [], h = this.collisionLayer, c = this.collisionMask;
if (c === 0)
return this._yRemainder -= e, this._y += e, this.updateView(), [];
this._yRemainder -= e;
const n = Math.sign(e);
let d = Math.abs(e);
const u = n;
for (s && (s.collideable = !1); d > 0; ) {
const p = this._y + u, r = this.getSolidsAt(this._x, p);
let y = !1;
for (const l of r) {
if (!l.canCollide || !(h & l.collisionMask) || !(l.collisionLayer & c))
continue;
const f = {
collided: !0,
solid: l,
normal: { x: 0, y: -n },
penetration: u > 0 ? this.y + this.height - l.y : l.y + l.height - this.y,
pushingSolid: s
};
o.push(f), i && i(f), this.onCollide(f), y = !0;
}
if (y)
break;
this._y = p, d--, (d % 4 === 0 || d === 0) && this.updateView();
}
return s && (s.collideable = !0), Math.abs(e) - d > 0 && this.updateView(), o;
}
/**
* Updates the actor's view position.
*/
updateView() {
this.view && this.view.visible && (this.view.x = this._x, this.view.y = this._y);
}
/**
* Gets all solids at the specified position that could collide with this actor.
*
* @param _x - X position to check
* @param _y - Y position to check
* @returns Array of solids at the position
*/
getSolidsAt(t, i) {
return this.system.getSolidsAt(t, i, this);
}
/**
* Checks if this actor is colliding with another actor.
* The collision will only occur if:
* 1. Both actors are active
* 2. Neither actor has disabled actor collisions
* 3. The collision layers and masks match:
* - (this.collisionLayer & other.collisionMask) !== 0
* - (other.collisionLayer & this.collisionMask) !== 0
*
* @param actor - The actor to check collision with
* @returns Collision result with information about the collision
*/
checkActorCollision(t) {
if (!this.active || !t.active)
return { collided: !1, actor: t };
if (this.disableActorCollisions || t.disableActorCollisions)
return { collided: !1, actor: t };
if (!(this.collisionLayer & t.collisionMask) || !(t.collisionLayer & this.collisionMask))
return { collided: !1, actor: t };
const i = this.x, s = this.x + this.width, e = this.y, o = this.y + this.height, h = t.x, c = t.x + t.width, n = t.y, d = t.y + t.height;
if (s > h && i < c && o > n && e < d) {
const u = Math.min(s - h, c - i), p = Math.min(o - n, d - e);
let r, y;
return u < p ? (y = u, r = {
x: i < h ? -1 : 1,
y: 0
}) : (y = p, r = {
x: 0,
y: e < n ? -1 : 1
}), {
collided: !0,
actor: t,
normal: r,
penetration: y
};
}
return { collided: !1, actor: t };
}
/**
* Resolves a collision with another actor.
*
* @param result - The collision result to resolve
* @param shouldMove - Whether this actor should move to resolve the collision
* @returns The updated collision result
*/
resolveActorCollision(t) {
return !t.collided || !t.normal || !t.penetration || this.onActorCollide(t), t;
}
/**
* Sets the actor's size and updates grid cells if needed.
*
* @param width - New width in pixels
* @param height - New height in pixels
*/
setSize(t, i) {
const s = this.width !== t || this.height !== i;
this.width = t, this.height = i, s && this.system.enableActorCollisions && this.updateGridCells();
}
/**
* Sets the actor's width and updates grid cells if needed.
*
* @param value - New width in pixels
*/
setWidth(t) {
this.width !== t && (this.width = t, this.system.enableActorCollisions && this.updateGridCells());
}
/**
* Sets the actor's height and updates grid cells if needed.
*
* @param value - New height in pixels
*/
setHeight(t) {
this.height !== t && (this.height = t, this.system.enableActorCollisions && this.updateGridCells());
}
}
class F extends w {
constructor() {
super(...arguments), this.entityType = "Group", this.childOffsets = /* @__PURE__ */ new Map(), this.isStatic = !0;
}
/**
* Gets the relative offset of a child entity from the group's position.
* @param entity - The child entity to get the offset for
* @returns The relative {x, y} offset of the entity
*/
getChildOffset(t) {
return this.childOffsets.get(t) ?? { x: 0, y: 0 };
}
/**
* Sets the group's X position, affecting all child entities.
*/
set x(t) {
this._x = t;
}
get x() {
return this._x;
}
/**
* Sets the group's Y position, affecting all child entities.
*/
set y(t) {
this._y = t;
}
get y() {
return this._y;
}
/**
* Add an entity to this container
* @param entity The entity to add
* @param preserveWorldPosition If true, the entity's world position will be preserved
*/
add(t, i = { x: 0, y: 0 }) {
return t.setGroup(this, i), this;
}
/**
* Remove an entity from this container
*/
remove(t) {
return t.setGroup(null), this;
}
/**
* Move the container and all its children
*/
move(t, i) {
this._x += t, this._y += i, this.updateView();
}
/**
* Force move the container and all its children to an absolute position
*/
moveTo(t, i) {
const s = t - this.x, e = i - this.y;
this.move(s, e);
}
/**
* Get all children of a specific type
*/
getChildrenByType(t) {
return this.system.getEntitiesInGroup(this).filter((i) => i.entityType === t || i.type === t);
}
/**
* Get all actors in this container
*/
getActors() {
return this.getChildrenByType("Actor");
}
/**
* Get all solids in this container
*/
getSolids() {
return this.getChildrenByType("Solid");
}
/**
* Get all sensors in this container
*/
getSensors() {
return this.getChildrenByType("Sensor");
}
/**
* Get all groups in this container
*/
getGroups() {
return this.getChildrenByType("Group");
}
destroy() {
super.destroy(), this.system.removeGroup(this);
}
}
class V extends w {
constructor() {
super(...arguments), this.entityType = "Sensor", this.shouldRemoveOnCull = !1, this.collidableTypes = [], this.velocity = { x: 0, y: 0 }, this.isStatic = !1, this.overlappingActors = /* @__PURE__ */ new Set(), this._isRidingSolidCache = null, this._currentSensorOverlaps = /* @__PURE__ */ new Set(), this._currentOverlaps = /* @__PURE__ */ new Set();
}
setPosition(t, i) {
this.isStatic ? (this._x = t, this._y = i, this._xRemainder = 0, this._yRemainder = 0, this.updateView(), this.checkActorOverlaps()) : super.setPosition(t, i);
}
set x(t) {
super.x = t, this.isStatic && (this._xRemainder = 0, this.updateView(), this.checkActorOverlaps());
}
get x() {
return super.x;
}
set y(t) {
super.y = t, this.isStatic && (this._yRemainder = 0, this.updateView(), this.checkActorOverlaps());
}
get y() {
return super.y;
}
/**
* Initializes or reinitializes the sensor with new configuration.
*
* @param config - Configuration for the sensor
*/
init(t) {
super.init(t), this.velocity || (this.velocity = { x: 0, y: 0 }), this.velocity.x = 0, this.velocity.y = 0, this.overlappingActors || (this.overlappingActors = /* @__PURE__ */ new Set()), this._isRidingSolidCache = null;
}
/**
* Checks if this sensor is riding the given solid.
* Takes into account gravity direction for proper riding detection.
*
* @param solid - The solid to check against
* @returns True if riding the solid
*/
isRiding(t) {
if (Math.sign(this.system.gravity) > 0) {
const s = this.y + this.height, e = Math.abs(s - t.y) <= 1, o = this.x + this.width > t.x && this.x < t.x + t.width;
return e && o;
} else {
const s = this.y, e = Math.abs(s - (t.y + t.height)) <= 1, o = this.x + this.width > t.x && this.x < t.x + t.width;
return e && o;
}
}
/**
* Checks if this sensor is riding any solid.
* Uses caching to optimize multiple checks per frame.
*
* @returns True if riding any solid
*/
isRidingSolid() {
if (this._isRidingSolidCache !== null)
return this._isRidingSolidCache;
const t = this.getSolidsAt(this.x, this.system.gravity > 0 ? this.y + 1 : this.y - 1);
return this._isRidingSolidCache = t.some((i) => this.isRiding(i)), this._isRidingSolidCache;
}
/**
* Force moves the sensor to a new position, ignoring static state and collisions.
*
* @param x - New X position
* @param y - New Y position
*/
moveStatic(t, i) {
this._x = t, this._y = i, this._xRemainder = 0, this._yRemainder = 0, this.updateView(), this.checkActorOverlaps();
}
/**
* Moves the sensor horizontally, passing through solids.
*
* @param amount - Distance to move in pixels
*/
moveX(t) {
if (this.isStatic)
return;
this._xRemainder += t;
const i = Math.round(this._xRemainder);
if (i !== 0) {
this._xRemainder -= i;
const s = Math.sign(i);
let e = Math.abs(i);
for (; e > 0; ) {
const o = s, h = this.x + o;
let c = !1;
for (const n of this.getSolidsAt(h, this.y))
if (n.canCollide) {
c = !0;
break;
}
if (!c)
this._x = h, e--, this.updateView(), this.checkActorOverlaps();
else {
this.velocity.x = 0;
break;
}
}
}
}
/**
* Moves the sensor vertically, colliding with solids for riding.
*
* @param amount - Distance to move in pixels
*/
moveY(t) {
if (this.isStatic)
return;
this._yRemainder += t;
const i = Math.round(this._yRemainder);
if (i !== 0) {
this._yRemainder -= i;
const s = Math.sign(i);
let e = Math.abs(i);
for (; e > 0; ) {
const o = s, h = this.y + o;
let c = !1;
if (Math.sign(this.system.gravity) === s) {
for (const n of this.getSolidsAt(this.x, h))
if (n.canCollide) {
c = !0;
break;
}
}
if (!c)
this._y = h, e--, this.updateView(), this.checkActorOverlaps();
else {
this.velocity.y = 0;
break;
}
}
}
}
/**
* Updates the sensor's position and checks for overlapping actors.
*
* @param deltaTime - Delta time in seconds
*/
update(t) {
this._isRidingSolidCache = null, this.active && (!this.isStatic && !this.isRidingSolid() && (this.velocity.y += this.system.gravity * t), this.velocity.x !== 0 && this.moveX(this.velocity.x * t), this.velocity.y !== 0 && this.moveY(this.velocity.y * t));
}
/**
* Checks for overlapping actors and triggers callbacks.
*
* @returns Set of current overlaps
*/
checkActorOverlaps() {
if (this.collisionMask === 0 || !this.active)
return /* @__PURE__ */ new Set();
this._currentSensorOverlaps.clear(), this._currentOverlaps.clear();
const t = this.collisionLayer, i = this.collisionMask, s = this.system.getActorsByType(this.collidableTypes);
for (const o of s)
o.active && (!(t & o.collisionMask) || !(o.collisionLayer & i) || this.system.aabbOverlap(this, o) && (this._currentOverlaps.add(o), this.overlappingActors.has(o) || (this._currentSensorOverlaps.add({
actor: o,
sensor: this,
type: `${o.type}|${this.type}`
}), this.onActorEnter(o))));
for (const o of this.overlappingActors)
this._currentOverlaps.has(o) || this.onActorExit(o);
const e = this.overlappingActors;
return this.overlappingActors = this._currentOverlaps, this._currentOverlaps = e, this._currentOverlaps.clear(), this._currentSensorOverlaps;
}
reset() {
super.reset(), this._currentSensorOverlaps.clear(), this._currentOverlaps.clear(), this._isRidingSolidCache = null, this.velocity = { x: 0, y: 0 }, this.overlappingActors = /* @__PURE__ */ new Set();
}
/**
* Called when an actor starts overlapping with this sensor.
* Override this to handle overlap start events.
*
* @param actor - The actor that entered
*/
onActorEnter(t) {
}
/**
* Called when an actor stops overlapping with this sensor.
* Override this to handle overlap end events.
*
* @param actor - The actor that exited
*/
onActorExit(t) {
}
/**
* Gets all solids at the specified position.
*
* @param x - X position to check
* @param y - Y position to check
* @returns Array of solids at the position
*/
getSolidsAt(t, i) {
return this.system.getSolidsAt(t, i, this);
}
}
class X extends w {
constructor() {
super(...arguments), this.entityType = "Solid", this.shouldRemoveOnCull = !1, this._canCollide = !0, this.collideable = !0, this.moving = !1;
}
get canCollide() {
return this._canCollide;
}
setPosition(t, i) {
this.x = t, this.y = i;
}
/**
* Sets the solid's X position.
* For moving solids, this queues the movement to be applied on next update.
*/
set x(t) {
this._nextX = Math.round(t), this.moving = !0;
}
get x() {
return this._x;
}
/**
* Sets the solid's Y position.
* For moving solids, this queues the movement to be applied on next update.
*/
set y(t) {
this._nextY = Math.round(t), this.moving = !0;
}
get y() {
return this._y;
}
/**
* Initializes or reinitializes the solid with new configuration.
*
* @param config - Configuration for the solid
*/
init(t) {
super.init(t), t && (this._nextX = this._x, this._nextY = this._y);
}
/** Right edge X coordinate */
get right() {
return this.x + this.width;
}
/** Left edge X coordinate */
get left() {
return this.x;
}
/** Top edge Y coordinate */
get top() {
return this.y;
}
/** Bottom edge Y coordinate */
get bottom() {
return this.y + this.height;
}
/**
* Checks if this solid can collide with the given entity type.
* This method is kept for backward compatibility.
*
* @returns Always returns true as we're now using only collision layers/masks
*/
canCollideWith() {
return !!this.collideable;
}
/**
* Moves the solid by the specified amount, carrying any riding entities.
* Also pushes any overlapping entities out of the way.
*
* @param x - X distance to move
* @param y - Y distance to move
* @param actors - Set of actors to check for riding/pushing
* @param sensors - Set of sensors to check for riding/pushing
*/
move(t, i, s = this.system.actors, e = this.system.sensors, o = !1) {
if (!this.active)
return;
const h = t + (this._nextX - this._x), c = i + (this._nextY - this._y);
this._xRemainder += h, this._yRemainder += c;
const n = Math.round(this._xRemainder), d = Math.round(this._yRemainder);
if (n !== 0 || d !== 0 || o) {
if (this.collideable) {
const u = /* @__PURE__ */ new Set(), p = /* @__PURE__ */ new Set();
for (const r of s)
r.collisionLayer & this.collisionMask && this.collisionLayer & r.collisionMask && r.isRiding(this) && u.add(r);
for (const r of e)
r.collisionLayer & this.collisionMask && this.collisionLayer & r.collisionMask && r.isRiding(this) && p.add(r);
if (this._canCollide = !1, n !== 0)
if (this._xRemainder -= n, this._x += n, n > 0) {
for (const r of s)
this.overlaps(r) && r.collisionLayer & this.collisionMask && this.collisionLayer & r.collisionMask ? r.moveX(this.right - r.x, r.squish, this) : u.has(r) && r.moveX(n);
for (const r of e)
this.overlaps(r) && r.collisionLayer & this.collisionMask && this.collisionLayer & r.collisionMask ? r.moveX(this.right - r.x) : p.has(r) && r.moveX(n);
} else {
for (const r of s)
this.overlaps(r) && r.collisionLayer & this.collisionMask && this.collisionLayer & r.collisionMask ? r.moveX(this.left - (r.x + r.width), r.squish, this) : u.has(r) && r.moveX(n);
for (const r of e)
this.overlaps(r) && r.collisionLayer & this.collisionMask && this.collisionLayer & r.collisionMask ? r.moveX(this.left - (r.x + r.width)) : p.has(r) && r.moveX(n);
}
if (d !== 0)
if (this._yRemainder -= d, this._y += d, d > 0) {
for (const r of s)
this.overlaps(r) && r.collisionLayer & this.collisionMask && this.collisionLayer & r.collisionMask ? r.moveY(this.bottom - r.y, r.squish, this) : u.has(r) && r.moveY(d);
for (const r of e)
this.overlaps(r) && r.collisionLayer & this.collisionMask && this.collisionLayer & r.collisionMask ? r.moveY(this.bottom - r.y) : p.has(r) && r.moveY(d);
} else {
for (const r of s)
this.overlaps(r) && r.collisionLayer & this.collisionMask && this.collisionLayer & r.collisionMask ? r.moveY(this.top - (r.y + r.height), r.squish, this) : u.has(r) && r.moveY(d);
for (const r of e)
this.overlaps(r) && r.collisionLayer & this.collisionMask && this.collisionLayer & r.collisionMask ? r.moveY(this.top - (r.y + r.height)) : p.has(r) && r.moveY(d);
}
this._canCollide = !0;
} else
n !== 0 && (this._xRemainder -= n, this._x += n), d !== 0 && (this._yRemainder -= d, this._y += d);
this._nextX = this._x, this._nextY = this._y, this.updateView();
}
}
/**
* Checks if this solid overlaps with the given entity.
*
* @param entity - Entity to check for overlap
* @returns True if overlapping
*/
overlaps(t) {
return !(this.collisionLayer & t.collisionMask) || !(t.collisionLayer & this.collisionMask) ? !1 : this.system.aabbOverlap(this, t);
}
}
class Y {
constructor(t) {
this.entities = /* @__PURE__ */ new Set(), this._flaggedEntities = /* @__PURE__ */ new Set(), this.actors = /* @__PURE__ */ new Set(), this.solids = /* @__PURE__ */ new Set(), this.sensors = /* @__PURE__ */ new Set(), this.groups = /* @__PURE__ */ new Set(), this.actorsByType = /* @__PURE__ */ new Map(), this.solidsByType = /* @__PURE__ */ new Map(), this.sensorsByType = /* @__PURE__ */ new Map(), this.groupsByType = /* @__PURE__ */ new Map(), this.followers = /* @__PURE__ */ new Map(), this.groupWithEntities = /* @__PURE__ */ new Map(), this.grid = /* @__PURE__ */ new Map(), this.collisions = [], this.sensorOverlaps = [], this.actorCollisions = [], this._checkedPairs = /* @__PURE__ */ new Set(), this._collisionResultPool = [], this._collisionResultPoolIndex = 0, this._actorCollisionPool = [], this._actorCollisionPoolIndex = 0, this._debugGfx = null, this._debug = !1, this._movedActors = /* @__PURE__ */ new Set(), this._activeGridCells = /* @__PURE__ */ new Set(), this._potentialCollisions = /* @__PURE__ */ new Map(), this.options = {
...t,
culling: t.culling ?? !1
}, this.debug = t.debug ?? !1, this._debugContainer = new S(), t.plugin.container.addChild(this._debugContainer);
}
set debug(t) {
var i, s;
this._debug = t, this._debug ? (this._debugContainer || (this._debugContainer = this.options.plugin.container.addChild(new S())), this._debugGfx || (this._debugGfx = new B()), this._debugContainer.addChild(this._debugGfx)) : ((i = this._debugGfx) == null || i.clear(), (s = this._debugContainer) == null || s.removeChildren());
}
set gridSize(t) {
this.options.gridSize = t, this.grid.clear();
for (const i of this.solids)
this.addSolidToGrid(i);
}
set gravity(t) {
this.options.gravity = t;
}
get gravity() {
return this.options.gravity;
}
set maxVelocity(t) {
this.options.maxVelocity = t;
}
get maxVelocity() {
return this.options.maxVelocity;
}
set boundary(t) {
this.options.boundary = t;
}
get boundary() {
return this.options.boundary;
}
get container() {
return this.options.plugin.container;
}
addView(t) {
this.container.addChild(t);
}
_resetPositionFlags(t) {
t.updatedFollowPosition = !1, t.updatedGroupPosition = !1;
}
update(t) {
if (!this.options.plugin.enabled) return;
this.collisions.length = 0, this.sensorOverlaps.length = 0, this.actorCollisions.length = 0, this.resetCollisionPools();
const i = t / 60;
for (const s of this.groups)
s.update(i);
this.updateSolids(i);
for (const s of this.sensors)
this.updateSensor(s, i);
for (const s of this.actors)
s.actorCollisions.length > 0 && (s.actorCollisions.length = 0);
this.updateActors(i), this.updateEntityPositions(), this.processCollisionsAndOverlaps(), this.options.culling && this.options.boundary && this.cullOutOfBounds(), this._debug && this.debugRender();
}
// New optimized methods to break up the update loop
updateSolids(t) {
let i = !1;
for (const e of this.solids)
e.preUpdate(), e.update(t), e.postUpdate(), e.moving && (i = !0);
if (!i) return;
const s = /* @__PURE__ */ new Set();
for (const e of this.solids)
e.moving && (this.removeSolidFromGrid(e), e.move(0, 0, this.actors, this.sensors, !0), s.add(e));
for (const e of s)
this.addSolidToGrid(e);
}
updateActors(t) {
if (this.actors.size !== 0) {
for (const i of this.actors)
if (i.active) {
i.preUpdate(), i.update(t), i.postUpdate();
const s = i.collisions;
if (s.length > 0)
for (const e of s)
this.collisions.push({
type: `${i.type}|${e.solid.type}`,
entity1: i,
entity2: e.solid,
result: {
collided: e.collided,
normal: e.normal,
penetration: e.penetration,
solid: e.solid
}
});
}
}
}
updateEntityPositions() {
this._flaggedEntities.clear();
for (const [t, i] of this.followers) {
const s = t.x, e = t.y;
for (const o of i)
if (!o.updatedFollowPosition) {
const h = o.group !== null;
if (h && o.group === t.group) {
const n = t.groupOffset, d = o.group.x, u = o.group.y;
o.setPosition(
d + n.x + o.followOffset.x,
u + n.y + o.followOffset.y
);
} else h ? o.setPosition(
s + o.followOffset.x + o.group.x + o.groupOffset.x,
e + o.followOffset.y + o.group.y + o.groupOffset.y
) : o.setPosition(s + o.followOffset.x, e + o.followOffset.y);
o.updatedFollowPosition = !0, this._flaggedEntities.add(o), h && (o.updatedGroupPosition = !0);
}
}
for (const [t, i] of this.groupWithEntities) {
const s = t.x, e = t.y;
for (const o of i)
!o.updatedGroupPosition && !o.updatedFollowPosition && (o.setPosition(s + o.groupOffset.x, e + o.groupOffset.y), o.updatedGroupPosition = !0, this._flaggedEntities.add(o));
}
this._flaggedEntities.forEach(this._resetPositionFlags);
}
processCollisionsAndOverlaps() {
this.options.overlapResolver && this.sensorOverlaps.length > 0 && this.options.overlapResolver(this.sensorOverlaps), this.options.enableActorCollisions && this.actors.size > 1 && this.checkActorCollisions(), this.collisions.length > 0 && this.options.collisionResolver && this.options.collisionResolver(this.collisions), this.actorCollisions.length > 0 && this.options.actorCollisionResolver && this.options.actorCollisionResolver(this.actorCollisions);
}
// Optimize the cullOutOfBounds method
cullOutOfBounds() {
if (!this.options.boundary) return;
const t = this.options.boundary, i = [], s = [], e = [], o = [], h = t.x, c = t.y, n = h + t.width, d = c + t.height;
for (const l of this.actors) {
const f = l.x + l.width, g = l.y + l.height;
!(l.x >= n || // Completely to the right
f <= h || // Completely to the left
l.y >= d || // Completely below
g <= c) ? l.isCulled && l.onUncull() : (l.isCulled || l.onCull(), l.shouldRemoveOnCull && i.push(l));
}
for (const l of this.solids) {
const f = l.x + l.width, g = l.y + l.height;
!(l.x >= n || f <= h || l.y >= d || g <= c) ? l.isCulled && l.onUncull() : (l.isCulled || l.onCull(), l.shouldRemoveOnCull && s.push(l));
}
for (const l of this.sensors) {
const f = l.x + l.width, g = l.y + l.height;
!(l.x >= n || f <= h || l.y >= d || g <= c) ? l.isCulled && l.onUncull() : (l.isCulled || l.onCull(), l.shouldRemoveOnCull && e.push(l));
}
for (const l of this.groups) {
const f = l.x + l.width, g = l.y + l.height;
!(l.x >= n || f <= h || l.y >= d || g <= c) ? l.isCulled && l.onUncull() : (l.isCulled || l.onCull(), l.shouldRemoveOnCull && o.push(l));
}
const u = i.length, p = s.length, r = e.length, y = o.length;
if (u + p + r + y !== 0) {
for (let l = 0; l < u; l++)
this.removeActor(i[l]);
for (let l = 0; l < p; l++)
this.removeSolid(s[l]);
for (let l = 0; l < r; l++)
this.removeSensor(e[l]);
for (let l = 0; l < y; l++)
this.removeGroup(o[l]);
}
}
updateSensor(t, i) {
t.preUpdate(), t.update(i), t.postUpdate();
const s = t.checkActorOverlaps();
this.sensorOverlaps.push(...s);
}
createEntity(t) {
if (t.type === "actor")
return this.createActor(t);
if (t.type === "solid")
return this.createSolid(t);
if (t.type === "sensor")