CollisionShape.js

import AABB from './AABB.js';
import Vector from './Vector.js';
import config from 'config';
import recycle from 'recycle';

export default (function () {
    var circleRectCollision = function (circle, rect) {
            var rectAabb         = rect.aABB,
                hh = rectAabb.halfHeight,
                hw = rectAabb.halfWidth,
                abs = Math.abs,
                pow = Math.pow,
                shapeDistanceX = abs(circle.x - rect.x),
                shapeDistanceY = 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)) && ((pow((shapeDistanceX - hw), 2) + pow((shapeDistanceY - hh), 2)) < pow(radius, 2)));
        },
        collidesCircle = function (shape) {
            var pow = Math.pow;
            
            return this.aABB.collides(shape.aABB) && (
                ((shape.type === 'rectangle') && circleRectCollision(this, shape)) ||
                ((shape.type === 'circle')    && ((pow((this.x - shape.x), 2) + pow((this.y - shape.y), 2)) <= pow((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.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) {
            var regX = definition.regX,
                regY = definition.regY,
                width = definition.width || definition.radius * 2 || 0,
                height = definition.height || definition.radius * 2 || 0,
                radius = definition.radius || 0,
                type = definition.type || 'rectangle';

            // 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.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') {
                width = height = radius * 2;
                this.collides = collidesCircle;
            } else if (type === 'rectangle') {
                this.collides = collidesRectangle;
            } else {
                this.collides = collidesDefault;
            }
            this.size.setXYZ(width, height);
            this.radius = radius;

            if (typeof regX !== 'number') {
                regX = width / 2;
            }
            if (typeof regY !== 'number') {
                regY = height / 2;
            }
            this.offset.setXYZ(definition.offsetX || ((width  / 2) - regX), definition.offsetY || ((height / 2) - regY));

            if (owner) {
                this.position.setXYZ(owner.x, owner.y).add(this.offset);
            } else {
                this.position.setXYZ(definition.x, definition.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) {
        var 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) {
        var 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) {
        var pow = Math.pow;

        return this.aABB.containsPoint(x, y) && (
            (this.type === 'rectangle') ||
            ((this.type === 'circle') && ((pow((this.x - x), 2) + pow((this.y - y), 2)) <= pow(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 () {
        var json = {},
            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;
        }
        json.type = this.type;

        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, null, true, config.dev);
    
    return CollisionShape;
}());