Source: components/physic/PhysicComponent.js

/*global FM*/
/**
 * Parent component of physics components to add to a game object for collisions
 * and physics behavior.
 * @class FM.PhysicComponent
 * @extends FM.Component
 * @param {int} pWidth Width of the collider.
 * @param {int} pHeight Height of the collider.
 * @param {FM.GameObject} pOwner The object that owns this component.
 * @constructor
 * @author Simon Chauvin
 */
FM.PhysicComponent = function (pWidth, pHeight, pOwner) {
    "use strict";
    //Calling the constructor of the FM.Component
    FM.Component.call(this, FM.ComponentTypes.PHYSIC, pOwner);
    /**
     * World of the game.
     * @type FM.World
     * @private
     */
    this.world = FM.Game.getCurrentState().getWorld();
    /**
    * Quad tree containing all game objects with a physic component.
    * @type FM.QuadTree
    * @private
     */
    this.quad = FM.Game.getCurrentState().getQuad();
    /**
     * Represent the mass of the physic game object, 0 means infinite mass.
     * @type int
     * @private
     */
    this.mass = 1;
    /**
     * Represent the inverse mass.
     * @type float
     * @private
     */
    this.invMass = 1 / this.mass;
    /**
     * Array storing the types of game objects that can collide with this one.
     * @type Array
     * @private
     */
    this.collidesWith = [];
    /**
     * Store the collisions that this object has.
     * @type Array
     * @private
     */
    this.collisions = [];
    /**
     * Store the types of tile map that this object collides with.
     * @type Array
     * @private
     */
    this.tilesCollisions = [];
    /**
     * Spatial component reference.
     * @type FM.SpatialComponent
     * @private
     */
    this.spatial = pOwner.components[FM.ComponentTypes.SPATIAL];
    /**
     * Offset of the bounding box or circle.
     * @type FM.Vector
     * @public
     */
    this.offset = new FM.Vector(0, 0);
    /**
     * Width of the collider.
     * @type int
     * @public
     */
    this.width = pWidth;
    /**
     * Height of the collider.
     * @type int
     * @public
     */
    this.height = pHeight;
    /**
     * Velocity of the physic component.
     * @type FM.Vector
     * @public
     */
    this.velocity = new FM.Vector(0, 0);
    /**
     * Acceleration applied to the physic object.
     * @type FM.Vector
     * @public
     */
    this.acceleration = new FM.Vector(0, 0);
    /**
     * How much the object's velocity is decreasing when acceleration is
     * equal to 0.
     * @type FM.Vector
     * @public
     */
    this.drag = new FM.Vector(0, 0);
    /**
     * Angular velocity.
     * @type int
     * @public
     */
    this.angularVelocity = 0;
    /**
     * How much the object's velocity is decreasing when acceleration is
     * equal to 0.
     * @type FM.Vector
     * @public
     */
    this.angularDrag = new FM.Vector(0, 0);
    /**
     * Represent the maximum absolute value of the velocity.
     * @type FM.Vector
     * @public
     */
    this.maxVelocity = new FM.Vector(1000, 1000);
    /**
     * Maximum angular velocity.
     * @type int
     * @public
     */
    this.maxAngularVelocity = 10000;
    /**
     * Elasticity is a factor between 0 and 1 used for bouncing purposes.
     * @type float
     * @public
     */
    this.elasticity = 0;

    //Check if a spatial component is present
    if (!this.spatial && FM.Parameters.debug) {
        console.log("ERROR: No spatial component was added and you need one for physics.");
    }
};
/**
 * FM.PhysicComponent inherits from FM.Component.
 */
FM.PhysicComponent.prototype = Object.create(FM.Component.prototype);
FM.PhysicComponent.prototype.constructor = FM.PhysicComponent;
/**
 * Correct the position of the physic component.
 * @method FM.PhysicComponent#correctPosition
 * @memberOf FM.PhysicComponent
 * @param {FM.Collision} pCollision The collision object containing the
 * that needs position correcting.
 * @private
 */
