Source: physics/World.js

import ScenePlugin from '../core/ScenePlugin.js';
import Body from './Body.js';

/**
 * Manages multiple physics bodies and colliders
 *
 * @extends module:core~ScenePlugin
 * @memberof module:physics~
 */
class World extends ScenePlugin {

    /**
     * @param {object} options - An options object to setup some properties of this world. Usually passed in by the game config.
     * @param {number} [options.gravityX=0] - The world's gravity along the x-axis.
     * @param {number} [options.gravityY=0] - The world's gravity along the y-axis.
     * @param {number} [options.worldBoundsRestitution=1] - How hard the bodies bounce off the bounds of this world. 0 means they don't bounce. 1 means their velocity gets reversed.
     */
    constructor({
        gravityX = 0,
        gravityY = 0,
        worldBoundsRestitution = 1
    } = {}) {
        super();
        /**
         * The world's gravity along the x-axis.
         *
         * @type {number}
         * @default 0
         */
        this.gravityX = gravityX;
        /**
         * The world's gravity along the y-axis.
         *
         * @type {number}
         * @default 0
         */
        this.gravityY = gravityY;
        /**
         * How hard the bodies bounce off the bounds of this world. 0 means they don't bounce. 1 means their velocity gets reversed.
         *
         * @type {number}
         * @default 1
         */
        this.worldBoundsRestitution = worldBoundsRestitution;
    }

    /**
     * Called by the scene when it starts to do some internal setup for the world.
     */
    init()
    {
        /**
         * Where all the bodies are stored.
         *
         * @type {module:physics~Body[]}
         */
        this.bodies = [];
        /**
         * Where all the colliders are stored.
         *
         * @type {objects[]}
         */
        this.colliders = [];
        this.scene.on('addedchild', (child) => {
            if (child.body) {
                this.addBody(child.body);
            }
        });
        this.scene.on('removedchild', (child) => {
            if (child.body) {
                this.removeBody(child.body);
            }
        });
    }

    /**
     * Adds a body to this world.
     *
     * @param {module:physics~Body} body - The body to add.
     */
    addBody(body)
    {
        this.bodies.push(body);
    }

    /**
     * Remove a body from this world and unlinks the colliders.
     *
     * @param {module:physics~Body} body - The body to remove.
     */
    removeBody(body)
    {
        let colliders = this.getCollidersOfBody(body);
        colliders.forEach((collider) => {
            this.removeCollider(collider);
        });
        this.bodies.splice(this.bodies.indexOf(body), 1);
    }

    /**
     * Returns all the colliders associated with the body.
     *
     * @param {module:physics~Body} body - The body to check.
     * @returns {objects[]}
     */
    getCollidersOfBody(body)
    {
        let colliders = [];
        this.colliders.forEach((collider) => {
            if (collider.a == body || collider.b == body) {
                colliders.push(collider);
            }
        });
        return colliders;
    }

    /**
     * Adds a collider.
     *
     * @param {object} config - A config object setting the properties for the collider.
     * @param {module:gameobjects~GameObject} config.a - The first game object.
     * @param {module:gameobjects~GameObject} config.b - The second game object.
     * @param {function} [config.callback] - A callback to execute when both gameobjects collide.
     * @param {boolean} [config.seperate=true] - Wether or not to seperate the game objects. Set to false to overlap only.
     * @returns {object}
     */
    addCollider({
        a = undefined,
        b = undefined,
        callback = undefined,
        seperate = true
    } = {}){
        if (!a || !b || !a.body || !b.body) {
            return;
        }
        let collider = {a: a.body, b: b.body, callback: callback, seperate: seperate};
        this.colliders.push(collider);
        return collider;
    }

    /**
     * Removes a collider.
     *
     * @param {object} collider - The object that was returned by [addCollider]{@link module:physics~World#addCollider}
     */
    removeCollider(collider)
    {
        this.colliders.splice(this.colliders.indexOf(collider), 1);
    }

    /**
     * Checks wether 2 bodies are overlapping.
     *
     * @param {module:physics~Body} a - The first body.
     * @param {module:physics~Body} b - The second body.
     * @returns {boolean}
     */
    collideRect(a, b)
    {
        if (a.bottom < b.top || a.top > b.bottom || a.right < b.left || a.left > b.right) {
            return false;
        }

        return true;
    }

