Source: State.js

/*global FM*/
/**
 * Object acting as a container of game objects. It helps structure the game in 
 * states.
 * @class FM.State
 * @constructor
 * @author Simon Chauvin
 */
FM.State = function () {
    "use strict";
    /**
     * Width of the screen.
     * @field
     * @type int
     * @private
     */
    this.screenWidth = 0;
    /**
     * Height of the screen.
     * @field
     * @type int
     * @private
     */
    this.screenHeight = 0;
    /**
    * The game object that makes the screen scrolls.
    * @field
    * @type FM.GameObject
    * @private
    */
    this.scroller = null;
    /**
     * Frame of the camera (used in case of scrolling).
     * @field
     * @type FM.Rectangle
     * @private
     */
    this.followFrame = null;
    /**
     * Quad tree containing all game objects with a physic component.
     * @field
     * @type FM.QuadTree
     * @private
     */
    this.quad = null;
    /**
     * Object representing the world topology (bounds, tiles, collisions, 
     * objects).
     * @field
     * @type FM.World
     * @private
     */
    this.world = null;
    /**
     * Whether the camera following is smooth or not.
     * @field
     * @type boolean
     * @private
     */
    this.smoothFollow = false;
    /**
     * Whether the camera movement is smooth or not.
     * @field
     * @type boolean
     * @private
     */
    this.smoothCamera = false;
    /**
     * Speed of the camera for following.
     * @field
     * @type FM.Vector
     * @private
     */
    this.followSpeed = new FM.Vector(60, 60);
    /**
     * Speed of the camera for movement.
     * @field
     * @type FM.Vector
     * @private
     */
    this.cameraSpeed = new FM.Vector(60, 60);
    /**
     * Array containing every game objects of the state.
     * @field
     * @type Array
     * @public
     */
    this.members = [];
    /**
     * Static attributes used to store the last ID affected to a game object.
     * @field
     * @type int
     * @static
     * @public
     */
    FM.State.lastId = 0;
    /**
    * Camera (limited by the screen resolution of the game).
    * @field
    * @type FM.Rectangle
    * @public
    */
    this.camera = new FM.Rectangle(0, 0, 0, 0);
};
FM.State.prototype.constructor = FM.State;
/**
 * Private method that sort game objects according to their z index.
 * @method FM.State#sortZIndex
 * @memberOf FM.State
 * @param {FM.GameObject} gameObjectA First game object to be sorted.
 * @param {FM.GameObject} gameObjectB Second game object to be sorted.
 * @return {int} A negative value means that gameObjectA has a lower z index 
 * whereas a positive value means that it has a bigger z index. 0 means that
 * both have the same z index.
 * @private
 */
FM.State.prototype.sortZIndex = function (gameObjectA, gameObjectB) {
    "use strict";
    return (gameObjectA.zIndex - gameObjectB.zIndex);
};
/**
 * Initialize the state. Can be redefined in sub classes for 
 * specialization.
 * @method FM.State#init
 * @memberOf FM.State
 * @param {int} pWorldWidth The width of the world to create.
 * @param {int} pWorldHeight The height of the world to create.
 */
FM.State.prototype.init = function (pWorldWidth, pWorldHeight) {
    "use strict";
    this.screenWidth = FM.Game.getScreenWidth();
    this.screenHeight = FM.Game.getScreenHeight();
    //By default init the world to the size of the screen
    this.world = new FM.World(pWorldWidth || this.screenWidth, pWorldHeight
            || this.screenHeight);
    //Create the quad tree
    this.quad = new FM.QuadTree(0, new FM.Rectangle(0, 0,
        pWorldWidth || this.screenWidth, pWorldHeight || this.screenHeight));
    //Set the camera size by the chosen screen size
    this.camera.width = this.screenWidth;
    this.camera.height = this.screenHeight;

    if (FM.Parameters.debug) {
        console.log("INIT: The state has been created.");
    }
};
/**
 * Add a game object to the state.
 * @method FM.State#add
 * @memberOf FM.State
 * @param {FM.GameObject} gameObject The game object to add to the state.
 */
FM.State.prototype.add = function (gameObject) {
    "use strict";
    if (gameObject.components) {
        //Add the game object to the state
        this.members.push(gameObject);
        //Affect an ID to the game object
        gameObject.setId(FM.State.lastId);
        FM.State.lastId += 1;
        //Add the game object to the quad tree if it's got a physic component
        if (gameObject.components[FM.ComponentTypes.PHYSIC]) {
            this.quad.insert(gameObject);
        }
    } else {
        if (FM.Parameters.debug) {
            console.log("ERROR: you're trying to add something else" +
                "than a game object to the state. This is not allowed.");
        }
    }
};
/**
 * Remove an object from the state and destroy it.
 * @method FM.State#remove
 * @memberOf FM.State
 * @param {FM.GameObject} gameObject The game object to remove and destroy.
 */
