Source: src/plugins/path/PathManagerPlugin.js

/**
* @author       Richard Davey <rich@photonstorm.com>
* @author       Pete Baron <pete@photonstorm.com>
* @copyright    2016 Photon Storm Ltd.
* @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
*/

/**
* PathManager controls a list of Paths and a list of PathFollowers.
* It is the central control for the majority of the Pathing API.
*
* @class Phaser.Plugin.PathManager
* @constructor
* @param {Phaser.Game} game - A reference to the current Phaser.Game instance.
* @param {Phaser.PluginManager} parent - The Phaser Plugin Manager which looks after this plugin.
*/
Phaser.Plugin.PathManager = function (game, parent) {

    Phaser.Plugin.call(this, game, parent);

    /**
     * @property {array} _list - list of paths
     * @private
     */
    this._list = [];

    /**
     * @property {array} _followers - list of path followers
     * @private
     */
    this._followers = [];

    this._branchRegistry = {};

};

Phaser.Plugin.PathManager.prototype = Object.create(Phaser.Plugin.prototype);
Phaser.Plugin.PathManager.prototype.constructor = Phaser.Plugin.PathManager;

/**
 * create a new Path from JSON data
 *
 * JSON data format:
 * required: "coordinateSystem":, "smoothness":, "loops":, "speed":, "pointList":[ {"x":, "y":}, ... ]
 * optional: "branchFrom": { "path":, "point": }, "joinTo": { "path":, "point": }
 */
Phaser.Plugin.PathManager.prototype.createPathsFromJSON = function(jsonKey) {

    var parse = this.game.cache.getJSON(jsonKey);
    var path;
    var createdPaths = [];
    var branchList = [];

    parse.paths.forEach(function(config) {
        path = new Phaser.Path(config.coordinateSystem, config.loops);
        path.name = config.name;
        this.addPoints(path, config.pointList, config.speed);
        this._list.push(path);
        createdPaths.push(path);
        config.pointList.reduce(function(list, pnt, index) {
            if (pnt.branchType === Phaser.Path.BranchTypes.ATTACHED) {
                list.push({                    path: path.name,
                    branchPath: pnt.branchPath,
                    pointIndex: index,
                    type: pnt.branchType
                });
            } else if (pnt.branchType === Phaser.Path.BranchTypes.JOINED) {
                list.push({                    path: pnt.branchPath,
                    branchPath: path.name,
                    pointIndex: pnt.branchPointIndex,
                    type: pnt.branchType
                });
            }
            return list;
        }, branchList);
    }, this);
    branchList.forEach(function(branch) {
        var mainPath = this.findPathByName(branch.path);
        var branchPath = this.findPathByName(branch.branchPath);
        var mainPathPointIndex = branch.pointIndex;
        if (branch.type === Phaser.Path.BranchTypes.ATTACHED) {
            this.attachBranch(branchPath, mainPath, mainPathPointIndex);
        } else if (branch.type === Phaser.Path.BranchTypes.JOINED) {
            this.joinBranch(branchPath, mainPath, mainPathPointIndex, false);
        }
    }, this);

    return createdPaths;
};

Phaser.Plugin.PathManager.prototype.addPath = function(path) {

    //  if path has points then addPoints, otherwise don't
    // this.addPoints(path, parse.pointList, parse.speed);

    this._list.push(path);

    return path;

};