FM.PhysicComponent.prototype.correctPosition = function (pCollision) {
    "use strict";
    //Position correction
    var correction = new FM.Vector(pCollision.penetration * pCollision.normal.x, pCollision.penetration * pCollision.normal.y),
        aSpatial = pCollision.a.owner.components[FM.ComponentTypes.SPATIAL],
        bSpatial = pCollision.b.owner.components[FM.ComponentTypes.SPATIAL],
        //aPhysic = collision.a.owner.components[FM.ComponentTypes.PHYSIC],
        //bPhysic = collision.b.owner.components[FM.ComponentTypes.PHYSIC],
        massSum = 0,
        aInvMass = pCollision.a.getInvMass(),
        bInvMass = pCollision.b.getInvMass();
    massSum = aInvMass + bInvMass;

    //TODO make it work instead of the other below
    /*var percent = 0.2; // usually 20% to 80%
    var slop = 0.01; // usually 0.01 to 0.1
    correction.x = (Math.max(collision.penetration - slop, 0) / (massSum)) * percent * collision.normal.x;
    correction.y = (Math.max(collision.penetration - slop, 0) / (massSum)) * percent * collision.normal.y;
    aSpatial.position.x -= invMass * correction.x;
    aSpatial.position.y -= invMass * correction.y;
    bSpatial.position.x += otherInvMass * correction.x;
    bSpatial.position.y += otherInvMass * correction.y;*/

    //TODO this is here that it goes wrong, need to add offset ?
    aSpatial.position.x -= correction.x * (aInvMass / massSum);
    aSpatial.position.y -= correction.y * (aInvMass / massSum);
    bSpatial.position.x += correction.x * (bInvMass / massSum);
    bSpatial.position.y += correction.y * (bInvMass / massSum);
    //TODO try with physic tiles with fixed integer position value
    //aSpatial.position.reset(Math.floor(aSpatial.position.x), Math.floor(aSpatial.position.y));
    //bSpatial.position.reset(Math.floor(bSpatial.position.x), Math.floor(bSpatial.position.y));
};
/**
 * Check collisions with a given array of tiles.
 * @method FM.PhysicComponent#correctPosition
 * @memberOf FM.PhysicComponent
 * @param {Array} pTiles The list of tile IDs to test for collisions.
 * @param {int} pTileWidth Width of a tile.
 * @param {int} pTileHeight Height of a tile.
 * @param {int} pXPos X position of the object to test.
 * @param {int} pYPos Y position of the object to test.
 * @return {boolean} Whether there is collision between with a tile.
 * @private
 */
FM.PhysicComponent.prototype.checkCollisionsWithTiles = function (pTiles, pTileWidth, pTileHeight, pXPos, pYPos) {
    "use strict";
    var i1 = Math.floor(pYPos / pTileHeight),
        j1 = Math.floor(pXPos / pTileWidth),
        i2 = Math.floor((pYPos + this.height) / pTileHeight),
        j2 = Math.floor((pXPos + this.width) / pTileWidth),
        i,
        j;
    for (i = i1; i <= i2; i = i + 1) {
        for (j = j1; j <= j2; j = j + 1) {
            if (pTiles[i] !== 0 && pTiles[i][j] !== -1) {
                if (j === j1 || j === j2 || i === i1 || i === i2) {
                    return true;
                }
            }
        }
    }
    return false;
};
/**
 * Try to move the physic object and rollback if it collides with a tile.
 * @method FM.PhysicComponent#tryToMove
 * @memberOf FM.PhysicComponent
 * @param {Array} pTiles The list of tile IDs to test for collisions.
 * @param {int} pTileWidth The width of a tile.
 * @param {int} pTileHeight The height of a tile.
 * @param {float} pXVel The x velocity of the object.
 * @param {float} pYVel The y velocity of the object.
 * @return {boolean} Whether the object can move or not.
 * @private
 */
FM.PhysicComponent.prototype.tryToMove = function (pTiles, pTileWidth, pTileHeight, pXVel, pYVel) {
    "use strict";
    var spX = this.spatial.position.x + pXVel,
        spY = this.spatial.position.y + pYVel;
    if (!this.checkCollisionsWithTiles(pTiles, pTileWidth, pTileHeight, spX + this.offset.x, spY + this.offset.y)) {
        this.spatial.position.x = spX;
        this.spatial.position.y = spY;
        return true;
    }
    return false;
};
/**
 * Move the physic object one pixel at a time.
 * @method FM.PhysicComponent#move
 * @memberOf FM.PhysicComponent
 * @param {FM.TileMap} pTileMap The tile map on which to move.
 * @param {float} pXVel The x velocity of the object.
 * @param {float} pYVel The y velocity of the object.
 * @return {boolean} Whether the object has collided against the tile
 * map or not.
 * @private
 */
