waves/public/assets/g/tekpro/pathfind.js
2025-04-09 17:11:14 -05:00

685 lines
15 KiB
JavaScript

// Pathfind.js
// Written by Ashley Gullen
// Copyright (c) 2013 Scirra Ltd.
// A* pathfinding javascript implementation
"use strict";
(function() {
var PF_CLEAR = 0;
var PF_OBSTACLE = 32767;
function node()
{
this.parent = null;
this.x = 0;
this.y = 0;
this.f = 0;
this.g = 0;
this.h = 0;
};
var nodeCache = []; // for recycling nodes
function allocNode()
{
var ret;
if (nodeCache.length)
ret = nodeCache.pop();
else
ret = new node();
ret.parent = null;
ret.x = 0;
ret.y = 0;
ret.f = 0;
ret.g = 0;
ret.h = 0;
return ret;
};
function freeNode(n)
{
if (nodeCache.length < 64000)
nodeCache.push(n);
};
function resultNode(x_, y_)
{
this.x = x_ || 0;
this.y = y_ || 0;
};
var resultNodeCache = []; // for recycling resultNodes
function allocResultNode()
{
if (resultNodeCache.length)
return resultNodeCache.pop();
else
return new resultNode(0, 0);
};
function freeResultNode(n)
{
if (resultNodeCache.length < 10000)
resultNodeCache.push(n);
};
var workersSupported = (typeof Worker !== "undefined");
var isInWebWorker = (typeof document === "undefined"); // no DOM in a worker
var myInstance = null; // single pathfinder instance for worker
if (isInWebWorker)
{
self.addEventListener("message", function (e)
{
var d = e.data;
if (!d)
return; // could be empty postMessage() to start worker
if (d.cmd === "init")
{
if (!myInstance)
myInstance = new pathfinder();
myInstance.init(d.hcells, d.vcells, d.data, d.diagonals);
}
else if (d.cmd === "find")
{
// receive a pathfind job, process it them dispatch result
myInstance.pathEnd.parent = null;
myInstance.targetX = d.endX;
myInstance.targetY = d.endY;
if (myInstance.doLongFindPath(d.startX, d.startY))
{
self.postMessage({ cmd: "result", pathList: myInstance.pathList });
}
else
{
self.postMessage({ cmd: "result", pathList: null });
}
}
else if (d.cmd === "region")
{
myInstance.writeCells(d.offx, d.offy, d.lenx, d.leny, d.data);
}
else if (d.cmd === "setdiag")
{
// update diagonalsEnabled flag
if (myInstance)
myInstance.diagonalsEnabled = d.diagonals;
}
}, false);
}
function createWorker(url, callback)
{
// Create normally as a same-origin worker
try {
var worker = new Worker(url);
callback(worker);
}
catch (err)
{
// WKWebView throws because it treats this as cross-origin. We also can't fetch the script
// as a blob either for the same reason. So fall back to the Cordova file API.
var errorFunc = function (err) {
console.error("Error creating worker: ", err);
};
var path = window["cordova"]["file"]["applicationDirectory"] + "www/" + url;
window["resolveLocalFileSystemURL"](path, function (entry)
{
entry["file"](function (file)
{
var reader = new FileReader();
reader.onerror = errorFunc;
reader.onload = function (e)
{
var arrayBuffer = e.target.result;
var blob = new Blob([arrayBuffer], { type: "application/javascript" });
var worker = new Worker(URL.createObjectURL(blob));
callback(worker);
};
reader.readAsArrayBuffer(file);
}, errorFunc);
}, errorFunc);
}
}
function pathfinder()
{
this.hcells = 0;
this.vcells = 0;
this.pathEnd = allocNode();
this.cells = null;
this.openList = [];
this.closedList = [];
this.closedCache = {};
this.pathList = [];
this.currentNode = null;
this.targetX = 0;
this.targetY = 0;
this.diagonalsEnabled = true;
this.worker = null;
this.workerQueue = []; // jobs awaiting completion from worker in order of requests made
this.workerRecycle = [];
this.sendMessageQueue = []; // messages queued while worker still creating
var self = this;
var i, len;
if (workersSupported && !isInWebWorker)
{
// Create worker and receive results of pathfind jobs from it
createWorker("pathfind.js", function (worker)
{
self.worker = worker;
self.worker.addEventListener("message", function (e) {
if (!e || !e.data)
return;
if (e.data.cmd === "result")
{
if (e.data.pathList)
{
for (i = 0, len = self.pathList.length; i < len; i++)
freeResultNode(self.pathList[i]);
self.pathList = e.data.pathList;
self.workerQueue[0].success();
}
else
self.workerQueue[0].fail();
self.workerRecycle.push(self.workerQueue.shift());
}
}, false);
self.worker.addEventListener("error", function (e) {
console.error(e);
}, false);
self.worker.postMessage(null);
// Send any messages that were queued while the worker was creating
for (var i = 0, len = self.sendMessageQueue.length; i < len; ++i)
self.worker.postMessage(self.sendMessageQueue[i]);
self.sendMessageQueue.length = 0;
});
}
};
pathfinder.prototype.postToWorker = function (data)
{
if (this.worker)
{
this.worker.postMessage(data);
}
else
{
// If worker still creating, queue messages to be sent when it's ready
this.sendMessageQueue.push(data);
}
};
pathfinder.prototype.init = function (hcells_, vcells_, data_, diagonals_)
{
this.hcells = hcells_;
this.vcells = vcells_;
this.cells = data_;
this.diagonalsEnabled = diagonals_;
if (workersSupported && !isInWebWorker)
{
this.postToWorker({
cmd: "init",
hcells: hcells_,
vcells: vcells_,
diagonals: diagonals_,
data: data_
});
}
};
pathfinder.prototype.updateRegion = function (cx1_, cy1_, lenx_, leny_, data_)
{
this.writeCells(cx1_, cy1_, lenx_, leny_, data_);
if (workersSupported && !isInWebWorker)
{
this.postToWorker({
cmd: "region",
offx: cx1_,
offy: cy1_,
lenx: lenx_,
leny: leny_,
data: data_
});
}
};
pathfinder.prototype.writeCells = function (cx1, cy1, lenx, leny, data_)
{
var x, y;
for (x = 0; x < lenx; ++x)
{
for (y = 0; y < leny; ++y)
{
this.cells[cx1 + x][cy1 + y] = data_[x][y];
}
}
};
pathfinder.prototype.unsetReady = function ()
{
// revert to no data state
this.cells = null;
};
pathfinder.prototype.isReady = function ()
{
return !!this.cells;
};
pathfinder.prototype.setDiagonals = function (d)
{
if (this.diagonalsEnabled === d)
return;
this.diagonalsEnabled = d;
if (workersSupported && !isInWebWorker)
{
this.postToWorker({
cmd: "setdiag",
diagonals: d,
});
}
};
pathfinder.prototype.at = function (x_, y_)
{
if (x_ < 0 || y_ < 0 || x_ >= this.hcells || y_ >= this.vcells)
return PF_OBSTACLE;
return this.cells[x_][y_];
};
pathfinder.prototype.findPath = function (startX, startY, endX, endY, successCallback, failCallback)
{
if (!this.cells)
{
// not yet initialised
failCallback();
return;
}
startX = Math.floor(startX);
startY = Math.floor(startY);
endX = Math.floor(endX);
endY = Math.floor(endY);
this.targetX = endX;
this.targetY = endY;
this.pathEnd.parent = null;
// Check the box made by the start and dest cells.
// If the complete box is empty, allow a direct move to target.
var minX = Math.min(startX, endX);
var maxX = Math.max(startX, endX);
var minY = Math.min(startY, endY);
var maxY = Math.max(startY, endY);
// Path goes out of bounds: no calculable path
if (minX < 0 || minY < 0 || maxX >= this.hcells || maxY >= this.vcells)
{
failCallback();
return;
}
var x, y, i, len, c, h, n;
if (this.diagonalsEnabled)
{
var canMoveDirect = true;
for (x = minX; x <= maxX; x++)
{
for (y = minY; y <= maxY; y++)
{
if (this.cells[x][y] !== 0)
{
canMoveDirect = false;
// Break both loops
x = maxX + 1;
break;
}
}
}
// A "direct" path is available (box is empty)
if (canMoveDirect)
{
for (i = 0, len = this.pathList.length; i < len; i++)
freeResultNode(this.pathList[i]);
this.pathList.length = 0;
this.pathEnd.x = endX;
this.pathEnd.y = endY;
this.pathEnd.parent = null;
n = allocResultNode();
n.x = endX;
n.y = endY;
this.pathList.push(n);
successCallback();
return;
}
}
if (workersSupported)
{
// recycle objects in the worker queue
if (this.workerRecycle.length)
h = this.workerRecycle.pop();
else
h = {};
h.success = successCallback;
h.fail = failCallback;
// dispatch the heavy lifting to the worker thread
this.workerQueue.push(h);
this.postToWorker({
cmd: "find",
startX: startX,
startY: startY,
endX: endX,
endY: endY
});
}
else
{
// no web worker support, just run on main thread
if (this.doLongFindPath(startX, startY))
successCallback();
else
failCallback();
}
};
pathfinder.prototype.doLongFindPath = function (startX, startY)
{
var i, len, c, n, p, lastDir = 8, curDir = -1, addNode;
for (i = 0, len = this.openList.length; i < len; i++)
freeNode(this.openList[i]);
for (i = 0, len = this.closedList.length; i < len; i++)
freeNode(this.closedList[i]);
for (i = 0, len = this.pathList.length; i < len; i++)
freeResultNode(this.pathList[i]);
this.openList.length = 0;
this.closedList.length = 0;
this.closedCache = {};
this.pathList.length = 0;
// Add the start node to the open list
var startNode = allocNode();
startNode.x = startX;
startNode.y = startY;
this.openList.push(startNode);
var obsLeft = false, obsTop = false, obsRight = false, obsBottom = false;
var diagonals = this.diagonalsEnabled;
// While there are nodes on the open list
while (this.openList.length)
{
// Move the best F value to closed list
c = this.openList.shift();
this.closedList.unshift(c);
this.closedCache[((c.x << 16) + c.y).toString()] = true;
// Are we there yet?
if (c.x === this.targetX && c.y === this.targetY)
{
this.pathEnd.parent = c.parent;
this.pathEnd.x = c.x;
this.pathEnd.y = c.y;
// Link up the whole path to an indexable array
p = this.pathEnd;
while (p)
{
// filter redundant nodes in straight lines
if (this.pathList.length === 0)
{
addNode = true;
if (p.parent)
{
lastDir = this.nodeDirection(p, p.parent);
curDir = lastDir;
}
}
else if (!p.parent)
addNode = false;
else
{
curDir = this.nodeDirection(p, p.parent);
addNode = (curDir !== lastDir);
}
if (addNode)
{
n = allocResultNode();
n.x = p.x;
n.y = p.y;
this.pathList.unshift(n);
lastDir = curDir;
}
p = p.parent;
}
return true;
}
// Get current node
this.currentNode = c;
var x = c.x;
var y = c.y;
var obsLeft = (this.at(x - 1, y) === PF_OBSTACLE);
var obsTop = (this.at(x, y - 1) === PF_OBSTACLE);
var obsRight = (this.at(x + 1, y) === PF_OBSTACLE);
var obsBottom = (this.at(x, y + 1) === PF_OBSTACLE);
// Check adjacent 8 nodes. No diagonals allowed if either cell being crossed is obstacle.
if (!obsLeft)
this.addCellToOpenList(x - 1, y, 10);
if (diagonals && !obsLeft && !obsTop && (this.at(x - 1, y - 1) !== PF_OBSTACLE))
this.addCellToOpenList(x - 1, y - 1, 14);
if (!obsTop)
this.addCellToOpenList(x, y - 1, 10);
if (diagonals && !obsTop && !obsRight && (this.at(x + 1, y - 1) !== PF_OBSTACLE))
this.addCellToOpenList(x + 1, y - 1, 14);
if (!obsRight)
this.addCellToOpenList(x + 1, y, 10);
if (diagonals && !obsRight && !obsBottom && (this.at(x + 1, y + 1) !== PF_OBSTACLE))
this.addCellToOpenList(x + 1, y + 1, 14);
if (!obsBottom)
this.addCellToOpenList(x, y + 1, 10);
if (diagonals && !obsBottom && !obsLeft && (this.at(x - 1, y + 1) !== PF_OBSTACLE))
this.addCellToOpenList(x - 1, y + 1, 14);
}
return false;
};
pathfinder.prototype.insertToOpenList = function (c)
{
var i, len;
// Needs to go at end
if (c.f >= this.openList[this.openList.length - 1].f)
{
this.openList.push(c);
}
else
{
for (i = 0, len = this.openList.length; i < len; i++)
{
if (c.f < this.openList[i].f)
{
this.openList.splice(i, 0, c);
break;
}
}
}
};
pathfinder.prototype.addCellToOpenList = function (x_, y_, g_)
{
// Ignore if cell on closed list
if (this.closedCache.hasOwnProperty(((x_ << 16) + y_).toString()))
return;
var i, len, c;
// Cell costs can be increased by changing the number in the map
var curCellCost = this.at(x_, y_);
// Is this cell already on the open list?
for (i = 0, len = this.openList.length; i < len; i++)
{
c = this.openList[i];
if (x_ === c.x && y_ === c.y)
{
// Is this a better path?
if (this.currentNode.g + g_ + curCellCost < c.g)
{
// Update F, G and H and update parent
c.parent = this.currentNode;
c.g = this.currentNode.g + g_ + curCellCost;
c.h = this.estimateH(c.x, c.y);
c.f = c.g + c.h;
// This node's F has changed: Delete it then re-insert it in the right place
if (this.openList.length === 1)
{
// no need to remove then re-insert same node, just leave it there
return;
}
this.openList.splice(i, 1);
this.insertToOpenList(c);
}
return;
}
}
// Not on the open list; add it in the right place
c = allocNode();
c.x = x_;
c.y = y_;
c.h = this.estimateH(x_, y_);
c.g = this.currentNode.g + g_ + curCellCost;
c.f = c.h + c.g;
c.parent = this.currentNode;
// Insert this node sorted in the open list
// The loop below won't add new largest F values
if (!this.openList.length)
{
this.openList.push(c);
return;
}
this.insertToOpenList(c);
};
function quickAbs(x)
{
return x < 0 ? -x : x;
};
pathfinder.prototype.estimateH = function (x_, y_)
{
var dx = quickAbs(x_ - this.targetX);
var dy = quickAbs(y_ - this.targetY);
return dx * 10 + dy * 10;
};
pathfinder.prototype.nodeDirection = function (a, b)
{
var ax = a.x;
var ay = a.y;
var bx = b.x;
var by = b.y;
if (ax === bx)
{
if (by > ay) return 6;
if (by < ay) return 2;
if (ay == by) return 8;
}
else if (ay === by)
{
if (bx > ax) return 4;
if (by < ax) return 0;
}
else
{
if (bx < ax && by < ay) return 1;
if (bx > ax && by < ay) return 3;
if (bx < ax && by > ay) return 7;
if (bx > ax && by > ay) return 5;
}
return 8;
};
if (!isInWebWorker)
{
window.PF_CLEAR = PF_CLEAR;
window.PF_OBSTACLE = PF_OBSTACLE;
window.Pathfinder = pathfinder;
window.ResultNode = resultNode;
window.allocResultNode = allocResultNode;
window.freeResultNode = freeResultNode;
}
})();