Source: components/physics.js

/*
 * File: physics.js
 *  
 * core of the physics component
 * 
 */
"use strict";  // Operate in Strict mode such that variables must be declared before used!


import CollisionInfo from "../rigid_shapes/collision_info.js";


/**
 * Core of the physics component supporting motion and collision resolution
 * <p>Found in Chapter 9, page 558 of the textbook </p>
 * Examples:
 * 
 * {@link https://mylesacd.github.io/build-your-own-2d-game-engine-2e-doc/BookSourceCode/chapter9/9.5.rigid_shape_movements/index.html 9.5 Rigid Shape Movements}, 
 * {@link https://mylesacd.github.io/build-your-own-2d-game-engine-2e-doc/BookSourceCode/chapter9/9.8.collision_angular_resolution/index.html 9.8 Collision Angular Resolution},
 * {@link https://mylesacd.github.io/build-your-own-2d-game-engine-2e-doc/BookSourceCode/chapter9/9.9.physics_presets/index.html 9.9 Physics Presets}
 * @module physics
 */


let mSystemAcceleration = [0, -20];        // system-wide default acceleration
let mPosCorrectionRate = 0.8;               // percentage of separation to project objects
let mRelaxationCount = 15;                  // number of relaxation iterations

let mCorrectPosition = true;
let mHasMotion = true;

// getters and setters
/**
 * Returns the acceleration of the system in world coordinates
 * @export physics
 * @returns {vec2} the [X,Y] acceleration vector
 */
function getSystemAcceleration() { return vec2.clone(mSystemAcceleration); }
/**
 * Sets the acceleration of the sytsem in world coordinates
 * @export physics
 * @param {float} x - the acceleration in the X direction
 * @param {flaot} y - the acceleration in the Y direction
 */
function setSystemAcceleration(x, y) {
    mSystemAcceleration[0] = x;
    mSystemAcceleration[1] = y;
}
/**
 * Returns whether the RigidShapes are in correct positions
 * @export physics
 * @returns {boolean} true if every RigidShape is in the correct position
 */
function getPositionalCorrection() { return mCorrectPosition; }
/**
 * Toggles the state of the correction flag
 * @export physics
 * @method
 */
function togglePositionalCorrection() { mCorrectPosition = !mCorrectPosition; }

/**
 * Returns whether there is motion
 * @export physics
 * @returns {boolean} true if there is motion
 */
function getHasMotion() { return mHasMotion; }
/**
 * Toggles the state of the has motion flag
 * @export physics
 */
function toggleHasMotion() { mHasMotion = !mHasMotion; }

/**
 * Returns the relaxation count
 * @export physics
 * @returns {integer} mRelaxationCount - the number of relaxation cycles
 */
function getRelaxationCount() { return mRelaxationCount; }

/**
 * Add a value to the relaxation count
 * @export physics
 * @param {integer} dc - the number of relaxation cycles to add
 */
function incRelaxationCount(dc) { mRelaxationCount += dc; }

let mS1toS2 = [0, 0];
let mCInfo = new CollisionInfo();

function positionalCorrection(s1, s2, collisionInfo) {
    if (!mCorrectPosition)
        return;

    let s1InvMass = s1.getInvMass();
    let s2InvMass = s2.getInvMass();

    let num = collisionInfo.getDepth() / (s1InvMass + s2InvMass) * mPosCorrectionRate;
    let correctionAmount = [0, 0];
    vec2.scale(correctionAmount, collisionInfo.getNormal(), num);
    s1.adjustPositionBy(correctionAmount, -s1InvMass);
    s2.adjustPositionBy(correctionAmount, s2InvMass);
}