FM.PhysicComponent.prototype.move = function (tileMap, xVel, yVel) {
    "use strict";
    var tiles = tileMap.getData(),
        tileWidth = tileMap.getTileWidth(),
        tileHeight = tileMap.getTileHeight(),
        hasCollided = false;
    if (Math.abs(xVel) >= tileWidth || Math.abs(yVel) >= tileHeight) {
        this.move(tileMap, xVel / 2, yVel / 2);
        this.move(tileMap, xVel - xVel / 2, yVel - yVel / 2);
        return;
    }

    var hor = this.tryToMove(tiles, tileWidth, tileHeight, xVel, 0),
        ver = this.tryToMove(tiles, tileWidth, tileHeight, 0, yVel),
        i,
        maxSpeed,
        vel;
    if (hor && ver) {
        return;
    }
    if (!hor) {
        this.velocity.x = 0;
        maxSpeed = Math.abs(xVel);
        for (i = 0; i < maxSpeed; i = i + 1) {
            if (xVel === 0) {
                vel = 0;
            } else if (xVel > 0) {
                vel = 1;
            } else {
                vel = -1;
            }
            if (!this.tryToMove(tiles, tileWidth, tileHeight, vel, 0)) {
                hasCollided = true;
                break;
            } else {
                this.velocity.x += vel;
            }
        }
    }
    if (!ver) {
        this.velocity.y = 0;
        maxSpeed = Math.abs(yVel);
        for (i = 0; i < maxSpeed; i = i + 1) {
            if (yVel === 0) {
                vel = 0;
            } else if (yVel > 0) {
                vel = 1;
            } else {
                vel = -1;
            }
            if (!this.tryToMove(tiles, tileWidth, tileHeight, 0, vel)) {
                hasCollided = true;
                break;
            } else {
                this.velocity.y += vel;
            }
        }
    }
    return hasCollided;
};
/**
 * Update the component.
 * @method FM.PhysicComponent#update
 * @memberOf FM.PhysicComponent
 * @param {float} dt The fixed delta time since the last frame.
 */
FM.PhysicComponent.prototype.update = function (dt) {
    "use strict";
    this.collisions = [];
    this.tilesCollisions = [];

    //Limit velocity to a max value
    //TODO maxvelocity should be in pixels per seconds
    var currentVelocity = this.velocity.x + (this.invMass * this.acceleration.x) * dt,
        maxVelocity = this.maxVelocity.x + (this.invMass * this.acceleration.x) * dt,
        canMove = true,
        hasCollided = false,
        tileMap,
        gameObjects,
        i,
        j,
        otherGameObject,
        otherPhysic,
        collision = null;
    if (Math.abs(currentVelocity) <= maxVelocity) {
        this.velocity.x = currentVelocity;
    } else if (currentVelocity < 0) {
        this.velocity.x = -maxVelocity;
    } else if (currentVelocity > 0) {
        this.velocity.x = maxVelocity;
    }
    currentVelocity = this.velocity.y + (this.invMass * this.acceleration.y) * dt;
    maxVelocity = this.maxVelocity.y + (this.invMass * this.acceleration.y) * dt;
    if (Math.abs(currentVelocity) <= maxVelocity) {
        this.velocity.y = currentVelocity;
    } else if (currentVelocity < 0) {
        this.velocity.y = -maxVelocity;
    } else if (currentVelocity > 0) {
        this.velocity.y = maxVelocity;
    }

    //Apply drag
    if (this.acceleration.x === 0) {
        if (this.velocity.x > 0) {
            this.velocity.x -= this.drag.x;
            if (this.velocity.x < 0) {
                this.velocity.x = 0;
            }
        } else if (this.velocity.x < 0) {
            this.velocity.x += this.drag.x;
            if (this.velocity.x > 0) {
                this.velocity.x = 0;
            }
        }
    }
    if (this.acceleration.y === 0) {
        if (this.velocity.y > 0) {
            this.velocity.y -= this.drag.y;
            if (this.velocity.y < 0) {
                this.velocity.y = 0;
            }
        } else if (this.velocity.y < 0) {
            this.velocity.y += this.drag.y;
            if (this.velocity.y > 0) {
                this.velocity.y = 0;
            }
        }
    }

    if (this.collidesWith.length > 0) {
        if (this.world.hasTileCollisions()) {
            for (i = 0; i < this.collidesWith.length; i = i + 1) {
                tileMap = this.world.getTileMapFromType(this.collidesWith[i]);
                if (tileMap && tileMap.canCollide()) {
                    hasCollided = this.move(tileMap, this.velocity.x * dt, this.velocity.y * dt);
                    if (hasCollided) {
                        this.tilesCollisions.push({a: this.owner, b: tileMap});
                    }
                    canMove = false;
                }
            }
        }
    }

    //Update position
    if (canMove) {
        this.spatial.position.x += this.velocity.x * dt;
        this.spatial.position.y += this.velocity.y * dt;
    }

    //If this game object collides with at least one type of game object
    if (this.collidesWith.length > 0) {
        this.quad = FM.Game.getCurrentState().getQuad();
        gameObjects = this.quad.retrieve(this.owner);
        //If there are other game objects near this one
        for (i = 0; i < gameObjects.length; i = i + 1) {
            otherGameObject = gameObjects[i];
            otherPhysic = otherGameObject.components[FM.ComponentTypes.PHYSIC];
            //If a game object is found and is alive and is not the current one
            if (otherGameObject.isAlive() && this.owner.getId() !== otherGameObject.getId() && !otherPhysic.isCollidingWith(this.owner) && !this.isCollidingWith(otherGameObject)) {
                for (j = 0; j < this.collidesWith.length; j = j + 1) {
                    if (otherGameObject.hasType(this.collidesWith[j])) {
                        collision = this.owner.components[FM.ComponentTypes.PHYSIC].overlapsWithObject(otherPhysic);
                        if (collision !== null) {
                            this.addCollision(collision);
                            otherPhysic.addCollision(collision);
                            this.resolveCollision(otherPhysic, collision);
                            otherPhysic.resolveCollision(this, collision);
                            this.correctPosition(collision);
                        }
                    }
                }
            }
        }
    }
};
/**
 * Resolve collision between current game object and the specified one.
 * @method FM.PhysicComponent#resolveCollision
 * @memberOf FM.PhysicComponent
 * @param {FM.PhysicComponent} pOtherPhysic The other physic component of 
 * the collision.
 * @param {FM.Collision} pCollision The collision object containing the 
 * data about the collision to resolve.
 */
