/**
* 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;
};