// create a branching path and attach the start to an existing path
// when a PathFollower encounters the attachment point, it will be able to switch onto this new branch
//
// @param: count {value, optional}, make this branch counted (it won't be taken until a follower has passed it enough times)
Phaser.Plugin.PathManager.prototype.attachBranch = function(branchPath, mainPath, mainPathPointIndex, count) {

    if (typeof mainPath === 'string') {
        mainPath = this.findPathByName(mainPath);
    }

    var branchFromPoint = new Phaser.PathPoint();

    if (mainPath.getPathPoint(mainPathPointIndex, branchFromPoint)) {
        // move the first point of the branchPath to the branchFromPoint location
        branchPath.origin = branchFromPoint;
        var branchToPoint = branchPath.getPathPointReference(0);

        // attach the branch (use point reference so the changes go into the path)
        var branchFromPointRef = mainPath.getPathPointReference(mainPathPointIndex);

        this._branchAttach(branchFromPointRef, branchPath, 0);
        branchFromPointRef.branchType = Phaser.Path.BranchTypes.ATTACHED;

        // attach the branch's first point back to where it branched off from (for path reversal)
        branchToPoint.branchPath = mainPath;

        branchToPoint.branchPointIndex = mainPathPointIndex;

        // make sure this branch knows that it's using offset coordinates based on the first path point location
        branchPath.coordinateSystem = Phaser.Path.CoordinateSystems.OFFSET;
        branchPath.type = Phaser.Path.PathTypes.BRANCH;

        // set up counted branches data
        if (count !== undefined) {
            branchFromPointRef.data = {
                type: Phaser.PathPoint.DATA_COUNTER,
                value: count
            };
        }

        if (this._branchRegistry[branchPath.name]) {
            this._branchRegistry[branchPath.name].push(branchFromPointRef);
        } else {
            this._branchRegistry[branchPath.name] = [branchFromPointRef];
        }
    }

};



// attach the end of a path to an existing path
// when a PathFollower encounters the attachment point, it will automatically switch onto the attached path
Phaser.Plugin.PathManager.prototype.joinBranch = function(branchPath, mainPath, mainPathPointIndex, addPoint) {
    if (typeof addPoint === 'undefined') {
        addPoint = true;
    }
    if (typeof mainPath === 'string') {
        mainPath = this.findPathByName(mainPath);
    }

    var mainPathJoinPoint, branchLastPoint;

    mainPathJoinPoint = new Phaser.PathPoint();
    mainPath.getPathPoint(mainPathPointIndex, mainPathJoinPoint);

    if (mainPathJoinPoint) {
        if (addPoint) {


            var newBranchPoint = new Phaser.PathPoint();
            if (branchPath.getPathPoint(0, newBranchPoint)) {
                // make sure the newly added last path point is relative to the previously added first path point for the branch path by subtracting it out
                branchLastPoint = branchPath.addPathPoint(mainPathJoinPoint.x - newBranchPoint.x, mainPathJoinPoint.y - newBranchPoint.y, mainPathJoinPoint.vx, mainPathJoinPoint.vy, 1.0);
                this._branchAttach(branchLastPoint, mainPath, mainPathPointIndex);
            }
        } else {
            branchLastPoint = branchPath.getPathPointReference(branchPath.length - 1);
            this._branchAttach(branchLastPoint, mainPath, mainPathPointIndex);
        }
        branchLastPoint.branchType = Phaser.Path.BranchTypes.JOINED;
    }
    if (this._branchRegistry[branchPath.name]) {
        this._branchRegistry[branchPath.name].push(branchLastPoint);
    } else {
        this._branchRegistry[branchPath.name] = [branchLastPoint];
    }
};

// internal function, set the branching parameters of a PathPoint
Phaser.Plugin.PathManager.prototype._branchAttach = function(attachPoint, branchingPath, branchToPointIndex) {
    attachPoint.branchPath = branchingPath;
    attachPoint.branchPointIndex = branchToPointIndex;
};

Phaser.Plugin.PathManager.prototype._branchDetach = function(attachedPoint) {
    attachedPoint.branchPath = null;
    attachedPoint.branchPointIndex = null;
};


Phaser.Plugin.PathManager.prototype.removeBranch = function(branch) {
    if (typeof branch === 'string') {
        branch = this.findPathByName(branch);
    }
    this._branchRegistry[branch.name].forEach(function(point) {
        this._branchDetach(point);
    }, this);
    this._branchRegistry[branch.name] = null;
    this.removePath(this.pathIndex(branch));

};

