@esengine/pathfinding 13.1.0 → 13.2.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/dist/index.d.ts +125 -25
- package/dist/index.js +871 -313
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1409,24 +1409,255 @@ __name(createJPSPathfinder, "createJPSPathfinder");
|
|
|
1409
1409
|
|
|
1410
1410
|
// src/core/HPAPathfinder.ts
|
|
1411
1411
|
var DEFAULT_HPA_CONFIG = {
|
|
1412
|
-
clusterSize:
|
|
1413
|
-
maxEntranceWidth:
|
|
1414
|
-
cacheInternalPaths: true
|
|
1412
|
+
clusterSize: 64,
|
|
1413
|
+
maxEntranceWidth: 16,
|
|
1414
|
+
cacheInternalPaths: true,
|
|
1415
|
+
entranceStrategy: "end",
|
|
1416
|
+
lazyIntraEdges: true
|
|
1415
1417
|
};
|
|
1418
|
+
var _a3;
|
|
1419
|
+
var SubMap = (_a3 = class {
|
|
1420
|
+
constructor(parentMap, originX, originY, width, height) {
|
|
1421
|
+
__publicField(this, "parentMap");
|
|
1422
|
+
__publicField(this, "originX");
|
|
1423
|
+
__publicField(this, "originY");
|
|
1424
|
+
__publicField(this, "width");
|
|
1425
|
+
__publicField(this, "height");
|
|
1426
|
+
this.parentMap = parentMap;
|
|
1427
|
+
this.originX = originX;
|
|
1428
|
+
this.originY = originY;
|
|
1429
|
+
this.width = width;
|
|
1430
|
+
this.height = height;
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* @zh 局部坐标转全局坐标
|
|
1434
|
+
* @en Convert local to global coordinates
|
|
1435
|
+
*/
|
|
1436
|
+
localToGlobal(localX, localY) {
|
|
1437
|
+
return {
|
|
1438
|
+
x: this.originX + localX,
|
|
1439
|
+
y: this.originY + localY
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* @zh 全局坐标转局部坐标
|
|
1444
|
+
* @en Convert global to local coordinates
|
|
1445
|
+
*/
|
|
1446
|
+
globalToLocal(globalX, globalY) {
|
|
1447
|
+
return {
|
|
1448
|
+
x: globalX - this.originX,
|
|
1449
|
+
y: globalY - this.originY
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
isWalkable(x, y) {
|
|
1453
|
+
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
|
|
1454
|
+
return false;
|
|
1455
|
+
}
|
|
1456
|
+
return this.parentMap.isWalkable(this.originX + x, this.originY + y);
|
|
1457
|
+
}
|
|
1458
|
+
getNodeAt(x, y) {
|
|
1459
|
+
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
|
|
1460
|
+
return null;
|
|
1461
|
+
}
|
|
1462
|
+
const globalNode = this.parentMap.getNodeAt(this.originX + x, this.originY + y);
|
|
1463
|
+
if (!globalNode) return null;
|
|
1464
|
+
return {
|
|
1465
|
+
id: y * this.width + x,
|
|
1466
|
+
position: {
|
|
1467
|
+
x,
|
|
1468
|
+
y
|
|
1469
|
+
},
|
|
1470
|
+
cost: globalNode.cost,
|
|
1471
|
+
walkable: globalNode.walkable
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
getNeighbors(node) {
|
|
1475
|
+
const neighbors = [];
|
|
1476
|
+
const { x, y } = node.position;
|
|
1477
|
+
const directions = [
|
|
1478
|
+
{
|
|
1479
|
+
dx: 0,
|
|
1480
|
+
dy: -1
|
|
1481
|
+
},
|
|
1482
|
+
{
|
|
1483
|
+
dx: 1,
|
|
1484
|
+
dy: -1
|
|
1485
|
+
},
|
|
1486
|
+
{
|
|
1487
|
+
dx: 1,
|
|
1488
|
+
dy: 0
|
|
1489
|
+
},
|
|
1490
|
+
{
|
|
1491
|
+
dx: 1,
|
|
1492
|
+
dy: 1
|
|
1493
|
+
},
|
|
1494
|
+
{
|
|
1495
|
+
dx: 0,
|
|
1496
|
+
dy: 1
|
|
1497
|
+
},
|
|
1498
|
+
{
|
|
1499
|
+
dx: -1,
|
|
1500
|
+
dy: 1
|
|
1501
|
+
},
|
|
1502
|
+
{
|
|
1503
|
+
dx: -1,
|
|
1504
|
+
dy: 0
|
|
1505
|
+
},
|
|
1506
|
+
{
|
|
1507
|
+
dx: -1,
|
|
1508
|
+
dy: -1
|
|
1509
|
+
}
|
|
1510
|
+
// NW
|
|
1511
|
+
];
|
|
1512
|
+
for (const dir of directions) {
|
|
1513
|
+
const nx = x + dir.dx;
|
|
1514
|
+
const ny = y + dir.dy;
|
|
1515
|
+
if (nx < 0 || nx >= this.width || ny < 0 || ny >= this.height) {
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
if (!this.isWalkable(nx, ny)) {
|
|
1519
|
+
continue;
|
|
1520
|
+
}
|
|
1521
|
+
if (dir.dx !== 0 && dir.dy !== 0) {
|
|
1522
|
+
if (!this.isWalkable(x + dir.dx, y) || !this.isWalkable(x, y + dir.dy)) {
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
const neighborNode = this.getNodeAt(nx, ny);
|
|
1527
|
+
if (neighborNode) {
|
|
1528
|
+
neighbors.push(neighborNode);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
return neighbors;
|
|
1532
|
+
}
|
|
1533
|
+
heuristic(a, b) {
|
|
1534
|
+
const dx = Math.abs(a.x - b.x);
|
|
1535
|
+
const dy = Math.abs(a.y - b.y);
|
|
1536
|
+
return dx + dy + (Math.SQRT2 - 2) * Math.min(dx, dy);
|
|
1537
|
+
}
|
|
1538
|
+
getMovementCost(from, to) {
|
|
1539
|
+
const dx = Math.abs(to.position.x - from.position.x);
|
|
1540
|
+
const dy = Math.abs(to.position.y - from.position.y);
|
|
1541
|
+
const baseCost = dx !== 0 && dy !== 0 ? Math.SQRT2 : 1;
|
|
1542
|
+
return baseCost * to.cost;
|
|
1543
|
+
}
|
|
1544
|
+
}, __name(_a3, "SubMap"), _a3);
|
|
1545
|
+
var _a4;
|
|
1546
|
+
var Cluster = (_a4 = class {
|
|
1547
|
+
constructor(id, originX, originY, width, height, parentMap) {
|
|
1548
|
+
__publicField(this, "id");
|
|
1549
|
+
__publicField(this, "originX");
|
|
1550
|
+
__publicField(this, "originY");
|
|
1551
|
+
__publicField(this, "width");
|
|
1552
|
+
__publicField(this, "height");
|
|
1553
|
+
__publicField(this, "subMap");
|
|
1554
|
+
/** @zh 集群内的抽象节点 ID 列表 @en Abstract node IDs in this cluster */
|
|
1555
|
+
__publicField(this, "nodeIds", []);
|
|
1556
|
+
/** @zh 预计算的距离缓存 @en Precomputed distance cache */
|
|
1557
|
+
__publicField(this, "distanceCache", /* @__PURE__ */ new Map());
|
|
1558
|
+
/** @zh 预计算的路径缓存 @en Precomputed path cache */
|
|
1559
|
+
__publicField(this, "pathCache", /* @__PURE__ */ new Map());
|
|
1560
|
+
this.id = id;
|
|
1561
|
+
this.originX = originX;
|
|
1562
|
+
this.originY = originY;
|
|
1563
|
+
this.width = width;
|
|
1564
|
+
this.height = height;
|
|
1565
|
+
this.subMap = new SubMap(parentMap, originX, originY, width, height);
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* @zh 检查点是否在集群内
|
|
1569
|
+
* @en Check if point is in cluster
|
|
1570
|
+
*/
|
|
1571
|
+
containsPoint(x, y) {
|
|
1572
|
+
return x >= this.originX && x < this.originX + this.width && y >= this.originY && y < this.originY + this.height;
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* @zh 添加节点 ID
|
|
1576
|
+
* @en Add node ID
|
|
1577
|
+
*/
|
|
1578
|
+
addNodeId(nodeId) {
|
|
1579
|
+
if (!this.nodeIds.includes(nodeId)) {
|
|
1580
|
+
this.nodeIds.push(nodeId);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* @zh 移除节点 ID
|
|
1585
|
+
* @en Remove node ID
|
|
1586
|
+
*/
|
|
1587
|
+
removeNodeId(nodeId) {
|
|
1588
|
+
const idx = this.nodeIds.indexOf(nodeId);
|
|
1589
|
+
if (idx !== -1) {
|
|
1590
|
+
this.nodeIds.splice(idx, 1);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* @zh 生成缓存键
|
|
1595
|
+
* @en Generate cache key
|
|
1596
|
+
*/
|
|
1597
|
+
getCacheKey(fromId, toId) {
|
|
1598
|
+
return `${fromId}->${toId}`;
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* @zh 设置缓存
|
|
1602
|
+
* @en Set cache
|
|
1603
|
+
*/
|
|
1604
|
+
setCache(fromId, toId, cost, path) {
|
|
1605
|
+
const key = this.getCacheKey(fromId, toId);
|
|
1606
|
+
this.distanceCache.set(key, cost);
|
|
1607
|
+
this.pathCache.set(key, path);
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* @zh 获取缓存的距离
|
|
1611
|
+
* @en Get cached distance
|
|
1612
|
+
*/
|
|
1613
|
+
getCachedDistance(fromId, toId) {
|
|
1614
|
+
return this.distanceCache.get(this.getCacheKey(fromId, toId));
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* @zh 获取缓存的路径
|
|
1618
|
+
* @en Get cached path
|
|
1619
|
+
*/
|
|
1620
|
+
getCachedPath(fromId, toId) {
|
|
1621
|
+
return this.pathCache.get(this.getCacheKey(fromId, toId));
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* @zh 清除缓存
|
|
1625
|
+
* @en Clear cache
|
|
1626
|
+
*/
|
|
1627
|
+
clearCache() {
|
|
1628
|
+
this.distanceCache.clear();
|
|
1629
|
+
this.pathCache.clear();
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* @zh 获取缓存大小
|
|
1633
|
+
* @en Get cache size
|
|
1634
|
+
*/
|
|
1635
|
+
getCacheSize() {
|
|
1636
|
+
return this.distanceCache.size;
|
|
1637
|
+
}
|
|
1638
|
+
}, __name(_a4, "Cluster"), _a4);
|
|
1416
1639
|
var _HPAPathfinder = class _HPAPathfinder {
|
|
1417
1640
|
constructor(map, config) {
|
|
1418
1641
|
__publicField(this, "map");
|
|
1419
1642
|
__publicField(this, "config");
|
|
1420
|
-
__publicField(this, "
|
|
1421
|
-
__publicField(this, "
|
|
1643
|
+
__publicField(this, "mapWidth");
|
|
1644
|
+
__publicField(this, "mapHeight");
|
|
1645
|
+
// 集群管理
|
|
1422
1646
|
__publicField(this, "clusters", []);
|
|
1423
|
-
__publicField(this, "entrances", []);
|
|
1424
|
-
__publicField(this, "abstractNodes", /* @__PURE__ */ new Map());
|
|
1425
1647
|
__publicField(this, "clusterGrid", []);
|
|
1426
|
-
__publicField(this, "
|
|
1648
|
+
__publicField(this, "clustersX", 0);
|
|
1649
|
+
__publicField(this, "clustersY", 0);
|
|
1650
|
+
// 抽象图
|
|
1651
|
+
__publicField(this, "abstractNodes", /* @__PURE__ */ new Map());
|
|
1652
|
+
__publicField(this, "nodesByCluster", /* @__PURE__ */ new Map());
|
|
1427
1653
|
__publicField(this, "nextNodeId", 0);
|
|
1428
|
-
|
|
1654
|
+
// 入口统计
|
|
1655
|
+
__publicField(this, "entranceCount", 0);
|
|
1656
|
+
// 内部寻路器
|
|
1429
1657
|
__publicField(this, "localPathfinder");
|
|
1658
|
+
// 完整路径缓存
|
|
1659
|
+
__publicField(this, "pathCache");
|
|
1660
|
+
__publicField(this, "mapVersion", 0);
|
|
1430
1661
|
__publicField(this, "preprocessed", false);
|
|
1431
1662
|
this.map = map;
|
|
1432
1663
|
this.config = {
|
|
@@ -1434,28 +1665,26 @@ var _HPAPathfinder = class _HPAPathfinder {
|
|
|
1434
1665
|
...config
|
|
1435
1666
|
};
|
|
1436
1667
|
const bounds = this.getMapBounds();
|
|
1437
|
-
this.
|
|
1438
|
-
this.
|
|
1668
|
+
this.mapWidth = bounds.width;
|
|
1669
|
+
this.mapHeight = bounds.height;
|
|
1439
1670
|
this.localPathfinder = new AStarPathfinder(map);
|
|
1671
|
+
this.pathCache = new PathCache({
|
|
1672
|
+
maxEntries: 1e3,
|
|
1673
|
+
ttlMs: 0
|
|
1674
|
+
});
|
|
1440
1675
|
}
|
|
1676
|
+
// =========================================================================
|
|
1677
|
+
// 公共 API | Public API
|
|
1678
|
+
// =========================================================================
|
|
1441
1679
|
/**
|
|
1442
1680
|
* @zh 预处理地图(构建抽象图)
|
|
1443
1681
|
* @en Preprocess map (build abstract graph)
|
|
1444
|
-
*
|
|
1445
|
-
* @zh 在地图变化后需要重新调用
|
|
1446
|
-
* @en Need to call again after map changes
|
|
1447
1682
|
*/
|
|
1448
1683
|
preprocess() {
|
|
1449
|
-
this.
|
|
1450
|
-
this.entrances = [];
|
|
1451
|
-
this.abstractNodes.clear();
|
|
1452
|
-
this.clusterGrid = [];
|
|
1453
|
-
this.internalPathCache.clear();
|
|
1454
|
-
this.nextEntranceId = 0;
|
|
1455
|
-
this.nextNodeId = 0;
|
|
1684
|
+
this.clear();
|
|
1456
1685
|
this.buildClusters();
|
|
1457
|
-
this.
|
|
1458
|
-
this.
|
|
1686
|
+
this.buildEntrances();
|
|
1687
|
+
this.buildIntraEdges();
|
|
1459
1688
|
this.preprocessed = true;
|
|
1460
1689
|
}
|
|
1461
1690
|
/**
|
|
@@ -1486,23 +1715,33 @@ var _HPAPathfinder = class _HPAPathfinder {
|
|
|
1486
1715
|
nodesSearched: 1
|
|
1487
1716
|
};
|
|
1488
1717
|
}
|
|
1718
|
+
const cached = this.pathCache.get(startX, startY, endX, endY, this.mapVersion);
|
|
1719
|
+
if (cached) {
|
|
1720
|
+
return cached;
|
|
1721
|
+
}
|
|
1489
1722
|
const startCluster = this.getClusterAt(startX, startY);
|
|
1490
1723
|
const endCluster = this.getClusterAt(endX, endY);
|
|
1491
1724
|
if (!startCluster || !endCluster) {
|
|
1492
1725
|
return EMPTY_PATH_RESULT;
|
|
1493
1726
|
}
|
|
1727
|
+
let result;
|
|
1494
1728
|
if (startCluster.id === endCluster.id) {
|
|
1495
|
-
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1729
|
+
result = this.findLocalPath(startX, startY, endX, endY, opts);
|
|
1730
|
+
} else {
|
|
1731
|
+
const startTemp = this.insertTempNode(startX, startY, startCluster);
|
|
1732
|
+
const endTemp = this.insertTempNode(endX, endY, endCluster);
|
|
1733
|
+
const abstractPath = this.abstractSearch(startTemp, endTemp, opts);
|
|
1734
|
+
this.removeTempNode(startTemp, startCluster);
|
|
1735
|
+
this.removeTempNode(endTemp, endCluster);
|
|
1736
|
+
if (!abstractPath || abstractPath.length === 0) {
|
|
1737
|
+
return EMPTY_PATH_RESULT;
|
|
1738
|
+
}
|
|
1739
|
+
result = this.refinePath(abstractPath, startX, startY, endX, endY, opts);
|
|
1740
|
+
}
|
|
1741
|
+
if (result.found) {
|
|
1742
|
+
this.pathCache.set(startX, startY, endX, endY, result, this.mapVersion);
|
|
1504
1743
|
}
|
|
1505
|
-
return
|
|
1744
|
+
return result;
|
|
1506
1745
|
}
|
|
1507
1746
|
/**
|
|
1508
1747
|
* @zh 清理状态
|
|
@@ -1510,10 +1749,13 @@ var _HPAPathfinder = class _HPAPathfinder {
|
|
|
1510
1749
|
*/
|
|
1511
1750
|
clear() {
|
|
1512
1751
|
this.clusters = [];
|
|
1513
|
-
this.entrances = [];
|
|
1514
|
-
this.abstractNodes.clear();
|
|
1515
1752
|
this.clusterGrid = [];
|
|
1516
|
-
this.
|
|
1753
|
+
this.abstractNodes.clear();
|
|
1754
|
+
this.nodesByCluster.clear();
|
|
1755
|
+
this.nextNodeId = 0;
|
|
1756
|
+
this.entranceCount = 0;
|
|
1757
|
+
this.pathCache.invalidateAll();
|
|
1758
|
+
this.mapVersion++;
|
|
1517
1759
|
this.preprocessed = false;
|
|
1518
1760
|
}
|
|
1519
1761
|
/**
|
|
@@ -1521,19 +1763,34 @@ var _HPAPathfinder = class _HPAPathfinder {
|
|
|
1521
1763
|
* @en Notify map region change
|
|
1522
1764
|
*/
|
|
1523
1765
|
notifyRegionChange(minX, minY, maxX, maxY) {
|
|
1524
|
-
this.
|
|
1525
|
-
|
|
1766
|
+
const affectedClusters = this.getAffectedClusters(minX, minY, maxX, maxY);
|
|
1767
|
+
for (const cluster of affectedClusters) {
|
|
1768
|
+
cluster.clearCache();
|
|
1769
|
+
for (const nodeId of cluster.nodeIds) {
|
|
1770
|
+
const node = this.abstractNodes.get(nodeId);
|
|
1771
|
+
if (node) {
|
|
1772
|
+
node.edges = node.edges.filter((e) => e.isInterEdge);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
this.buildClusterIntraEdges(cluster);
|
|
1776
|
+
}
|
|
1777
|
+
this.pathCache.invalidateRegion(minX, minY, maxX, maxY);
|
|
1778
|
+
this.mapVersion++;
|
|
1526
1779
|
}
|
|
1527
1780
|
/**
|
|
1528
1781
|
* @zh 获取预处理统计信息
|
|
1529
1782
|
* @en Get preprocessing statistics
|
|
1530
1783
|
*/
|
|
1531
1784
|
getStats() {
|
|
1785
|
+
let cacheSize = 0;
|
|
1786
|
+
for (const cluster of this.clusters) {
|
|
1787
|
+
cacheSize += cluster.getCacheSize();
|
|
1788
|
+
}
|
|
1532
1789
|
return {
|
|
1533
1790
|
clusters: this.clusters.length,
|
|
1534
|
-
entrances: this.
|
|
1791
|
+
entrances: this.entranceCount,
|
|
1535
1792
|
abstractNodes: this.abstractNodes.size,
|
|
1536
|
-
cacheSize
|
|
1793
|
+
cacheSize
|
|
1537
1794
|
};
|
|
1538
1795
|
}
|
|
1539
1796
|
// =========================================================================
|
|
@@ -1552,354 +1809,657 @@ var _HPAPathfinder = class _HPAPathfinder {
|
|
|
1552
1809
|
height: 1e3
|
|
1553
1810
|
};
|
|
1554
1811
|
}
|
|
1812
|
+
/**
|
|
1813
|
+
* @zh 构建集群
|
|
1814
|
+
* @en Build clusters
|
|
1815
|
+
*/
|
|
1555
1816
|
buildClusters() {
|
|
1556
1817
|
const clusterSize = this.config.clusterSize;
|
|
1557
|
-
|
|
1558
|
-
|
|
1818
|
+
this.clustersX = Math.ceil(this.mapWidth / clusterSize);
|
|
1819
|
+
this.clustersY = Math.ceil(this.mapHeight / clusterSize);
|
|
1559
1820
|
this.clusterGrid = [];
|
|
1560
|
-
for (let
|
|
1561
|
-
this.clusterGrid[
|
|
1821
|
+
for (let cx = 0; cx < this.clustersX; cx++) {
|
|
1822
|
+
this.clusterGrid[cx] = [];
|
|
1823
|
+
for (let cy = 0; cy < this.clustersY; cy++) {
|
|
1824
|
+
this.clusterGrid[cx][cy] = null;
|
|
1825
|
+
}
|
|
1562
1826
|
}
|
|
1563
1827
|
let clusterId = 0;
|
|
1564
|
-
for (let cy = 0; cy < clustersY; cy++) {
|
|
1565
|
-
for (let cx = 0; cx < clustersX; cx++) {
|
|
1566
|
-
const
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
height: Math.min(clusterSize, this.height - cy * clusterSize),
|
|
1572
|
-
entrances: []
|
|
1573
|
-
};
|
|
1828
|
+
for (let cy = 0; cy < this.clustersY; cy++) {
|
|
1829
|
+
for (let cx = 0; cx < this.clustersX; cx++) {
|
|
1830
|
+
const originX = cx * clusterSize;
|
|
1831
|
+
const originY = cy * clusterSize;
|
|
1832
|
+
const width = Math.min(clusterSize, this.mapWidth - originX);
|
|
1833
|
+
const height = Math.min(clusterSize, this.mapHeight - originY);
|
|
1834
|
+
const cluster = new Cluster(clusterId, originX, originY, width, height, this.map);
|
|
1574
1835
|
this.clusters.push(cluster);
|
|
1575
|
-
this.clusterGrid[cx][cy] =
|
|
1836
|
+
this.clusterGrid[cx][cy] = clusterId;
|
|
1837
|
+
this.nodesByCluster.set(clusterId, []);
|
|
1838
|
+
clusterId++;
|
|
1576
1839
|
}
|
|
1577
1840
|
}
|
|
1578
1841
|
}
|
|
1579
|
-
|
|
1842
|
+
/**
|
|
1843
|
+
* @zh 检测入口并创建抽象节点
|
|
1844
|
+
* @en Detect entrances and create abstract nodes
|
|
1845
|
+
*/
|
|
1846
|
+
buildEntrances() {
|
|
1580
1847
|
const clusterSize = this.config.clusterSize;
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
const
|
|
1586
|
-
if (
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
this.
|
|
1848
|
+
for (let cy = 0; cy < this.clustersY; cy++) {
|
|
1849
|
+
for (let cx = 0; cx < this.clustersX; cx++) {
|
|
1850
|
+
const clusterId = this.clusterGrid[cx][cy];
|
|
1851
|
+
if (clusterId === null) continue;
|
|
1852
|
+
const cluster1 = this.clusters[clusterId];
|
|
1853
|
+
if (cx < this.clustersX - 1) {
|
|
1854
|
+
const cluster2Id = this.clusterGrid[cx + 1][cy];
|
|
1855
|
+
if (cluster2Id !== null) {
|
|
1856
|
+
const cluster2 = this.clusters[cluster2Id];
|
|
1857
|
+
this.detectAndCreateEntrances(cluster1, cluster2, "vertical");
|
|
1591
1858
|
}
|
|
1592
1859
|
}
|
|
1593
|
-
if (cy < clustersY - 1) {
|
|
1594
|
-
const
|
|
1595
|
-
if (
|
|
1596
|
-
this.
|
|
1860
|
+
if (cy < this.clustersY - 1) {
|
|
1861
|
+
const cluster2Id = this.clusterGrid[cx][cy + 1];
|
|
1862
|
+
if (cluster2Id !== null) {
|
|
1863
|
+
const cluster2 = this.clusters[cluster2Id];
|
|
1864
|
+
this.detectAndCreateEntrances(cluster1, cluster2, "horizontal");
|
|
1597
1865
|
}
|
|
1598
1866
|
}
|
|
1599
1867
|
}
|
|
1600
1868
|
}
|
|
1601
1869
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1870
|
+
/**
|
|
1871
|
+
* @zh 检测并创建两个相邻集群之间的入口
|
|
1872
|
+
* @en Detect and create entrances between two adjacent clusters
|
|
1873
|
+
*/
|
|
1874
|
+
detectAndCreateEntrances(cluster1, cluster2, boundaryDirection) {
|
|
1875
|
+
const spans = this.detectEntranceSpans(cluster1, cluster2, boundaryDirection);
|
|
1876
|
+
for (const span of spans) {
|
|
1877
|
+
this.createEntranceNodes(cluster1, cluster2, span, boundaryDirection);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* @zh 检测边界上的连续可通行区间
|
|
1882
|
+
* @en Detect continuous walkable spans on boundary
|
|
1883
|
+
*/
|
|
1884
|
+
detectEntranceSpans(cluster1, cluster2, boundaryDirection) {
|
|
1885
|
+
const spans = [];
|
|
1886
|
+
if (boundaryDirection === "vertical") {
|
|
1887
|
+
const x1 = cluster1.originX + cluster1.width - 1;
|
|
1888
|
+
const x2 = cluster2.originX;
|
|
1889
|
+
const startY = Math.max(cluster1.originY, cluster2.originY);
|
|
1890
|
+
const endY = Math.min(cluster1.originY + cluster1.height, cluster2.originY + cluster2.height);
|
|
1891
|
+
let spanStart = null;
|
|
1611
1892
|
for (let y = startY; y < endY; y++) {
|
|
1612
1893
|
const walkable1 = this.map.isWalkable(x1, y);
|
|
1613
1894
|
const walkable2 = this.map.isWalkable(x2, y);
|
|
1614
1895
|
if (walkable1 && walkable2) {
|
|
1615
|
-
if (
|
|
1616
|
-
|
|
1617
|
-
entranceLength = 1;
|
|
1618
|
-
} else {
|
|
1619
|
-
entranceLength++;
|
|
1896
|
+
if (spanStart === null) {
|
|
1897
|
+
spanStart = y;
|
|
1620
1898
|
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1899
|
+
} else {
|
|
1900
|
+
if (spanStart !== null) {
|
|
1901
|
+
spans.push({
|
|
1902
|
+
start: spanStart,
|
|
1903
|
+
end: y - 1
|
|
1904
|
+
});
|
|
1905
|
+
spanStart = null;
|
|
1625
1906
|
}
|
|
1626
|
-
} else if (entranceStart !== null) {
|
|
1627
|
-
this.createEntrance(cluster1, cluster2, x1, x2, entranceStart, entranceStart + entranceLength - 1, "horizontal");
|
|
1628
|
-
entranceStart = null;
|
|
1629
|
-
entranceLength = 0;
|
|
1630
1907
|
}
|
|
1631
1908
|
}
|
|
1909
|
+
if (spanStart !== null) {
|
|
1910
|
+
spans.push({
|
|
1911
|
+
start: spanStart,
|
|
1912
|
+
end: endY - 1
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1632
1915
|
} else {
|
|
1633
|
-
const y1 = cluster1.
|
|
1634
|
-
const y2 = cluster2.
|
|
1635
|
-
const startX = Math.max(cluster1.
|
|
1636
|
-
const endX = Math.min(cluster1.
|
|
1916
|
+
const y1 = cluster1.originY + cluster1.height - 1;
|
|
1917
|
+
const y2 = cluster2.originY;
|
|
1918
|
+
const startX = Math.max(cluster1.originX, cluster2.originX);
|
|
1919
|
+
const endX = Math.min(cluster1.originX + cluster1.width, cluster2.originX + cluster2.width);
|
|
1920
|
+
let spanStart = null;
|
|
1637
1921
|
for (let x = startX; x < endX; x++) {
|
|
1638
1922
|
const walkable1 = this.map.isWalkable(x, y1);
|
|
1639
1923
|
const walkable2 = this.map.isWalkable(x, y2);
|
|
1640
1924
|
if (walkable1 && walkable2) {
|
|
1641
|
-
if (
|
|
1642
|
-
|
|
1643
|
-
entranceLength = 1;
|
|
1644
|
-
} else {
|
|
1645
|
-
entranceLength++;
|
|
1925
|
+
if (spanStart === null) {
|
|
1926
|
+
spanStart = x;
|
|
1646
1927
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1928
|
+
} else {
|
|
1929
|
+
if (spanStart !== null) {
|
|
1930
|
+
spans.push({
|
|
1931
|
+
start: spanStart,
|
|
1932
|
+
end: x - 1
|
|
1933
|
+
});
|
|
1934
|
+
spanStart = null;
|
|
1651
1935
|
}
|
|
1652
|
-
} else if (entranceStart !== null) {
|
|
1653
|
-
this.createEntrance(cluster1, cluster2, entranceStart, entranceStart + entranceLength - 1, y1, y2, "vertical");
|
|
1654
|
-
entranceStart = null;
|
|
1655
|
-
entranceLength = 0;
|
|
1656
1936
|
}
|
|
1657
1937
|
}
|
|
1938
|
+
if (spanStart !== null) {
|
|
1939
|
+
spans.push({
|
|
1940
|
+
start: spanStart,
|
|
1941
|
+
end: endX - 1
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1658
1944
|
}
|
|
1945
|
+
return spans;
|
|
1659
1946
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
x: coord2Start,
|
|
1672
|
-
y: midY
|
|
1673
|
-
};
|
|
1674
|
-
center = {
|
|
1675
|
-
x: coord1Start,
|
|
1676
|
-
y: midY
|
|
1677
|
-
};
|
|
1947
|
+
/**
|
|
1948
|
+
* @zh 为入口区间创建抽象节点
|
|
1949
|
+
* @en Create abstract nodes for entrance span
|
|
1950
|
+
*/
|
|
1951
|
+
createEntranceNodes(cluster1, cluster2, span, boundaryDirection) {
|
|
1952
|
+
const spanLength = span.end - span.start + 1;
|
|
1953
|
+
const maxWidth = this.config.maxEntranceWidth;
|
|
1954
|
+
const strategy = this.config.entranceStrategy;
|
|
1955
|
+
const positions = [];
|
|
1956
|
+
if (spanLength <= maxWidth) {
|
|
1957
|
+
positions.push(Math.floor((span.start + span.end) / 2));
|
|
1678
1958
|
} else {
|
|
1679
|
-
const
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1959
|
+
const numNodes = Math.ceil(spanLength / maxWidth);
|
|
1960
|
+
const spacing = spanLength / numNodes;
|
|
1961
|
+
for (let i = 0; i < numNodes; i++) {
|
|
1962
|
+
const pos = Math.floor(span.start + spacing * (i + 0.5));
|
|
1963
|
+
positions.push(Math.min(pos, span.end));
|
|
1964
|
+
}
|
|
1965
|
+
if (strategy === "end") {
|
|
1966
|
+
if (!positions.includes(span.start)) {
|
|
1967
|
+
positions.unshift(span.start);
|
|
1968
|
+
}
|
|
1969
|
+
if (!positions.includes(span.end)) {
|
|
1970
|
+
positions.push(span.end);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1692
1973
|
}
|
|
1693
|
-
const
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1974
|
+
for (const pos of positions) {
|
|
1975
|
+
let p1, p2;
|
|
1976
|
+
if (boundaryDirection === "vertical") {
|
|
1977
|
+
p1 = {
|
|
1978
|
+
x: cluster1.originX + cluster1.width - 1,
|
|
1979
|
+
y: pos
|
|
1980
|
+
};
|
|
1981
|
+
p2 = {
|
|
1982
|
+
x: cluster2.originX,
|
|
1983
|
+
y: pos
|
|
1984
|
+
};
|
|
1985
|
+
} else {
|
|
1986
|
+
p1 = {
|
|
1987
|
+
x: pos,
|
|
1988
|
+
y: cluster1.originY + cluster1.height - 1
|
|
1989
|
+
};
|
|
1990
|
+
p2 = {
|
|
1991
|
+
x: pos,
|
|
1992
|
+
y: cluster2.originY
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
const node1 = this.createAbstractNode(p1, cluster1);
|
|
1996
|
+
const node2 = this.createAbstractNode(p2, cluster2);
|
|
1997
|
+
const interCost = 1;
|
|
1709
1998
|
node1.edges.push({
|
|
1710
1999
|
targetNodeId: node2.id,
|
|
1711
|
-
cost:
|
|
1712
|
-
isInterEdge: true
|
|
2000
|
+
cost: interCost,
|
|
2001
|
+
isInterEdge: true,
|
|
2002
|
+
innerPath: null
|
|
1713
2003
|
});
|
|
1714
2004
|
node2.edges.push({
|
|
1715
2005
|
targetNodeId: node1.id,
|
|
1716
|
-
cost:
|
|
1717
|
-
isInterEdge: true
|
|
2006
|
+
cost: interCost,
|
|
2007
|
+
isInterEdge: true,
|
|
2008
|
+
innerPath: null
|
|
1718
2009
|
});
|
|
1719
|
-
|
|
1720
|
-
for (const cluster of this.clusters) {
|
|
1721
|
-
this.connectIntraClusterNodes(cluster);
|
|
2010
|
+
this.entranceCount++;
|
|
1722
2011
|
}
|
|
1723
2012
|
}
|
|
1724
|
-
|
|
2013
|
+
/**
|
|
2014
|
+
* @zh 创建抽象节点
|
|
2015
|
+
* @en Create abstract node
|
|
2016
|
+
*/
|
|
2017
|
+
createAbstractNode(position, cluster) {
|
|
2018
|
+
const concreteId = position.y * this.mapWidth + position.x;
|
|
2019
|
+
for (const nodeId of cluster.nodeIds) {
|
|
2020
|
+
const existing = this.abstractNodes.get(nodeId);
|
|
2021
|
+
if (existing && existing.concreteNodeId === concreteId) {
|
|
2022
|
+
return existing;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
1725
2025
|
const node = {
|
|
1726
2026
|
id: this.nextNodeId++,
|
|
1727
|
-
position
|
|
1728
|
-
|
|
1729
|
-
|
|
2027
|
+
position: {
|
|
2028
|
+
x: position.x,
|
|
2029
|
+
y: position.y
|
|
2030
|
+
},
|
|
2031
|
+
clusterId: cluster.id,
|
|
2032
|
+
concreteNodeId: concreteId,
|
|
1730
2033
|
edges: []
|
|
1731
2034
|
};
|
|
1732
2035
|
this.abstractNodes.set(node.id, node);
|
|
2036
|
+
cluster.addNodeId(node.id);
|
|
2037
|
+
const clusterNodes = this.nodesByCluster.get(cluster.id);
|
|
2038
|
+
if (clusterNodes) {
|
|
2039
|
+
clusterNodes.push(node.id);
|
|
2040
|
+
}
|
|
1733
2041
|
return node;
|
|
1734
2042
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
2043
|
+
/**
|
|
2044
|
+
* @zh 构建所有集群的 intra-edges
|
|
2045
|
+
* @en Build intra-edges for all clusters
|
|
2046
|
+
*/
|
|
2047
|
+
buildIntraEdges() {
|
|
2048
|
+
for (const cluster of this.clusters) {
|
|
2049
|
+
this.buildClusterIntraEdges(cluster);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* @zh 构建单个集群的 intra-edges
|
|
2054
|
+
* @en Build intra-edges for single cluster
|
|
2055
|
+
*/
|
|
2056
|
+
buildClusterIntraEdges(cluster) {
|
|
2057
|
+
const nodeIds = cluster.nodeIds;
|
|
2058
|
+
if (nodeIds.length < 2) return;
|
|
2059
|
+
if (this.config.lazyIntraEdges) {
|
|
2060
|
+
this.buildLazyIntraEdges(cluster);
|
|
2061
|
+
} else {
|
|
2062
|
+
this.buildEagerIntraEdges(cluster);
|
|
1741
2063
|
}
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
2064
|
+
}
|
|
2065
|
+
/**
|
|
2066
|
+
* @zh 延迟构建 intra-edges(只用启发式距离)
|
|
2067
|
+
* @en Build lazy intra-edges (using heuristic distance only)
|
|
2068
|
+
*/
|
|
2069
|
+
buildLazyIntraEdges(cluster) {
|
|
2070
|
+
const nodeIds = cluster.nodeIds;
|
|
2071
|
+
for (let i = 0; i < nodeIds.length; i++) {
|
|
2072
|
+
for (let j = i + 1; j < nodeIds.length; j++) {
|
|
2073
|
+
const node1 = this.abstractNodes.get(nodeIds[i]);
|
|
2074
|
+
const node2 = this.abstractNodes.get(nodeIds[j]);
|
|
2075
|
+
const heuristicCost = this.heuristic(node1.position, node2.position);
|
|
1747
2076
|
node1.edges.push({
|
|
1748
2077
|
targetNodeId: node2.id,
|
|
1749
|
-
cost,
|
|
1750
|
-
isInterEdge: false
|
|
2078
|
+
cost: heuristicCost,
|
|
2079
|
+
isInterEdge: false,
|
|
2080
|
+
innerPath: null
|
|
2081
|
+
// 标记为未计算
|
|
1751
2082
|
});
|
|
1752
2083
|
node2.edges.push({
|
|
1753
2084
|
targetNodeId: node1.id,
|
|
1754
|
-
cost,
|
|
1755
|
-
isInterEdge: false
|
|
2085
|
+
cost: heuristicCost,
|
|
2086
|
+
isInterEdge: false,
|
|
2087
|
+
innerPath: null
|
|
1756
2088
|
});
|
|
1757
2089
|
}
|
|
1758
2090
|
}
|
|
1759
2091
|
}
|
|
2092
|
+
/**
|
|
2093
|
+
* @zh 立即构建 intra-edges(计算真实路径)
|
|
2094
|
+
* @en Build eager intra-edges (compute actual paths)
|
|
2095
|
+
*/
|
|
2096
|
+
buildEagerIntraEdges(cluster) {
|
|
2097
|
+
const nodeIds = cluster.nodeIds;
|
|
2098
|
+
const subPathfinder = new AStarPathfinder(cluster.subMap);
|
|
2099
|
+
for (let i = 0; i < nodeIds.length; i++) {
|
|
2100
|
+
for (let j = i + 1; j < nodeIds.length; j++) {
|
|
2101
|
+
const node1 = this.abstractNodes.get(nodeIds[i]);
|
|
2102
|
+
const node2 = this.abstractNodes.get(nodeIds[j]);
|
|
2103
|
+
const local1 = cluster.subMap.globalToLocal(node1.position.x, node1.position.y);
|
|
2104
|
+
const local2 = cluster.subMap.globalToLocal(node2.position.x, node2.position.y);
|
|
2105
|
+
const result = subPathfinder.findPath(local1.x, local1.y, local2.x, local2.y);
|
|
2106
|
+
if (result.found && result.path.length > 0) {
|
|
2107
|
+
const globalPath = result.path.map((p) => {
|
|
2108
|
+
const global = cluster.subMap.localToGlobal(p.x, p.y);
|
|
2109
|
+
return global.y * this.mapWidth + global.x;
|
|
2110
|
+
});
|
|
2111
|
+
if (this.config.cacheInternalPaths) {
|
|
2112
|
+
cluster.setCache(node1.id, node2.id, result.cost, globalPath);
|
|
2113
|
+
cluster.setCache(node2.id, node1.id, result.cost, [
|
|
2114
|
+
...globalPath
|
|
2115
|
+
].reverse());
|
|
2116
|
+
}
|
|
2117
|
+
node1.edges.push({
|
|
2118
|
+
targetNodeId: node2.id,
|
|
2119
|
+
cost: result.cost,
|
|
2120
|
+
isInterEdge: false,
|
|
2121
|
+
innerPath: this.config.cacheInternalPaths ? globalPath : null
|
|
2122
|
+
});
|
|
2123
|
+
node2.edges.push({
|
|
2124
|
+
targetNodeId: node1.id,
|
|
2125
|
+
cost: result.cost,
|
|
2126
|
+
isInterEdge: false,
|
|
2127
|
+
innerPath: this.config.cacheInternalPaths ? [
|
|
2128
|
+
...globalPath
|
|
2129
|
+
].reverse() : null
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* @zh 按需计算 intra-edge 的真实路径
|
|
2137
|
+
* @en Compute actual path for intra-edge on demand
|
|
2138
|
+
*/
|
|
2139
|
+
computeIntraEdgePath(fromNode, toNode, edge) {
|
|
2140
|
+
const cluster = this.clusters[fromNode.clusterId];
|
|
2141
|
+
if (!cluster) return null;
|
|
2142
|
+
const cachedPath = cluster.getCachedPath(fromNode.id, toNode.id);
|
|
2143
|
+
const cachedCost = cluster.getCachedDistance(fromNode.id, toNode.id);
|
|
2144
|
+
if (cachedPath && cachedCost !== void 0) {
|
|
2145
|
+
edge.cost = cachedCost;
|
|
2146
|
+
edge.innerPath = cachedPath;
|
|
2147
|
+
return {
|
|
2148
|
+
cost: cachedCost,
|
|
2149
|
+
path: cachedPath
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
const subPathfinder = new AStarPathfinder(cluster.subMap);
|
|
2153
|
+
const local1 = cluster.subMap.globalToLocal(fromNode.position.x, fromNode.position.y);
|
|
2154
|
+
const local2 = cluster.subMap.globalToLocal(toNode.position.x, toNode.position.y);
|
|
2155
|
+
const result = subPathfinder.findPath(local1.x, local1.y, local2.x, local2.y);
|
|
2156
|
+
if (result.found && result.path.length > 0) {
|
|
2157
|
+
const globalPath = result.path.map((p) => {
|
|
2158
|
+
const global = cluster.subMap.localToGlobal(p.x, p.y);
|
|
2159
|
+
return global.y * this.mapWidth + global.x;
|
|
2160
|
+
});
|
|
2161
|
+
if (this.config.cacheInternalPaths) {
|
|
2162
|
+
cluster.setCache(fromNode.id, toNode.id, result.cost, globalPath);
|
|
2163
|
+
cluster.setCache(toNode.id, fromNode.id, result.cost, [
|
|
2164
|
+
...globalPath
|
|
2165
|
+
].reverse());
|
|
2166
|
+
}
|
|
2167
|
+
edge.cost = result.cost;
|
|
2168
|
+
edge.innerPath = globalPath;
|
|
2169
|
+
const reverseEdge = toNode.edges.find((e) => e.targetNodeId === fromNode.id);
|
|
2170
|
+
if (reverseEdge) {
|
|
2171
|
+
reverseEdge.cost = result.cost;
|
|
2172
|
+
reverseEdge.innerPath = [
|
|
2173
|
+
...globalPath
|
|
2174
|
+
].reverse();
|
|
2175
|
+
}
|
|
2176
|
+
return {
|
|
2177
|
+
cost: result.cost,
|
|
2178
|
+
path: globalPath
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
return null;
|
|
2182
|
+
}
|
|
1760
2183
|
// =========================================================================
|
|
1761
2184
|
// 搜索方法 | Search Methods
|
|
1762
2185
|
// =========================================================================
|
|
2186
|
+
/**
|
|
2187
|
+
* @zh 获取指定位置的集群
|
|
2188
|
+
* @en Get cluster at position
|
|
2189
|
+
*/
|
|
1763
2190
|
getClusterAt(x, y) {
|
|
2191
|
+
const cx = Math.floor(x / this.config.clusterSize);
|
|
2192
|
+
const cy = Math.floor(y / this.config.clusterSize);
|
|
2193
|
+
if (cx < 0 || cx >= this.clustersX || cy < 0 || cy >= this.clustersY) {
|
|
2194
|
+
return null;
|
|
2195
|
+
}
|
|
2196
|
+
const clusterId = this.clusterGrid[cx]?.[cy];
|
|
2197
|
+
if (clusterId === null || clusterId === void 0) {
|
|
2198
|
+
return null;
|
|
2199
|
+
}
|
|
2200
|
+
return this.clusters[clusterId] || null;
|
|
2201
|
+
}
|
|
2202
|
+
/**
|
|
2203
|
+
* @zh 获取受影响的集群
|
|
2204
|
+
* @en Get affected clusters
|
|
2205
|
+
*/
|
|
2206
|
+
getAffectedClusters(minX, minY, maxX, maxY) {
|
|
2207
|
+
const affected = [];
|
|
1764
2208
|
const clusterSize = this.config.clusterSize;
|
|
1765
|
-
const
|
|
1766
|
-
const
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
2209
|
+
const minCX = Math.floor(minX / clusterSize);
|
|
2210
|
+
const maxCX = Math.floor(maxX / clusterSize);
|
|
2211
|
+
const minCY = Math.floor(minY / clusterSize);
|
|
2212
|
+
const maxCY = Math.floor(maxY / clusterSize);
|
|
2213
|
+
for (let cy = minCY; cy <= maxCY; cy++) {
|
|
2214
|
+
for (let cx = minCX; cx <= maxCX; cx++) {
|
|
2215
|
+
if (cx >= 0 && cx < this.clustersX && cy >= 0 && cy < this.clustersY) {
|
|
2216
|
+
const clusterId = this.clusterGrid[cx]?.[cy];
|
|
2217
|
+
if (clusterId !== null && clusterId !== void 0) {
|
|
2218
|
+
affected.push(this.clusters[clusterId]);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
return affected;
|
|
2224
|
+
}
|
|
2225
|
+
/**
|
|
2226
|
+
* @zh 插入临时节点
|
|
2227
|
+
* @en Insert temporary node
|
|
2228
|
+
*/
|
|
2229
|
+
insertTempNode(x, y, cluster) {
|
|
2230
|
+
const concreteId = y * this.mapWidth + x;
|
|
2231
|
+
for (const nodeId of cluster.nodeIds) {
|
|
2232
|
+
const existing = this.abstractNodes.get(nodeId);
|
|
2233
|
+
if (existing && existing.concreteNodeId === concreteId) {
|
|
2234
|
+
return existing;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
const tempNode = {
|
|
2238
|
+
id: this.nextNodeId++,
|
|
2239
|
+
position: {
|
|
2240
|
+
x,
|
|
2241
|
+
y
|
|
2242
|
+
},
|
|
2243
|
+
clusterId: cluster.id,
|
|
2244
|
+
concreteNodeId: concreteId,
|
|
2245
|
+
edges: []
|
|
2246
|
+
};
|
|
2247
|
+
this.abstractNodes.set(tempNode.id, tempNode);
|
|
2248
|
+
cluster.addNodeId(tempNode.id);
|
|
2249
|
+
const subPathfinder = new AStarPathfinder(cluster.subMap);
|
|
2250
|
+
const localPos = cluster.subMap.globalToLocal(x, y);
|
|
2251
|
+
for (const existingNodeId of cluster.nodeIds) {
|
|
2252
|
+
if (existingNodeId === tempNode.id) continue;
|
|
2253
|
+
const existingNode = this.abstractNodes.get(existingNodeId);
|
|
2254
|
+
if (!existingNode) continue;
|
|
2255
|
+
const targetLocalPos = cluster.subMap.globalToLocal(existingNode.position.x, existingNode.position.y);
|
|
2256
|
+
const result = subPathfinder.findPath(localPos.x, localPos.y, targetLocalPos.x, targetLocalPos.y);
|
|
2257
|
+
if (result.found && result.path.length > 0) {
|
|
2258
|
+
const globalPath = result.path.map((p) => {
|
|
2259
|
+
const global = cluster.subMap.localToGlobal(p.x, p.y);
|
|
2260
|
+
return global.y * this.mapWidth + global.x;
|
|
2261
|
+
});
|
|
1782
2262
|
tempNode.edges.push({
|
|
1783
|
-
targetNodeId:
|
|
1784
|
-
cost,
|
|
1785
|
-
isInterEdge: false
|
|
2263
|
+
targetNodeId: existingNode.id,
|
|
2264
|
+
cost: result.cost,
|
|
2265
|
+
isInterEdge: false,
|
|
2266
|
+
innerPath: globalPath
|
|
1786
2267
|
});
|
|
1787
|
-
|
|
2268
|
+
existingNode.edges.push({
|
|
1788
2269
|
targetNodeId: tempNode.id,
|
|
1789
|
-
cost,
|
|
1790
|
-
isInterEdge: false
|
|
2270
|
+
cost: result.cost,
|
|
2271
|
+
isInterEdge: false,
|
|
2272
|
+
innerPath: [
|
|
2273
|
+
...globalPath
|
|
2274
|
+
].reverse()
|
|
1791
2275
|
});
|
|
1792
2276
|
}
|
|
1793
2277
|
}
|
|
1794
|
-
return
|
|
2278
|
+
return tempNode;
|
|
1795
2279
|
}
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
2280
|
+
/**
|
|
2281
|
+
* @zh 移除临时节点
|
|
2282
|
+
* @en Remove temporary node
|
|
2283
|
+
*/
|
|
2284
|
+
removeTempNode(node, cluster) {
|
|
2285
|
+
for (const existingNodeId of cluster.nodeIds) {
|
|
2286
|
+
if (existingNodeId === node.id) continue;
|
|
2287
|
+
const existingNode = this.abstractNodes.get(existingNodeId);
|
|
2288
|
+
if (existingNode) {
|
|
2289
|
+
existingNode.edges = existingNode.edges.filter((e) => e.targetNodeId !== node.id);
|
|
1803
2290
|
}
|
|
1804
|
-
this.abstractNodes.delete(node.id);
|
|
1805
2291
|
}
|
|
2292
|
+
cluster.removeNodeId(node.id);
|
|
2293
|
+
this.abstractNodes.delete(node.id);
|
|
1806
2294
|
}
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
const openList = new
|
|
1813
|
-
const
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
2295
|
+
/**
|
|
2296
|
+
* @zh 在抽象图上进行 A* 搜索
|
|
2297
|
+
* @en Perform A* search on abstract graph
|
|
2298
|
+
*/
|
|
2299
|
+
abstractSearch(startNode, endNode, opts) {
|
|
2300
|
+
const openList = new IndexedBinaryHeap((a, b) => a.f - b.f);
|
|
2301
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
2302
|
+
const endPosition = endNode.position;
|
|
2303
|
+
const h = this.heuristic(startNode.position, endPosition) * opts.heuristicWeight;
|
|
2304
|
+
const startSearchNode = {
|
|
2305
|
+
node: startNode,
|
|
2306
|
+
g: 0,
|
|
2307
|
+
h,
|
|
2308
|
+
f: h,
|
|
2309
|
+
parent: null,
|
|
2310
|
+
closed: false,
|
|
2311
|
+
opened: true,
|
|
2312
|
+
heapIndex: -1
|
|
2313
|
+
};
|
|
2314
|
+
openList.push(startSearchNode);
|
|
2315
|
+
nodeMap.set(startNode.id, startSearchNode);
|
|
1824
2316
|
let nodesSearched = 0;
|
|
1825
2317
|
while (!openList.isEmpty && nodesSearched < opts.maxNodes) {
|
|
1826
2318
|
const current = openList.pop();
|
|
2319
|
+
current.closed = true;
|
|
1827
2320
|
nodesSearched++;
|
|
1828
|
-
if (
|
|
1829
|
-
return this.
|
|
1830
|
-
}
|
|
1831
|
-
if (closedSet.has(current.abstractNode.id)) {
|
|
1832
|
-
continue;
|
|
2321
|
+
if (current.node.id === endNode.id) {
|
|
2322
|
+
return this.reconstructPath(current);
|
|
1833
2323
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
if (
|
|
1837
|
-
|
|
2324
|
+
for (const edge of current.node.edges) {
|
|
2325
|
+
let neighbor = nodeMap.get(edge.targetNodeId);
|
|
2326
|
+
if (!neighbor) {
|
|
2327
|
+
const neighborNode = this.abstractNodes.get(edge.targetNodeId);
|
|
2328
|
+
if (!neighborNode) continue;
|
|
2329
|
+
const nh = this.heuristic(neighborNode.position, endPosition) * opts.heuristicWeight;
|
|
2330
|
+
neighbor = {
|
|
2331
|
+
node: neighborNode,
|
|
2332
|
+
g: Infinity,
|
|
2333
|
+
h: nh,
|
|
2334
|
+
f: Infinity,
|
|
2335
|
+
parent: null,
|
|
2336
|
+
closed: false,
|
|
2337
|
+
opened: false,
|
|
2338
|
+
heapIndex: -1
|
|
2339
|
+
};
|
|
2340
|
+
nodeMap.set(edge.targetNodeId, neighbor);
|
|
1838
2341
|
}
|
|
1839
|
-
|
|
1840
|
-
if (!neighbor) continue;
|
|
2342
|
+
if (neighbor.closed) continue;
|
|
1841
2343
|
const tentativeG = current.g + edge.cost;
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
2344
|
+
if (!neighbor.opened) {
|
|
2345
|
+
neighbor.g = tentativeG;
|
|
2346
|
+
neighbor.f = tentativeG + neighbor.h;
|
|
2347
|
+
neighbor.parent = current;
|
|
2348
|
+
neighbor.opened = true;
|
|
2349
|
+
openList.push(neighbor);
|
|
2350
|
+
} else if (tentativeG < neighbor.g) {
|
|
2351
|
+
neighbor.g = tentativeG;
|
|
2352
|
+
neighbor.f = tentativeG + neighbor.h;
|
|
2353
|
+
neighbor.parent = current;
|
|
2354
|
+
openList.update(neighbor);
|
|
2355
|
+
}
|
|
1850
2356
|
}
|
|
1851
2357
|
}
|
|
1852
2358
|
return null;
|
|
1853
2359
|
}
|
|
1854
|
-
|
|
2360
|
+
/**
|
|
2361
|
+
* @zh 重建抽象路径
|
|
2362
|
+
* @en Reconstruct abstract path
|
|
2363
|
+
*/
|
|
2364
|
+
reconstructPath(endNode) {
|
|
1855
2365
|
const path = [];
|
|
1856
2366
|
let current = endNode;
|
|
1857
2367
|
while (current) {
|
|
1858
|
-
path.unshift(current.
|
|
2368
|
+
path.unshift(current.node);
|
|
1859
2369
|
current = current.parent;
|
|
1860
2370
|
}
|
|
1861
2371
|
return path;
|
|
1862
2372
|
}
|
|
2373
|
+
/**
|
|
2374
|
+
* @zh 细化抽象路径为具体路径
|
|
2375
|
+
* @en Refine abstract path to concrete path
|
|
2376
|
+
*/
|
|
1863
2377
|
refinePath(abstractPath, startX, startY, endX, endY, opts) {
|
|
2378
|
+
if (abstractPath.length === 0) {
|
|
2379
|
+
return EMPTY_PATH_RESULT;
|
|
2380
|
+
}
|
|
1864
2381
|
const fullPath = [];
|
|
1865
2382
|
let totalCost = 0;
|
|
1866
2383
|
let nodesSearched = abstractPath.length;
|
|
1867
|
-
let
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
const
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
2384
|
+
for (let i = 0; i < abstractPath.length - 1; i++) {
|
|
2385
|
+
const fromNode = abstractPath[i];
|
|
2386
|
+
const toNode = abstractPath[i + 1];
|
|
2387
|
+
const edge = fromNode.edges.find((e) => e.targetNodeId === toNode.id);
|
|
2388
|
+
if (!edge) {
|
|
2389
|
+
const segResult = this.findLocalPath(fromNode.position.x, fromNode.position.y, toNode.position.x, toNode.position.y, opts);
|
|
2390
|
+
if (segResult.found) {
|
|
2391
|
+
this.appendPath(fullPath, segResult.path);
|
|
2392
|
+
totalCost += segResult.cost;
|
|
2393
|
+
nodesSearched += segResult.nodesSearched;
|
|
2394
|
+
}
|
|
2395
|
+
} else if (edge.isInterEdge) {
|
|
2396
|
+
if (fullPath.length === 0 || fullPath[fullPath.length - 1].x !== fromNode.position.x || fullPath[fullPath.length - 1].y !== fromNode.position.y) {
|
|
2397
|
+
fullPath.push({
|
|
2398
|
+
x: fromNode.position.x,
|
|
2399
|
+
y: fromNode.position.y
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2402
|
+
fullPath.push({
|
|
2403
|
+
x: toNode.position.x,
|
|
2404
|
+
y: toNode.position.y
|
|
2405
|
+
});
|
|
2406
|
+
totalCost += edge.cost;
|
|
2407
|
+
} else if (edge.innerPath && edge.innerPath.length > 0) {
|
|
2408
|
+
const concretePath = edge.innerPath.map((id) => ({
|
|
2409
|
+
x: id % this.mapWidth,
|
|
2410
|
+
y: Math.floor(id / this.mapWidth)
|
|
2411
|
+
}));
|
|
2412
|
+
this.appendPath(fullPath, concretePath);
|
|
2413
|
+
totalCost += edge.cost;
|
|
2414
|
+
} else {
|
|
2415
|
+
const computed = this.computeIntraEdgePath(fromNode, toNode, edge);
|
|
2416
|
+
if (computed && computed.path.length > 0) {
|
|
2417
|
+
const concretePath = computed.path.map((id) => ({
|
|
2418
|
+
x: id % this.mapWidth,
|
|
2419
|
+
y: Math.floor(id / this.mapWidth)
|
|
2420
|
+
}));
|
|
2421
|
+
this.appendPath(fullPath, concretePath);
|
|
2422
|
+
totalCost += computed.cost;
|
|
2423
|
+
} else {
|
|
2424
|
+
const segResult = this.findLocalPath(fromNode.position.x, fromNode.position.y, toNode.position.x, toNode.position.y, opts);
|
|
2425
|
+
if (segResult.found) {
|
|
2426
|
+
this.appendPath(fullPath, segResult.path);
|
|
2427
|
+
totalCost += segResult.cost;
|
|
2428
|
+
nodesSearched += segResult.nodesSearched;
|
|
1883
2429
|
}
|
|
1884
|
-
return EMPTY_PATH_RESULT;
|
|
1885
2430
|
}
|
|
1886
|
-
|
|
1887
|
-
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
if (fullPath.length > 0 && (fullPath[0].x !== startX || fullPath[0].y !== startY)) {
|
|
2434
|
+
const firstPoint = fullPath[0];
|
|
2435
|
+
if (Math.abs(firstPoint.x - startX) <= 1 && Math.abs(firstPoint.y - startY) <= 1) {
|
|
2436
|
+
fullPath.unshift({
|
|
2437
|
+
x: startX,
|
|
2438
|
+
y: startY
|
|
2439
|
+
});
|
|
2440
|
+
} else {
|
|
2441
|
+
const segResult = this.findLocalPath(startX, startY, firstPoint.x, firstPoint.y, opts);
|
|
2442
|
+
if (segResult.found) {
|
|
2443
|
+
fullPath.splice(0, 0, ...segResult.path.slice(0, -1));
|
|
2444
|
+
totalCost += segResult.cost;
|
|
1888
2445
|
}
|
|
1889
|
-
totalCost += segment.cost;
|
|
1890
|
-
nodesSearched += segment.nodesSearched;
|
|
1891
2446
|
}
|
|
1892
|
-
currentX = targetX;
|
|
1893
|
-
currentY = targetY;
|
|
1894
2447
|
}
|
|
1895
|
-
if (
|
|
1896
|
-
const
|
|
1897
|
-
if (
|
|
1898
|
-
|
|
1899
|
-
fullPath.push(
|
|
2448
|
+
if (fullPath.length > 0) {
|
|
2449
|
+
const lastPoint = fullPath[fullPath.length - 1];
|
|
2450
|
+
if (lastPoint.x !== endX || lastPoint.y !== endY) {
|
|
2451
|
+
if (Math.abs(lastPoint.x - endX) <= 1 && Math.abs(lastPoint.y - endY) <= 1) {
|
|
2452
|
+
fullPath.push({
|
|
2453
|
+
x: endX,
|
|
2454
|
+
y: endY
|
|
2455
|
+
});
|
|
2456
|
+
} else {
|
|
2457
|
+
const segResult = this.findLocalPath(lastPoint.x, lastPoint.y, endX, endY, opts);
|
|
2458
|
+
if (segResult.found) {
|
|
2459
|
+
fullPath.push(...segResult.path.slice(1));
|
|
2460
|
+
totalCost += segResult.cost;
|
|
2461
|
+
}
|
|
1900
2462
|
}
|
|
1901
|
-
totalCost += finalSegment.cost;
|
|
1902
|
-
nodesSearched += finalSegment.nodesSearched;
|
|
1903
2463
|
}
|
|
1904
2464
|
}
|
|
1905
2465
|
return {
|
|
@@ -1909,39 +2469,37 @@ var _HPAPathfinder = class _HPAPathfinder {
|
|
|
1909
2469
|
nodesSearched
|
|
1910
2470
|
};
|
|
1911
2471
|
}
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
return cached;
|
|
2472
|
+
/**
|
|
2473
|
+
* @zh 追加路径(避免重复点)
|
|
2474
|
+
* @en Append path (avoid duplicate points)
|
|
2475
|
+
*/
|
|
2476
|
+
appendPath(fullPath, segment) {
|
|
2477
|
+
if (segment.length === 0) return;
|
|
2478
|
+
let startIdx = 0;
|
|
2479
|
+
if (fullPath.length > 0) {
|
|
2480
|
+
const last = fullPath[fullPath.length - 1];
|
|
2481
|
+
if (last.x === segment[0].x && last.y === segment[0].y) {
|
|
2482
|
+
startIdx = 1;
|
|
1924
2483
|
}
|
|
1925
2484
|
}
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2485
|
+
for (let i = startIdx; i < segment.length; i++) {
|
|
2486
|
+
fullPath.push({
|
|
2487
|
+
x: segment[i].x,
|
|
2488
|
+
y: segment[i].y
|
|
2489
|
+
});
|
|
1931
2490
|
}
|
|
1932
|
-
return result.found ? [
|
|
1933
|
-
...result.path
|
|
1934
|
-
] : null;
|
|
1935
2491
|
}
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
}
|
|
1943
|
-
return cost;
|
|
2492
|
+
/**
|
|
2493
|
+
* @zh 局部寻路
|
|
2494
|
+
* @en Local pathfinding
|
|
2495
|
+
*/
|
|
2496
|
+
findLocalPath(startX, startY, endX, endY, opts) {
|
|
2497
|
+
return this.localPathfinder.findPath(startX, startY, endX, endY, opts);
|
|
1944
2498
|
}
|
|
2499
|
+
/**
|
|
2500
|
+
* @zh 启发式函数(Octile 距离)
|
|
2501
|
+
* @en Heuristic function (Octile distance)
|
|
2502
|
+
*/
|
|
1945
2503
|
heuristic(a, b) {
|
|
1946
2504
|
const dx = Math.abs(a.x - b.x);
|
|
1947
2505
|
const dy = Math.abs(a.y - b.y);
|
|
@@ -1956,8 +2514,8 @@ function createHPAPathfinder(map, config) {
|
|
|
1956
2514
|
__name(createHPAPathfinder, "createHPAPathfinder");
|
|
1957
2515
|
|
|
1958
2516
|
// src/navmesh/NavMesh.ts
|
|
1959
|
-
var
|
|
1960
|
-
var NavMeshNode = (
|
|
2517
|
+
var _a5;
|
|
2518
|
+
var NavMeshNode = (_a5 = class {
|
|
1961
2519
|
constructor(polygon) {
|
|
1962
2520
|
__publicField(this, "id");
|
|
1963
2521
|
__publicField(this, "position");
|
|
@@ -1970,7 +2528,7 @@ var NavMeshNode = (_a3 = class {
|
|
|
1970
2528
|
this.walkable = true;
|
|
1971
2529
|
this.polygon = polygon;
|
|
1972
2530
|
}
|
|
1973
|
-
}, __name(
|
|
2531
|
+
}, __name(_a5, "NavMeshNode"), _a5);
|
|
1974
2532
|
var _NavMesh = class _NavMesh {
|
|
1975
2533
|
constructor() {
|
|
1976
2534
|
__publicField(this, "polygons", /* @__PURE__ */ new Map());
|