    /**
     * Seprates two bodies that are colliding and displaces the first body.
     *
     * @param {module:physics~Body} a - The first body.
     * @param {module:physics~Body} b - The second body.
     * @param {number} delta - The time elapsed (in seconds) since the last frame.
     */
    seperateAFromB(a, b, delta)
    {
        // To find the side of entry calculate based on
        // the normalized sides
        let dx = (b.midX - a.midX) / b.halfWidth;
        let dy = (b.midY - a.midY) / b.halfHeight;

        // Calculate the absolute change in x and y
        let absDX = Math.abs(dx);
        let absDY = Math.abs(dy);

        // If the distance between the normalized x and y
        // position is less than a small threshold (.1 in this case)
        // then this object is approaching from a corner
        if (Math.abs(absDX - absDY) < .1) {

            // If a is approaching from the right
            if (dx < 0) {
                a.x = b.right;
                a.blocked.left = true;

            // If a is approaching from the left
            } else {
                a.x = b.left - a.width;
                a.blocked.right = true;
            }

            // If a is approaching from the bottom
            if (dy < 0) {
                a.y = b.bottom;
                a.blocked.top = true;

            // If a is approaching from the top
            } else {
                a.y = b.top - a.height;
                a.blocked.bottom = true;
            }

            // Randomly select a x/y direction to reflect velocity on
            if (Math.random() < .5) {

                a.vx = -a.vx * b.restitution;

                // If the object's velocity is nearing 0, set it to 0
                // treshhold is set to .0004
                if (Math.abs(a.vx) < .0004) {
                    a.vx = 0;
                }

                a.x += b.vx * b.frictionX * delta;

            } else {

                a.vy = -a.vy * b.restitution;
                if (Math.abs(a.vy) < .0004) {
                    a.vy = 0;
                }

                a.y += b.vy * b.frictionY * delta;
            }

        // If a is approaching from the sides
        } else if (absDX > absDY) {

            // If a is approaching from the right
            if (dx < 0) {
                a.x = b.right;
                a.blocked.left = true;
            } else {
            // If a is approaching from the left
                a.x = b.left - a.width;
                a.blocked.right = true;
            }

            a.vx = -a.vx * b.restitution;
            a.x += b.vx * b.frictionX * delta;

            if (Math.abs(a.vx) < .0004) {
                a.vx = 0;
            }

        // If this collision is coming from the top or bottom
        } else {

            // If a is approaching from the bottom
            if (dy < 0) {
                a.y = b.bottom;
                a.blocked.top = true;
            } else {
            // If a is approaching from the top
                a.y = b.top - a.height;
                a.blocked.bottom = true;
            }

            a.vy = -a.vy * b.restitution;
            a.y += b.vy * b.frictionY * delta;

            if (Math.abs(a.vy) < .0004) {
                a.vy = 0;
            }
        }

        a.blocked.none = false;
    }

    /**
     * Seprates two bodies that are colliding and displaces them both.
     *
     * @param {module:physics~Body} a - The first body.
     * @param {module:physics~Body} b - The second body.
     * @param {number} delta - The time elapsed (in seconds) since the last frame.
     */
    seperateBoth(a, b, delta)
    {
        // To find the side of entry calculate based on
        // the normalized sides
        let dx = (b.midX - a.midX) / b.halfWidth;
        let dy = (b.midY - a.midY) / b.halfHeight;

        // Calculate the absolute change in x and y
        let absDX = Math.abs(dx);
        let absDY = Math.abs(dy);

        // If the distance between the normalized x and y
        // position is less than a small threshold (.1 in this case)
        // then this object is approaching from a corner
        if (Math.abs(absDX - absDY) < .1) {

            // If a is approaching from the right
            if (dx < 0) {
                let overlap = (b.right - a.left) / 2;
                a.x += overlap;
                b.x -= overlap;

                a.touching.left = true;
                b.touching.right = true;

            // If a is approaching from the left
            } else {
                let overlap = (a.right - b.left) / 2;
                a.x -= overlap;
                b.x += overlap;

                a.touching.right = true;
                b.touching.left = true;
            }

            // If a is approaching from the bottom
            if (dy < 0) {
                let overlap = (b.bottom - a.top) / 2;
                a.y += overlap;
                b.y -= overlap;

                a.touching.top = true;
                b.touching.bottom = true;

            // If a is approaching from the top
            } else {
                let overlap = (a.bottom - b.top) / 2;
                a.y -= overlap;
                b.y += overlap;

                a.touching.bottom = true;
                b.touching.top = true;
            }

        // If a is approaching from the sides
        } else if (absDX > absDY) {

            // If a is approaching from the right
            if (dx < 0) {
                let overlap = (b.right - a.left) / 2;
                a.x += overlap;
                b.x -= overlap;

                a.touching.left = true;
                b.touching.right = true;

                if (this.gravityX > 0) {
                    b.vy = 0;
                    b.y += a.vy * a.frictionY * delta;
                } else if (this.gravityX < 0) {
                    a.vy = 0;
                    a.y += b.vy * b.frictionY * delta;
                }
            } else {
            // If a is approaching from the left
                let overlap = (a.right - b.left) / 2;
                a.x -= overlap;
                b.x += overlap;

                a.touching.right = true;
                b.touching.left = true;

                if (this.gravityX > 0) {
                    a.vy = 0;
                    a.y += b.vy * b.frictionY * delta;
                } else if (this.gravityX < 0) {
                    b.vy = 0;
                    b.y += a.vy * a.frictionY * delta;
                }
            }

        // If this collision is coming from the top or bottom
        } else {

            // If a is approaching from the bottom
            if (dy < 0) {
                let overlap = (b.bottom - a.top) / 2;
                a.y += overlap;
                b.y -= overlap;

                a.touching.top = true;
                b.touching.bottom = true;

                if (this.gravityY > 0) {
                    b.vy = 0;
                    b.x += a.vx * a.frictionX * delta;
                } else if (this.gravityY < 0) {
                    a.vy = 0;
                    a.x += b.vx * b.frictionX * delta;
                }

            } else {
            // If a is approaching from the top
                let overlap = (a.bottom - b.top) / 2;
                a.y -= overlap;
                b.y += overlap;

                a.touching.bottom = true;
                b.touching.top = true;

                if (this.gravityY > 0) {
                    a.vy = 0;
                    a.x += b.vx * b.frictionX * delta;
                } else if (this.gravityY < 0) {
                    b.vy = 0;
                    b.x += a.vx * a.frictionX * delta;
                }
            }
        }
    }

