import AABB from './AABB.js';
import Vector from './Vector.js';
import recycle from 'recycle';
const
circleRectCollision = function (circle, rect) {
const
rectAabb = rect.aABB,
hh = rectAabb.halfHeight,
hw = rectAabb.halfWidth,
shapeDistanceX = Math.abs(circle.x - rect.x),
shapeDistanceY = Math.abs(circle.y - rect.y),
radius = circle.radius;
/* This checks the following in order:
- Is the x or y distance between shapes less than half the width or height respectively of the rectangle? If so, we know they're colliding.
- Is the x or y distance between the shapes greater than the half width/height plus the radius of the circle? Then we know they're not colliding.
- Otherwise, we check the distance between a corner of the rectangle and the center of the circle. If that distance is less than the radius of the circle, we know that there is a collision; otherwise there is not.
*/
return (shapeDistanceX < hw) || (shapeDistanceY < hh) || ((shapeDistanceX < (hw + radius)) && (shapeDistanceY < (hh + radius)) && ((((shapeDistanceX - hw) ** 2) + ((shapeDistanceY - hh) ** 2)) < radius ** 2));
},
collidesCircle = function (shape) {
return this.aABB.collides(shape.aABB) && (
((shape.type === 'rectangle') && circleRectCollision(this, shape)) ||
((shape.type === 'circle') && ((((this.x - shape.x) ** 2) + ((this.y - shape.y) ** 2)) <= ((this.radius + shape.radius) ** 2)))
);
},
collidesDefault = function () {
return false;
},
collidesRectangle = function (shape) {
return this.aABB.collides(shape.aABB) && (
(shape.type === 'rectangle') ||
((shape.type === 'circle') && circleRectCollision(shape, this))
);
},
/**
* This class defines a collision shape, which defines the 'space' an entity occupies in the collision system. Currently only rectangle and circle shapes can be created. Collision shapes include an axis-aligned bounding box (AABB) that tightly wraps the shape. The AABB is used for initial collision checks.
*
* @memberof platypus
* @class CollisionShape
* @param owner {platypus.Entity} The entity that uses this shape.
* @param definition {Object} This is an object of key/value pairs describing the shape.
* @param definition.x {number} The x position of the shape. The x is always located in the center of the object.
* @param definition.y {number} The y position of the shape. The y is always located in the center of the object.
* @param [definition.type="rectangle"] {String} The type of shape this is. Currently this can be either "rectangle" or "circle".
* @param [definition.jumpThrough] {Object} x/y unit determining jump-through face.
* @param [definition.width] {number} The width of the shape if it's a rectangle.
* @param [definition.height] {number} The height of the shape if it's a rectangle.
* @param [definition.radius] {number} The radius of the shape if it's a circle.
* @param [definition.offsetX] {number} The x offset of the collision shape from the owner entity's location.
* @param [definition.offsetY] {number} The y offset of the collision shape from the owner entity's location.
* @param [definition.regX] {number} The registration x of the collision shape with the owner entity's location if offsetX is not provided.
* @param [definition.regY] {number} The registration y of the collision shape with the owner entity's location if offsetX is not provided.
* @param collisionType {String} A string describing the collision type of this shape.
*/
CollisionShape = function (owner, definition, collisionType) {
const
{x, y} = owner ?? definition,
{offsetX, offsetY, radius = 0, type = 'rectangle'} = definition,
diameter = radius * 2,
width = type === 'circle' ? diameter : definition.width ?? diameter,
height = type === 'circle' ? diameter : definition.height ?? diameter,
regX = definition.regX ?? (width / 2),
regY = definition.regY ?? (height / 2);
// If this shape is recycled, the vectors will already be in place.
if (!this.initialized) {
this.initialized = true;
Vector.assign(this, 'offset', 'offsetX', 'offsetY');
Vector.assign(this, 'position', 'x', 'y');
Vector.assign(this, 'size', 'width', 'height');
this.aABB = AABB.setUp();
}
this.owner = owner;
this.collisionType = collisionType;
this.jumpThrough = definition.jumpThrough ? Vector.setUp(definition.jumpThrough.x, definition.jumpThrough.y) : null;
this.type = type;
this.subType = '';
/**
* Determines whether shapes collide.
*
* @method platypus.CollisionShape#collides
* @param shape {platypus.CollisionShape} The shape to check against for collision.
* @return {Boolean} Whether the shapes collide.
*/
if (type === 'circle') {
this.collides = collidesCircle;
} else if (type === 'rectangle') {
this.collides = collidesRectangle;
} else {
this.collides = collidesDefault;
}
this.size.setXYZ(width, height);
this.radius = radius;
this.offset.setXYZ(offsetX ?? ((width / 2) - regX), offsetY ?? ((height / 2) - regY));
this.position.setXYZ(x, y).add(this.offset);
this.aABB.setAll(this.x, this.y, width, height);
},
proto = CollisionShape.prototype;
/**
* Updates the shape to match another shape.
*
* @method platypus.CollisionShape#updateAll
* @param updateAll {platypus.CollisionShape} The shape to copy into this one.
*/
proto.updateAll = function (shape) {
this.owner = shape.owner;
this.collisionType = shape.collisionType;
this.type = shape.type;
this.subType = shape.subType;
this.offset.x = shape.offset.x;
this.offset.y = shape.offset.y;
this.position.x = shape.position.x;
this.position.y = shape.position.y;
this.size.x = shape.size.x;
this.size.y = shape.size.y;
this.radius = shape.radius;
this.aABB.setAll(this.x, this.y, this.width, this.height);
if (this.type === 'circle') {
this.collides = collidesCircle;
} else if (this.type === 'rectangle') {
this.collides = collidesRectangle;
} else {
this.collides = collidesDefault;
}
};
/**
* Updates the location of the shape and AABB. The position you send should be that of the owner, the offset of the shape is added inside the function.
*
* @method platypus.CollisionShape#update
* @param ownerX {number} The x position of the owner.
* @param ownerY {number} The y position of the owner.
*/
proto.update = function (ownerX, ownerY) {
const
x = ownerX + this.offsetX,
y = ownerY + this.offsetY;
this.position.setXYZ(x, y);
this.aABB.move(x, y);
};
/**
* Move the shape's x position.
*
* @method platypus.CollisionShape#moveX
* @param x {number} The x position to which the shape should be moved.
*/
proto.moveX = function (x) {
this.x = x;
this.aABB.moveX(x);
};
/**
* Move the shape's y position.
*
* @method platypus.CollisionShape#moveY
* @param y {number} The y position to which the shape should be moved.
*/
proto.moveY = function (y) {
this.y = y;
this.aABB.moveY(y);
};
/**
* Move the shape's x and y position.
*
* @method platypus.CollisionShape#moveXY
* @param x {number} The x position to which the shape should be moved.
* @param y {number} The y position to which the shape should be moved.
*/
proto.moveXY = function (x, y) {
this.x = x;
this.y = y;
this.aABB.move(x, y);
};
/**
* Returns the axis-aligned bounding box of the shape.
*
* @method platypus.CollisionShape#getAABB
* @return {platypus.AABB} The AABB of the shape.
*/
proto.getAABB = function () {
return this.aABB;
};
/**
* Set the shape's position as if the entity's x position is in a certain location.
*
* @method platypus.CollisionShape#setXWithEntityX
* @param entityX {number} The x position of the entity.
*/
proto.setXWithEntityX = function (entityX) {
this.x = entityX + this.offsetX;
this.aABB.moveX(this.x);
};
/**
* Set the shape's position as if the entity's y position is in a certain location.
*
* @method platypus.CollisionShape#setYWithEntityY
* @param entityY {number} The y position of the entity.
*/
proto.setYWithEntityY = function (entityY) {
this.y = entityY + this.offsetY;
this.aABB.moveY(this.y);
};
/**
* Transform the shape using a matrix transformation.
*
* @method platypus.CollisionShape#multiply
* @param matrix {Array} A matrix used to transform the shape.
*/
proto.multiply = function (m) {
const
pos = this.position,
own = this.owner.position;
pos.subtractVector(own);
pos.multiply(m);
this.offset.multiply(m);
this.size.multiply(m);
pos.addVector(own);
this.width = Math.abs(this.width);
this.height = Math.abs(this.height);
this.aABB.setAll(this.x, this.y, this.width, this.height);
};
/**
* Expresses whether this shape contains the given point.
*
* @method platypus.CollisionShape#containsPoint
* @param x {number} The x-axis value.
* @param y {number} The y-axis value.
* @return {boolean} Returns `true` if this shape contains the point.
*/
proto.containsPoint = function (x, y) {
return this.aABB.containsPoint(x, y) && (
(this.type === 'rectangle') ||
((this.type === 'circle') && ((((this.x - x) ** 2) + ((this.y - y) ** 2)) <= this.radius ** 2))
);
};
/**
* Returns a JSON object describing the collision shape.
*
* @method platypus.CollisionShape#toJSON
* @return {Object} Returns a JSON definition that can be used to recreate the collision shape.
**/
proto.toJSON = function () {
const
json = {
type: this.type
},
width = this.size.width,
height = this.size.height;
if (width / 2 !== this.regX) {
json.regX = this.regX;
}
if (height / 2 !== this.regY) {
json.regY = this.regY;
}
if (this.offset.x !== ((width / 2) - this.regX)) {
json.offsetX = this.offset.x;
}
if (this.offset.y !== ((height / 2) - this.regY)) {
json.offsetY = this.offset.y;
}
if (this.type === 'circle') {
json.radius = this.radius;
} else {
json.width = width;
json.height = height;
}
return json;
};
/**
* Returns an CollisionShape from cache or creates a new one if none are available.
*
* @method platypus.CollisionShape.setUp
* @return {platypus.CollisionShape} The instantiated CollisionShape.
*/
/**
* Returns a CollisionShape back to the cache.
*
* @method platypus.CollisionShape.recycle
* @param {platypus.CollisionShape} The CollisionShape to be recycled.
*/
/**
* Relinquishes properties of the CollisionShape and recycles it.
*
* @method platypus.CollisionShape#recycle
*/
recycle.add(CollisionShape, 'CollisionShape', CollisionShape, function () {
if (this.jumpThrough) {
this.jumpThrough.recycle();
}
this.jumpThrough = null;
}, true);
export default CollisionShape;