Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
AroriaNetwork
GitHub Repository: AroriaNetwork/3kho-backup
Path: blob/main/projects/tank-trouble-2/pathfind.js
1834 views
1
// Pathfind.js
2
// Written by Ashley Gullen
3
// Copyright (c) 2013 Scirra Ltd.
4
5
// A* pathfinding javascript implementation
6
"use strict";
7
8
(function() {
9
10
var PF_CLEAR = 0;
11
var PF_OBSTACLE = 32767;
12
13
function node()
14
{
15
this.parent = null;
16
17
this.x = 0;
18
this.y = 0;
19
20
this.f = 0;
21
this.g = 0;
22
this.h = 0;
23
};
24
25
var nodeCache = []; // for recycling nodes
26
27
function allocNode()
28
{
29
var ret;
30
31
if (nodeCache.length)
32
ret = nodeCache.pop();
33
else
34
ret = new node();
35
36
ret.parent = null;
37
ret.x = 0;
38
ret.y = 0;
39
ret.f = 0;
40
ret.g = 0;
41
ret.h = 0;
42
return ret;
43
};
44
45
function freeNode(n)
46
{
47
if (nodeCache.length < 64000)
48
nodeCache.push(n);
49
};
50
51
function resultNode(x_, y_)
52
{
53
this.x = x_ || 0;
54
this.y = y_ || 0;
55
};
56
57
var resultNodeCache = []; // for recycling resultNodes
58
59
function allocResultNode()
60
{
61
if (resultNodeCache.length)
62
return resultNodeCache.pop();
63
else
64
return new resultNode(0, 0);
65
};
66
67
function freeResultNode(n)
68
{
69
if (resultNodeCache.length < 10000)
70
resultNodeCache.push(n);
71
};
72
73
var workersSupported = (typeof Worker !== "undefined");
74
var isInWebWorker = (typeof document === "undefined"); // no DOM in a worker
75
var myInstance = null; // single pathfinder instance for worker
76
77
if (isInWebWorker)
78
{
79
self.addEventListener("message", function (e)
80
{
81
var d = e.data;
82
83
if (!d)
84
return; // could be empty postMessage() to start worker
85
86
if (d.cmd === "init")
87
{
88
if (!myInstance)
89
myInstance = new pathfinder();
90
91
myInstance.init(d.hcells, d.vcells, d.data, d.diagonals);
92
}
93
else if (d.cmd === "find")
94
{
95
// receive a pathfind job, process it them dispatch result
96
myInstance.pathEnd.parent = null;
97
myInstance.targetX = d.endX;
98
myInstance.targetY = d.endY;
99
100
if (myInstance.doLongFindPath(d.startX, d.startY))
101
{
102
self.postMessage({ cmd: "result", pathList: myInstance.pathList });
103
}
104
else
105
{
106
self.postMessage({ cmd: "result", pathList: null });
107
}
108
}
109
else if (d.cmd === "region")
110
{
111
myInstance.writeCells(d.offx, d.offy, d.lenx, d.leny, d.data);
112
}
113
else if (d.cmd === "setdiag")
114
{
115
// update diagonalsEnabled flag
116
if (myInstance)
117
myInstance.diagonalsEnabled = d.diagonals;
118
}
119
120
}, false);
121
}
122
123
function pathfinder()
124
{
125
this.hcells = 0;
126
this.vcells = 0;
127
128
this.pathEnd = allocNode();
129
130
this.cells = null;
131
this.openList = [];
132
this.closedList = [];
133
this.closedCache = {};
134
this.pathList = [];
135
136
this.currentNode = null;
137
this.targetX = 0;
138
this.targetY = 0;
139
this.diagonalsEnabled = true;
140
141
this.worker = null;
142
this.workerQueue = []; // jobs awaiting completion from worker in order of requests made
143
this.workerRecycle = [];
144
145
var self = this;
146
var i, len;
147
148
if (workersSupported && !isInWebWorker)
149
{
150
// Create worker and receive results of pathfind jobs from it
151
this.worker = new Worker("pathfind.js");
152
153
this.worker.addEventListener("message", function (e) {
154
155
if (!e || !e.data)
156
return;
157
158
if (e.data.cmd === "result")
159
{
160
if (e.data.pathList)
161
{
162
for (i = 0, len = self.pathList.length; i < len; i++)
163
freeResultNode(self.pathList[i]);
164
165
self.pathList = e.data.pathList;
166
self.workerQueue[0].success();
167
}
168
else
169
self.workerQueue[0].fail();
170
171
self.workerRecycle.push(self.workerQueue.shift());
172
}
173
}, false);
174
175
this.worker.addEventListener("error", function (e) {
176
console.error(e);
177
}, false);
178
179
this.worker.postMessage(null);
180
}
181
};
182
183
pathfinder.prototype.init = function (hcells_, vcells_, data_, diagonals_)
184
{
185
this.hcells = hcells_;
186
this.vcells = vcells_;
187
this.cells = data_;
188
this.diagonalsEnabled = diagonals_;
189
190
if (workersSupported && !isInWebWorker)
191
{
192
this.worker.postMessage({
193
cmd: "init",
194
hcells: hcells_,
195
vcells: vcells_,
196
diagonals: diagonals_,
197
data: data_
198
});
199
}
200
};
201
202
pathfinder.prototype.updateRegion = function (cx1_, cy1_, lenx_, leny_, data_)
203
{
204
this.writeCells(cx1_, cy1_, lenx_, leny_, data_);
205
206
if (workersSupported && !isInWebWorker)
207
{
208
this.worker.postMessage({
209
cmd: "region",
210
offx: cx1_,
211
offy: cy1_,
212
lenx: lenx_,
213
leny: leny_,
214
data: data_
215
});
216
}
217
};
218
219
pathfinder.prototype.writeCells = function (cx1, cy1, lenx, leny, data_)
220
{
221
var x, y;
222
223
for (x = 0; x < lenx; ++x)
224
{
225
for (y = 0; y < leny; ++y)
226
{
227
this.cells[cx1 + x][cy1 + y] = data_[x][y];
228
}
229
}
230
};
231
232
pathfinder.prototype.unsetReady = function ()
233
{
234
// revert to no data state
235
this.cells = null;
236
};
237
238
pathfinder.prototype.isReady = function ()
239
{
240
return !!this.cells;
241
};
242
243
pathfinder.prototype.setDiagonals = function (d)
244
{
245
if (this.diagonalsEnabled === d)
246
return;
247
248
this.diagonalsEnabled = d;
249
250
if (workersSupported && !isInWebWorker)
251
{
252
this.worker.postMessage({
253
cmd: "setdiag",
254
diagonals: d,
255
});
256
}
257
};
258
259
pathfinder.prototype.at = function (x_, y_)
260
{
261
if (x_ < 0 || y_ < 0 || x_ >= this.hcells || y_ >= this.vcells)
262
return PF_OBSTACLE;
263
264
return this.cells[x_][y_];
265
};
266
267
pathfinder.prototype.findPath = function (startX, startY, endX, endY, successCallback, failCallback)
268
{
269
if (!this.cells)
270
{
271
// not yet initialised
272
failCallback();
273
return;
274
}
275
276
startX = Math.floor(startX);
277
startY = Math.floor(startY);
278
endX = Math.floor(endX);
279
endY = Math.floor(endY);
280
281
this.targetX = endX;
282
this.targetY = endY;
283
this.pathEnd.parent = null;
284
285
// Check the box made by the start and dest cells.
286
// If the complete box is empty, allow a direct move to target.
287
var minX = Math.min(startX, endX);
288
var maxX = Math.max(startX, endX);
289
var minY = Math.min(startY, endY);
290
var maxY = Math.max(startY, endY);
291
292
// Path goes out of bounds: no calculable path
293
if (minX < 0 || minY < 0 || maxX >= this.hcells || maxY >= this.vcells)
294
{
295
failCallback();
296
return;
297
}
298
299
var x, y, i, len, c, h, n;
300
301
if (this.diagonalsEnabled)
302
{
303
var canMoveDirect = true;
304
305
for (x = minX; x <= maxX; x++)
306
{
307
for (y = minY; y <= maxY; y++)
308
{
309
if (this.cells[x][y] !== 0)
310
{
311
canMoveDirect = false;
312
313
// Break both loops
314
x = maxX + 1;
315
break;
316
}
317
}
318
}
319
320
// A "direct" path is available (box is empty)
321
if (canMoveDirect)
322
{
323
for (i = 0, len = this.pathList.length; i < len; i++)
324
freeResultNode(this.pathList[i]);
325
this.pathList.length = 0;
326
327
this.pathEnd.x = endX;
328
this.pathEnd.y = endY;
329
this.pathEnd.parent = null;
330
n = allocResultNode();
331
n.x = endX;
332
n.y = endY;
333
this.pathList.push(n);
334
successCallback();
335
return;
336
}
337
}
338
339
if (workersSupported)
340
{
341
// recycle objects in the worker queue
342
if (this.workerRecycle.length)
343
h = this.workerRecycle.pop();
344
else
345
h = {};
346
347
h.success = successCallback;
348
h.fail = failCallback;
349
350
// dispatch the heavy lifting to the worker thread
351
this.workerQueue.push(h);
352
353
this.worker.postMessage({
354
cmd: "find",
355
startX: startX,
356
startY: startY,
357
endX: endX,
358
endY: endY
359
});
360
}
361
else
362
{
363
// no web worker support, just run on main thread
364
if (this.doLongFindPath(startX, startY))
365
successCallback();
366
else
367
failCallback();
368
}
369
};
370
371
pathfinder.prototype.doLongFindPath = function (startX, startY)
372
{
373
var i, len, c, n, p, lastDir = 8, curDir = -1, addNode;
374
for (i = 0, len = this.openList.length; i < len; i++)
375
freeNode(this.openList[i]);
376
for (i = 0, len = this.closedList.length; i < len; i++)
377
freeNode(this.closedList[i]);
378
for (i = 0, len = this.pathList.length; i < len; i++)
379
freeResultNode(this.pathList[i]);
380
this.openList.length = 0;
381
this.closedList.length = 0;
382
this.closedCache = {};
383
this.pathList.length = 0;
384
385
// Add the start node to the open list
386
var startNode = allocNode();
387
startNode.x = startX;
388
startNode.y = startY;
389
390
this.openList.push(startNode);
391
var obsLeft = false, obsTop = false, obsRight = false, obsBottom = false;
392
var diagonals = this.diagonalsEnabled;
393
394
// While there are nodes on the open list
395
while (this.openList.length)
396
{
397
// Move the best F value to closed list
398
c = this.openList.shift();
399
this.closedList.unshift(c);
400
this.closedCache[((c.x << 16) + c.y).toString()] = true;
401
402
// Are we there yet?
403
if (c.x === this.targetX && c.y === this.targetY)
404
{
405
this.pathEnd.parent = c.parent;
406
this.pathEnd.x = c.x;
407
this.pathEnd.y = c.y;
408
409
// Link up the whole path to an indexable array
410
p = this.pathEnd;
411
412
while (p)
413
{
414
// filter redundant nodes in straight lines
415
if (this.pathList.length === 0)
416
{
417
addNode = true;
418
419
if (p.parent)
420
{
421
lastDir = this.nodeDirection(p, p.parent);
422
curDir = lastDir;
423
}
424
}
425
else if (!p.parent)
426
addNode = false;
427
else
428
{
429
curDir = this.nodeDirection(p, p.parent);
430
addNode = (curDir !== lastDir);
431
}
432
433
if (addNode)
434
{
435
n = allocResultNode();
436
n.x = p.x;
437
n.y = p.y;
438
this.pathList.unshift(n);
439
lastDir = curDir;
440
}
441
442
p = p.parent;
443
}
444
445
return true;
446
}
447
448
// Get current node
449
this.currentNode = c;
450
var x = c.x;
451
var y = c.y;
452
453
var obsLeft = (this.at(x - 1, y) === PF_OBSTACLE);
454
var obsTop = (this.at(x, y - 1) === PF_OBSTACLE);
455
var obsRight = (this.at(x + 1, y) === PF_OBSTACLE);
456
var obsBottom = (this.at(x, y + 1) === PF_OBSTACLE);
457
458
// Check adjacent 8 nodes. No diagonals allowed if either cell being crossed is obstacle.
459
if (!obsLeft)
460
this.addCellToOpenList(x - 1, y, 10);
461
462
if (diagonals && !obsLeft && !obsTop)
463
this.addCellToOpenList(x - 1, y - 1, 14);
464
465
if (!obsTop)
466
this.addCellToOpenList(x, y - 1, 10);
467
468
if (diagonals && !obsTop && !obsRight)
469
this.addCellToOpenList(x + 1, y - 1, 14);
470
471
if (!obsRight)
472
this.addCellToOpenList(x + 1, y, 10);
473
474
if (diagonals && !obsRight && !obsBottom)
475
this.addCellToOpenList(x + 1, y + 1, 14);
476
477
if (!obsBottom)
478
this.addCellToOpenList(x, y + 1, 10);
479
480
if (diagonals && !obsBottom && !obsLeft)
481
this.addCellToOpenList(x - 1, y + 1, 14);
482
}
483
484
return false;
485
};
486
487
pathfinder.prototype.insertToOpenList = function (c)
488
{
489
var i, len;
490
491
// Needs to go at end
492
if (c.f >= this.openList[this.openList.length - 1].f)
493
{
494
this.openList.push(c);
495
}
496
else
497
{
498
for (i = 0, len = this.openList.length; i < len; i++)
499
{
500
if (c.f < this.openList[i].f)
501
{
502
this.openList.splice(i, 0, c);
503
break;
504
}
505
}
506
}
507
};
508
509
pathfinder.prototype.addCellToOpenList = function (x_, y_, g_)
510
{
511
// Ignore if cell on closed list
512
if (this.closedCache.hasOwnProperty(((x_ << 16) + y_).toString()))
513
return;
514
515
var i, len, c;
516
517
// Cell costs can be increased by changing the number in the map
518
var curCellCost = this.at(x_, y_);
519
520
// Is this cell already on the open list?
521
for (i = 0, len = this.openList.length; i < len; i++)
522
{
523
c = this.openList[i];
524
525
if (x_ === c.x && y_ === c.y)
526
{
527
// Is this a better path?
528
if (this.currentNode.g + g_ + curCellCost < c.g)
529
{
530
// Update F, G and H and update parent
531
c.parent = this.currentNode;
532
c.g = this.currentNode.g + g_ + curCellCost;
533
c.h = this.estimateH(c.x, c.y);
534
c.f = c.g + c.h;
535
536
// This node's F has changed: Delete it then re-insert it in the right place
537
538
if (this.openList.length === 1)
539
{
540
// no need to remove then re-insert same node, just leave it there
541
return;
542
}
543
544
this.openList.splice(i, 1);
545
this.insertToOpenList(c);
546
}
547
548
return;
549
}
550
}
551
552
// Not on the open list; add it in the right place
553
c = allocNode();
554
c.x = x_;
555
c.y = y_;
556
c.h = this.estimateH(x_, y_);
557
c.g = this.currentNode.g + g_ + curCellCost;
558
c.f = c.h + c.g;
559
c.parent = this.currentNode;
560
561
// Insert this node sorted in the open list
562
// The loop below won't add new largest F values
563
if (!this.openList.length)
564
{
565
this.openList.push(c);
566
return;
567
}
568
569
this.insertToOpenList(c);
570
};
571
572
function quickAbs(x)
573
{
574
return x < 0 ? -x : x;
575
};
576
577
pathfinder.prototype.estimateH = function (x_, y_)
578
{
579
var dx = quickAbs(x_ - this.targetX);
580
var dy = quickAbs(y_ - this.targetY);
581
582
return dx * 10 + dy * 10;
583
};
584
585
pathfinder.prototype.nodeDirection = function (a, b)
586
{
587
var ax = a.x;
588
var ay = a.y;
589
var bx = b.x;
590
var by = b.y;
591
592
if (ax === bx)
593
{
594
if (by > ay) return 6;
595
if (by < ay) return 2;
596
if (ay == by) return 8;
597
}
598
else if (ay === by)
599
{
600
if (bx > ax) return 4;
601
if (by < ax) return 0;
602
}
603
else
604
{
605
if (bx < ax && by < ay) return 1;
606
if (bx > ax && by < ay) return 3;
607
if (bx < ax && by > ay) return 7;
608
if (bx > ax && by > ay) return 5;
609
}
610
return 8;
611
};
612
613
if (!isInWebWorker)
614
{
615
window.PF_CLEAR = PF_CLEAR;
616
window.PF_OBSTACLE = PF_OBSTACLE;
617
window.Pathfinder = pathfinder;
618
619
window.ResultNode = resultNode;
620
window.allocResultNode = allocResultNode;
621
window.freeResultNode = freeResultNode;
622
}
623
624
})();
625