// @return: the Path object which is at 'index' in the list
Phaser.Plugin.PathManager.prototype.getPath = function(index) {
    return this._list[index];
};



// add a list of points to a Path
Phaser.Plugin.PathManager.prototype.addPoints = function(path, pointList, speed) {
    if (speed === undefined) speed = 1.0;

    for (var i = 0; i < pointList.length; i++) {        path.addPathPoint(pointList[i].x, pointList[i].y, pointList[i].vx, pointList[i].vy, speed, pointList[i].data);
    }

    return path.numPoints();
};

// @return: the Path object matching 'name' in the list
Phaser.Plugin.PathManager.prototype.findPathByName = function(name) {
    for (var i = 0; i < this._list.length; i++) {        if (this._list[i].name == name) {
            return this._list[i];
        }
    }

    return null;

};


Phaser.Plugin.PathManager.prototype.findPathByPoint = function(point) {
    var l = this._list.length;

    for (var i = 0; i < l; i++) {        if (this._list[i].pointIndex(point) > -1) {
            return this._list[i];
        }
    }

};

/*
 *  FOLLOWERS
 *
 *  the following functions control PathFollower objects
 *
 */

// create a new PathFollower and add it to the list
// @param: physicsAdjustTime - how quickly does a physics object attempt to get back to the path's virtual particle position (milliseconds), 0 = it's not a physics object
// @return: the new PathFollower object
Phaser.Plugin.PathManager.prototype.addFollower = function(path, follower, speed, rotationOffset, angularOffset, callbackAtEnd, physicsAdjustTime) {

    var f = new Phaser.PathFollower(path, follower, speed, rotationOffset, angularOffset, callbackAtEnd, physicsAdjustTime);

    this._followers.push(f);

    return f;

};

// update all PathFollower objects in the _followers list
// this will automatically move them along the Paths
//  was called updateFollowers
Phaser.Plugin.PathManager.prototype.update = function() {

    //  move this to a plugin var
    //var elapsedTime = 1.0;

    for (var i = this._followers.length - 1; i >= 0; --i) {
        var f = this._followers[i];

        // when a follower's update returns false, kill it
        if (!f.update(this.game.time.elpased)) {
            // callback for this follower when it dies
            if (f.callbackAtEnd) {
                f.callbackAtEnd(f.follower);
            }

            // destroy the follower
            f.destroy();

            // remove the follower from the list
            this._followers.splice(i, 1);
        }
    }

};

// remove all PathFollowers on this path without destroying their attached graphic objects
// (eg. a long line of enemies use a path to enter, then switch to AI control on arrival maintaining their relative positions)
Phaser.Plugin.PathManager.prototype.removeAllFollowers = function(path) {
    for (var i = this._followers.length - 1; i >= 0; --i) {
        var f = this._followers[i];

        if (f.path == path) {
            // callback for this follower when it dies
            if (f.callbackAtEnd) {
                f.callbackAtEnd(f.follower);
            }

            // destroy the follower
            f.destroy();

            // remove the follower from the list
            this._followers.splice(i, 1);
        }
    }

};

Phaser.Plugin.PathManager.prototype.pathIndex = function(path) {
    return this._list.indexOf(path);
};

Phaser.Plugin.PathManager.prototype.removePath = function(pathIndex) {
    this.removeAllFollowers(this.getPath(pathIndex));
    if (pathIndex < this._list.length) {        return this._list.splice(pathIndex, 1);
    } else {
        throw new Error("ERROR: Cannot remove non-existent path");
    }
};



/*
 *  DEBUG DRAWING OF ALL PATHS
 */

// draw all paths
Phaser.Plugin.PathManager.prototype.drawPaths = function(graphics) {
    for (var i = 0; i < this._list.length; i++) {        this._list[i].debug(graphics);
    }
};