FM.State.prototype.remove = function (gameObject) {
    "use strict";
    //Remove the game object from the state
    this.members.splice(this.members.indexOf(gameObject), 1);
    //Remove the object from the quad tree if it is a physic object
    if (gameObject.components[FM.ComponentTypes.PHYSIC]) {
        this.quad.remove(gameObject);
    }
    //Destroy the game object
    gameObject.destroy();
};
/**
 * Sort the members of the state by their z-index.
 * @method FM.State#sortByZIndex
 * @memberOf FM.State
 */
FM.State.prototype.sortByZIndex = function () {
    "use strict";
    this.members.sort(this.sortZIndex);
};
/**
 * Update the game physics.
 * @method FM.State#updatePhysics
 * @memberOf FM.State
 * @param {float} dt The fixed delta time in seconds since the 
 * last frame.
 */
FM.State.prototype.updatePhysics = function (dt) {
    "use strict";
    var i,
        gameObject,
        components,
        physic;
    //Clear and update the quadtree
    this.quad.clear();
    for (i = 0; i < this.members.length; i = i + 1) {
        gameObject = this.members[i];
        if (gameObject.isAlive()) {
            components = gameObject.components;
            physic = gameObject.components[FM.ComponentTypes.PHYSIC];
            //Add physic objects in the quad tree
            if (physic) {
                this.quad.insert(gameObject);
            }
        }
    }
    //Update the physic component of every game object present in the state
    for (i = 0; i < this.members.length; i = i + 1) {
        gameObject = this.members[i];
        if (gameObject.isAlive()) {
            components = gameObject.components;
            physic = components[FM.ComponentTypes.PHYSIC];
            //Update the physic component
            if (physic) {
                physic.update(dt);
            }
        }
    }
};
/**
 * Update the game objects of the state.
 * @method FM.State#update
 * @memberOf FM.State
 * @param {float} dt The fixed delta time in seconds since the last frame.
 */
FM.State.prototype.update = function (dt) {
    "use strict";
    var i,
        gameObject,
        components,
        spatial,
        physic,
        pathfinding,
        emitter,
        newOffset,
        frameWidth,
        frameHeight,
        xPosition,
        yPosition,
        farthestXPosition,
        farthestYPosition;
    //Update every game object present in the state
    for (i = 0; i < this.members.length; i = i + 1) {
        gameObject = this.members[i];
        if (gameObject.isAlive()) {
            components = gameObject.components;
            spatial = components[FM.ComponentTypes.SPATIAL];
            physic = components[FM.ComponentTypes.PHYSIC];
            pathfinding = components[FM.ComponentTypes.PATHFINDING];
            emitter = components[FM.ComponentTypes.FX];
            //Update the path
            if (pathfinding) {
                pathfinding.update(dt);
            }
            //Update the emitter
            if (emitter) {
                emitter.update(dt);
            }
            //Update scrolling
            if (physic) {
                if (this.scroller === gameObject) {
                    frameWidth = this.followFrame.width;
                    frameHeight = this.followFrame.height;
                    xPosition = spatial.position.x + physic.offset.x;
                    yPosition = spatial.position.y + physic.offset.y;
                    farthestXPosition = xPosition + physic.width;
                    farthestYPosition = yPosition + physic.height;

                    // Going left
                    if (xPosition <= this.followFrame.x) {
                        newOffset = this.camera.x - (this.followFrame.x - xPosition);
                        if (newOffset >= 0) {
                            if (this.smoothFollow) {
                                this.camera.x -= this.followSpeed.x * dt;
                                this.followFrame.x -= this.followSpeed.x * dt;
                            } else {
                                this.camera.x = newOffset;
                                this.followFrame.x = xPosition;
                            }
                        }
                    }
                    // Going up
                    if (yPosition <= this.followFrame.y) {
                        newOffset = this.camera.y - (this.followFrame.y - yPosition);
                        if (newOffset >= 0) {
                            if (this.smoothFollow) {
                                this.camera.y -= this.followSpeed.y * dt;
                                this.followFrame.y -= this.followSpeed.y * dt;
                            } else {
                                this.camera.y = newOffset;
                                this.followFrame.y = yPosition;
                            }
                        }
                    }
                    // Going right
                    if (farthestXPosition >= this.followFrame.x + frameWidth) {
                        newOffset = this.camera.x + (farthestXPosition - (this.followFrame.x + frameWidth));
                        if (newOffset + this.camera.width <= this.world.width) {
                            if (this.smoothFollow) {
                                this.camera.x += this.followSpeed.x * dt;
                                this.followFrame.x += this.followSpeed.x * dt;
                            } else {
                                this.camera.x = newOffset;
                                this.followFrame.x = farthestXPosition - frameWidth;
                            }
                        }
                    }
                    // Going down
                    if (farthestYPosition >= this.followFrame.y + frameHeight) {
                        newOffset = this.camera.y + (farthestYPosition - (this.followFrame.y + frameHeight));
                        if (newOffset + this.camera.height <= this.world.height) {
                            if (this.smoothFollow) {
                                this.camera.y += this.followSpeed.y * dt;
                                this.followFrame.y += this.followSpeed.y * dt;
                            } else {
                                this.camera.y = newOffset;
                                this.followFrame.y = farthestYPosition - frameHeight;
                            }
                        }
                    }
                    //Check if the scroller is in the follow frame and stop the smooth movement
                    if (this.smoothFollow && xPosition >= this.followFrame.x && farthestXPosition <= this.followFrame.x + frameWidth
                            && yPosition >= this.followFrame.y && farthestYPosition <= this.followFrame.y + frameHeight) {
                        this.smoothFollow = false;
                    }
                }
            } else {
                if (FM.Parameters.debug && this.scroller === gameObject) {
                    console.log("ERROR: The scrolling object must have a physic component.");
                }
            }
            //Update the game object
            if (gameObject.update) {
                gameObject.update(dt);
            }
        }
    }
};
/**
 * Draw the game objects of the state.
 * @method FM.State#draw
 * @memberOf FM.State
 * @param {CanvasRenderingContext2D} bufferContext Context (buffer) on wich 
 * drawing is done.
 * @param {float} dt Variable delta time since the last frame.
 */