function resolveCollision(b, a, collisionInfo) {
    let n = collisionInfo.getNormal();

    // Step A: Compute relative velocity
    let va = a.getVelocity();
    let vb = b.getVelocity();

    // Step A1: Compute the intersection position p
    // the direction of collisionInfo is always from b to a
    // but the Mass is inverse, so start scale with a and end scale with b
    let invSum = 1 / (b.getInvMass() + a.getInvMass());
    let start = [0, 0], end = [0, 0], p = [0, 0];
    vec2.scale(start, collisionInfo.getStart(), a.getInvMass() * invSum);
    vec2.scale(end, collisionInfo.getEnd(), b.getInvMass() * invSum);
    vec2.add(p, start, end);

    // Step A2: Compute relative velocity with rotation components 
    //    Vectors from center to P
    //    r is vector from center of object to collision point
    let rBP = [0, 0], rAP = [0, 0];
    vec2.subtract(rAP, p, a.getCenter());
    vec2.subtract(rBP, p, b.getCenter());   

    // newV = V + mAngularVelocity cross R
    let vAP1 = [-1 * a.getAngularVelocity() * rAP[1], a.getAngularVelocity() * rAP[0]];
    vec2.add(vAP1, vAP1, va);

    let vBP1 = [-1 * b.getAngularVelocity() * rBP[1], b.getAngularVelocity() * rBP[0]];
    vec2.add(vBP1, vBP1, vb);

    let relativeVelocity = [0, 0];
    vec2.subtract(relativeVelocity, vAP1, vBP1);

    // Step B: Determine relative velocity in normal direction
    let rVelocityInNormal = vec2.dot(relativeVelocity, n);

    // if objects moving apart ignore
    if (rVelocityInNormal > 0) {
        return;
    }

    // Step C: Compute collision tangent direction
    let tangent = [0, 0];
    vec2.scale(tangent, n, rVelocityInNormal);
    vec2.subtract(tangent, tangent, relativeVelocity);
    vec2.normalize(tangent, tangent);
    // Relative velocity in tangent direction
    let rVelocityInTangent = vec2.dot(relativeVelocity, tangent);

    // Step D: Determine the effective coefficients    
    let newRestituion = (a.getRestitution() + b.getRestitution()) * 0.5;
    let newFriction = 1 - ((a.getFriction() + b.getFriction()) * 0.5);

    // Step E: Impulse in the normal and tangent directions
    // R cross N
    let rBPcrossN = rBP[0] * n[1] - rBP[1] * n[0]; // rBP cross n
    let rAPcrossN = rAP[0] * n[1] - rAP[1] * n[0]; // rAP cross n
    // Calc impulse scalar
    // the formula of jN can be found in http://www.myphysicslab.com/collision.html
    let jN = -(1 + newRestituion) * rVelocityInNormal;
    jN = jN / (b.getInvMass() + a.getInvMass() +
        rBPcrossN * rBPcrossN * b.getInertia() +
        rAPcrossN * rAPcrossN * a.getInertia());

    let rBPcrossT = rBP[0] * tangent[1] - rBP[1] * tangent[0]; // rBP.cross(tangent);
    let rAPcrossT = rAP[0] * tangent[1] - rAP[1] * tangent[0]; // rAP.cross(tangent);
    let jT = (newFriction - 1) * rVelocityInTangent;
    jT = jT / (b.getInvMass() + a.getInvMass() +
        rBPcrossT * rBPcrossT * b.getInertia() +
        rAPcrossT * rAPcrossT * a.getInertia());

    // Step F: Update linear and angular velocities
    vec2.scaleAndAdd(va, va, n, (jN * a.getInvMass()));
    vec2.scaleAndAdd(va, va, tangent, (jT * a.getInvMass()));
    a.setAngularVelocityDelta((rAPcrossN * jN * a.getInertia() + rAPcrossT * jT * a.getInertia()));

    vec2.scaleAndAdd(vb, vb, n, -(jN * b.getInvMass()));
    vec2.scaleAndAdd(vb, vb, tangent, -(jT * b.getInvMass()));
    b.setAngularVelocityDelta(-(rBPcrossN * jN * b.getInertia() + rBPcrossT * jT * b.getInertia()));    
}

// collide two rigid shapes
/**
 * Collide two rigid shapes
 * @export physics
 * @param {RigidShape} s1 - the first RigidShape involved
 * @param {RigidShape} s2 - the second RigidShape involved
 * @param {CollisionInfo[]} infoSet - list of CollisionInfo objects to append to
 * @returns {boolean} true if a collision occured
 */
