Source: out/plugins/AStar/AStar.js

/**
 * 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<this._open.length; i++)         {
            if (this._open[i].f < f)             {
                x = this._open[i];
                f = x.f;
            }
        }

        //Solution found, return solution
        if (x == goal)
        {
            path.nodes = this.reconstructPath(goal);
            this._lastPath = path;
            if(this._debug === true) path.visited = this._visited;
            return path;
        }

        //Close current node
        this._open.splice(this._open.indexOf(x), 1);
        this._closed.push(x);

        //Then get its neighbors
        var n = this.neighbors(x);

        for(var yIndex=0; yIndex < n.length; yIndex++)         {

            var y = n[yIndex];

            if (-1 != this._closed.indexOf(y))
                continue;

            var g = x.g + y.travelCost;
            var better = false;

            //Add the node for being considered next loop.
            if (-1 == this._open.indexOf(y))
            {
                    this._open.push(y);
                    better = true;
                    if(this._debug === true) this.visit(y);
            }
            else if (g < y.g)             {
                    better = true;
            }

            if (better) {
                    y.parent = x;
                    y.g = g;
                    y.h = this[this._distanceFunction](y, goal);
                    y.f = y.g + y.h;
            }

        }

    }

    //If no solution found, does A* try to return the closest result?
    if(this._findClosest === true)
    {
        var min = Infinity;
        var closestGoal, node, dist;
        for(var i=0, ii=this._closed.length; i<ii; i++)         {
            node = this._closed[i];

            var dist = this[this._distanceFunction](goal, node);
            if (dist < min)             {
                min = dist;
                closestGoal = node;
            }
        }

        //Reconstruct a path a path from the closestGoal
        path.nodes = this.reconstructPath(closestGoal);
        if(this._debug === true) path.visited = this._visited;
    }

    this._lastPath = path;

    return path;
};


/**
 * Reconstruct the result path backwards from the goal point, crawling its parents. Internal method.
 * @method Phaser.Plugin.AStar-reconstructPath
 * @private
 * @param {Phaser.Plugin.AStar.AStarNode} n - The astar node from wich you want to rebuild the path.
 * @return {array} An array of Phaser.Plugin.AStar.AStarNode
 */
Phaser.Plugin.AStar.prototype.reconstructPath = function(n)
{
    var solution = [];
    var nn = n;
    while(nn.parent) {
            solution.push({x: nn.x, y: nn.y});
            nn = nn.parent;
    }
    return solution;
};


/**
 * Add a node into visited if it is not already in. Debug only.
 * @method Phaser.Plugin.AStar-visit
 * @private
 * @param {Phaser.Plugin.AStar.AStarNode} node - The astar node you want to register as visited
 * @return {void}
 */
Phaser.Plugin.AStar.prototype.visit = function(node)
{
    for(var i in this._visited)
    {
        if (this._visited[i] == node) return;
    }

    this._visited.push(node);
};


/**
 * Add a node into visited if it is not already in. Debug only.
 * @method Phaser.Plugin.AStar-neighbors
 * @private
 * @param {Phaser.Plugin.AStar.AStarNode} n - The astar node you want to register as visited
 * @return {void}
 */
Phaser.Plugin.AStar.prototype.neighbors = function(node)
{
    var x = node.x;
    var y = node.y;
    var n = null;
    var neighbors = [];

    var map = this._tilemap.layers[this._layerIndex].data;

    //West
    if (x > 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<pathLength; i++)        {
            node = astar._lastPath.nodes[i];
            this.context.lineTo((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);
        }

        this.context.lineTo((astar._lastPath.start.x * astar._tilemap.tileWidth) + (astar._tilemap.tileWidth/2) - game.camera.view.x, (astar._lastPath.start.y * astar._tilemap.tileHeight) + (astar._tilemap.tileHeight/2) - game.camera.view.y);

        this.context.stroke();

        //Draw circles on visited nodes
        if(showVisited !== false)
        {
            var visitedNode;
            for(var j=0; j < astar._lastPath.visited.length; j++)            {
                visitedNode = astar._lastPath.visited[j];
                this.context.beginPath();
                this.context.arc((visitedNode.x * astar._tilemap.tileWidth) + (astar._tilemap.tileWidth/2) - game.camera.view.x, (visitedNode.y * astar._tilemap.tileHeight) + (astar._tilemap.tileHeight/2) - game.camera.view.y, 2, 0, Math.PI*2, true);
                this.context.stroke();
            }
        }
    }

    this.line('Path length: ' + pathLength);
    this.line('Distance func: ' + astar._distanceFunction);
    this.line('Use diagonal: ' + astar._useDiagonal);
    this.line('Find Closest: ' + astar._findClosest);

    game.debug.stop();
};