FM.PhysicComponent.prototype.resolveCollision = function (pOtherPhysic, pCollision) {
    "use strict";
    var relativeVelocity = FM.Math.substractVectors(pOtherPhysic.velocity, this.velocity),
        velocityAlongNormal = relativeVelocity.dotProduct(pCollision.normal),
        //Compute restitution
        e = Math.min(this.elasticity, pOtherPhysic.elasticity),
        j = 0,
        otherInvMass = pOtherPhysic.getInvMass(),
        impulse = new FM.Vector(0, 0);
    //Do not resolve if velocities are separating.
    if (velocityAlongNormal > 0) {
        return;
    }
    //Compute impulse scalar
    j = -(1 + e) * velocityAlongNormal;
    j /= this.invMass + otherInvMass;
    //Apply impulse
    impulse.reset(j * pCollision.normal.x, j * pCollision.normal.y);
    this.velocity.x -= this.invMass * impulse.x;
    this.velocity.y -= this.invMass * impulse.y;
    pOtherPhysic.velocity.x += otherInvMass * impulse.x;
    pOtherPhysic.velocity.y += otherInvMass * impulse.y;
};
/**
 * Ensure that a game object collides with a certain type of other game 
 * objects (with physic components of course).
 * @method FM.PhysicComponent#addTypeToCollideWith
 * @memberOf FM.PhysicComponent
 * @param {FM.ObjectType} pType The type to add to the list of types that
 * this object can collide with.
 */
FM.PhysicComponent.prototype.addTypeToCollideWith = function (pType) {
    "use strict";
    this.collidesWith.push(pType);
};
/**
 * Remove a type that was supposed to collide with this game object.
 * @method FM.PhysicComponent#removeTypeToCollideWith
 * @memberOf FM.PhysicComponent
 * @param {FM.ObjectType} pType The type to remove from the list of types
 * that this object can collide with.
 */
FM.PhysicComponent.prototype.removeTypeToCollideWith = function (pType) {
    "use strict";
    this.collidesWith.splice(this.collidesWith.indexOf(pType), 1);
};
/**
 * Add a collision object representing the collision to the list of current
 * collisions.
 * @method FM.PhysicComponent#addCollision
 * @memberOf FM.PhysicComponent
 * @param {FM.Collision} pCollision The collision object to add.
 */
