Grid.js

/**
 * Represents a Grid object.
 * @constructor
 * @param {number} gridW - The width of the grid in number of tiles.
 * @param {number} gridH - The height of the grid in number of tiles.
 * @param {number} gridX - The x coordinate of the center of the grid in WC.
 * @param {number} gridY - The y coordinate of the center of the grid in WC.
 * @param {number} tileW - The width of each tile in WC.
 * @param {number} tileH - The height of each tile in WC.
 */
function Grid(gridW, gridH, gridX, gridY, tileW, tileH) {
    this.mTiles = []; // This array will be filled with array objects of tiles
    this.mTileW = tileW;
    this.mTileH = tileH;

    this.mGridW  = gridW;
    this.mGridH = gridH;
    this.mGridX = gridX;
    this.mGridY = gridY;

    // Track objects that have been given to the Grid to control movement of.
    this.mObjects = [];

    // Filling grid with tile object in grid dimensions given. May move to another func.
    for (var i = 0; i < gridH; i++) { // Col loop
      var row = [];
      for (var j = 0; j < gridW; j++) { // Row loop
        var tilex = (gridX - (tileW * Math.floor(gridW / 2))) + (j * tileW);
        var tiley = (gridY - (tileH * Math.floor(gridH / 2))) + (i * tileH);
        var isEdge = false;
        // Even case for X; offset
        if (gridW % 2 === 0) { 
          tilex = tilex + (tileW / 2);
        }
        // Even case for Y; offset
        if (gridH % 2 === 0) { 
          tiley = tiley + (tileH / 2);
        }
        // Tile is an edge tile; Mark it as such.
        if (i === (gridH - 1) || i === 0 || j === (gridW - 1) || j === 0) {
          isEdge = true;
        }
        row.push(new Tile(tileW, tileH, tilex, tiley, isEdge));
      }
      this.mTiles.push(row);
    }

    // add walls to the edges of the table
    // for prototyping purposes
    for (var i = 0; i < this.mGridW; i++) {
        this.mTiles[0][i].addWall([0, -1]); // add wall to bottom
        this.mTiles[this.mGridH - 1][i].addWall([0, 1]); // add wall to top
    }
    
    for (var i = 0; i < this.mGridH; i++) {
        this.mTiles[i][0].addWall([-1, 0]); // add wall to left
        this.mTiles[i][this.mGridW - 1].addWall([1, 0]); // add wall to right
    }
}

/**
 * Enum for Directional values.
 * @readonly
 * @enum {array}
 */
Grid.eDirection = Object.freeze({
    eNorth: [0, 1],
    eEast: [1, 0],  
    eSouth: [0, -1],
    eWest: [-1, 0]
});

/**
 * Draws all tiles within the grid.
 * @param {Camera} cam - The camera object to draw the grid to.
 */
Grid.prototype.draw = function(cam) {
  for (var i = 0; i < this.mGridH; i++) { // Col loop
    for (var j = 0; j < this.mGridW; j++) { // Row loop
      var tile = this.mTiles[i][j];
      tile.draw(cam); // Will draw textures if visible, and edges if set to do so
    }
  }
};

/**
 * Sets all the visualize edge states of the tiles within the grid to given boolean value.
 * @param {boolean} bool - State to set the visualization of the edges of tiles to.
 */
Grid.prototype.setVisualizeEdges = function(bool) {
  for (var i = 0; i < this.mGridH; i++) { // Col loop
    for (var j = 0; j < this.mGridW; j++) { // Row loop
      var tile = this.mTiles[i][j];
      tile.setGridLineVisibility(bool);
    }
  }
};

// Setting all the edge visualization flags of the tiles to given bool
Grid.prototype.setVisualizeWalls = function(bool) {
  for (var i = 0; i < this.mGridH; i++) { // Col loop
    for (var j = 0; j < this.mGridW; j++) { // Row loop
      var tile = this.mTiles[i][j];
      tile.setWallVisibility(bool);
    }
  }
};

/**
 * Get the current width of tiles within the Grid in WC.
 * @returns {number} The width of all tiles in WC.
 */
Grid.prototype.getTileWidth = function() {
  return this.mTileW;
};

/**
 * Get the current height of tiles within the Grid in WC.
 * @returns {number} The height of all tiles in WC.
 */
