UNPKG

@dill-pixel/plugin-crunch-physics

Version:

Crunch Physics

1,440 lines 73.2 kB
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._visible = !0, 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 }; } set visible(t) { this.system.getEntitiesInGroup(this).forEach((i) => { i.view.visible = t; }); } get visible() { return this._visible; } /** * 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() { this.system.removeGroup(this), super.destroy(); } } 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(..