Source: mapClass.js

// Util functions
/**
 * @global
 * @function
 * @param t A constructor for the blocks/tiles to be created
 * @param w The number of blocks/tiles of type t that will be returned
 * @returns {Array} An array of length w of blocks/tiles of type t
 *
 */
function getTileRow(t, w) { //Creates an array of w tile t objects
    var row = [];
    for (var i = 0; i < w; i++) {
        row.push(new t());
    }
    return row;
}

/**
 * @global
 * @function
 * @param {Array} first
 * @param {Array} last
 * @description Concats two 2D arrays into one 2D array
 * @returns {Array} A 2D array created by concatenating the last array onto the end of the first array
 */
function concatRows(first, last) {
    var a = [];
    for (var r = 0; r < first.length; r++) {
        a[r] = first[r].concat(last[r]);
    }
    return a;
}

/**
 * @name BlockSector
 * @class
 * @description An object representing a sector. Contains all 2D arrays of blocks and any sector-specific information.
 */
function BlockSector() {
    /** 
	  * @field {Array} rows - A 2D array of Block IDs in the BlockSector 
	  * @memberof BlockSector
      */
    this.rows = [];
}


/**
 * @class SectoredBlockMap
 * @name SectoredBlockMap
 * @constructor
 * @param {Integer} sectorWidth - The number of blocks wide each sector should be (must be odd!)
 * @param {Integer} sectorHeight - The number of blocks high each sector should be (must be odd!)
 * @param {WorldGenerator} worldGenerator - The {@link WorldGenerator} Object to be used for this map
 * @prop {WorldGenerator} generator - The {@link WorldGenerator} to be used for this map
 * @prop {Array} sectorRows - A 2D array of BlockSectors and undefined values that make up the map
 * @prop {Integer} sWidth - A number representing the width of the map in sectors.
 * @prop {Integer} sHeight - A number representing the height of the map in sectors.
 * @prop {Integer} sectorWidth - An odd number representing the width of a sector in blocks
 * @prop {Integer} sectorHeight - An odd number representing the height of a sector in blocks
 * @prop {Integer} sectorCenterX - The index in the center row where the center sector of the map is
 * @prop {Integer} sectorCenterY - The index of the center row where the center sector of the map is
 * @prop {Integer} originSectorX - The x coordinate of the origin sector of the map
 * @prop {Integer} originSectorY - The y coordinate of the origin sector of the map
 * @prop {Integer} sectorRightBound - The x coordinate of the furthest right sector of the map
 * @prop {Integer} sectorLeftBound - The x coordinate of the furthest left sector of the map
 * @prop {Integer} sectorTopBound - The y coordinate of the furthest up sector of the map
 * @prop {Integer} sectorBottomBound - The y coordinate of the furthest down sector of the map
 * @prop {Array} generationQue - An array of sector coordinates that need generating during the next update call.
 * 
 */