Grid.prototype.getTileHeight = function() {
  return this.mTileH;
};

/**
 * Checks to see if a given WC is contained within the grid object.
 * @param {number} x - The x coordinate of the given WC position.
 * @param {number} y - The y coordinate of the given WC position.
 * @returns {boolean} True if given WC position lies within the Grid, false otherwise.
 */
Grid.prototype.isWCValidInGrid = function(x, y) {
  // This is a bit ugly, so I'm gonna condense it later.
  // For now, I just wanted to make the math on this easier to read. ✓
  if (x < (this.mGridX - ((this.mGridW / 2) * this.mTileW))) {
      return false;
  } else if (x > (this.mGridX + ((this.mGridW / 2) * this.mTileW))) {
      return false;
  } else if (y < (this.mGridY - ((this.mGridH / 2) * this.mTileH))) {
      return false;
  } else if (y > (this.mGridY + ((this.mGridH / 2) * this.mTileH))) {
      return false;
  }
  return true;
};

/**
 * Gets the index of the tile at the given WC.
 * @param {number} x - The x coordinate of the given WC position.
 * @param {number} y - The y coordinate of the given WC position.
 * @returns {array} The Grid index coordinate of the corresponding tile, [-1, -1] if given WC is not in the Grid.
 */
Grid.prototype.getTileAtWC = function(x, y) {
    // return -1, -1 if the WC is not in the grid
    if (!this.isWCValidInGrid(x, y)) {
        return [-1, -1];
    } else {
        // subtract the left bound of the grid from x
        x = x - (this.mGridX - ((this.mGridW / 2) * this.mTileW));
        // subtract the bottom bound of the grid form y
        y = y - (this.mGridY - ((this.mGridH / 2) * this.mTileH));
        
        // We need to do some error checking here, just to make sure
        // we don't return an invalid coordinate - cause guess what?
        // There is an exactly 1 (one) pixel wide/tall line where you
        // can get a coordinate larger than is actually contained in the grid. 
        var foundX = Math.min(this.mGridW - 1, Math.floor(x / this.mTileW));
        var foundY = Math.min(this.mGridH - 1, Math.floor(y / this.mTileH));
        
        return vec2.fromValues(foundX, foundY);
    }
};

/**
 * Gets the tile corresponding to the given index coordinate.
 * @param {number} x - The x coordinate of the desired Grid index coordinate.
 * @param {number} y - The y coordinate of the desired Grid index coordinate.
 * @returns {Tile} The tile object corresponding to the given index. Returns null if given invalid index.
 */
Grid.prototype.getTileAtIndex = function(x, y) {
  if (x >= 0 && x <= (this.mGridW - 1) && y >= 0 && y <= (this.mGridH - 1)) {
    return this.mTiles[y][x];
  } else {
    return null;
  }
};


/**
 * Adds a wall of the given direction to the tile at the index shown. Also adds a wall to 
 * the tile the wall resides next to, so that the wall cannot be passed through in either direction.
 * @param {number} x - The x coordinate of the desired Grid index coordinate.
 * @param {number} y - The y coordinate of the desired Grid index coordinate.
 * @returns {boolean} True if the wall was able to be added, false otherwise.
 */
Grid.prototype.addWall = function(x, y, direction) {
  var initTile = this.getTileAtIndex(x, y);
  if (initTile === null) {
    return false;
  }

  var successful = initTile.addWall(direction);
  if (!successful) {
    return false;
  }

  var oppositeDir = [direction[0] * -1, direction[1] * -1];
  var nextTile = this.getTileAtIndex(x + direction[0], y + direction[1]);
  if (nextTile !== null) {
    nextTile.addWall(oppositeDir);
  }
  
  return true;
};


/**
 * Runs an A* Search algorithm given two Grid index positions within the grid to find the optimal 
 * path between them. Considers moving into collidable tiles & through walls as invalid moves.
 * @param {} start -
 * @param {} finish -
 * @returns {array} An array of sequential moves to get to the desired position. Will be empty if 
 * a path was not found.
 */
