Home Reference Source

src/helper/collision-checker.js

import { Model } from './../model';
import { Collection } from './collection';

export class CollisionChecker extends Collection {
    /**
     * @param {Model} model
     * @param {Array<Model>|Model} platforms
     * @param {Callable} [event=function()]
     */
    add(model, platforms, event = () => {}) {
        if (!Array.isArray(platforms)) {
            platforms = [platforms];
        }

        this.push({ model, platforms, event });
    }

    /**
     * @param {number} dt
     */
    check(dt) {
        let model, platform;
        this.items.forEach(pair => {
            model = Object.create(pair.model);
            model.step(dt);
            platform = CollisionChecker.hasCollisions(model, pair.platforms);

            if (!!platform) {
                pair.event(pair.model, platform);
            }
        });
    }

    /**
     * @param {Callable} callback
     */
    remove(callback) {
        const index = this.items.findIndex(item => callback(item.model));

        if (index > -1) {
            return this.items.splice(index, 1);
        }

        return false;
    }

    /**
     * Check if there are any collisions between model and array of models
     * @param {Model} model
     * @param {Array<Model>|Model} models
     * @return {boolean}
     */
    static hasCollisions(model, models) {
        if (!model instanceof Model) {
            throw new Error(`Parameter model has to be an instance of Model, it's an instance of ${typeof model} instead.`);
        }

        if (!Array.isArray(models)) {
            models = [models];
        }

        let m;
        for (let i = 0; i < models.length; i++) {
            m = models[i];
            if (!model instanceof Model) {
                throw new Error(`Parameter from models[${i}] has to be an instance of Model, it's an instance of ${typeof m} instead.`);
            }

            if (
                !!model.hitbox.radius &&
                !!m.hitbox.radius &&
                CollisionChecker.hasCollisionBetweenCircleAndCircle(model.hitbox, m.hitbox)
            ) {
                return m;
            } else if (
                !!model.hitbox.radius &&
                !m.hitbox.radius &&
                CollisionChecker.hasCollisionBetweenCircleAndRectangle(model.hitbox, m.hitbox)
            ) {
                return m;
            } else if (
                !model.hitbox.radius &&
                !!m.hitbox.radius &&
                CollisionChecker.hasCollisionBetweenCircleAndRectangle(m.hitbox, model.hitbox)
            ) {
                return m;
            } else if (CollisionChecker.hasCollisionBetweenRectangleAndRectangle(model.hitbox, m.hitbox)) {
                return m;
            }
        }

        return false;
    }

    /**
     * Check collision between circle and another circle
     * @param {Object} circleA
     * @param {number} circleA.x
     * @param {number} circleA.y
     * @param {number} circleA.radius
     * @param {Object} circleB
     * @param {number} circleB.x
     * @param {number} circleB.y
     * @param {number} circleB.radius
     *
     * @return {boolean}
     */
    static hasCollisionBetweenCircleAndCircle(circleA, circleB) {
        const dx = circleA.x - circleB.x;
        const dy = circleA.y - circleB.y;
        const distance = Math.sqrt(dx * dx + dy * dy);

        if (distance < circleA.radius + circleB.radius) {
            return true;
        }

        return false;
    }

    /**
     * Check collision between circle and rectangle
     * @param {Object} circle
     * @param {number} circle.x
     * @param {number} circle.y
     * @param {number} circle.radius
     * @param {Object} rectangle
     * @param {number} rectangle.x
     * @param {number} rectangle.y
     * @param {number} rectangle.width
     * @param {number} rectangle.height
     *
     * @return {boolean}
     */
    static hasCollisionBetweenCircleAndRectangle(circle, rectangle) {
        const distX = Math.abs(circle.x - rectangle.x - rectangle.width / 2);
        const distY = Math.abs(circle.y - rectangle.y - rectangle.height / 2);

        if (distX > rectangle.width / 2 + circle.radius) {
            return false;
        }

        if (distY > rectangle.height / 2 + circle.radius) {
            return false;
        }

        if (distX <= rectangle.width / 2) {
            return true;
        }

        if (distY <= rectangle.height / 2) {
            return true;
        }

        var dx = distX - rectangle.width / 2;
        var dy = distY - rectangle.height / 2;

        return dx * dx + dy * dy <= (circle.radius * circle.radius);
    }

    /**
     * Check collision between rectangle and rectangle
     * @param {Object} rectangleA
     * @param {number} rectangleA.x
     * @param {number} rectangleA.y
     * @param {number} rectangleA.width
     * @param {number} rectangleA.height
     * @param {Object} rectangleB
     * @param {number} rectangleB.x
     * @param {number} rectangleB.y
     * @param {number} rectangleB.width
     * @param {number} rectangleB.height
     *
     * @return {boolean}
     */
    static hasCollisionBetweenRectangleAndRectangle(rectangleA, rectangleB) {
        if (rectangleA.x < rectangleB.x + rectangleB.width &&
            rectangleA.x + rectangleA.width > rectangleB.x &&
            rectangleA.y < rectangleB.y + rectangleB.height &&
            rectangleA.y + rectangleA.height > rectangleB.y
        ) {
            return true;
        }

        return false;
    }
}