function SectoredBlockMap(sectorWidth, sectorHeight, worldGenerator) {
    /*
     * The sectorRows in this sector map will create ONE rectangular shape, however, sectors/sector indexes may be filled in as "undefined" allowing gaps to exist in maps.
     *
     * For a SectoredBlockMap and for Sectors:
     *
     * +----------> Right is increasing X (the leftmost sector/block is 0)
     * | Down is increasing Y (The highest sector/block is 0)
     * |
     * V
     *
     */
    this.generator = worldGenerator;
    this.sectorRows = [];
    this.generationQue = []; // An array of sector coordinates that need generated.
    this.sWidth = 1; //Width and height is in sectors, NOT BLOCKS!
    this.sHeight = 1;
    this.sectorXOffset = 0; //This allows us to add in one sector at a time, we don't need to create a duplicate/empty sector on the other side, simply compensate for the difference
    this.sectorYOffset = 0; //If there is more sectors on the right, sectorXOffset should be negative, more on the left, it should be positive

    this.sectorWidth = sectorWidth; // Width of sector in blocks
    if (this.sectorWidth % 2 == 0) {
        this.sectorWidth++; //If the width is not odd, add one to make it odd
    }

    this.sectorHeight = sectorHeight; // Height of sector in blocks
    if (this.sectorHeight % 2 == 0) {
        this.sectorHeight++; //If the height is not odd, add one to make it odd
    }

    //Add the origin sector
    this.sectorRows.push(new Array());

    this.sectorCenterX = ((this.sectorWidth - 1 - this.sectorXOffset) / 2); //For example, width of 3: 3-1 = 2, 2 / 2 = 1, centerX is 1: [0][1][2]
    this.sectorCenterY = ((this.sectorHeight - 1 - this.sectorYOffset) / 2);
    this.originSectorX = 0; //Currently, there is only 1 sector existing, so the origin's index is 0,0
    this.originSectorY = 0;
    this.sectorRightBound = 0; //The X position of the rightmost sector
    this.sectorLeftBound = 0; //The X position of the leftmost sector
    this.sectorTopBound = 0; //The Y position of the topmost sector
    this.sectorBottomBound = 0; //The Y position of the bottommost sector
    
   /**
     * @method init
     * @memberof SectoredBlockMap
     * @description Itializes the  map, should be called before the map is used!
     */
    this.init = function() {
        this.generator.generateOrigin(this);
    };

    /**
     * @method sectorCoordsToSectorIndex
     * @memberof SectoredBlockMap
     * @arg {Number} sectorX
     * @arg {Number} sectorY
     * @description This methods converts x and y coordinates into the indexes in the map's 2D array to access the specified sector.
     * @returns (Object) An object with two properties "sectorXIndex" and "sectorYIndex" that contain the x and y indexes of the sector specified with the x and y coords.
     */
    this.sectorCoordsToSectorIndex = function(sectorX, sectorY) {
        return {
            sectorXIndex: sectorX + this.originSectorX,
            sectorYIndex: sectorY + this.originSectorY
        };
    };

    /**
     * @method getOriginSector
     * @memberof SectoredBlockMap
     * @returns {BlockSector} The origin BlockSector of the map.
     */
    this.getOriginSector = function() {
        return this.sectorRows[this.originSectorY][this.originSectorX]; //Returns the center tile of the map
    };
    this.getOriginSector.bind(this);

    /**
     * @method getSector
	 * @memberof SectoredBlockMap
     * @arg sectorX
     * @arg sectorY
     * @description Returns the BlockSector specified by the sector coords. Will return a BlockSector full of air/empty blocks if the sector doesn't exist!
     * @returns {BlockSector} Returns the BlockSector specified by the sector coords. Will return a BlockSector full of air/empty blocks if the sector doesn't exist!
     *
     */
    this.getSector = function(sectorX, sectorY) {
        if (!this.sectorExists(sectorX, sectorY)) {
            return new BlockSector(this.sectorWidth, this.sectorHeight); //Return a sector of air/empty blocks if the sector doesn't exist
        }
        var i = this.sectorCoordsToSectorIndex(sectorX, sectorY);

        return this.sectorRows[i.sectorYIndex][i.sectorXIndex];
    };

    /**
     * @method sectorInBounds
     * @memberof SectoredBlockMap
     * @arg sectorX
     * @arg sectorY
     * @description Tests to see if the specified sector is within the current bounds of the SectoredBlockMap.
     * @returns {Boolean} true if the sector is within the current bounds, false if not. This does not check to see if the sector exists!
     */
    this.sectorInBounds = function(sectorX, sectorY) { //Checks to see if the given coords point within the current boundaries
        if ((sectorX <= this.sectorRightBound) && (sectorX >= this.sectorLeftBound)) { //Too far to the right
            if ((sectorY >= this.sectorTopBound) && (sectorY <= this.sectorBottomBound)) {
                return true; //Everything checks out, return true
            }
        }
        return false;
    };
    this.sectorInBounds.bind(this);

    /**
     * @method sectorExists
	 * @memberof SectoredBlockMap
     * @arg {Number} sectorX
     * @arg {Number} sectorY
     * @returns {Boolean} True if the sector is within the map bounds and has been generated, false if otherwise.
     */
    this.sectorExists = function(sectorX, sectorY) { //Checks to see if the given coords point to an extisting sector
        if (this.sectorInBounds(sectorX, sectorY)) {
            var i = this.sectorCoordsToSectorIndex(sectorX, sectorY);
            if (this.sectorRows[i.sectorYIndex][i.sectorXIndex] == undefined) {
                return false; //Sector has not been generated/created yet, return false
            } else {
                return true; //Sector has been generated, return true
            }
        }
        return false; //Sector isn't even within the boundaries, return false
    };

    //Return 1 if an old sector was replaced with the new one, return 2 if new sector was in the bounds and replaced an undefinied sector, return 3 if the sector was out of the boundaries and the map's structure had to be changed
    this.setSector = function(newSector, sectorX, sectorY) { //This can replace sectors and also add on new ones
        if (newSector.width != this.sectorWidth || newSector.height != this.sectorHeight) {
        	if (STUDIO_DEV) {
            Studio.error("The new sector's dimensions do not match the sector size of the SectoredBlockMap!");
        }
        else {
        	console.log("The new sector's dimensions do not match the sector size of the SectoredBlockMap!");
        }
            return false;
        }

        if (this.sectorExists(sectorX, sectorY)) { //Sector already existed, replacing with new Sector, returning 1
            var i = this.sectorCoordsToSectorIndex(sectorX, sectorY);
            this.sectorRows[i.sectorYIndex][i.sectorXIndex] = newSector;
            return 1;
        } else {
            if (this.sectorInBounds(sectorX, sectorY)) { //Ok, the sector is within the bounds, we don't have to do anything to the map structure, just put in the sector, return 2
                var i = this.sectorCoordsToSectorIndex(sectorX, sectorY);
                this.sectorRows[i.sectorYIndex][i.sectorXIndex] = newSector;
                return 2;
            } else {
                //Ok, sector is not in the bounds, we will have to adjust the map to fit it in, returning 3
                if (sectorX > this.sectorRightBound) {
                    //console.log("right" + sectorX);
                    var diff = sectorX - this.sectorRightBound;
                    for (var row = 0; row < this.sHeight; row++) {
                        for (var i = 0; i < diff; i++) {
                            this.sectorRows[i].push(undefined);

                        }

                    }
                    this.sectorRightBound += diff;
                    this.sWidth += diff;
                    //When we push elements, the origin sector does not change.
                } else if (sectorX < this.sectorLeftBound) {
                    //console.log("left" + sectorX);
                    var diff = this.sectorLeftBound - sectorX;
                    for (var row = 0; row < this.sHeight; row++) {
                        for (var i = 0; i < diff; i++) {
                            this.sectorRows[i].unshift(undefined);
                        }
                    }
                    this.sectorLeftBound -= diff;
                    this.sWidth += diff;
                    this.originSectorX += diff;
                }

                if (sectorY < this.sectorTopBound) {
                    //console.log("top" + sectorY);
                    var diff = this.sectorTopBound - sectorY;
                    var blankRow = [];
                    for (var col = 0; col < this.sWidth; col++) {
                        blankRow.push(undefined);
                    }
                    for (var i = 0; i < diff; i++) {
                        this.sectorRows.unshift(blankRow);
                    }
                    this.sHeight += diff;
                    this.originSectorY += diff;
                    this.sectorTopBound -= diff;
                } else if (sectorY > this.sectorBottomBound) {
                   // console.log("bottom" + sectorY);
                    var diff = sectorY - this.sectorBottomBound;
                    var blankRow = [];
                    for (var col = 0; col < this.sWidth; col++) {
                        blankRow.push(undefined);
                    }
                    for (var i = 0; i < diff; i++) {
                        this.sectorRows.push(blankRow);
                    }
                    this.sHeight += diff;
                    this.sectorBottomBound += diff;
                }

                var index = this.sectorCoordsToSectorIndex(sectorX, sectorY);
                this.sectorRows[index.sectorYIndex][index.sectorXIndex] = newSector;
                return 3;
            }
        }
    };

	this.getBlockIdAt = function(blockPosition) {
		
        return this.getSector(blockPosition.sectorX, blockPosition.sectorY).rows[Math.ceil(blockPosition.localY)][Math.floor(blockPosition.localX)];
    };

    this.setBlockIdAt = function(blockPosition, newId) {
        this.getSector(blockPosition.sectorX, blockPosition.sectorY).rows[Math.round(blockPosition.localY)][Math.round(blockPosition.localX)] = newId;
    };

    //TODO: Figure out what the below function does and why it even exists....
    this.isInMap = function(blockX, blockY) {
        //blockX and blockY are the distance in blocks from the origin block
        if (!game.generation.GENERATE_WITH_PLAYER) { //if we have an infinite world, we should always have some map for the cursor to land in
            var nX = Math.abs(Math.floor(blockX + this.sectorCenterX));
            var nY = Math.abs(Math.floor(blockY + this.sectorCenterY));
            var sectorX = (nX / this.sectorWidth) - ((nX % this.sectorWidth) * this.sectorWidth);
            var sectorY = (nY / this.sectorHeight) - ((nY % this.sectorHeight) * this.sectorHeight);
            if (this.sectorExists(sectorX, sectorY)) {
                return true;
            } else {
                return false;
            }
        } else {
            return true;
        }
    };
    this.isInMap.bind(this);
    this.setBlockIdAt.bind(this);
    this.getBlockIdAt.bind(this);

    // Get the BlockPosition of the block that is relX to right of and relY below the block given by originPosition
    /**
      * @memberof SectoredBlockMap
      * @method getPositionRelativeTo
      * @description Returns a new {@link BlockPosition} based upon relX blocks left and right and relY blocks up and down from the original {@link BlockPosition} originPosition
      * @param {BlockPosition} originPosition - The position that will be used as a point of reference.
      * @param {Integer} relX - Number of blocks to the right of the point of reference. (left if negative)
      * @param {Integer} relY - Number of blocks below the point of reference (up if negative).
      * @returns {BlockPosition}
      */
    this.getPositionRelativeTo = function(originPosition, relX, relY) {
        var newLocalX = (originPosition.localX) + relX;
        var newLocalY = (originPosition.localY) + relY;
        var sectorX = originPosition.sectorX;
        var sectorY = originPosition.sectorY;

        while (newLocalY < 0) {
            sectorY--;
            newLocalY += this.sectorHeight;
        }

        while (newLocalY > this.sectorHeight) {
            newLocalY = Math.abs(this.sectorHeight - newLocalY);
            sectorY++;
        }

        while (newLocalX < 0) {
            sectorX--;
            newLocalX += this.sectorWidth;
        }

        while ((newLocalX/(this.sectorWidth-1)) >= 1) {
            newLocalX = Math.abs(this.sectorWidth - newLocalX);
            sectorX++;
        }

        return new BlockPosition(sectorX, sectorY, newLocalX, newLocalY);
    };
    this.getPositionRelativeTo.bind(this);


	/**
	 * @memberof SectoredBlockMap
	 * @method getWindow
	 * @param {BlockPosition} centerPosition - The position of the block that the window is centered around
	 * @param {Integer} width - The number of blocks from left bound to right bound of the window (must be odd!)
	 * @param {Integer} height - The number of blocks from the top bound to the bottom bound of the window (must be odd!)
	 * @description This function takes a center position and dimensions to select an area of blocks from the SectoredBlockMap to return as a 2D array of blocks. It is a way to select all blocks in an area, even if that area contains multiple sectors.
	 * @returns {Array} A 2D array of blocks that are within the window
	 *
	 */
    this.getWindow = function(centerPosition, width, height) { //Height and Width must be odd!
        //centerPoition holds the position of the center block of the window:
        // x is the localX position of the center block
        // y is the localY position of the center block
        // width is the number of blocks from left bound to right bound of the window
        // height is the number of blocks from the top bound to the bottom bound of the window

        //First let's figure out the number rows and columns this window will span

        var localLeftBound = Math.floor((centerPosition.localX - ((width-1) / 2)));
        var localRightBound = Math.floor((centerPosition.localX + ((width-1) / 2)));
        var localTopBound = Math.floor((centerPosition.localY - ((height-1) / 2)));
        var localBottomBound = Math.floor((centerPosition.localY + ((height-1) / 2)));

        //Now lets calculate the number of sectors in each direction (other than the given sector) that will need to be added
        var sectorsAbove = 0;
        var sectorsBelow = 0;
        var sectorsLeft = 0;
        var sectorsRight = 0;

        if (localTopBound < 0) {
            sectorsAbove = Math.ceil((localTopBound + this.sectorHeight) / this.sectorHeight);
        }
        if (localBottomBound > this.sectorHeight) {
            sectorsBelow = Math.ceil((localBottomBound - this.sectorHeight) / this.sectorHeight)
        }

        if (localLeftBound < 0) {
            sectorsLeft = Math.ceil((localLeftBound + this.sectorWidth) / this.sectorWidth);
        }
        if (localRightBound > this.sectorWidth) {
            sectorsRight = Math.ceil((localRightBound - this.sectorWidth) / this.sectorWidth);
        }

        var windowList = []; // The windowList is an array of rows. Each row is an array of blocks. Top left is 0,0, bottom right is length,length
        //Now we will piece together our windowList, we will start from top left, go to the right, and then drop down one sector and repeat until we reach bottom right
        var currentSectorX = centerPosition.sectorX - sectorsLeft;

        for (var currentSectorY = centerPosition.sectorY - sectorsAbove; currentSectorY <= centerPosition.sectorY + sectorsBelow; currentSectorY++) {
            var windowRow = [];
            for (var currentSectorX = centerPosition.sectorX - sectorsLeft; currentSectorX <= centerPosition.sectorX + sectorsRight; currentSectorX++) {
                //We will calculate the center of a large window in terms of blocks (local) relative to the local bounds of the target sector

                var signX = (currentSectorX == centerPosition.sectorX) ? 1 : -1 * (Math.abs(currentSectorX - centerPosition.sectorX) / (currentSectorX - centerPosition.sectorX));
                var signY = (currentSectorY == centerPosition.sectorY) ? 1 : -1 * (Math.abs(currentSectorY - centerPosition.sectorY) / (currentSectorY - centerPosition.sectorY));
                var relativeLocalX = (game.generation.SECTOR_SIZE * (Math.abs(currentSectorX - centerPosition.sectorX))) + (signX * centerPosition.localX);
                var relativeLocalY = (game.generation.SECTOR_SIZE * (Math.abs(currentSectorY - centerPosition.sectorY))) + (signY * centerPosition.localY);
                windowRow.push(this.windowOfSector(new BlockPosition(currentSectorX, currentSectorY, relativeLocalX, relativeLocalY), width, height, game.generation.GENERATE_WITH_PLAYER));
            }
            windowList.push(windowRow);
        }

        //Now we have to merge all of these windows into one big window to return
        var rows = [];
        for (var windowRow = 0; windowRow < windowList.length; windowRow++) { // For each row of windows
            var mergedRows = [];
            for (var windowColumn = 0; windowColumn < windowList[windowRow].length; windowColumn++) { // For each window in a row
                //For each row in a window
                for (var r = 0; r < windowList[windowRow][windowColumn].length; r++) {
                    if (windowColumn != 0) {
                        mergedRows[r] = mergedRows[r].concat(windowList[windowRow][windowColumn][r]);
                    } else {
                        mergedRows[r] = windowList[windowRow][windowColumn][r];
                    }
                }
            }

            if (windowRow != 0) {
                rows = rows.concat(mergedRows);
            } else {
                rows = mergedRows;
            }
        }
        //console.log(rows);
        return rows;
    };

    //This returns a window of the specified sector. Note, it does not include from other sectors. So if the window only include the top right corner of the specified sector, only the top right corner is returned.
    this.windowOfSector = function(centerPosition, width, height, generateIfNeeded) {
        //centerPosition holds the BlockPosition of the center block of the window
        //localX is the x position of the center of the window
        //localY is the y position of the center of the window

        var sector = this.getSector(centerPosition.sectorX, centerPosition.sectorY).rows;
        //First lets calculate the normal boundaries	   
        var localLeftBound = Math.floor((centerPosition.localX - (width / 2)));
        var localRightBound = Math.floor((centerPosition.localX + (width / 2)));
        var localTopBound = Math.floor((centerPosition.localY - (height / 2)));
        var localBottomBound = Math.floor((centerPosition.localY + (height / 2)));

        //Now lets limit the boundaries to the sector bounds
        if (localTopBound < 0) {
            localTopBound = 0;
        }
        if (localBottomBound > this.sectorHeight) {
            localBottomBound = this.sectorHeight - 1;
        }

        if (localLeftBound < 0) {
            localLeftBound = 0;
        }
        if (localRightBound > this.sectorWidth) {
            localRightBound = this.sectorWidth - 1;
        }

        if (this.sectorExists(centerPosition.sectorX, centerPosition.sectorY)) {
            //Now lets splice the array to get our array to return
            var rows = sector.slice(localTopBound, localBottomBound + 1);
            for (var i = 0; i < rows.length; i++) {
                rows[i] = rows[i].slice(localLeftBound, localRightBound + 1);
            }
            return rows;
        } else {
            //Sector doesnt exist

            if (generateIfNeeded) {
                //Add the non-existant sector to the generation que for the next tick if we are generating as the player moves/this sector needs generated
                this.generationQue.push(centerPosition);
            }

            //In the meantime, lets generate a blank set of blocks to be generated
            var blankRows = [];
            var row = [];
            var h = localBottomBound - localTopBound;
            var w = localRightBound - localLeftBound;
            for (var c = 0; c < w + 1; c++) {
                row.push(game.blockIdList["sky"]);
            }
            for (var r = 0; r < h + 1; r++) {
                blankRows.push(row);
            }
            
            var bs = new BlockSector();
		    bs.rows = blankRows;
		    bs.height = bs.rows.length;
		    bs.width = bs.rows[0].length;
		    
		    
            return blankRows;
        }
    };

    //Below, allows methods to access their parent
    this.getWindow.bind(this);
    this.sectorExists.bind(this);
    this.setSector.bind(this);
    this.windowOfSector.bind(this);
    this.sectorCoordsToSectorIndex.bind(this);
    this.getSector.bind(this);
}