Grid.prototype.AStarSearch = function(start, finish) {
    
    // clear any data from previous searches
    this.readySearch();
    
    // initialize search
    var openList = [];
    var visitedList = [];
    openList.push(start);
    
    while (openList.length > 0) {
        // find the tile in our list with the lowest combined
        // cost and heuristic score (we get this with getF())
        var bestIndex = 0;
        for (var i = 0; i < openList.length; i++) {
            var tempTile = this.getTile(openList[bestIndex]);
            var tempTile2 = this.getTile(openList[i]);
            // find the lowest f score (f = cost + heuristic)
            if (tempTile.getF() > tempTile2.getF()) {
                bestIndex = i;
            }
        }
        // currentPos - cPos. Is a vec2 of a position in grid
        var cPos = openList[bestIndex];
        
        // if we've reached the end case, return the path found
        if (cPos[0] === finish[0] && cPos[1] === finish[1]) {
            // get the direction from the parent
            var curr = cPos;
            var ret = [];
            var totalMoves = this.getTile(cPos).getC();
            for (var i = 0; i < totalMoves; i++) {
                var dir = this.getTile(curr).getD();
                // push the direction from the tile
                ret.push(dir);
                // and move to the tiles parent
                curr = [curr[0] - dir[0], curr[1] - dir[1]];
            }
            // return the retrieved directions in reverse order
            return ret.reverse();
        }
        
        // remove currentTile from openList, and add to visited
        cPos = openList.splice(bestIndex, 1)[0];
        visitedList.push(cPos);
        var neighbors = this.getNeighbors(cPos);
        
        // now, go through all possible successors of current tile
        for (var i = 0; i < neighbors.length; i++) {
            var neighbor =  neighbors[i];
            var nTile = this.getTile(neighbor);
            
            // don't bother examining if we haven't visited already
            var visitedAlready = false;
            for (var k = 0; k < visitedList.length; k++) {
                var temp = visitedList[k];
                if (neighbor[0] === temp[0] && neighbor[1] === temp[1]) {
                    visitedAlready = true;
                }
            }
            
            // if we haven't visited it before
            if (!visitedAlready) {
                var cScore = this.getTile(cPos).getC() + 1;
                var cScoreIsBest = false;
                var found = false;
                // determine if we've seen this node before
                for (var k = 0; k < openList.length; k++) {
                    var temp = openList[k];
                    if (neighbor[0] === temp[0] && neighbor[1] === temp[1]) {
                        found = true;
                    }
                }
                // if we have not seen this tile, add it to the openList
                if (!found) {
                    cScoreIsBest = true;
                    nTile.setH(this.manhattanDistance(neighbor, finish));
                    openList.push(neighbor);
                }
                else if (cScore < nTile.getC()) {
                    // we've seen the node, but we have a better score
                    cScoreIsBest = true;
                }

                if (cScoreIsBest) {
                    // found an optimal path to the tile
                    nTile.setC(cScore);

                    // so, every tile stores the direction to it from the
                    // predecessor. so, grab the direction we took from
                    // the current tile.
                    nTile.setD([neighbor[0] - cPos[0], neighbor[1] - cPos[1]]);
                }
            }
        }
    }
    
    // if we've reached here, that means no path has been
    // found. return an empty array.
    return [];
};

/**
 * Helper function for A* search that resets data from any previous searches performed.
 */
Grid.prototype.readySearch = function() {
    for (var x = 0; x < this.mGridW; x++) {
        for (var y = 0; y < this.mGridH; y++) {
            this.mTiles[y][x].readySearch();
        }
    }
};

/**
 * Manhattan distance heuristic for A* search.
 * @param {array} pos1 - A position
 * @param {array} pos2 - A different position
 * @returns {number} Manhattan distance heuristic result 
 */
Grid.prototype.manhattanDistance = function(pos1, pos2) {
    return Math.abs(pos2[0] - pos1[0]) + Math.abs(pos2[1] - pos1[1]);
};

/**
 * A function used for grabbing the given position's neighboring tiles, based off of valid
 * movement from the given tile.
 * @param {array} curr - short for current position
 * @returns {array} containing neighboring tiles that can be reached from the given position
 */
Grid.prototype.getNeighbors = function(curr) {
    var ret = [];
    var cTile = this.getTile(curr);
    var moves = cTile.getMoves();
    for (var i = 0; i < moves.length; i++) {
        var temp = vec2.fromValues(curr[0] + moves[i][0], curr[1] + moves[i][1]);
        ret.push(temp);
    }
    return ret;
};