FM.State.prototype.draw = function (bufferContext, dt) {
    "use strict";
    //Clear the screen
    bufferContext.clearRect(0, 0, this.screenWidth, this.screenHeight);

    //Update offsets
    bufferContext.xOffset = this.camera.x;
    bufferContext.yOffset = this.camera.y;

    //Search for renderer in the game object list
    var i, gameObject, newPosition, spatial, physic, renderer;
    for (i = 0; i < this.members.length; i = i + 1) {
        gameObject = this.members[i];

        //If the game object is visible or is in debug mode and alive
        if (gameObject.isVisible() || (FM.Parameters.debug && FM.Game.isDebugActivated() && gameObject.isAlive())) {
            spatial = gameObject.components[FM.ComponentTypes.SPATIAL];
            //If there is a spatial component then test if the game object is on the screen
            if (spatial) {
                renderer = gameObject.components[FM.ComponentTypes.RENDERER];
                spatial.previous.copy(spatial.position);
                newPosition = new FM.Vector(spatial.position.x * dt + spatial.previous.x * (1.0 - dt),
                    spatial.position.y * dt + spatial.previous.y * (1.0 - dt));
                //Draw objects
                if (renderer && gameObject.isVisible()) {
                    var xPosition = newPosition.x, yPosition = newPosition.y,
                        farthestXPosition = xPosition + renderer.getWidth(),
                        farthestYPosition = yPosition + renderer.getHeight(),
                        newViewX = 0, newViewY = 0;
                    //If the game object has a scrolling factor then apply it
                    newViewX = (this.camera.x + (this.screenWidth - this.camera.width) / 2) * gameObject.scrollFactor.x;
                    newViewY = (this.camera.y + (this.screenHeight - this.camera.height) / 2) * gameObject.scrollFactor.y;

                    //Draw the game object if it is within the bounds of the screen
                    if (farthestXPosition >= newViewX && farthestYPosition >= newViewY
                            && xPosition <= newViewX + this.camera.width && yPosition <= newViewY + this.camera.height) {
                        renderer.draw(bufferContext, newPosition);
                    }
                }
                //Draw physic debug
                if (FM.Parameters.debug && gameObject.isAlive()) {
                    if (FM.Game.isDebugActivated()) {
                        physic = gameObject.components[FM.ComponentTypes.PHYSIC];
                        if (physic) {
                            physic.drawDebug(bufferContext, newPosition);
                        }
                    }
                }
            }
        }
    }
    // Debug
    if (FM.Parameters.debug) {
        if (FM.Game.isDebugActivated()) {
            //Display the world bounds
            bufferContext.strokeStyle = '#f0f';
            bufferContext.strokeRect(0 - this.camera.x, 0 - this.camera.y, this.world.width, this.world.height);

            //Display the camera bounds
            bufferContext.strokeStyle = '#8fc';
            bufferContext.strokeRect((this.screenWidth - this.camera.width) / 2, (this.screenHeight - this.camera.height) / 2, this.camera.width, this.camera.height);

            //Display the scrolling bounds
            if (this.followFrame) {
                bufferContext.strokeStyle = '#f4f';
                bufferContext.strokeRect(this.followFrame.x - this.camera.x, this.followFrame.y - this.camera.y, this.followFrame.width, this.followFrame.height);
            }
        }
    }
};
/**
 * Center the camera on a specific game object.
 * @method FM.State#centerCameraOn
 * @memberOf FM.State
 * @param {FM.GameObject} gameObject The game object to center the camera on.
 */