function collideShape(s1, s2, infoSet = null) {
    let hasCollision = false;
    if ((s1 !== s2) && ((s1.getInvMass() !== 0) || (s2.getInvMass() !== 0))) {
        if (s1.boundTest(s2)) {
            hasCollision = s1.collisionTest(s2, mCInfo);
            if (hasCollision) {
                // make sure mCInfo is always from s1 towards s2
                vec2.subtract(mS1toS2, s2.getCenter(), s1.getCenter());
                if (vec2.dot(mS1toS2, mCInfo.getNormal()) < 0)
                    mCInfo.changeDir();
                positionalCorrection(s1, s2, mCInfo);
                resolveCollision(s1, s2, mCInfo);
                // for showing off collision mCInfo!
                if (infoSet !== null) {
                    infoSet.push(mCInfo);
                    mCInfo = new CollisionInfo();
                }
            }
        }
    }
    return hasCollision;
}

// collide a given GameObject with a GameObjectSet
/**
 * Collide a given GameObject with an entire GameObjectSet
 * @export physics
 * @param {GameObject} obj - the specific GameObject to test collision with
 * @param {GameObjectSet} set - the GameObjectSet to test collision against
 * @param {CollisionInfo[]} infoSet - list of CollisionInfo objects to append to
 * @returns {boolean} true if a collision occured
 */
function processObjToSet(obj, set, infoSet = null) {
    let j = 0, r = 0;
    let hasCollision = false;
    let s1 = obj.getRigidBody();
    for (r = 0; r < mRelaxationCount; r++) {
        for (j = 0; j < set.size(); j++) {
            let s2 = set.getObjectAt(j).getRigidBody();
            hasCollision = collideShape(s1, s2, infoSet) || hasCollision;
        }
    }
    return hasCollision;
}

// collide between all objects in two different GameObjectSets
/**
 * Collide every GameObject within a GameObjectSet with an entire other GameObjectSet
 * @export physics
 * @param {GameObjectSet} set1 - the first GameObjectSet to test collision with
 * @param {GameObjectSet} set2 - the second GameObjectSet to test collision against
 * @param {CollisionInfo[]} infoSet - list of CollisionInfo objects to append to
 * @returns {boolean} true if a collision occured
 */
function processSetToSet(set1, set2, infoSet = null) {
    let i = 0, j = 0, r = 0;
    let hasCollision = false;
    for (r = 0; r < mRelaxationCount; r++) {
        for (i = 0; i < set1.size(); i++) {
            let s1 = set1.getObjectAt(i).getRigidBody();
            for (j = 0; j < set2.size(); j++) {
                let s2 = set2.getObjectAt(j).getRigidBody();
                hasCollision = collideShape(s1, s2, infoSet) || hasCollision;
                            }
                        }
                    }
    return hasCollision;
                }

// collide all objects in the GameObjectSet with themselves
/**
 * Collide every GameObject within the GameObjectSet with each other
 * @export physics
 * @param {GameObjectSet} set - the GameObjectSet to test collision with and against
 * @param {CollisionInfo[]} infoSet - list of CollisionInfo objects to append to
 * @returns {boolean} true if a collision occured
 */
function processSet(set, infoSet = null) {
    let i = 0, j = 0, r = 0;
    let hasCollision = false;
    for (r = 0; r < mRelaxationCount; r++) {
        for (i = 0; i < set.size(); i++) {
            let s1 = set.getObjectAt(i).getRigidBody();
            for (j = i + 1; j < set.size(); j++) {
                let s2 = set.getObjectAt(j).getRigidBody();
                hasCollision = collideShape(s1, s2, infoSet) || hasCollision;
            }
        }
    }
    return hasCollision;
}

export {
    // Physics system attributes
    getSystemAcceleration, setSystemAcceleration,


    togglePositionalCorrection,
    getPositionalCorrection,

    getRelaxationCount,
    incRelaxationCount,
    
    getHasMotion,
    toggleHasMotion,

    // collide and response two shapes 
    collideShape,

    // Collide
    processSet, processObjToSet, processSetToSet
}