FM.PhysicComponent.prototype.addCollision = function (pCollision) {
    "use strict";
    this.collisions.push(pCollision);
};
/**
 * Get the velocity.
 * @method FM.PhysicComponent#getLinearVelocity
 * @memberOf FM.PhysicComponent
 * @return {FM.Vector} The vector containing the current velocity of the 
 * object.
 */
FM.PhysicComponent.prototype.getLinearVelocity = function () {
    "use strict";
    return this.velocity;
};
/**
 * Check if the current physic component is colliding a specified type of physic component.
 * @method FM.PhysicComponent#isCollidingWithType
 * @memberOf FM.PhysicComponent
 * @param {FM.ObjectType} pOtherType The type of objects to test for
 * collision with this one.
 * @return {boolean} Whether there is already collision between the the current physic component and the specified type of physic component.
 */
FM.PhysicComponent.prototype.isCollidingWithType = function (pOtherType) {
    "use strict";
    var i, collision;
    for (i = 0; i < this.collisions.length; i = i + 1) {
        collision = this.collisions[i];
        if ((collision.b && collision.b.owner.hasType(pOtherType))
                || (collision.a && collision.a.owner.hasType(pOtherType))) {
            return true;
        }
    }
    for (i = 0; i < this.tilesCollisions.length; i = i + 1) {
        collision = this.tilesCollisions[i];
        if ((collision.b && collision.b.hasType(pOtherType))
                || (collision.a && collision.a.hasType(pOtherType))) {
            return true;
        }
    }
    return false;
};
/**
 * Check if the current physic component is colliding with another one.
 * @method FM.PhysicComponent#isCollidingWith
 * @memberOf FM.PhysicComponent
 * @param {FM.GameObject} pOtherGameObject The game object to test for 
 * collision with this one.
 * @return {boolean} Whether there is already collision between the physic components.
 */
FM.PhysicComponent.prototype.isCollidingWith = function (pOtherGameObject) {
    "use strict";
    var i, collision;
    for (i = 0; i < this.collisions.length; i = i + 1) {
        collision = this.collisions[i];
        if ((collision.b && collision.b.owner.getId() === pOtherGameObject.getId())
                || (collision.a && collision.a.owner.getId() === pOtherGameObject.getId())) {
            return true;
        }
    }
    return false;
};
/**
 * Set the mass of the physic object.
 * @method FM.PhysicComponent#setMass
 * @memberOf FM.PhysicComponent
 * @param {int} pNewMass The new mass to set.
 */
FM.PhysicComponent.prototype.setMass = function (pNewMass) {
    "use strict";
    this.mass = pNewMass;
    if (this.mass === 0) {
        this.invMass = 0;
    } else {
        this.invMass = 1 / this.mass;
    }
};
/**
 * Retrieve the mass of the physic object.
 * @method FM.PhysicComponent#getMass
 * @memberOf FM.PhysicComponent
 * @return {int} The mass of this object.
 */
FM.PhysicComponent.prototype.getMass = function () {
    "use strict";
    return this.mass;
};
/**
 * Retrieve the inverse mass of the physic object.
 * @method FM.PhysicComponent#getInvMass
 * @memberOf FM.PhysicComponent
 * @return {int} The inverse mass of this object.
 */
FM.PhysicComponent.prototype.getInvMass = function () {
    "use strict";
    return this.invMass;
};
/**
 * Destroy the component and its objects.
 * @method FM.PhysicComponent#destroy
 * @memberOf FM.PhysicComponent
 */
FM.PhysicComponent.prototype.destroy = function () {
    "use strict";
    this.quad = null;
    this.world = null;
    this.collisions = null;
    this.tilesCollisions = null;
    this.collidesWith = null;
    this.offset.destroy();
    this.offset = null;
    this.velocity.destroy();
    this.velocity = null;
    this.acceleration.destroy();
    this.acceleration = null;
    this.spatial = null;
    this.mass = null;
    this.invMass = null;
    this.width = null;
    this.height = null;
    this.maxAngularVelocity = null;
    this.drag.destroy();
    this.drag = null;
    this.angularDrag.destroy();
    this.angularDrag = null;
    this.maxVelocity.destroy();
    this.maxVelocity = null;
    this.elasticity = null;
    FM.Component.prototype.destroy.call(this);
};