FM.State.prototype.centerCameraOn = function (gameObject) {
    "use strict";
    var spatial = gameObject.components[FM.ComponentTypes.SPATIAL],
        newPosition = spatial.position.x - this.camera.width / 2;
    if (newPosition > this.world.x && newPosition < this.world.width) {
        this.camera.x = newPosition;
    }
    newPosition = spatial.position.y - this.camera.height / 2;
    if (newPosition > this.world.y && newPosition < this.world.height) {
        this.camera.y = newPosition;
    }
};
/**
 * Center the camera at a specific given position.
 * @method FM.State#centerCameraAt
 * @memberOf FM.State
 * @param {int} xPosition The target x position of the camera.
 * @param {int} yPosition The target y position of the camera.
 */
FM.State.prototype.centerCameraAt = function (xPosition, yPosition) {
    "use strict";
    var newPosition = xPosition - this.camera.width / 2;
    if (newPosition > this.world.x && newPosition < this.world.width) {
        this.camera.x = newPosition;
    }
    newPosition = yPosition - this.camera.height / 2;
    if (newPosition > this.world.y && newPosition < this.world.height) {
        this.camera.y = newPosition;
    }
};
/**
 * Make an object as the scroller.
 * @method FM.State#follow
 * @memberOf FM.State
 * @param {FM.GameObject} gameObject The game object to follow.
 * @param {int} width The width of the camera.
 * @param {int} height The height of the camera.
 */
FM.State.prototype.follow = function (gameObject, width, height, pSmooth, pFollowSpeed) {
    "use strict";
    this.scroller = gameObject;
    this.followFrame = new FM.Rectangle((this.screenWidth - width) / 2 + this.camera.x, (this.screenHeight - height) / 2 + this.camera.y, width, height);
    this.smoothFollow = pSmooth || false;
    this.followSpeed = pFollowSpeed || new FM.Vector(60, 60);
};
/**
 * Delete the scroller.
 * @method FM.State#unFollow
 * @memberOf FM.State
 */
FM.State.prototype.unFollow = function () {
    "use strict";
    this.followFrame = null;
    this.scroller = null;
};
/**
 * Destroy the state and its objects.
 * @method FM.State#destroy
 * @memberOf FM.State
 */
FM.State.prototype.destroy = function () {
    "use strict";
    this.scroller = null;
    if (this.followFrame) {
        this.followFrame.destroy();
    }
    this.followFrame = null;
    this.camera.destroy();
    this.camera = null;
    //In case it's the preloader
    if (this.world) {
        this.world.destroy();
        this.world = null;
        this.quad.clear();
        this.quad.destroy();
        this.quad = null;
    }
    var i;
    for (i = 0; i < this.members.length; i = i + 1) {
        this.members[i].destroy();
    }
    this.members = null;
};
/**
 * Get the game object which ID matches the one given.
 * @method FM.State#getGameObjectById
 * @memberOf FM.State
 * @param {int} pId The ID of the object to retrieve.
 * @return {FM.GameObject} The game object that corresponds or null if it
 * finds nothing.
 */
FM.State.prototype.getGameObjectById = function (pId) {
    "use strict";
    var gameObject, i;
    for (i = 0; i < this.members.length; i = i + 1) {
        gameObject = this.members[i];
        if (gameObject.getId() === pId) {
            return gameObject;
        }
    }
    return null;
};
/**
 * Get the object that scrolls the screen.
 * @method FM.State#getScroller
 * @memberOf FM.State
 * @return {FM.GameObject} The game object that scrolls the screen.
 */
FM.State.prototype.getScroller = function () {
    "use strict";
    return this.scroller;
};
/**
 * Get the world object.
 * @method FM.State#getWorld
 * @memberOf FM.State
 * @return {FM.World} The world of the game.
 */
FM.State.prototype.getWorld = function () {
    "use strict";
    return this.world;
};
/**
 * Get the quad tree.
 * @method FM.State#getQuad
 * @memberOf FM.State
 * @return {FM.QuadTree} The quad tree containing every game object with a 
 * physic component.
 */
FM.State.prototype.getQuad = function () {
    "use strict";
    return this.quad;
};