physics/CollisionSystem.js

import { System } from 'detect-collisions';
import { isVector } from '../math/Utils.js';
import Vector from '../math/Vector.js';

/**
 * wrapper for detect-collisions
 * @see https://sinova.github.io/Collisions/
 * @example
 * import { Box } from "detect-collisions";
 * 
 * let s = new CollisionSystem();
 * 
 * //inserting a new game object
 * let a = {pos: { x: 0, y: 0 }};
 * a.physicsBody = new Box(a.pos, 10, 10);
 * a.physicsBody.gameObj = a;
 * 
 * s.insert(a.physicsBody);
 * 
 * //handle and resolve collisions
 * s.handleCollisions();
 */
class CollisionSystem {
    constructor() {
        this.collisions = new System();
    }

    /**
     * @readonly
     */
    get bodies() {
        return this.collisions.all()
    }

    /**
     * @param {Óbject} body detect-collisions body
     * @returns {Body}
     */
    insert(body) {
        return this.collisions.insert(body);
    }

    /**
     * update the physics bodies and handle collisions
     */
    update() {
        //update collision system
        this.collisions.update();

        //handle collisions
        for (let index = 0; index < 5; index++) {
            this.handleCollisions();
        }
    }

    /**
     * get all overlapping bodies
     * @param {Body} b
     * @param {boolean} [check_layers=false]
     * @return {Body[]} 
     */
    getOverlapping(b, check_layers = false) {
        let out = [];

        let potentials = this.collisions.getPotentials(b);

        for (const body2 of potentials) {
            if (check_layers && b.collide_with && body2.collide_layers) {
                let overlap = b.collide_with.filter(l => body2.collide_layers.includes(l));
                if (!overlap.length) continue;
            }

            if (system.checkCollision(b, body2)) out.push(body2);
        }

        return out;
    }

    /**
     * check collisions in the system and resolve them.
     *
     * This assumes the bodies have a `gameObj` property with a vector `pos`, a vector `v`
     */
    handleCollisions() {
        this.collisions.checkAll((result) => {
            if (result.overlap == 0) return;

            let body = result.a;
            let body2 = result.b;

            if (body.collide_with && body2.collide_layers) {
                let overlap = body.collide_with.filter(l => body2.collide_layers.includes(l));
                if (!overlap.length) return;
            }

            let obj = body.gameObj;
            let obj2 = body2.gameObj;

            if (body2.isStatic) {
                body.setPosition(body.x - result.overlapV.x, body.y - result.overlapV.y);
            } else if (body.isStatic) {
                body2.setPosition(body2.x - result.overlapV.x, body2.y - result.overlapV.y);
            } else {
                //both object are non-static
                let resulting_direction = result.overlapV;

                //split it equally
                let a = 0.5;
                let b = 0.5;

                // if the objects have velocities we can go into more detail
                if ((obj.v instanceof Vector) && (obj2.v instanceof Vector)) {
                    let a_v = obj.v.length();
                    let b_v = obj2.v.length();
                    let v_sum_mag = a_v + b_v; //sum of the velocity length

                    if (v_sum_mag !== 0) {
                        //there was some velocity at the point of collision, split it proportionate
                        //not that the variables are flipped because
                        b = a_v / v_sum_mag;
                        a = b_v / v_sum_mag;
                    }
                }

                //the resulting correction vectors
                let a_result = Vector.multiply(resulting_direction, a);
                let b_result = Vector.multiply(resulting_direction, b);

                body.setPosition(body.x - a_result.x, body.y - a_result.y);
                body2.setPosition(body2.x + b_result.x, body2.y + b_result.y);
                //TODO at this point we could calculate the resulting velocities based on weight and bounce

                //https://gamedevelopment.tutsplus.com/tutorials/how-to-create-a-custom-2d-physics-engine-the-basics-and-impulse-resolution--gamedev-6331
            }


            //set pos for non static objects
            if (!body.isStatic) {
                obj.pos.x = body.x;
                obj.pos.y = body.y;
            }

            if (!body2.isStatic) {
                obj2.pos.x = body2.x;
                obj2.pos.y = body2.y;
            }

            //set velocity to 0 for non static objects in the overlapping axis
            if (!body.isStatic && isVector(obj.v)) {
                if (result.overlapV.y != 0) obj.v.y = 0;
                if (result.overlapV.x != 0) obj.v.x = 0;
            }

            if (!body2.isStatic && isVector(obj2.v)) {
                if (result.overlapV.y != 0) obj2.v.y = 0;
                if (result.overlapV.x != 0) obj2.v.x = 0;
            }
        });
    }

    /**
     * Add a rectangle
     *
     * @param {Rectangle} r
     * @param {Number[]} collide_layers Layers this body is a member of
     * @param {Number[]} collide_with layers this body collides with
     * @return {Body} 
     */
    addRectangle(r, collide_layers = [], collide_with = []) {
        let s = this.collisions.createPolygon(r.x, r.y, [
            [0, 0],
            [r.w, 0],
            [r.w, r.h],
            [0, r.h]
        ]);
        s.isStatic = true;
        s.collide_layers = collide_layers;
        s.collide_with = collide_with;
        return s;
    }

    /**
     * remove all bodies
     */
    cleanup() {
        for (const o in this.bodies) {
            this.bvh.remove(this.bodies[o], false);
        }
    }
}

export default CollisionSystem;