@flare-engine/pathfinding 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/index.d.mts +110 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.js +696 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +655 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +42 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
// src/grid.ts
|
|
2
|
+
function createGrid(config) {
|
|
3
|
+
const { width, height, defaultCost = 1 } = config;
|
|
4
|
+
if (width <= 0 || height <= 0) {
|
|
5
|
+
throw new RangeError("Grid dimensions must be positive");
|
|
6
|
+
}
|
|
7
|
+
const w = width | 0;
|
|
8
|
+
const h = height | 0;
|
|
9
|
+
let data;
|
|
10
|
+
if (config.data) {
|
|
11
|
+
if (config.data.length !== w * h) {
|
|
12
|
+
throw new RangeError(`Grid data length ${config.data.length} does not match ${w}x${h}`);
|
|
13
|
+
}
|
|
14
|
+
data = config.data;
|
|
15
|
+
} else {
|
|
16
|
+
data = new Uint8Array(w * h);
|
|
17
|
+
if (defaultCost > 0) {
|
|
18
|
+
data.fill(defaultCost);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return { width: w, height: h, data };
|
|
22
|
+
}
|
|
23
|
+
function inBounds(grid, x, y) {
|
|
24
|
+
return x >= 0 && y >= 0 && x < grid.width && y < grid.height;
|
|
25
|
+
}
|
|
26
|
+
function isWalkable(grid, x, y) {
|
|
27
|
+
if (x < 0 || y < 0 || x >= grid.width || y >= grid.height) return false;
|
|
28
|
+
return grid.data[(y | 0) * grid.width + (x | 0)] > 0;
|
|
29
|
+
}
|
|
30
|
+
function getCost(grid, x, y) {
|
|
31
|
+
if (x < 0 || y < 0 || x >= grid.width || y >= grid.height) return 0;
|
|
32
|
+
return grid.data[(y | 0) * grid.width + (x | 0)];
|
|
33
|
+
}
|
|
34
|
+
function setWalkable(grid, x, y, walkable) {
|
|
35
|
+
if (x < 0 || y < 0 || x >= grid.width || y >= grid.height) return;
|
|
36
|
+
grid.data[(y | 0) * grid.width + (x | 0)] = walkable ? 1 : 0;
|
|
37
|
+
}
|
|
38
|
+
function setCost(grid, x, y, cost) {
|
|
39
|
+
if (x < 0 || y < 0 || x >= grid.width || y >= grid.height) return;
|
|
40
|
+
grid.data[(y | 0) * grid.width + (x | 0)] = cost > 255 ? 255 : cost < 0 ? 0 : cost | 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/astar.ts
|
|
44
|
+
var SQRT2 = Math.SQRT2;
|
|
45
|
+
function manhattanDistance(ax, ay, bx, by) {
|
|
46
|
+
return Math.abs(ax - bx) + Math.abs(ay - by);
|
|
47
|
+
}
|
|
48
|
+
function octileDistance(ax, ay, bx, by) {
|
|
49
|
+
const dx = Math.abs(ax - bx);
|
|
50
|
+
const dy = Math.abs(ay - by);
|
|
51
|
+
return dx > dy ? dx + (SQRT2 - 1) * dy : dy + (SQRT2 - 1) * dx;
|
|
52
|
+
}
|
|
53
|
+
function chebyshevDistance(ax, ay, bx, by) {
|
|
54
|
+
return Math.max(Math.abs(ax - bx), Math.abs(ay - by));
|
|
55
|
+
}
|
|
56
|
+
function euclideanDistance(ax, ay, bx, by) {
|
|
57
|
+
const dx = ax - bx;
|
|
58
|
+
const dy = ay - by;
|
|
59
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
60
|
+
}
|
|
61
|
+
function heapCreate(capacity) {
|
|
62
|
+
return {
|
|
63
|
+
f: new Float64Array(capacity),
|
|
64
|
+
idx: new Int32Array(capacity),
|
|
65
|
+
size: 0
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function heapPush(h, f, nodeIdx) {
|
|
69
|
+
let i = h.size++;
|
|
70
|
+
h.f[i] = f;
|
|
71
|
+
h.idx[i] = nodeIdx;
|
|
72
|
+
while (i > 0) {
|
|
73
|
+
const parent = i - 1 >> 1;
|
|
74
|
+
if (h.f[parent] <= h.f[i]) break;
|
|
75
|
+
const tf = h.f[parent];
|
|
76
|
+
const ti = h.idx[parent];
|
|
77
|
+
h.f[parent] = h.f[i];
|
|
78
|
+
h.idx[parent] = h.idx[i];
|
|
79
|
+
h.f[i] = tf;
|
|
80
|
+
h.idx[i] = ti;
|
|
81
|
+
i = parent;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function heapPop(h) {
|
|
85
|
+
const result = h.idx[0];
|
|
86
|
+
h.size--;
|
|
87
|
+
if (h.size > 0) {
|
|
88
|
+
h.f[0] = h.f[h.size];
|
|
89
|
+
h.idx[0] = h.idx[h.size];
|
|
90
|
+
let i = 0;
|
|
91
|
+
for (; ; ) {
|
|
92
|
+
let smallest = i;
|
|
93
|
+
const left = 2 * i + 1;
|
|
94
|
+
const right = 2 * i + 2;
|
|
95
|
+
if (left < h.size && h.f[left] < h.f[smallest]) smallest = left;
|
|
96
|
+
if (right < h.size && h.f[right] < h.f[smallest]) smallest = right;
|
|
97
|
+
if (smallest === i) break;
|
|
98
|
+
const tf = h.f[i];
|
|
99
|
+
const ti = h.idx[i];
|
|
100
|
+
h.f[i] = h.f[smallest];
|
|
101
|
+
h.idx[i] = h.idx[smallest];
|
|
102
|
+
h.f[smallest] = tf;
|
|
103
|
+
h.idx[smallest] = ti;
|
|
104
|
+
i = smallest;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
var DIR4_X = [0, 1, 0, -1];
|
|
110
|
+
var DIR4_Y = [-1, 0, 1, 0];
|
|
111
|
+
var DIR8_X = [0, 1, 1, 1, 0, -1, -1, -1];
|
|
112
|
+
var DIR8_Y = [-1, -1, 0, 1, 1, 1, 0, -1];
|
|
113
|
+
var DIR8_COST = [1, SQRT2, 1, SQRT2, 1, SQRT2, 1, SQRT2];
|
|
114
|
+
function findPath(grid, startX, startY, endX, endY, options) {
|
|
115
|
+
const sx = startX | 0;
|
|
116
|
+
const sy = startY | 0;
|
|
117
|
+
const ex = endX | 0;
|
|
118
|
+
const ey = endY | 0;
|
|
119
|
+
if (!isWalkable(grid, sx, sy) || !isWalkable(grid, ex, ey)) return null;
|
|
120
|
+
if (sx === ex && sy === ey) return { path: [{ x: sx, y: sy }], cost: 0, explored: 0 };
|
|
121
|
+
const diagonal = options?.diagonal !== false;
|
|
122
|
+
const maxIter = options?.maxIterations ?? grid.width * grid.height;
|
|
123
|
+
const heuristic = options?.heuristic ?? (diagonal ? octileDistance : manhattanDistance);
|
|
124
|
+
const w = grid.width;
|
|
125
|
+
const total = w * grid.height;
|
|
126
|
+
const g = new Float64Array(total);
|
|
127
|
+
g.fill(Number.POSITIVE_INFINITY);
|
|
128
|
+
const closed = new Uint8Array(total);
|
|
129
|
+
const cameFrom = new Int32Array(total);
|
|
130
|
+
cameFrom.fill(-1);
|
|
131
|
+
const startIdx = sy * w + sx;
|
|
132
|
+
const endIdx = ey * w + ex;
|
|
133
|
+
g[startIdx] = 0;
|
|
134
|
+
const heap = heapCreate(Math.min(total, 4096));
|
|
135
|
+
heapPush(heap, heuristic(sx, sy, ex, ey), startIdx);
|
|
136
|
+
const dirX = diagonal ? DIR8_X : DIR4_X;
|
|
137
|
+
const dirY = diagonal ? DIR8_Y : DIR4_Y;
|
|
138
|
+
const dirCost = diagonal ? DIR8_COST : null;
|
|
139
|
+
const dirCount = dirX.length;
|
|
140
|
+
let explored = 0;
|
|
141
|
+
while (heap.size > 0 && explored < maxIter) {
|
|
142
|
+
const currentIdx = heapPop(heap);
|
|
143
|
+
if (currentIdx === endIdx) {
|
|
144
|
+
return reconstructPath(cameFrom, w, currentIdx, g[currentIdx], explored);
|
|
145
|
+
}
|
|
146
|
+
if (closed[currentIdx]) continue;
|
|
147
|
+
closed[currentIdx] = 1;
|
|
148
|
+
explored++;
|
|
149
|
+
const cx = currentIdx % w;
|
|
150
|
+
const cy = currentIdx / w | 0;
|
|
151
|
+
const currentG = g[currentIdx];
|
|
152
|
+
for (let d = 0; d < dirCount; d++) {
|
|
153
|
+
const nx = cx + dirX[d];
|
|
154
|
+
const ny = cy + dirY[d];
|
|
155
|
+
if (nx < 0 || ny < 0 || nx >= w || ny >= grid.height) continue;
|
|
156
|
+
const nIdx = ny * w + nx;
|
|
157
|
+
if (closed[nIdx]) continue;
|
|
158
|
+
const tileCost = grid.data[nIdx];
|
|
159
|
+
if (tileCost === 0) continue;
|
|
160
|
+
if (diagonal && d & 1) {
|
|
161
|
+
const dx = dirX[d];
|
|
162
|
+
const dy = dirY[d];
|
|
163
|
+
if (!isWalkable(grid, cx + dx, cy) || !isWalkable(grid, cx, cy + dy)) continue;
|
|
164
|
+
}
|
|
165
|
+
const moveCost = dirCost ? dirCost[d] * tileCost : tileCost;
|
|
166
|
+
const tentativeG = currentG + moveCost;
|
|
167
|
+
if (tentativeG < g[nIdx]) {
|
|
168
|
+
g[nIdx] = tentativeG;
|
|
169
|
+
cameFrom[nIdx] = currentIdx;
|
|
170
|
+
const f = tentativeG + heuristic(nx, ny, ex, ey);
|
|
171
|
+
heapPush(heap, f, nIdx);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
function reconstructPath(cameFrom, w, endIdx, cost, explored) {
|
|
178
|
+
const path = [];
|
|
179
|
+
let idx = endIdx;
|
|
180
|
+
while (idx !== -1) {
|
|
181
|
+
path.push({ x: idx % w, y: idx / w | 0 });
|
|
182
|
+
idx = cameFrom[idx];
|
|
183
|
+
}
|
|
184
|
+
path.reverse();
|
|
185
|
+
return { path, cost, explored };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/jps.ts
|
|
189
|
+
function heapCreate2(capacity) {
|
|
190
|
+
return {
|
|
191
|
+
f: new Float64Array(capacity),
|
|
192
|
+
idx: new Int32Array(capacity),
|
|
193
|
+
size: 0
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function heapPush2(h, f, nodeIdx) {
|
|
197
|
+
let i = h.size++;
|
|
198
|
+
h.f[i] = f;
|
|
199
|
+
h.idx[i] = nodeIdx;
|
|
200
|
+
while (i > 0) {
|
|
201
|
+
const parent = i - 1 >> 1;
|
|
202
|
+
if (h.f[parent] <= h.f[i]) break;
|
|
203
|
+
const tf = h.f[parent];
|
|
204
|
+
const ti = h.idx[parent];
|
|
205
|
+
h.f[parent] = h.f[i];
|
|
206
|
+
h.idx[parent] = h.idx[i];
|
|
207
|
+
h.f[i] = tf;
|
|
208
|
+
h.idx[i] = ti;
|
|
209
|
+
i = parent;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function heapPop2(h) {
|
|
213
|
+
const result = h.idx[0];
|
|
214
|
+
h.size--;
|
|
215
|
+
if (h.size > 0) {
|
|
216
|
+
h.f[0] = h.f[h.size];
|
|
217
|
+
h.idx[0] = h.idx[h.size];
|
|
218
|
+
let i = 0;
|
|
219
|
+
for (; ; ) {
|
|
220
|
+
let smallest = i;
|
|
221
|
+
const left = 2 * i + 1;
|
|
222
|
+
const right = 2 * i + 2;
|
|
223
|
+
if (left < h.size && h.f[left] < h.f[smallest]) smallest = left;
|
|
224
|
+
if (right < h.size && h.f[right] < h.f[smallest]) smallest = right;
|
|
225
|
+
if (smallest === i) break;
|
|
226
|
+
const tf = h.f[i];
|
|
227
|
+
const ti = h.idx[i];
|
|
228
|
+
h.f[i] = h.f[smallest];
|
|
229
|
+
h.idx[i] = h.idx[smallest];
|
|
230
|
+
h.f[smallest] = tf;
|
|
231
|
+
h.idx[smallest] = ti;
|
|
232
|
+
i = smallest;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
var SQRT22 = Math.SQRT2;
|
|
238
|
+
function findPathJPS(grid, startX, startY, endX, endY, options) {
|
|
239
|
+
const sx = startX | 0;
|
|
240
|
+
const sy = startY | 0;
|
|
241
|
+
const ex = endX | 0;
|
|
242
|
+
const ey = endY | 0;
|
|
243
|
+
if (!isWalkable(grid, sx, sy) || !isWalkable(grid, ex, ey)) return null;
|
|
244
|
+
if (sx === ex && sy === ey) return { path: [{ x: sx, y: sy }], cost: 0, explored: 0 };
|
|
245
|
+
const w = grid.width;
|
|
246
|
+
const h = grid.height;
|
|
247
|
+
const total = w * h;
|
|
248
|
+
const maxIter = options?.maxIterations ?? total;
|
|
249
|
+
const heuristic = options?.heuristic ?? octileDistance;
|
|
250
|
+
const g = new Float64Array(total);
|
|
251
|
+
g.fill(Number.POSITIVE_INFINITY);
|
|
252
|
+
const closed = new Uint8Array(total);
|
|
253
|
+
const cameFrom = new Int32Array(total);
|
|
254
|
+
cameFrom.fill(-1);
|
|
255
|
+
const startIdx = sy * w + sx;
|
|
256
|
+
const endIdx = ey * w + ex;
|
|
257
|
+
g[startIdx] = 0;
|
|
258
|
+
const heap = heapCreate2(Math.min(total, 4096));
|
|
259
|
+
heapPush2(heap, heuristic(sx, sy, ex, ey), startIdx);
|
|
260
|
+
let explored = 0;
|
|
261
|
+
while (heap.size > 0 && explored < maxIter) {
|
|
262
|
+
const currentIdx = heapPop2(heap);
|
|
263
|
+
if (currentIdx === endIdx) {
|
|
264
|
+
return buildPath(cameFrom, w, currentIdx, g[currentIdx], explored);
|
|
265
|
+
}
|
|
266
|
+
if (closed[currentIdx]) continue;
|
|
267
|
+
closed[currentIdx] = 1;
|
|
268
|
+
explored++;
|
|
269
|
+
const cx = currentIdx % w;
|
|
270
|
+
const cy = currentIdx / w | 0;
|
|
271
|
+
const neighbors = identifySuccessors(grid, cx, cy, cameFrom, w, h, ex, ey);
|
|
272
|
+
for (let i = 0; i < neighbors.length; i += 2) {
|
|
273
|
+
const nx = neighbors[i];
|
|
274
|
+
const ny = neighbors[i + 1];
|
|
275
|
+
const nIdx = ny * w + nx;
|
|
276
|
+
if (closed[nIdx]) continue;
|
|
277
|
+
const dx = Math.abs(nx - cx);
|
|
278
|
+
const dy = Math.abs(ny - cy);
|
|
279
|
+
const diag = Math.min(dx, dy);
|
|
280
|
+
const straight = Math.abs(dx - dy);
|
|
281
|
+
const dist2 = diag * SQRT22 + straight;
|
|
282
|
+
const tentativeG = g[currentIdx] + dist2;
|
|
283
|
+
if (tentativeG < g[nIdx]) {
|
|
284
|
+
g[nIdx] = tentativeG;
|
|
285
|
+
cameFrom[nIdx] = currentIdx;
|
|
286
|
+
heapPush2(heap, tentativeG + heuristic(nx, ny, ex, ey), nIdx);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
function identifySuccessors(grid, cx, cy, cameFrom, w, h, ex, ey) {
|
|
293
|
+
const results = [];
|
|
294
|
+
const parentIdx = cameFrom[cy * w + cx];
|
|
295
|
+
if (parentIdx === -1) {
|
|
296
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
297
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
298
|
+
if (dx === 0 && dy === 0) continue;
|
|
299
|
+
const jp = jump(grid, cx, cy, dx, dy, w, h, ex, ey);
|
|
300
|
+
if (jp !== -1) {
|
|
301
|
+
results.push(jp % w, jp / w | 0);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
const px = parentIdx % w;
|
|
307
|
+
const py = parentIdx / w | 0;
|
|
308
|
+
const dx = clampDir(cx - px);
|
|
309
|
+
const dy = clampDir(cy - py);
|
|
310
|
+
if (dx !== 0 && dy !== 0) {
|
|
311
|
+
if (isWalkable(grid, cx, cy + dy)) {
|
|
312
|
+
const jp = jump(grid, cx, cy, 0, dy, w, h, ex, ey);
|
|
313
|
+
if (jp !== -1) results.push(jp % w, jp / w | 0);
|
|
314
|
+
}
|
|
315
|
+
if (isWalkable(grid, cx + dx, cy)) {
|
|
316
|
+
const jp = jump(grid, cx, cy, dx, 0, w, h, ex, ey);
|
|
317
|
+
if (jp !== -1) results.push(jp % w, jp / w | 0);
|
|
318
|
+
}
|
|
319
|
+
if (isWalkable(grid, cx + dx, cy + dy)) {
|
|
320
|
+
const jp = jump(grid, cx, cy, dx, dy, w, h, ex, ey);
|
|
321
|
+
if (jp !== -1) results.push(jp % w, jp / w | 0);
|
|
322
|
+
}
|
|
323
|
+
if (!isWalkable(grid, cx - dx, cy) && isWalkable(grid, cx - dx, cy + dy)) {
|
|
324
|
+
const jp = jump(grid, cx, cy, -dx, dy, w, h, ex, ey);
|
|
325
|
+
if (jp !== -1) results.push(jp % w, jp / w | 0);
|
|
326
|
+
}
|
|
327
|
+
if (!isWalkable(grid, cx, cy - dy) && isWalkable(grid, cx + dx, cy - dy)) {
|
|
328
|
+
const jp = jump(grid, cx, cy, dx, -dy, w, h, ex, ey);
|
|
329
|
+
if (jp !== -1) results.push(jp % w, jp / w | 0);
|
|
330
|
+
}
|
|
331
|
+
} else if (dx !== 0) {
|
|
332
|
+
if (isWalkable(grid, cx + dx, cy)) {
|
|
333
|
+
const jp = jump(grid, cx, cy, dx, 0, w, h, ex, ey);
|
|
334
|
+
if (jp !== -1) results.push(jp % w, jp / w | 0);
|
|
335
|
+
}
|
|
336
|
+
if (!isWalkable(grid, cx, cy + 1) && isWalkable(grid, cx + dx, cy + 1)) {
|
|
337
|
+
const jp = jump(grid, cx, cy, dx, 1, w, h, ex, ey);
|
|
338
|
+
if (jp !== -1) results.push(jp % w, jp / w | 0);
|
|
339
|
+
}
|
|
340
|
+
if (!isWalkable(grid, cx, cy - 1) && isWalkable(grid, cx + dx, cy - 1)) {
|
|
341
|
+
const jp = jump(grid, cx, cy, dx, -1, w, h, ex, ey);
|
|
342
|
+
if (jp !== -1) results.push(jp % w, jp / w | 0);
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
if (isWalkable(grid, cx, cy + dy)) {
|
|
346
|
+
const jp = jump(grid, cx, cy, 0, dy, w, h, ex, ey);
|
|
347
|
+
if (jp !== -1) results.push(jp % w, jp / w | 0);
|
|
348
|
+
}
|
|
349
|
+
if (!isWalkable(grid, cx + 1, cy) && isWalkable(grid, cx + 1, cy + dy)) {
|
|
350
|
+
const jp = jump(grid, cx, cy, 1, dy, w, h, ex, ey);
|
|
351
|
+
if (jp !== -1) results.push(jp % w, jp / w | 0);
|
|
352
|
+
}
|
|
353
|
+
if (!isWalkable(grid, cx - 1, cy) && isWalkable(grid, cx - 1, cy + dy)) {
|
|
354
|
+
const jp = jump(grid, cx, cy, -1, dy, w, h, ex, ey);
|
|
355
|
+
if (jp !== -1) results.push(jp % w, jp / w | 0);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return results;
|
|
360
|
+
}
|
|
361
|
+
function jump(grid, cx, cy, dx, dy, w, h, ex, ey) {
|
|
362
|
+
const nx = cx + dx;
|
|
363
|
+
const ny = cy + dy;
|
|
364
|
+
if (!isWalkable(grid, nx, ny)) return -1;
|
|
365
|
+
if (nx === ex && ny === ey) return ny * w + nx;
|
|
366
|
+
if (dx !== 0 && dy !== 0) {
|
|
367
|
+
if (!isWalkable(grid, nx - dx, ny) && isWalkable(grid, nx - dx, ny + dy) || !isWalkable(grid, nx, ny - dy) && isWalkable(grid, nx + dx, ny - dy)) {
|
|
368
|
+
return ny * w + nx;
|
|
369
|
+
}
|
|
370
|
+
if (jump(grid, nx, ny, dx, 0, w, h, ex, ey) !== -1) return ny * w + nx;
|
|
371
|
+
if (jump(grid, nx, ny, 0, dy, w, h, ex, ey) !== -1) return ny * w + nx;
|
|
372
|
+
} else if (dx !== 0) {
|
|
373
|
+
if (!isWalkable(grid, nx, ny + 1) && isWalkable(grid, nx + dx, ny + 1) || !isWalkable(grid, nx, ny - 1) && isWalkable(grid, nx + dx, ny - 1)) {
|
|
374
|
+
return ny * w + nx;
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
if (!isWalkable(grid, nx + 1, ny) && isWalkable(grid, nx + 1, ny + dy) || !isWalkable(grid, nx - 1, ny) && isWalkable(grid, nx - 1, ny + dy)) {
|
|
378
|
+
return ny * w + nx;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return jump(grid, nx, ny, dx, dy, w, h, ex, ey);
|
|
382
|
+
}
|
|
383
|
+
function clampDir(v) {
|
|
384
|
+
return v > 0 ? 1 : v < 0 ? -1 : 0;
|
|
385
|
+
}
|
|
386
|
+
function buildPath(cameFrom, w, endIdx, cost, explored) {
|
|
387
|
+
const jumpPoints = [];
|
|
388
|
+
let idx = endIdx;
|
|
389
|
+
while (idx !== -1) {
|
|
390
|
+
jumpPoints.push(idx);
|
|
391
|
+
idx = cameFrom[idx];
|
|
392
|
+
}
|
|
393
|
+
jumpPoints.reverse();
|
|
394
|
+
const path = [];
|
|
395
|
+
for (let i = 0; i < jumpPoints.length; i++) {
|
|
396
|
+
const jpIdx = jumpPoints[i];
|
|
397
|
+
const jx = jpIdx % w;
|
|
398
|
+
const jy = jpIdx / w | 0;
|
|
399
|
+
if (i === 0) {
|
|
400
|
+
path.push({ x: jx, y: jy });
|
|
401
|
+
} else {
|
|
402
|
+
const prev = path[path.length - 1];
|
|
403
|
+
let cx = prev.x;
|
|
404
|
+
let cy = prev.y;
|
|
405
|
+
while (cx !== jx || cy !== jy) {
|
|
406
|
+
if (cx < jx) cx++;
|
|
407
|
+
else if (cx > jx) cx--;
|
|
408
|
+
if (cy < jy) cy++;
|
|
409
|
+
else if (cy > jy) cy--;
|
|
410
|
+
path.push({ x: cx, y: cy });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return { path, cost, explored };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/navmesh.ts
|
|
418
|
+
function createNavMesh(polygons) {
|
|
419
|
+
return { polygons };
|
|
420
|
+
}
|
|
421
|
+
function pointInConvexPolygon(verts, px, py) {
|
|
422
|
+
const n = verts.length;
|
|
423
|
+
if (n < 3) return false;
|
|
424
|
+
let sign = 0;
|
|
425
|
+
for (let i = 0; i < n; i++) {
|
|
426
|
+
const a = verts[i];
|
|
427
|
+
const b = verts[(i + 1) % n];
|
|
428
|
+
const cross = (b.x - a.x) * (py - a.y) - (b.y - a.y) * (px - a.x);
|
|
429
|
+
if (cross !== 0) {
|
|
430
|
+
const s = cross > 0 ? 1 : -1;
|
|
431
|
+
if (sign === 0) sign = s;
|
|
432
|
+
else if (sign !== s) return false;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
function findPolygonAt(mesh, x, y) {
|
|
438
|
+
for (let i = 0; i < mesh.polygons.length; i++) {
|
|
439
|
+
if (pointInConvexPolygon(mesh.polygons[i].vertices, x, y)) {
|
|
440
|
+
return mesh.polygons[i].id;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return -1;
|
|
444
|
+
}
|
|
445
|
+
function centroid(verts) {
|
|
446
|
+
let cx = 0;
|
|
447
|
+
let cy = 0;
|
|
448
|
+
for (let i = 0; i < verts.length; i++) {
|
|
449
|
+
cx += verts[i].x;
|
|
450
|
+
cy += verts[i].y;
|
|
451
|
+
}
|
|
452
|
+
const n = verts.length;
|
|
453
|
+
return { x: cx / n, y: cy / n };
|
|
454
|
+
}
|
|
455
|
+
function distSq(ax, ay, bx, by) {
|
|
456
|
+
const dx = ax - bx;
|
|
457
|
+
const dy = ay - by;
|
|
458
|
+
return dx * dx + dy * dy;
|
|
459
|
+
}
|
|
460
|
+
function dist(ax, ay, bx, by) {
|
|
461
|
+
return Math.sqrt(distSq(ax, ay, bx, by));
|
|
462
|
+
}
|
|
463
|
+
function findNavMeshPath(mesh, start, end) {
|
|
464
|
+
const startPolyId = findPolygonAt(mesh, start.x, start.y);
|
|
465
|
+
const endPolyId = findPolygonAt(mesh, end.x, end.y);
|
|
466
|
+
if (startPolyId === -1 || endPolyId === -1) return null;
|
|
467
|
+
if (startPolyId === endPolyId) {
|
|
468
|
+
return {
|
|
469
|
+
path: [
|
|
470
|
+
{ x: start.x, y: start.y },
|
|
471
|
+
{ x: end.x, y: end.y }
|
|
472
|
+
],
|
|
473
|
+
corridor: [startPolyId]
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
const byId = /* @__PURE__ */ new Map();
|
|
477
|
+
for (let i = 0; i < mesh.polygons.length; i++) {
|
|
478
|
+
const p = mesh.polygons[i];
|
|
479
|
+
byId.set(p.id, p);
|
|
480
|
+
}
|
|
481
|
+
const gScore = /* @__PURE__ */ new Map();
|
|
482
|
+
const fScore = /* @__PURE__ */ new Map();
|
|
483
|
+
const cameFrom = /* @__PURE__ */ new Map();
|
|
484
|
+
gScore.set(startPolyId, 0);
|
|
485
|
+
const endPoly = byId.get(endPolyId);
|
|
486
|
+
const endCenter = centroid(endPoly.vertices);
|
|
487
|
+
const startPoly = byId.get(startPolyId);
|
|
488
|
+
const startCenter = centroid(startPoly.vertices);
|
|
489
|
+
fScore.set(startPolyId, dist(startCenter.x, startCenter.y, endCenter.x, endCenter.y));
|
|
490
|
+
const open = [startPolyId];
|
|
491
|
+
const inOpen = /* @__PURE__ */ new Set([startPolyId]);
|
|
492
|
+
const closed = /* @__PURE__ */ new Set();
|
|
493
|
+
while (open.length > 0) {
|
|
494
|
+
let bestI = 0;
|
|
495
|
+
let bestF = fScore.get(open[0]) ?? Number.POSITIVE_INFINITY;
|
|
496
|
+
for (let i = 1; i < open.length; i++) {
|
|
497
|
+
const f = fScore.get(open[i]) ?? Number.POSITIVE_INFINITY;
|
|
498
|
+
if (f < bestF) {
|
|
499
|
+
bestF = f;
|
|
500
|
+
bestI = i;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const currentId = open[bestI];
|
|
504
|
+
if (currentId === endPolyId) {
|
|
505
|
+
const corridor = [];
|
|
506
|
+
let id = endPolyId;
|
|
507
|
+
while (id !== void 0) {
|
|
508
|
+
corridor.push(id);
|
|
509
|
+
id = cameFrom.get(id);
|
|
510
|
+
}
|
|
511
|
+
corridor.reverse();
|
|
512
|
+
const path = funnelSmooth(mesh, byId, corridor, start, end);
|
|
513
|
+
return { path, corridor };
|
|
514
|
+
}
|
|
515
|
+
open[bestI] = open[open.length - 1];
|
|
516
|
+
open.length--;
|
|
517
|
+
inOpen.delete(currentId);
|
|
518
|
+
closed.add(currentId);
|
|
519
|
+
const current = byId.get(currentId);
|
|
520
|
+
const currentCenter = centroid(current.vertices);
|
|
521
|
+
const currentG = gScore.get(currentId);
|
|
522
|
+
for (let i = 0; i < current.portals.length; i++) {
|
|
523
|
+
const portal = current.portals[i];
|
|
524
|
+
const neighborId = portal.neighborId;
|
|
525
|
+
if (closed.has(neighborId)) continue;
|
|
526
|
+
const neighbor = byId.get(neighborId);
|
|
527
|
+
if (!neighbor) continue;
|
|
528
|
+
const portalMidX = (portal.a.x + portal.b.x) * 0.5;
|
|
529
|
+
const portalMidY = (portal.a.y + portal.b.y) * 0.5;
|
|
530
|
+
const tentativeG = currentG + dist(currentCenter.x, currentCenter.y, portalMidX, portalMidY);
|
|
531
|
+
const prevG = gScore.get(neighborId);
|
|
532
|
+
if (prevG !== void 0 && tentativeG >= prevG) continue;
|
|
533
|
+
gScore.set(neighborId, tentativeG);
|
|
534
|
+
cameFrom.set(neighborId, currentId);
|
|
535
|
+
const neighborCenter = centroid(neighbor.vertices);
|
|
536
|
+
fScore.set(
|
|
537
|
+
neighborId,
|
|
538
|
+
tentativeG + dist(neighborCenter.x, neighborCenter.y, endCenter.x, endCenter.y)
|
|
539
|
+
);
|
|
540
|
+
if (!inOpen.has(neighborId)) {
|
|
541
|
+
open.push(neighborId);
|
|
542
|
+
inOpen.add(neighborId);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
function funnelSmooth(_mesh, byId, corridor, start, end) {
|
|
549
|
+
if (corridor.length <= 1) {
|
|
550
|
+
return [
|
|
551
|
+
{ x: start.x, y: start.y },
|
|
552
|
+
{ x: end.x, y: end.y }
|
|
553
|
+
];
|
|
554
|
+
}
|
|
555
|
+
const portalsLeft = [];
|
|
556
|
+
const portalsRight = [];
|
|
557
|
+
portalsLeft.push({ x: start.x, y: start.y });
|
|
558
|
+
portalsRight.push({ x: start.x, y: start.y });
|
|
559
|
+
for (let i = 0; i < corridor.length - 1; i++) {
|
|
560
|
+
const poly = byId.get(corridor[i]);
|
|
561
|
+
const nextId = corridor[i + 1];
|
|
562
|
+
let found = false;
|
|
563
|
+
for (let j = 0; j < poly.portals.length; j++) {
|
|
564
|
+
if (poly.portals[j].neighborId === nextId) {
|
|
565
|
+
portalsLeft.push(poly.portals[j].a);
|
|
566
|
+
portalsRight.push(poly.portals[j].b);
|
|
567
|
+
found = true;
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (!found) {
|
|
572
|
+
const c = centroid(poly.vertices);
|
|
573
|
+
portalsLeft.push(c);
|
|
574
|
+
portalsRight.push(c);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
portalsLeft.push({ x: end.x, y: end.y });
|
|
578
|
+
portalsRight.push({ x: end.x, y: end.y });
|
|
579
|
+
const path = [];
|
|
580
|
+
let apexPt = portalsLeft[0];
|
|
581
|
+
let leftPt = portalsLeft[0];
|
|
582
|
+
let rightPt = portalsRight[0];
|
|
583
|
+
let apexIdx = 0;
|
|
584
|
+
let leftIdx = 0;
|
|
585
|
+
let rightIdx = 0;
|
|
586
|
+
path.push({ x: apexPt.x, y: apexPt.y });
|
|
587
|
+
const n = portalsLeft.length;
|
|
588
|
+
for (let i = 1; i < n; i++) {
|
|
589
|
+
const pL = portalsLeft[i];
|
|
590
|
+
const pR = portalsRight[i];
|
|
591
|
+
if (triArea2(apexPt, rightPt, pR) <= 0) {
|
|
592
|
+
if (vEqual(apexPt, rightPt) || triArea2(apexPt, leftPt, pR) > 0) {
|
|
593
|
+
rightPt = pR;
|
|
594
|
+
rightIdx = i;
|
|
595
|
+
} else {
|
|
596
|
+
addUnique(path, leftPt);
|
|
597
|
+
apexPt = leftPt;
|
|
598
|
+
apexIdx = leftIdx;
|
|
599
|
+
leftPt = apexPt;
|
|
600
|
+
rightPt = apexPt;
|
|
601
|
+
leftIdx = apexIdx;
|
|
602
|
+
rightIdx = apexIdx;
|
|
603
|
+
i = apexIdx;
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (triArea2(apexPt, leftPt, pL) >= 0) {
|
|
608
|
+
if (vEqual(apexPt, leftPt) || triArea2(apexPt, rightPt, pL) < 0) {
|
|
609
|
+
leftPt = pL;
|
|
610
|
+
leftIdx = i;
|
|
611
|
+
} else {
|
|
612
|
+
addUnique(path, rightPt);
|
|
613
|
+
apexPt = rightPt;
|
|
614
|
+
apexIdx = rightIdx;
|
|
615
|
+
leftPt = apexPt;
|
|
616
|
+
rightPt = apexPt;
|
|
617
|
+
leftIdx = apexIdx;
|
|
618
|
+
rightIdx = apexIdx;
|
|
619
|
+
i = apexIdx;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
addUnique(path, end);
|
|
624
|
+
return path;
|
|
625
|
+
}
|
|
626
|
+
function addUnique(path, p) {
|
|
627
|
+
const last = path[path.length - 1];
|
|
628
|
+
if (!last || last.x !== p.x || last.y !== p.y) {
|
|
629
|
+
path.push({ x: p.x, y: p.y });
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function triArea2(a, b, c) {
|
|
633
|
+
return (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y);
|
|
634
|
+
}
|
|
635
|
+
function vEqual(a, b) {
|
|
636
|
+
return a.x === b.x && a.y === b.y;
|
|
637
|
+
}
|
|
638
|
+
export {
|
|
639
|
+
chebyshevDistance,
|
|
640
|
+
createGrid,
|
|
641
|
+
createNavMesh,
|
|
642
|
+
euclideanDistance,
|
|
643
|
+
findNavMeshPath,
|
|
644
|
+
findPath,
|
|
645
|
+
findPathJPS,
|
|
646
|
+
findPolygonAt,
|
|
647
|
+
getCost,
|
|
648
|
+
inBounds,
|
|
649
|
+
isWalkable,
|
|
650
|
+
manhattanDistance,
|
|
651
|
+
octileDistance,
|
|
652
|
+
setCost,
|
|
653
|
+
setWalkable
|
|
654
|
+
};
|
|
655
|
+
//# sourceMappingURL=index.mjs.map
|