/** * The MIT License (MIT) * Copyright (c) 2014 Raphaël Roux * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * * * */ /** * @author Raphaël Roux * @copyright 2014 Raphaël Roux * @license {@link http://opensource.org/licenses/MIT} */ /** * AStar is a phaser pathfinding plugin based on an A* kind of algorythm * It works with the Phaser.Tilemap * * @class Phaser.Plugin.AStar * @constructor * @param {Any} parent - The object that owns this plugin, usually Phaser.PluginManager. */ Phaser.Plugin.AStar = function (parent) { /** * @property {Any} parent - The parent of this plugin. If added to the PluginManager the parent will be set to that, otherwise it will be null. */ this.parent = parent; /** * @property {Phaser.Tilemap} _tilemap - A reference to the tilemap used to store astar nodes according to the Phaser.Tilemap structure. */ this._tilemap; /** * @property {number} _layerIndex - The layer index of the tilemap that is used to store astar nodes. */ this._layerIndex; /** * @property {number} _tilesetIndex - The tileset index of the tileset that handle tiles properties. */ this._tilesetIndex; /** * @property {array} _open - An array that references nodes to be considered by the search path algorythm. */ this._open; /** * @property {array} _closed - An array that references nodes not to consider anymore. */ this._closed; /** * @property {array} _visited - Internal array of visited tiles, use for debug pupose. */ this._visited; /** * @property {boolean} _useDiagonal - Does the astar algorythm can use tile diagonal? * @default true */ this._useDiagonal = true; /** * @property {boolean} _findClosest - Does the findPath algorythm must calculate the closest result if destination is unreachable. If not findPath will return an empty array * @default true */ this._findClosest = true; /** * @property {string} _walkablePropName - Wich name have the walkable propertiy in your tileset. * @default 'walkable' */ this._walkablePropName = 'walkable'; /** * @property {function} _distanceFunction - The function used to calculate distance. */ this._distanceFunction = Phaser.Plugin.AStar.DISTANCE_EUCLIDIAN; /** * @property {Phaser.Plugin.AStar.AStarPath} _lastPath - The last path calculated by astar. */ this._lastPath = null; /** * @property {boolean} _debug - Boolean to debug mode, stores visited nodes, and have a cost. Disable in production. * @default false */ this._debug = true; }; Phaser.Plugin.AStar.prototype = Object.create(Phaser.Plugin.prototype); Phaser.Plugin.AStar.prototype.constructor = Phaser.Plugin.AStar; Phaser.Plugin.AStar.VERSION = '0.0.101'; Phaser.Plugin.AStar.COST_ORTHOGONAL = 1; Phaser.Plugin.AStar.COST_DIAGONAL = Phaser.Plugin.AStar.COST_ORTHOGONAL*Math.sqrt(2); Phaser.Plugin.AStar.DISTANCE_MANHATTAN = 'distManhattan'; Phaser.Plugin.AStar.DISTANCE_EUCLIDIAN = 'distEuclidian'; /** * Sets the Phaser.Tilemap used to searchPath into. * @method Phaser.Plugin.AStar#setAStarMap * @public * @param {Phaser.Tilemap} map - the Phaser.Tilemap used to searchPath into. It must have a tileset with tile porperties to know if tiles are walkable or not. * @param {string} layerName - The name of the layer that handle tiles. * @param {string} tilesetName - The name of the tileset that have walkable properties. * @return {Phaser.Plugin.AStar} The Phaser.Plugin.AStar itself. */ Phaser.Plugin.AStar.prototype.setAStarMap = function(map, layerName, tilesetName) { this._tilemap = map; this._layerIndex = this._tilemap.getLayerIndex(layerName);; this._tilesetIndex = this._tilemap.getTilesetIndex(tilesetName); this.updateMap(); return this; }; /** * Sets the Phaser.Tilemap used to searchPath into. * @method Phaser.Plugin.AStar-setAStarMap * @private * @return {void} The Phaser.Plugin.AStar itself. */ Phaser.Plugin.AStar.prototype.updateMap = function() { var tile; var walkable; //for each tile, add a default AStarNode with x, y and walkable properties according to the tilemap/tileset datas for(var y=0; y < this._tilemap.height; y++) { for(var x=0; x < this._tilemap.width; x++) { tile = this._tilemap.layers[this._layerIndex].data[y][x]; walkable = this._tilemap.tilesets[this._tilesetIndex].tileProperties[tile.index - 1][this._walkablePropName] !== "false" ? true : false; tile.properties.astarNode = new Phaser.Plugin.AStar.AStarNode(x, y, walkable); } } }; /** * Find a path between to tiles coordinates * @method Phaser.Plugin.AStar#findPath * @public * @param {Phaser.Point} startPoint - The start point x, y in tiles coordinates to search a path. * @param {Phaser.Point} goalPoint - The goal point x, y in tiles coordinates that you trying to reach. * @return {Phaser.Plugin.AStar.AStarPath} The Phaser.Plugin.AStar.AStarPath that results */ Phaser.Plugin.AStar.prototype.findPath = function(startPoint, goalPoint) { var path = new Phaser.Plugin.AStar.AStarPath(); var start = this._tilemap.layers[this._layerIndex].data[startPoint.y][startPoint.x].properties.astarNode; //:AStarNode; var goal = this._tilemap.layers[this._layerIndex].data[goalPoint.y][goalPoint.x].properties.astarNode path.start = start; path.goal = goal; this._open = []; this._closed = []; this._visited = []; this._open.push(start); start.g = 0; start.h = this[this._distanceFunction](start, goal); start.f = start.h; start.parent = null; //Loop until there are no more nodes to search while(this._open.length > 0) { //Find lowest f in this._open var f = Infinity; var x; for (var i=0; i 0) { n = map[y][x-1].properties.astarNode; if (n.walkable) { n.travelCost = Phaser.Plugin.AStar.COST_ORTHOGONAL; neighbors.push(n); } } //East if (x < this._tilemap.width-1) { n = map[y][x+1].properties.astarNode; if (n.walkable) { n.travelCost = Phaser.Plugin.AStar.COST_ORTHOGONAL; neighbors.push(n); } } //North if (y > 0) { n = map[y-1][x].properties.astarNode; if (n.walkable) { n.travelCost = Phaser.Plugin.AStar.COST_ORTHOGONAL; neighbors.push(n); } } //South if (y < this._tilemap.height-1) { n = map[y+1][x].properties.astarNode; if (n.walkable) { n.travelCost = Phaser.Plugin.AStar.COST_ORTHOGONAL; neighbors.push(n); } } //If diagonals aren't used do not search for other neighbors and return orthogonal search result if(this._useDiagonal === false) return neighbors; //NorthWest if (x > 0 && y > 0) { n = map[y-1][x-1].properties.astarNode; if (n.walkable && map[y][x-1].properties.astarNode.walkable && map[y-1][x].properties.astarNode.walkable ) { n.travelCost = Phaser.Plugin.AStar.COST_DIAGONAL; neighbors.push(n); } } //NorthEast if (x < this._tilemap.width-1 && y > 0) { n = map[y-1][x+1].properties.astarNode; if (n.walkable && map[y][x+1].properties.astarNode.walkable && map[y-1][x].properties.astarNode.walkable ) { n.travelCost = Phaser.Plugin.AStar.COST_DIAGONAL; neighbors.push(n); } } //SouthWest if (x > 0 && y < this._tilemap.height-1) { n = map[y+1][x-1].properties.astarNode; if (n.walkable && map[y][x-1].properties.astarNode.walkable && map[y+1][x].properties.astarNode.walkable ) { n.travelCost = Phaser.Plugin.AStar.COST_DIAGONAL; neighbors.push(n); } } //SouthEast if (x < this._tilemap.width-1 && y < this._tilemap.height-1) { n = map[y+1][x+1].properties.astarNode; if (n.walkable && map[y][x+1].properties.astarNode.walkable && map[y+1][x].properties.astarNode.walkable ) { n.travelCost = Phaser.Plugin.AStar.COST_DIAGONAL; neighbors.push(n); } } return neighbors; }; /** * Calculate a distance between tow astar nodes coordinates according to the Manhattan method * @method Phaser.Plugin.AStar-distManhattan * @private * @param {Phaser.Plugin.AStar.AStarNode} nodeA - The A node. * @param {Phaser.Plugin.AStar.AStarNode} nodeB - The B node. * @return {number} The distance between nodeA and nodeB */ Phaser.Plugin.AStar.prototype.distManhattan = function (nodeA, nodeB) { return Math.abs(nodeA.x - nodeB.x) + Math.abs(nodeA.y - nodeB.y); }; /** * Calculate a distance between tow astar nodes coordinates according to the Euclidian method. More accurate * @method Phaser.Plugin.AStar-distEuclidian * @private * @param {Phaser.Plugin.AStar.AStarNode} nodeA - The A node. * @param {Phaser.Plugin.AStar.AStarNode} nodeB - The B node. * @return {number} The distance between nodeA and nodeB */ Phaser.Plugin.AStar.prototype.distEuclidian = function(nodeA, nodeB) { return Math.sqrt(Math.pow((nodeA.x - nodeB.x), 2) + Math.pow((nodeA.y -nodeB.y), 2)); }; /** * Tells if a tile is walkable from its tilemap coordinates * @method Phaser.Plugin.AStar-isWalkable * @public * @param {number} x - The x coordiante of the tile in tilemap's coordinate. * @param {number} y - The y coordinate of the tile in tilemap's coordinate. * @return {boolean} The distance between nodeA and nodeB */ Phaser.Plugin.AStar.prototype.isWalkable = function(x, y) { return this._tilemap.layers[this._layerIndex].data[y][x].properties.astarNode.walkable; }; /** * @properties {string} version - The version number of Phaser.Plugin.AStar read only */ Object.defineProperty(Phaser.Plugin.AStar.prototype, "version", { get: function () { return Phaser.Plugin.AStar.VERSION; } }); /** * AStarNode is an object that stores AStar value. Each tile have an AStarNode in their properties * @class Phaser.Plugin.AStar.AStarNode * @constructor * @param {number} x - The x coordinate of the tile. * @param {number} y - The y coordinate of the tile. * @param {boolean} isWalkable - Is this tile is walkable? */ Phaser.Plugin.AStar.AStarNode = function(x, y, isWalkable) { /** * @property {number} x - The x coordinate of the tile. */ this.x = x; /** * @property {number} y - The y coordinate of the tile. */ this.y = y; /** * @property {number} g - The total travel cost from the start point. Sum of COST_ORTHOGONAL and COST_DIAGONAL */ this.g = 0; /** * @property {number} h - The remaing distance as the crow flies between this node and the goal. */ this.h = 0; /** * @property {number} f - The weight. Sum of g + h. */ this.f = 0; /** * @property {Phaser.Plugin.AStar.AStarNode} parent - Where do we come from? It's an AStarNode reference needed to reconstruct a path backwards (from goal to start point) */ this.parent; /** * @property {boolean} walkable - Is this node is walkable? */ this.walkable = isWalkable; /** * @property {number} travelCost - The cost to travel to this node, COST_ORTHOGONAL or COST_DIAGONAL */ this.travelCost; }; /** * AStarPath is an object that stores a searchPath result. * @class Phaser.Plugin.AStar.AStarPath * @constructor * @param {array} nodes - An array of nodes coordinates sorted backward from goal to start point. * @param {Phaser.Plugin.AStarNode} start - The start AStarNode used for the searchPath. * @param {Phaser.Plugin.AStarNode} goal - The goal AStarNode used for the searchPath. */ Phaser.Plugin.AStar.AStarPath = function(nodes, start, goal) { /** * @property {array} nodes - Array of AstarNodes x, y coordiantes that are the path solution from goal to start point. */ this.nodes = nodes || []; /** * @property {Phaser.Plugin.Astar.AStarNode} start - Reference to the start point used by findPath. */ this.start = start || null; /** * @property {Phaser.Plugin.Astar.AStarNode} goal - Reference to the goal point used by findPath. */ this.goal = goal || null; /** * @property {array} visited - Array of AStarNodes that the findPath algorythm has visited. Used for debug only. */ this.visited = []; }; /** * Debug method to draw the last calculated path by AStar * @method Phaser.Utils.Debug.AStar * @param {Phaser.Plugin.AStar} astar- The AStar plugin that you want to debug. * @param {number} x - X position on camera for debug display. * @param {number} y - Y position on camera for debug display. * @param {string} color - Color to stroke the path line. * @return {void} */ Phaser.Utils.Debug.prototype.AStar = function(astar, x, y, color, showVisited) { if (this.context == null) { return; } var pathLength = 0; if(astar._lastPath !== null) { pathLength = astar._lastPath.nodes.length; } color = color || 'rgb(255,255,255)'; game.debug.start(x, y, color); if(pathLength > 0) { var node = astar._lastPath.nodes[0]; this.context.strokeStyle = color; this.context.beginPath(); this.context.moveTo((node.x * astar._tilemap.tileWidth) + (astar._tilemap.tileWidth/2) - game.camera.view.x, (node.y * astar._tilemap.tileHeight) + (astar._tilemap.tileHeight/2) - game.camera.view.y); for(var i=0; i