@dill-pixel/plugin-snap-physics
Version:
Snap Physics
1 lines • 127 kB
Source Map (JSON)
{"version":3,"file":"dill-pixel-plugin-snap-physics.mjs","sources":["../src/extras/pointExtras.ts","../src/SpatialHashGrid.ts","../src/Entity.ts","../src/Solid.ts","../src/Wall.ts","../src/utils.ts","../src/System.ts","../src/version.ts","../src/SnapPhysicsPlugin.ts","../src/Actor.ts","../src/mixins/WithVelocity.ts","../src/Sensor.ts"],"sourcesContent":["import { Point, PointData } from 'pixi.js';\n\nexport const pointExtras = {\n /**\n * Adds `other` to `this` point and outputs into `outPoint` or a new Point.\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method add\n * @memberof Point#\n * @param {PointData} other - The point to add to `this`.\n * @param {PointData} [outPoint] - A Point-like object in which to store the value,\n * optional (otherwise will create a new Point).\n * @returns {PointData} The `outPoint` reference or a new Point, with the result of the addition.\n */\n /**\n * Adds `other` to `this` point and outputs into `outPoint` or a new Point.\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method add\n * @memberof ObservablePoint#\n * @param {PointData} other - The point to add to `this`.\n * @param {PointData} [outPoint] - A Point-like object in which to store the value,\n * optional (otherwise will create a new Point).\n * @returns {PointData} The `outPoint` reference or a new Point, with the result of the addition.\n */\n add(other: PointData, outPoint: PointData) {\n if (!outPoint) {\n outPoint = new Point();\n }\n outPoint.x = (this as unknown as Point).x + other.x;\n outPoint.y = (this as unknown as Point).y + other.y;\n return outPoint;\n },\n /**\n * Subtracts `other` from `this` point and outputs into `outPoint` or a new Point.\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method subtract\n * @memberof Point#\n * @param {PointData} other - The point to subtract to `this`.\n * @param {PointData} [outPoint] - A Point-like object in which to store the value,\n * optional (otherwise will create a new Point).\n * @returns {PointData} The `outPoint` reference or a new Point, with the result of the subtraction.\n */\n /**\n * Subtracts `other` from `this` point and outputs into `outPoint` or a new Point.\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method subtract\n * @memberof ObservablePoint#\n * @param {PointData} other - The point to subtract to `this`.\n * @param {PointData} [outPoint] - A Point-like object in which to store the value,\n * optional (otherwise will create a new Point).\n * @returns {PointData} The `outPoint` reference or a new Point, with the result of the subtraction.\n */\n subtract(other: PointData, outPoint: PointData) {\n if (!outPoint) {\n outPoint = new Point();\n }\n outPoint.x = (this as unknown as Point).x - other.x;\n outPoint.y = (this as unknown as Point).y - other.y;\n return outPoint;\n },\n /**\n * Multiplies component-wise `other` and `this` points and outputs into `outPoint` or a new Point.\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method multiply\n * @memberof Point#\n * @param {PointData} other - The point to multiply with `this`.\n * @param {PointData} [outPoint] - A Point-like object in which to store the value,\n * optional (otherwise will create a new Point).\n * @returns {PointData} The `outPoint` reference or a new Point, with the component-wise multiplication.\n */\n /**\n * Multiplies component-wise `other` and `this` points and outputs into `outPoint` or a new Point.\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method multiply\n * @memberof ObservablePoint#\n * @param {PointData} other - The point to multiply with `this`.\n * @param {PointData} [outPoint] - A Point-like object in which to store the value,\n * optional (otherwise will create a new Point).\n * @returns {PointData} The `outPoint` reference or a new Point, with the component-wise multiplication.\n */\n multiply(other: PointData, outPoint: PointData) {\n if (!outPoint) {\n outPoint = new Point();\n }\n outPoint.x = (this as unknown as Point).x * other.x;\n outPoint.y = (this as unknown as Point).y * other.y;\n return outPoint;\n },\n /**\n * Multiplies each component of `this` point with the number `scalar` and outputs into `outPoint` or a new Point.\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method multiplyScalar\n * @memberof Point#\n * @param {number} scalar - The number to multiply both components of `this`.\n * @param {PointData} [outPoint] - A Point-like object in which to store the value,\n * optional (otherwise will create a new Point).\n * @returns {PointData} The `outPoint` reference or a new Point, with the multiplication.\n */\n /**\n * Multiplies each component of `this` point with the number `scalar` and outputs into `outPoint` or a new Point.\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method multiplyScalar\n * @memberof ObservablePoint#\n * @param {number} scalar - The number to multiply both components of `this`.\n * @param {PointData} [outPoint] - A Point-like object in which to store the value,\n * optional (otherwise will create a new Point).\n * @returns {PointData} The `outPoint` reference or a new Point, with the multiplication.\n */\n multiplyScalar(scalar: number, outPoint: PointData) {\n if (!outPoint) {\n outPoint = new Point();\n }\n outPoint.x = (this as unknown as Point).x * scalar;\n outPoint.y = (this as unknown as Point).y * scalar;\n return outPoint;\n },\n /**\n * Computes the dot product of `other` with `this` point.\n * The dot product is the sum of the products of the corresponding components of two vectors.\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method dot\n * @memberof Point#\n * @param {PointData} other - The other point to calculate the dot product with `this`.\n * @returns {number} The result of the dot product. This is an scalar value.\n */\n /**\n * Computes the dot product of `other` with `this` point.\n * The dot product is the sum of the products of the corresponding components of two vectors.\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method dot\n * @memberof ObservablePoint#\n * @param {PointData} other - The other point to calculate the dot product with `this`.\n * @returns {number} The result of the dot product. This is an scalar value.\n */\n dot(other: PointData) {\n return (this as unknown as Point).x * other.x + (this as unknown as Point).y * other.y;\n },\n /**\n * Computes the cross product of `other` with `this` point.\n * Given two linearly independent R3 vectors a and b, the cross product, a × b (read \"a cross b\"),\n * is a vector that is perpendicular to both a and b, and thus normal to the plane containing them.\n * While cross product only exists on 3D space, we can assume the z component of 2D to be zero and\n * the result becomes a vector that will only have magnitude on the z axis.\n *\n * This function returns the z component of the cross product of the two points.\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method cross\n * @memberof Point#\n * @param {PointData} other - The other point to calculate the cross product with `this`.\n * @returns {number} The z component of the result of the cross product.\n */\n /**\n * Computes the cross product of `other` with `this` point.\n * Given two linearly independent R3 vectors a and b, the cross product, a × b (read \"a cross b\"),\n * is a vector that is perpendicular to both a and b, and thus normal to the plane containing them.\n * While cross product only exists on 3D space, we can assume the z component of 2D to be zero and\n * the result becomes a vector that will only have magnitude on the z axis.\n *\n * This function returns the z component of the cross product of the two points.\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method cross\n * @memberof ObservablePoint#\n * @param {PointData} other - The other point to calculate the cross product with `this`.\n * @returns {number} The z component of the result of the cross product.\n */\n cross(other: PointData) {\n return (this as unknown as Point).x * other.y - (this as unknown as Point).y * other.x;\n },\n /**\n * Computes a normalized version of `this` point.\n *\n * A normalized vector is a vector of magnitude (length) 1\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method normalize\n * @memberof Point#\n * @param {PointData} [outPoint] - A Point-like object in which to store the value,\n * optional (otherwise will create a new Point).\n * @returns {PointData} The normalized point.\n */\n /**\n * Computes a normalized version of `this` point.\n *\n * A normalized vector is a vector of magnitude (length) 1\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method normalize\n * @memberof ObservablePoint#\n * @param {PointData} [outPoint] - A Point-like object in which to store the value,\n * optional (otherwise will create a new Point).\n * @returns {PointData} The normalized point.\n */\n normalize(outPoint: PointData) {\n if (!outPoint) {\n outPoint = new Point();\n }\n const magnitude = Math.sqrt(\n (this as unknown as Point).x * (this as unknown as Point).x +\n (this as unknown as Point).y * (this as unknown as Point).y,\n );\n outPoint.x = (this as unknown as Point).x / magnitude;\n outPoint.y = (this as unknown as Point).y / magnitude;\n return outPoint;\n },\n /**\n * Computes the magnitude of this point (Euclidean distance from 0, 0).\n *\n * Defined as the square root of the sum of the squares of each component.\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method magnitude\n * @memberof Point#\n * @returns {number} The magnitude (length) of the vector.\n */\n /**\n * Computes the magnitude of this point (Euclidean distance from 0, 0).\n *\n * Defined as the square root of the sum of the squares of each component.\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method magnitude\n * @memberof ObservablePoint#\n * @returns {number} The magnitude (length) of the vector.\n */\n magnitude() {\n return Math.sqrt(\n (this as unknown as Point).x * (this as unknown as Point).x +\n (this as unknown as Point).y * (this as unknown as Point).y,\n );\n },\n /**\n * Computes the square magnitude of this point.\n * If you are comparing the lengths of vectors, you should compare the length squared instead\n * as it is slightly more efficient to calculate.\n *\n * Defined as the sum of the squares of each component.\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method magnitudeSquared\n * @memberof Point#\n * @returns {number} The magnitude squared (length squared) of the vector.\n */\n /**\n * Computes the square magnitude of this point.\n * If you are comparing the lengths of vectors, you should compare the length squared instead\n * as it is slightly more efficient to calculate.\n *\n * Defined as the sum of the squares of each component.\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method magnitudeSquared\n * @memberof ObservablePoint#\n * @returns {number} The magnitude squared (length squared) of the vector.\n */\n magnitudeSquared() {\n return (\n (this as unknown as Point).x * (this as unknown as Point).x +\n (this as unknown as Point).y * (this as unknown as Point).y\n );\n },\n /**\n * Computes vector projection of `this` on `onto`.\n *\n * Imagine a light source, parallel to `onto`, above `this`.\n * The light would cast rays perpendicular to `onto`.\n * `(this as unknown as Point).project(onto)` is the shadow cast by `this` on the line defined by `onto` .\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method project\n * @memberof Point#\n * @param {PointData} onto - A non zero vector describing a line on which to project `this`.\n * @param {PointData} [outPoint] - A Point-like object in which to store the value,\n * optional (otherwise will create a new Point).\n * @returns {PointData} The `this` on `onto` projection.\n */\n /**\n * Computes vector projection of `this` on `onto`.\n *\n * Imagine a light source, parallel to `onto`, above `this`.\n * The light would cast rays perpendicular to `onto`.\n * `(this as unknown as Point).project(onto)` is the shadow cast by `this` on the line defined by `onto` .\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method project\n * @memberof ObservablePoint#\n * @param {PointData} onto - A non zero vector describing a line on which to project `this`.\n * @param {PointData} [outPoint] - A Point-like object in which to store the value,\n * optional (otherwise will create a new Point).\n * @returns {PointData} The `this` on `onto` projection.\n */\n project(onto: PointData, outPoint: PointData) {\n if (!outPoint) {\n outPoint = new Point();\n }\n const normalizedScalarProjection =\n ((this as unknown as Point).x * onto.x + (this as unknown as Point).y * onto.y) /\n (onto.x * onto.x + onto.y * onto.y);\n outPoint.x = onto.x * normalizedScalarProjection;\n outPoint.y = onto.y * normalizedScalarProjection;\n return outPoint;\n },\n /**\n * Reflects `this` vector off of a plane orthogonal to `normal`.\n * `normal` is not normalized during this process. Consider normalizing your `normal` before use.\n *\n * Imagine a light source bouncing onto a mirror.\n * `this` vector is the light and `normal` is a vector perpendicular to the mirror.\n * `(this as unknown as Point).reflect(normal)` is the reflection of `this` on that mirror.\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method reflect\n * @memberof Point#\n * @param {PointData} normal - The normal vector of your reflecting plane.\n * @param {PointData} [outPoint] - A Point-like object in which to store the value,\n * optional (otherwise will create a new Point).\n * @returns {PointData} The reflection of `this` on your reflecting plane.\n */\n /**\n * Reflects `this` vector off of a plane orthogonal to `normal`.\n * `normal` is not normalized during this process. Consider normalizing your `normal` before use.\n *\n * Imagine a light source bouncing onto a mirror.\n * `this` vector is the light and `normal` is a vector perpendicular to the mirror.\n * `(this as unknown as Point).reflect(normal)` is the reflection of `this` on that mirror.\n *\n * _Note: Only available with **pixi.js/math-extras**._\n * @method reflect\n * @memberof ObservablePoint#\n * @param {PointData} normal - The normal vector of your reflecting plane.\n * @param {PointData} [outPoint] - A Point-like object in which to store the value,\n * optional (otherwise will create a new Point).\n * @returns {PointData} The reflection of `this` on your reflecting plane.\n */\n reflect(normal: PointData, outPoint: PointData) {\n if (!outPoint) {\n outPoint = new Point();\n }\n const dotProduct = (this as unknown as Point).x * normal.x + (this as unknown as Point).y * normal.y;\n outPoint.x = (this as unknown as Point).x - 2 * dotProduct * normal.x;\n outPoint.y = (this as unknown as Point).y - 2 * dotProduct * normal.y;\n return outPoint;\n },\n\n rotate(angle: number, outPoint: Point): Point {\n if (!outPoint) {\n outPoint = new Point();\n }\n const cos = Math.cos(angle);\n const sin = Math.sin(angle);\n const newX = (this as unknown as Point).x * cos - (this as unknown as Point).y * sin;\n const newY = (this as unknown as Point).x * sin + (this as unknown as Point).y * cos;\n outPoint.x = newX;\n outPoint.y = newY;\n\n return outPoint;\n },\n length(): number {\n return Math.sqrt(\n (this as unknown as Point).x * (this as unknown as Point).x +\n (this as unknown as Point).y * (this as unknown as Point).y,\n );\n },\n};\n","import { Graphics, Rectangle } from 'pixi.js';\nimport { Entity } from './Entity';\nimport { System } from './System';\nimport { SpatialHashGridFilter } from './types';\n\ntype GridKey = string;\n\nexport class SpatialHashGrid {\n private cells: Map<GridKey, Entity[]> = new Map();\n\n constructor(cellSize: number, insertEntities: boolean = false) {\n this._cellSize = cellSize;\n if (insertEntities) {\n System.all.forEach((entity) => this.insert(entity));\n }\n }\n\n private _cellSize: number;\n\n get cellSize(): number {\n return this._cellSize;\n }\n\n set cellSize(size: number) {\n this._cellSize = size;\n this.cells.clear();\n this.updateAll();\n }\n\n destroy() {\n this.cells.clear();\n }\n\n insert(entity: Entity): void {\n const bounds = entity.boundingRect;\n\n const startX = Math.floor(bounds.x / this._cellSize);\n const startY = Math.floor(bounds.y / this._cellSize);\n const endX = Math.floor((bounds.x + bounds.width) / this._cellSize);\n const endY = Math.floor((bounds.y + bounds.height) / this._cellSize);\n\n for (let x = startX; x <= endX; x++) {\n for (let y = startY; y <= endY; y++) {\n const key = this.getGridKey(x, y);\n if (!this.cells.has(key)) {\n this.cells.set(key, []);\n }\n\n const cellEntities = this.cells.get(key)!;\n if (!cellEntities.includes(entity)) {\n cellEntities.push(entity);\n }\n }\n }\n }\n\n remove(entity: Entity): void {\n const keysToRemove: GridKey[] = [];\n this.cells.forEach((entities, key) => {\n const index = entities.indexOf(entity);\n if (index !== -1) {\n entities.splice(index, 1);\n if (entities.length === 0) {\n keysToRemove.push(key);\n }\n }\n });\n keysToRemove.forEach((key) => this.cells.delete(key));\n }\n\n query<T extends Entity = Entity>(\n range: Rectangle,\n filter?: SpatialHashGridFilter,\n dx: number = 0,\n dy: number = 0,\n exclude?: Entity | Entity[],\n debug?: boolean,\n ): Set<T> {\n let uidsToExclude: number[] = [];\n if (exclude) {\n if (Array.isArray(exclude)) {\n uidsToExclude = exclude.map((e) => e.uid);\n } else {\n uidsToExclude = [exclude.uid];\n }\n }\n void debug;\n const expandedRange = new Rectangle(\n range.x - Math.abs(dx),\n range.y - Math.abs(dy),\n range.width + 2 * Math.abs(dx),\n range.height + 2 * Math.abs(dy),\n );\n const foundEntities = new Set<T>();\n\n const startX = Math.floor(Math.min(expandedRange.x, expandedRange.x + expandedRange.width) / this._cellSize);\n const startY = Math.floor(Math.min(expandedRange.y, expandedRange.y + expandedRange.height) / this._cellSize);\n const endX = Math.floor(Math.max(expandedRange.x, expandedRange.x + expandedRange.width) / this._cellSize);\n const endY = Math.floor(Math.max(expandedRange.y, expandedRange.y + expandedRange.height) / this._cellSize);\n\n for (let x = startX; x <= endX; x++) {\n for (let y = startY; y <= endY; y++) {\n const key = this.getGridKey(x, y);\n const cellEntities = this.cells.get(key);\n if (cellEntities) {\n cellEntities.forEach((entity) => {\n if (!uidsToExclude.includes(entity.uid)) {\n if (filter === undefined || this.matchesFilter(entity, filter)) {\n foundEntities.add(entity as T);\n }\n }\n });\n }\n }\n }\n return foundEntities;\n }\n\n private matchesFilter(entity: Entity, filter: SpatialHashGridFilter): boolean {\n switch (typeof filter) {\n case 'string':\n return (\n filter === entity.type ||\n (filter === 'solid' && entity.isSolid) ||\n (filter === 'actor' && entity.isActor) ||\n (filter === 'sensor' && entity.isSensor)\n );\n case 'object':\n return Array.isArray(filter) && filter.includes(entity.type);\n case 'function':\n return filter(entity);\n default:\n return false;\n }\n }\n\n updateAll() {\n System.all.forEach((entity) => this.updateEntity(entity));\n }\n\n updateEntity(entity: Entity): void {\n this.remove(entity);\n this.insert(entity);\n }\n\n draw(gfx: Graphics) {\n const rects = this._getDebugRects();\n rects.forEach((rect) => {\n gfx.rect(rect.left, rect.top, rect.width, rect.height);\n gfx.stroke({ color: 0x00ff00, pixelLine: true });\n });\n }\n\n private _getDebugRects() {\n const rects: Rectangle[] = [];\n this.cells.forEach((_cell, key) => {\n const [x, y] = key.split(':').map(Number);\n if (_cell.length) {\n rects.push(new Rectangle(x * this._cellSize, y * this._cellSize, this._cellSize, this._cellSize));\n }\n });\n return rects;\n }\n\n private getGridKey(cellX: number, cellY: number): GridKey {\n return `${cellX}:${cellY}`;\n }\n}\n","import { Application, Container } from 'dill-pixel';\nimport { Bounds, Circle, Container as PIXIContianer, Point, Rectangle, Sprite } from 'pixi.js';\nimport { ICollider } from './ICollider';\nimport { System } from './System';\nimport { EntityType, SnapBoundary } from './types';\n\nexport class Entity<T = any, A extends Application = Application> extends Container<A> implements ICollider {\n view: PIXIContianer;\n isActor: boolean = false;\n isSolid: boolean = false;\n isSensor: boolean = false;\n debug: boolean = false;\n debugColors = {\n bounds: 0xff0000,\n outerBounds: 0x00ff00,\n };\n type: EntityType = 'Solid';\n isCircle: boolean = false;\n isCollideable: boolean = true;\n xRemainder: number = 0;\n yRemainder: number = 0;\n config: T;\n\n protected subpixelX: number = 0;\n protected subpixelY: number = 0;\n protected remainder: Point = new Point(0, 0);\n\n constructor(config?: Partial<T>) {\n super({ autoUpdate: true });\n this.config = config as T;\n }\n\n protected _cachedBounds: SnapBoundary | null = null;\n\n get cachedBounds(): SnapBoundary {\n if (!this._cachedBounds || this._dirtyBounds) {\n const bounds = this.view.getBounds();\n bounds.scale(1 / this.system.container.worldTransform.d);\n if (this.isCircle) {\n bounds.width = bounds.height = Math.max(bounds.width, bounds.height);\n this._cachedBounds = new Circle(\n bounds.x + bounds.width * 0.5,\n bounds.y * bounds.width * 0.5,\n bounds.width * 0.5,\n );\n } else {\n this._cachedBounds = bounds;\n }\n }\n return this._cachedBounds ?? (this.isCircle ? new Circle() : new Rectangle());\n }\n\n set cachedBounds(value: Bounds) {\n this._cachedBounds = value;\n }\n\n protected _dirtyBounds: boolean = true;\n\n get dirtyBounds() {\n return this._dirtyBounds;\n }\n\n set dirtyBounds(value: boolean) {\n this._dirtyBounds = value;\n }\n\n get boundingRect(): Rectangle {\n const bb = this.getBoundingBox();\n if (this.isCircle) {\n return bb.getBounds();\n }\n return bb as Rectangle;\n }\n\n get top(): number {\n return this.boundingRect.top;\n }\n\n get bottom(): number {\n return this.boundingRect.bottom;\n }\n\n get left(): number {\n return this.boundingRect.left;\n }\n\n get right(): number {\n return this.boundingRect.right;\n }\n\n get system(): typeof System {\n return System;\n }\n\n getCollideables<T extends Entity = Entity>(dx: number = 0, dy: number = 0): Set<T> {\n void dx;\n void dy;\n return new Set<T>();\n }\n\n preFixedUpdate() {}\n\n fixedUpdate(deltaTime?: number) {\n void deltaTime;\n }\n\n postFixedUpdate() {}\n\n getWorldBounds(): SnapBoundary {\n const pos = this.system.container.toLocal(this.view.getGlobalPosition());\n const bounds = this.cachedBounds;\n bounds.x = pos.x;\n bounds.y = pos.y;\n\n if (this.view instanceof Sprite && this.view.anchor) {\n if (!this.isCircle) {\n bounds.x -= this.view.width * this.view.anchor.x;\n bounds.y -= this.view.height * this.view.anchor.y;\n }\n }\n return bounds;\n }\n\n getBoundingBox(): Rectangle | Circle {\n const bounds = this.getWorldBounds();\n return bounds instanceof Bounds ? bounds.rectangle : bounds;\n }\n\n getOuterBoundingBox(): Rectangle | Circle | null {\n return null;\n }\n\n moveX(amount: number): void {\n this.remainder.x += amount;\n const move = this.remainder.x;\n if (move !== 0) {\n this.remainder.x -= move;\n this.x += move;\n }\n }\n\n moveY(amount: number): void {\n this.remainder.y += amount;\n const move = this.remainder.y;\n if (move !== 0) {\n this.remainder.y -= move;\n this.y += move;\n }\n }\n\n // Improved collision detection with subpixel precision\n collidesWith(entity: Entity, dx: number = 0, dy: number = 0): boolean {\n if (!entity) {\n return false;\n }\n\n // Add subpixel remainders to the collision check\n const totalDx = dx + this.remainder.x;\n const totalDy = dy + this.remainder.y;\n\n if (this.isCircle) {\n if (entity.isCircle) {\n return System.getCircleToCircleIntersection(entity, this, totalDx, totalDy);\n } else {\n return System.getRectToCircletIntersection(entity, this, totalDx, totalDy);\n }\n }\n if (entity.isCircle) {\n return System.getRectToCircletIntersection(this, entity, totalDx, totalDy);\n }\n return System.getRectangleIntersection(entity, this, totalDx, totalDy);\n }\n\n protected initialize() {\n // noop\n }\n}\n","import { Application, filterSet } from 'dill-pixel';\nimport { gsap } from 'gsap';\nimport { Actor } from './Actor';\nimport { Entity } from './Entity';\nimport { System } from './System';\n\nexport class Solid<T = any, A extends Application = Application> extends Entity<T, A> {\n type = 'Solid';\n isSolid = true;\n riding: Set<Actor> = new Set();\n protected _animations: Set<gsap.core.Tween | gsap.core.Timeline> = new Set<gsap.core.Tween | gsap.core.Timeline>();\n\n protected _positionAnimation: {\n targetX: number;\n targetY: number;\n startX: number;\n startY: number;\n duration: number;\n elapsed: number;\n ease: gsap.EaseString;\n repeat: number;\n yoyo: boolean;\n repeatDelay: number;\n delayRemaining: number;\n iteration: number;\n isReversed: boolean;\n } | null = null;\n\n getCollideables<T extends Entity = Entity>(dx: number = 0, dy: number = 0): Set<T> {\n return System.getNearbyEntities<T>(this, 'actor', dx, dy) as Set<T>;\n }\n\n added() {\n System.addSolid(this);\n this.addSignalConnection(this.system.onSystemEnabledChanged.connect(this._handleSystemEnabledChanged));\n }\n\n private _handleSystemEnabledChanged(enabled: boolean) {\n if (enabled) {\n if (this._animations?.size > 0) {\n this._animations.forEach((animation) => animation?.resume());\n }\n } else {\n if (this._animations?.size > 0) {\n this._animations.forEach((animation) => animation?.pause());\n }\n }\n }\n\n removed() {\n System.removeSolid(this);\n if (this._animations?.size > 0) {\n this._animations.forEach((animation) => animation?.kill());\n }\n }\n\n getAllRiding(dx: number = 0, dy: number = 0) {\n return filterSet<Actor>(\n this.getCollideables(dx, dy),\n (entity: Actor) => entity.isActor && entity.isRiding(this),\n ) as Set<Actor>;\n }\n\n move(x: number, y: number): void {\n this.xRemainder += x;\n this.yRemainder += y;\n const moveX = Math.round(this.xRemainder);\n const moveY = Math.round(this.yRemainder);\n\n if (moveX !== 0 || moveY !== 0) {\n // Get all potential collisions before moving\n const ridingActors = this.getAllRiding(moveX, moveY);\n\n // For moving platforms, we need to check a larger area to catch fast-moving actors\n const sweepBox = this.boundingRect.clone();\n // Expand the sweep box in the direction of movement\n if (moveY < 0) {\n // Moving up - expand upward\n sweepBox.y += moveY;\n sweepBox.height -= moveY;\n } else {\n // Moving down - expand downward\n sweepBox.height += moveY;\n }\n if (moveX < 0) {\n // Moving left - expand left\n sweepBox.x += moveX;\n sweepBox.width -= moveX;\n } else {\n // Moving right - expand right\n sweepBox.width += moveX;\n }\n\n // Get potential collisions using the sweep box\n const potentialCollisions = new Set<Actor>();\n for (const actor of this.getCollideables<Actor>(moveX, moveY)) {\n if (actor.boundingRect.intersects(sweepBox)) {\n potentialCollisions.add(actor);\n }\n }\n\n // First move riding actors with the platform\n for (const actor of ridingActors) {\n if (actor.mostRiding === this) {\n // Move riding actors first to maintain contact\n actor.moveY(moveY);\n actor.moveX(moveX);\n }\n }\n\n // Then move the platform\n this.x += moveX;\n this.y += moveY;\n this.xRemainder -= moveX;\n this.yRemainder -= moveY;\n\n // Finally handle any collisions\n this.handleActorInteractions(moveX, moveY, ridingActors, potentialCollisions);\n }\n System.updateEntity(this);\n }\n\n animatePosition(x?: number | null, y?: number | null, vars: gsap.TweenVars = {}): gsap.core.Tween {\n const pos = this.position.clone();\n const tweenVars = Object.assign({ duration: 1, ease: 'linear.none' }, vars);\n\n const targetX = x === undefined || x === null ? pos.x : x;\n const targetY = y === undefined || y === null ? pos.y : y;\n\n // Store animation data for physics update\n this._positionAnimation = {\n targetX,\n targetY,\n startX: pos.x,\n startY: pos.y,\n duration: tweenVars.duration as number,\n elapsed: 0,\n ease: (tweenVars.ease?.toString() || 'linear.none') as gsap.EaseString,\n repeat: (tweenVars.repeat as number) || 0,\n yoyo: tweenVars.yoyo || false,\n repeatDelay: (tweenVars.repeatDelay as number) || 0,\n delayRemaining: 0,\n iteration: 0,\n isReversed: false,\n };\n\n // Create a dummy tween for compatibility\n const tween = gsap.to({}, tweenVars);\n this._animations.add(tween);\n return tween;\n }\n\n fixedUpdate(deltaTime: number) {\n super.fixedUpdate(deltaTime);\n\n // Update position animation in sync with physics\n if (this._positionAnimation) {\n // Handle repeat delay\n if (this._positionAnimation.delayRemaining > 0) {\n this._positionAnimation.delayRemaining -= deltaTime;\n return;\n }\n\n this._positionAnimation.elapsed += deltaTime;\n const progress = Math.min(this._positionAnimation.elapsed / this._positionAnimation.duration, 1);\n\n // Calculate the new position using GSAP's easing\n const easedProgress = gsap.parseEase(this._positionAnimation.ease)(\n this._positionAnimation.isReversed ? 1 - progress : progress,\n );\n\n const newX =\n this._positionAnimation.startX +\n (this._positionAnimation.targetX - this._positionAnimation.startX) * easedProgress;\n const newY =\n this._positionAnimation.startY +\n (this._positionAnimation.targetY - this._positionAnimation.startY) * easedProgress;\n\n // Apply movement through physics system\n this.move(newX - this.x, newY - this.y);\n\n // Handle completion of current iteration\n if (progress >= 1) {\n // Reset elapsed time\n this._positionAnimation.elapsed = 0;\n\n // Handle repeat logic\n if (\n this._positionAnimation.repeat === -1 ||\n this._positionAnimation.iteration < this._positionAnimation.repeat\n ) {\n this._positionAnimation.iteration++;\n\n // Handle yoyo\n if (this._positionAnimation.yoyo) {\n this._positionAnimation.isReversed = !this._positionAnimation.isReversed;\n }\n\n // Apply repeat delay\n if (this._positionAnimation.repeatDelay > 0) {\n this._positionAnimation.delayRemaining = this._positionAnimation.repeatDelay;\n }\n } else {\n // Animation complete\n this._positionAnimation = null;\n }\n }\n }\n }\n\n public handleActorInteractions(\n deltaX: number,\n deltaY: number,\n ridingActors = this.getAllRiding(),\n potentialCollisions = this.getCollideables<Actor>(deltaX, deltaY),\n ): void {\n for (const actor of potentialCollisions) {\n // Skip actors that are already riding (they were moved with the platform)\n if (ridingActors.has(actor)) continue;\n\n if (!actor.passThroughTypes.includes(this.type) && !actor.isPassingThrough(this)) {\n // For moving platforms, we need to do a more thorough collision check\n const isColliding = this.collidesWith(actor, deltaX, deltaY);\n const wasColliding = this.collidesWith(actor, 0, 0);\n\n // If either check detects a collision, handle it\n if (isColliding || wasColliding) {\n // Calculate overlaps\n const overlapX =\n deltaX !== 0\n ? deltaX > 0\n ? this.boundingRect.right - actor.boundingRect.left\n : this.boundingRect.left - actor.boundingRect.right\n : 0;\n\n const overlapY =\n deltaY !== 0\n ? deltaY > 0\n ? this.boundingRect.bottom - actor.boundingRect.top\n : this.boundingRect.top - actor.boundingRect.bottom\n : 0;\n\n // For fast-moving platforms, prioritize vertical resolution\n if (Math.abs(deltaY) > Math.abs(deltaX)) {\n if (overlapY !== 0) {\n actor.moveY(overlapY, actor.squish, null, this);\n }\n if (overlapX !== 0) {\n actor.moveX(overlapX, actor.squish, null, this);\n }\n } else {\n if (overlapX !== 0) {\n actor.moveX(overlapX, actor.squish, null, this);\n }\n if (overlapY !== 0) {\n actor.moveY(overlapY, actor.squish, null, this);\n }\n }\n }\n }\n }\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n protected handleCollisionChange(_isColliding?: boolean) {}\n}\n","import { Texture } from 'pixi.js';\nimport { Solid as SnapSolid } from './Solid';\n\nexport type WallConfig = {\n width: number;\n height: number;\n debugColor: number;\n};\n\nconst defaults: WallConfig = {\n width: 10,\n height: 10,\n debugColor: 0x00ffff,\n};\n\nexport class Wall extends SnapSolid<WallConfig> {\n type = 'Wall';\n\n constructor(config: Partial<WallConfig> = {}) {\n super({ ...defaults, ...config });\n this.initialize();\n }\n\n protected initialize() {\n this.view = this.add.sprite({\n asset: Texture.WHITE,\n width: this.config.width,\n height: this.config.height,\n tint: this.config.debugColor,\n anchor: 0.5,\n });\n }\n}\n","import { Circle, Point, Rectangle } from 'pixi.js';\nimport { Entity } from './Entity';\nimport { ICollider } from './ICollider';\nimport { System } from './System';\nimport { Collision, CollisionDirection } from './types';\n\nexport function checkPointIntersection(point: Point, collider: ICollider): boolean {\n return point.x > collider.left && point.x < collider.right && point.y > collider.top && point.y < collider.bottom;\n}\n\ntype Overlap = {\n x: number;\n y: number;\n area: number;\n};\n\nconst EPSILON = 1e-10;\n\nexport function getRectToRectIntersectionArea(rectA: Rectangle, rectB: Rectangle): Overlap {\n const xOverlap = Math.max(0, Math.min(rectA.right, rectB.right) - Math.max(rectA.left, rectB.left));\n const yOverlap = Math.max(0, Math.min(rectA.bottom, rectB.bottom) - Math.max(rectA.top, rectB.top));\n const area = xOverlap * yOverlap;\n return { x: xOverlap, y: yOverlap, area };\n}\n\nexport function getRectToCircleIntersectionArea(rect: Rectangle, circle: Circle): Overlap {\n const closestX = Math.max(rect.x, Math.min(rect.x + rect.width, circle.x));\n const closestY = Math.max(rect.y, Math.min(rect.y + rect.height, circle.y));\n const dx = circle.x - closestX;\n const dy = circle.y - closestY;\n const distanceSquared = dx * dx + dy * dy;\n\n const distance = Math.sqrt(distanceSquared);\n const angle = Math.acos(distance / circle.radius);\n const sectorArea = angle * circle.radius * circle.radius;\n const triangleArea = distance * Math.sqrt(circle.radius * circle.radius - distanceSquared);\n const intersectionArea = sectorArea - triangleArea;\n return { x: 0, y: 0, area: Math.max(0, intersectionArea) };\n}\n\nexport function getCircleToCircleIntersectionArea(circleA: Circle, circleB: Circle): Overlap {\n const dx = circleB.x - circleA.x;\n const dy = circleB.y - circleA.y;\n const distanceSquared = dx * dx + dy * dy;\n const distance = Math.sqrt(distanceSquared);\n\n const r1 = circleA.radius;\n const r2 = circleB.radius;\n const radiiSum = r1 + r2;\n\n // No overlap if the distance is greater than or equal to the sum of the radii\n if (distance >= radiiSum - EPSILON) {\n return { x: 0, y: 0, area: 0 };\n }\n\n // One circle is completely within the other\n if (distance <= Math.abs(r1 - r2) + EPSILON) {\n const smallerRadius = Math.min(r1, r2);\n const area = Math.PI * smallerRadius * smallerRadius;\n return { x: circleA.x, y: circleA.y, area };\n }\n\n // Calculate intersection area\n const a = (r1 * r1 - r2 * r2 + distanceSquared) / (2 * distance);\n const h = Math.sqrt(r1 * r1 - a * a);\n const area = r1 * r1 * Math.acos(a / r1) + r2 * r2 * Math.acos((distance - a) / r2) - distance * h;\n\n const cx = circleA.x + (a * dx) / distance;\n const cy = circleA.y + (a * dy) / distance;\n\n return { x: cx, y: cy, area: Math.max(0, area) };\n}\n\nexport function checkCollision(\n shapeA: Rectangle | Circle,\n shapeB: Rectangle | Circle,\n entity1: Entity,\n entity2: Entity,\n): Collision | false {\n const collision: Collision = {\n type: `${entity1.type}|${entity2.type}`,\n entity1,\n entity2,\n top: 0,\n bottom: 0,\n left: 0,\n right: 0,\n area: 0,\n direction: undefined,\n overlap: { x: 0, y: 0 },\n };\n let hasCollision = false;\n\n if (entity1.isCircle && entity2.isCircle) {\n const circleA = shapeA as Circle;\n const circleB = shapeB as Circle;\n const dx = circleB.x - circleA.x;\n const dy = circleB.y - circleA.y;\n const distance = Math.sqrt(dx * dx + dy * dy);\n if (distance < circleA.radius + circleB.radius) {\n hasCollision = true;\n circleToCircleCollision(circleA, circleB, collision);\n }\n } else if (entity1.isCircle !== entity2.isCircle) {\n // One shape is a circle, the other is a rectangle\n const circle = entity1.isCircle ? (shapeA as Circle) : (shapeB as Circle);\n const rect = entity1.isCircle ? (shapeB as Rectangle) : (shapeA as Rectangle);\n // Find the closest point on the rectangle to the circle's center\n const closestX = Math.max(rect.x, Math.min(circle.x, rect.x + rect.width));\n const closestY = Math.max(rect.y, Math.min(circle.y, rect.y + rect.height));\n const dx = circle.x - closestX;\n const dy = circle.y - closestY;\n const distanceSquared = dx * dx + dy * dy;\n\n if (distanceSquared <= circle.radius * circle.radius) {\n hasCollision = true;\n rectToCircleCollision(rect, circle, closestX, closestY, collision);\n }\n } else {\n // Both shapes are rectangles\n const rectA = shapeA as Rectangle;\n const rectB = shapeB as Rectangle;\n\n if (\n rectA.x < rectB.x + rectB.width &&\n rectA.x + rectA.width > rectB.x &&\n rectA.y < rectB.y + rectB.height &&\n rectA.y + rectA.height > rectB.y\n ) {\n hasCollision = true;\n rectToRectCollision(rectA, rectB, collision);\n }\n }\n\n if (hasCollision && collision.area > System.collisionThreshold) {\n collision.direction = getCollisionDirection(collision);\n return collision;\n }\n return false;\n}\n\nfunction getCollisionDirection(collision: Collision): CollisionDirection | undefined {\n // get the max value of all the collision sides\n let dir: CollisionDirection | undefined = undefined;\n let value = 0;\n\n if (collision.top > value) {\n value = collision.top;\n dir = 'top';\n }\n if (collision.bottom > value) {\n value = collision.bottom;\n dir = 'bottom';\n }\n if (collision.left > value) {\n value = collision.left;\n dir = 'left';\n }\n if (collision.right > value) {\n value = collision.right;\n dir = 'right';\n }\n return dir;\n}\n\nfunction rectToCircleCollision(\n rect: Rectangle,\n circle: Circle,\n closestX: number,\n closestY: number,\n collision: Collision,\n) {\n const dx = circle.x - closestX;\n const dy = circle.y - closestY;\n const distanceSquared = dx * dx + dy * dy;\n\n if (distanceSquared >= circle.radius * circle.radius - EPSILON) {\n // No intersection\n return { x: 0, y: 0, area: 0 };\n }\n\n // Circle center is inside the rectangle\n if (circle.x >= rect.x && circle.x <= rect.x + rect.width && circle.y >= rect.y && circle.y <= rect.y + rect.height) {\n return { x: circle.x, y: circle.y, area: Math.PI * circle.radius * circle.radius };\n }\n\n // Partial intersection\n const distance = Math.sqrt(distanceSquared);\n const angle = Math.acos(distance / circle.radius);\n const sectorArea = angle * circle.radius * circle.radius;\n const triangleArea = distance * Math.sqrt(circle.radius * circle.radius - distanceSquared);\n const intersectionArea = sectorArea - triangleArea;\n\n collision.overlap = { x: closestX, y: closestY };\n collision.area = Math.max(0, intersectionArea);\n\n if (distance < circle.radius) {\n // Reset collision sides\n collision.top = 0;\n collision.bottom = 0;\n collision.left = 0;\n collision.right = 0;\n\n // Set collision sides based on overlap distances and circle's position relative to the rectangle\n if (dx > 0) {\n collision.left = Math.abs(dx);\n } else {\n collision.right = Math.abs(dx);\n }\n if (dy > 0) {\n collision.top = Math.abs(dy);\n } else {\n collision.bottom = Math.abs(dy);\n }\n }\n}\n\nfunction circleToCircleCollision(circleA: Circle, circleB: Circle, collision: Collision) {\n const dx = circleB.x - circleA.x;\n const dy = circleB.y - circleA.y;\n const distanceSquared = dx * dx + dy * dy;\n const distance = Math.sqrt(distanceSquared);\n\n const r1 = circleA.radius;\n const r2 = circleB.radius;\n const radiiSum = r1 + r2;\n\n // Early exit if no collision\n if (distance >= radiiSum) {\n return;\n }\n\n // Calculate penetration depth\n const penetration = radiiSum - distance;\n\n // Calculate normalized collision normal\n const nx = dx / distance;\n const ny = dy / distance;\n\n // Set collision sides based on the normal vector\n // We use a larger threshold for circle collisions to ensure proper reflection\n if (Math.abs(nx) > 0.1) {\n if (nx > 0) {\n collision.left = penetration;\n } else {\n collision.right = penetration;\n }\n }\n if (Math.abs(ny) > 0.1) {\n if (ny > 0) {\n collision.top = penetration;\n } else {\n collision.bottom = penetration;\n }\n }\n\n // Calculate intersection area (for collision strength)\n if (distance <= Math.abs(r1 - r2)) {\n // One circle contains the other\n const smallerRadius = Math.min(r1, r2);\n collision.area = Math.PI * smallerRadius * smallerRadius;\n } else {\n // Partial intersection\n const a = (r1 * r1 - r2 * r2 + distanceSquared) / (2 * distance);\n const h = Math.sqrt(r1 * r1 - a * a);\n collision.area = r1 * r1 * Math.acos(a / r1) + r2 * r2 * Math.acos((distance - a) / r2) - distance * h;\n }\n\n // Set overlap point at the collision point\n collision.overlap = {\n x: circleA.x + nx * r1,\n y: circleA.y + ny * r1,\n };\n}\n\nfunction rectToRectCollision(r1: Rectangle, r2: Rectangle, collision: Collision) {\n const dx = r2.x - r1.x;\n const dy = r2.y - r1.y;\n const r1HalfWidth = r1.width / 2;\n const r1HalfHeight = r1.height / 2;\n const r2HalfWidth = r2.width / 2;\n const r2HalfHeight = r2.height / 2;\n\n const r1CenterX = r1.x + r1HalfWidth;\n const r1CenterY = r1.y + r1HalfHeight;\n const r2CenterX = r2.x + r2HalfWidth;\n const r2CenterY = r2.y + r2HalfHeight;\n\n const intersectX = Math.abs(r2CenterX - r1CenterX) - (r1HalfWidth + r2HalfWidth);\n const intersectY = Math.abs(r2CenterY - r1CenterY) - (r1HalfHeight + r2HalfHeight);\n\n // Calculate the coordinates of the intersection rectangle\n collision.overlap.x = Math.max(0, Math.min(r1.x + r1.width, r2.x + r2.width) - Math.max(r1.x, r2.x));\n collision.overlap.y = Math.max(0, Math.min(r1.y + r1.height, r2.y + r2.height) - Math.max(r1.y, r2.y));\n\n collision.area = collision.overlap.x * collision.overlap.y;\n\n if (intersectX < 0 && intersectY < 0) {\n const dx = r2CenterX - r1CenterX;\n const dy = r2CenterY - r1CenterY;\n\n if (dy > 0) {\n collision.bottom = Math.abs(dy);\n } else {\n collision.top = Math.abs(dy);\n }\n if (dx > 0) {\n collision.right = Math.abs(dx);\n } else {\n collision.left = Math.abs(dx);\n }\n } else {\n if (dx > 0) {\n collision.right = Math.abs(dx);\n } else {\n collision.left = Math.abs(dx);\n }\n if (dy > 0) {\n collision.bottom = Math.abs(dy);\n } else {\n collision.top = Math.abs(dy);\n }\n }\n}\n\nexport function approach(current: number, target: number, step: number): number {\n if (current < target) {\n return Math.min(current + step, target);\n } else if (current > target) {\n return Math.max(current - step, target);\n }\n return current;\n}\n","import { IApplication, ICamera, Logger, Signal } from 'dill-pixel';\nimport { Circle, Container, Graphics, Point, Rectangle } from 'pixi.js';\nimport { Collision, EntityType, Side, SpatialHashGridFilter } from './types';\n\nimport { Actor } from './Actor';\nimport { Entity } from './Entity';\nimport { Sensor } from './Sensor';\nimport { SnapPhysicsPlugin } from './SnapPhysicsPlugin';\nimport { Solid } from './Solid';\nimport { SpatialHashGrid } from './SpatialHashGrid';\nimport { Wall } from './Wall';\nimport {\n getCircleToCircleIntersectionArea,\n getRectToCircleIntersectionArea,\n getRectToRectIntersectionArea,\n} from './utils';\n\ntype SystemBoundary = {\n width: number;\n height: number;\n padding: number;\n};\n\ntype SnapPhysicsBoundaryOptions = {\n width: number;\n height: number;\n thickness: number;\n padding: number;\n sides: Side[];\n};\ntype OptionalSnapPhysicsBoundaryOptions = Partial<SnapPhysicsBoundaryOptions>;\ntype RequiredWidthAndHeight = Required<Pick<SnapPhysicsBoundaryOptions, 'width' | 'height'>>;\ntype CustomSnapPhysicsBoundaryOptions = OptionalSnapPhysicsBoundaryOptions & RequiredWidthAndHeight;\n\ntype SnapPhysicsSystemOptions = {\n gravity: number;\n fps: number;\n container: Container;\n debug: boolean;\n boundary: CustomSnapPhysicsBoundaryOptions;\n collisionResolver: (collision: Collision) => boolean;\n useSpatialHashGrid: boolean;\n cellSize: number;\n};\n\nexport class System {\n public static DEFAULT_COLLISION_THRESHOLD: number = 0;\n public static plugin: SnapPhysicsPlugin;\n public static app: IApplication;\n public static container: Container<any>;\n public static grid: SpatialHashGrid | null;\n public static fps: number = 60;\n //\n static debug: boolean = true;\n static typeMap: Map<EntityType, Entity[]> = new Map();\n static actors: Actor[] = [];\n static solids: Solid[] = [];\n static sensors: Sensor[] = [];\n static gravity: number = 10;\n static onCollision: Signal<(collision: Collision) => void> = new Signal<(collision: Collision) => void>();\n static worldBounds: Wall[] = [];\n static boundary: SystemBoundary;\n static camera?: ICamera;\n static collisionThreshold = 8;\n static updateHooks: Set<(deltaTime: number) => void> = new Set();\n static postUpdateHooks: Set<(deltaTime: number) => void> = new Set();\n\n private static _cleaningUp: boolean = false;\n private static gfx: Graphics;\n private static _enabled: boolean = false;\n private static _fixedTimeStep: number = 1000 / System.fps; // Default 60 FPS\n private static _fixedUpdateInterval: any = null;\n\n public static onSystemEnabledChanged: Signal<(enabled: boolean) => void> = new Signal<(enabled: boolean) => void>();\n\n static get enabled() {\n return System._enabled;\n }\n\n static set enabled(value: boolean) {\n if (!System._cleaningUp && value === System._enabled) return;\n System._enabled = value;\n\n // clear the interval if we are disabling\n if (System._enabled) {\n // Start fixed update loop\n System._fixedUpdateInterval = setInterval(() => {\n System.fixedUpdate(System._fixedTimeStep / 1000);\n }, System._fixedTimeStep);\n } else {\n // Stop fixed update loop\n if (System._fixedUpdateInterval) {\n clearInterval(System._fixedUpdateInterval);\n Syst