CollisionShape.js

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;