    /**
     * Called on each frame if the scene is active. Before all children are rendered. Updates all the bodies and processes collisions.
     *
     * @param {number} time - The total time (in milliesconds) since the start of the game.
     * @param {number} delta - The time elapsed (in milliseconds) since the last frame.
     */
    update(time, delta)
    {
        delta = delta / 1000; // convert delta to seconds, so we can define velocity in pixels per second

        let gx = this.gravityX * delta;
        let gy = this.gravityY * delta;

        this.bodies.forEach((body) => {

            if (body.allowGravity) {
                body.vx += gx;
                body.vy += gy;
            }

            body.x += body.vx * delta;
            body.y += body.vy * delta;

            // reset collision flags
            body.blocked.none = true;
            body.blocked.top = false;
            body.blocked.right = false;
            body.blocked.bottom = false;
            body.blocked.left = false;

            body.touching.none = true;
            body.touching.top = false;
            body.touching.right = false;
            body.touching.bottom = false;
            body.touching.left = false;
        });

        this.colliders.forEach((collider) => {
            if (this.collideRect(collider.a, collider.b)) {

                if (collider.seperate) {
                    if (!collider.a.immovable && collider.b.immovable) {
                        this.seperateAFromB(collider.a, collider.b, delta);
                    } else if (collider.a.immovable && !collider.b.immovable) {
                        this.seperateAFromB(collider.b, collider.a, delta);
                    } else if (!collider.a.immovable && !collider.b.immovable) {
                        this.seperateBoth(collider.a, collider.b, delta);
                    }
                }
                if (collider.callback) {
                    collider.callback(collider.a, collider.b);
                }
            }
        });

        if (this.bounds) {
            this.bodies.forEach((body) => {
                if (body.collideWorldBounds) {
                    if (body.left < this.bounds.x) {
                        body.x = this.bounds.x;
                        body.vx = -body.vx * this.worldBoundsRestitution;
                        body.blocked.left = true;
                    }
                    if (body.top < this.bounds.y) {
                        body.y = this.bounds.y;
                        body.vy = -body.vy * this.worldBoundsRestitution;
                        body.blocked.top = true;
                    }
                    if (body.right > this.bounds.x + this.bounds.width) {
                        body.x = this.bounds.x + this.bounds.width - body.width;
                        body.vx = -body.vx * this.worldBoundsRestitution;
                        body.blocked.right = true;
                    }
                    if (body.bottom > this.bounds.y + this.bounds.height) {
                        body.y = this.bounds.y + this.bounds.height - body.height;
                        body.vy = -body.vy * this.worldBoundsRestitution;
                        body.blocked.bottom = true;
                    }
                }
            });
        }
    }

    /**
     * Helper to set the bounds of this world.
     *
     * @param {number} x - The left x position of the area relative to the scene's origin.
     * @param {number} y - The top y position of the area relative to the scene's origin.
     * @param {number} width - The width of the area.
     * @param {number} height - The height of the area.
     */
    setBounds(x, y, width, height)
    {
        this.bounds = {x, y, width, height};
    }

    /**
     * Automatically called when the scen shuts down. Removes the references to the bodies and the colliders.
     */
    shutdown()
    {
        super.shutdown();
        this.bodies = [];
        this.colliders = [];
    }
}
export default World;