@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.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: 10,
1413
- maxEntranceWidth: 6,
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, "width");
1421
- __publicField(this, "height");
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, "nextEntranceId", 0);
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
- __publicField(this, "internalPathCache", /* @__PURE__ */ new Map());
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.width = bounds.width;
1438
- this.height = bounds.height;
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.clusters = [];
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.findEntrances();
1458
- this.buildAbstractGraph();
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
- return this.findLocalPath(startX, startY, endX, endY, opts);
1496
- }
1497
- const startNodes = this.insertTemporaryNode(startX, startY, startCluster);
1498
- const endNodes = this.insertTemporaryNode(endX, endY, endCluster);
1499
- const abstractPath = this.searchAbstractGraph(startNodes, endNodes, opts);
1500
- this.removeTemporaryNodes(startNodes);
1501
- this.removeTemporaryNodes(endNodes);
1502
- if (!abstractPath || abstractPath.length === 0) {
1503
- return EMPTY_PATH_RESULT;
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 this.refinePath(abstractPath, startX, startY, endX, endY, opts);
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.internalPathCache.clear();
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.preprocessed = false;
1525
- this.internalPathCache.clear();
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.entrances.length,
1791
+ entrances: this.entranceCount,
1535
1792
  abstractNodes: this.abstractNodes.size,
1536
- cacheSize: this.internalPathCache.size
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
- const clustersX = Math.ceil(this.width / clusterSize);
1558
- const clustersY = Math.ceil(this.height / clusterSize);
1818
+ this.clustersX = Math.ceil(this.mapWidth / clusterSize);
1819
+ this.clustersY = Math.ceil(this.mapHeight / clusterSize);
1559
1820
  this.clusterGrid = [];
1560
- for (let x = 0; x < clustersX; x++) {
1561
- this.clusterGrid[x] = [];
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 cluster = {
1567
- id: clusterId++,
1568
- x: cx * clusterSize,
1569
- y: cy * clusterSize,
1570
- width: Math.min(clusterSize, this.width - cx * clusterSize),
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] = cluster;
1836
+ this.clusterGrid[cx][cy] = clusterId;
1837
+ this.nodesByCluster.set(clusterId, []);
1838
+ clusterId++;
1576
1839
  }
1577
1840
  }
1578
1841
  }
1579
- findEntrances() {
1842
+ /**
1843
+ * @zh 检测入口并创建抽象节点
1844
+ * @en Detect entrances and create abstract nodes
1845
+ */
1846
+ buildEntrances() {
1580
1847
  const clusterSize = this.config.clusterSize;
1581
- const clustersX = Math.ceil(this.width / clusterSize);
1582
- const clustersY = Math.ceil(this.height / clusterSize);
1583
- for (let cy = 0; cy < clustersY; cy++) {
1584
- for (let cx = 0; cx < clustersX; cx++) {
1585
- const cluster = this.clusterGrid[cx][cy];
1586
- if (!cluster) continue;
1587
- if (cx < clustersX - 1) {
1588
- const rightCluster = this.clusterGrid[cx + 1]?.[cy];
1589
- if (rightCluster) {
1590
- this.findEntrancesBetween(cluster, rightCluster, "horizontal");
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 bottomCluster = this.clusterGrid[cx]?.[cy + 1];
1595
- if (bottomCluster) {
1596
- this.findEntrancesBetween(cluster, bottomCluster, "vertical");
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
- findEntrancesBetween(cluster1, cluster2, direction) {
1603
- const maxWidth = this.config.maxEntranceWidth;
1604
- let entranceStart = null;
1605
- let entranceLength = 0;
1606
- if (direction === "horizontal") {
1607
- const x1 = cluster1.x + cluster1.width - 1;
1608
- const x2 = cluster2.x;
1609
- const startY = Math.max(cluster1.y, cluster2.y);
1610
- const endY = Math.min(cluster1.y + cluster1.height, cluster2.y + cluster2.height);
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 (entranceStart === null) {
1616
- entranceStart = y;
1617
- entranceLength = 1;
1618
- } else {
1619
- entranceLength++;
1896
+ if (spanStart === null) {
1897
+ spanStart = y;
1620
1898
  }
1621
- if (entranceLength >= maxWidth || y === endY - 1) {
1622
- this.createEntrance(cluster1, cluster2, x1, x2, entranceStart, entranceStart + entranceLength - 1, "horizontal");
1623
- entranceStart = null;
1624
- entranceLength = 0;
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.y + cluster1.height - 1;
1634
- const y2 = cluster2.y;
1635
- const startX = Math.max(cluster1.x, cluster2.x);
1636
- const endX = Math.min(cluster1.x + cluster1.width, cluster2.x + cluster2.width);
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 (entranceStart === null) {
1642
- entranceStart = x;
1643
- entranceLength = 1;
1644
- } else {
1645
- entranceLength++;
1925
+ if (spanStart === null) {
1926
+ spanStart = x;
1646
1927
  }
1647
- if (entranceLength >= maxWidth || x === endX - 1) {
1648
- this.createEntrance(cluster1, cluster2, entranceStart, entranceStart + entranceLength - 1, y1, y2, "vertical");
1649
- entranceStart = null;
1650
- entranceLength = 0;
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
- createEntrance(cluster1, cluster2, coord1Start, coord1End, coord2Start, coord2End, direction) {
1661
- let point1;
1662
- let point2;
1663
- let center;
1664
- if (direction === "horizontal") {
1665
- const midY = Math.floor((coord1Start + coord1End) / 2);
1666
- point1 = {
1667
- x: coord1Start,
1668
- y: midY
1669
- };
1670
- point2 = {
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 midX = Math.floor((coord1Start + coord1End) / 2);
1680
- point1 = {
1681
- x: midX,
1682
- y: coord2Start
1683
- };
1684
- point2 = {
1685
- x: midX,
1686
- y: coord2End
1687
- };
1688
- center = {
1689
- x: midX,
1690
- y: coord2Start
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 entrance = {
1694
- id: this.nextEntranceId++,
1695
- cluster1Id: cluster1.id,
1696
- cluster2Id: cluster2.id,
1697
- point1,
1698
- point2,
1699
- center
1700
- };
1701
- this.entrances.push(entrance);
1702
- cluster1.entrances.push(entrance);
1703
- cluster2.entrances.push(entrance);
1704
- }
1705
- buildAbstractGraph() {
1706
- for (const entrance of this.entrances) {
1707
- const node1 = this.createAbstractNode(entrance.point1, entrance.cluster1Id, entrance.id);
1708
- const node2 = this.createAbstractNode(entrance.point2, entrance.cluster2Id, entrance.id);
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: 1,
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: 1,
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
- createAbstractNode(position, clusterId, entranceId) {
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
- clusterId,
1729
- entranceId,
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
- connectIntraClusterNodes(cluster) {
1736
- const nodesInCluster = [];
1737
- for (const node of this.abstractNodes.values()) {
1738
- if (node.clusterId === cluster.id) {
1739
- nodesInCluster.push(node);
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
- for (let i = 0; i < nodesInCluster.length; i++) {
1743
- for (let j = i + 1; j < nodesInCluster.length; j++) {
1744
- const node1 = nodesInCluster[i];
1745
- const node2 = nodesInCluster[j];
1746
- const cost = this.heuristic(node1.position, node2.position);
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 cx = Math.floor(x / clusterSize);
1766
- const cy = Math.floor(y / clusterSize);
1767
- return this.clusterGrid[cx]?.[cy] ?? null;
1768
- }
1769
- insertTemporaryNode(x, y, cluster) {
1770
- const tempNodes = [];
1771
- const tempNode = this.createAbstractNode({
1772
- x,
1773
- y
1774
- }, cluster.id, -1);
1775
- tempNodes.push(tempNode);
1776
- for (const node of this.abstractNodes.values()) {
1777
- if (node.clusterId === cluster.id && node.id !== tempNode.id) {
1778
- const cost = this.heuristic({
1779
- x,
1780
- y
1781
- }, node.position);
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: node.id,
1784
- cost,
1785
- isInterEdge: false
2263
+ targetNodeId: existingNode.id,
2264
+ cost: result.cost,
2265
+ isInterEdge: false,
2266
+ innerPath: globalPath
1786
2267
  });
1787
- node.edges.push({
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 tempNodes;
2278
+ return tempNode;
1795
2279
  }
1796
- removeTemporaryNodes(nodes) {
1797
- for (const node of nodes) {
1798
- for (const edge of node.edges) {
1799
- const targetNode = this.abstractNodes.get(edge.targetNodeId);
1800
- if (targetNode) {
1801
- targetNode.edges = targetNode.edges.filter((e) => e.targetNodeId !== node.id);
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
- searchAbstractGraph(startNodes, endNodes, opts) {
1808
- if (startNodes.length === 0 || endNodes.length === 0) {
1809
- return null;
1810
- }
1811
- const endNodeIds = new Set(endNodes.map((n) => n.id));
1812
- const openList = new BinaryHeap((a, b) => a.f - b.f);
1813
- const closedSet = /* @__PURE__ */ new Set();
1814
- for (const startNode of startNodes) {
1815
- const h = this.heuristic(startNode.position, endNodes[0].position);
1816
- openList.push({
1817
- abstractNode: startNode,
1818
- g: 0,
1819
- h: h * opts.heuristicWeight,
1820
- f: h * opts.heuristicWeight,
1821
- parent: null
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 (endNodeIds.has(current.abstractNode.id)) {
1829
- return this.reconstructAbstractPath(current);
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
- closedSet.add(current.abstractNode.id);
1835
- for (const edge of current.abstractNode.edges) {
1836
- if (closedSet.has(edge.targetNodeId)) {
1837
- continue;
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
- const neighbor = this.abstractNodes.get(edge.targetNodeId);
1840
- if (!neighbor) continue;
2342
+ if (neighbor.closed) continue;
1841
2343
  const tentativeG = current.g + edge.cost;
1842
- const h = this.heuristic(neighbor.position, endNodes[0].position) * opts.heuristicWeight;
1843
- openList.push({
1844
- abstractNode: neighbor,
1845
- g: tentativeG,
1846
- h,
1847
- f: tentativeG + h,
1848
- parent: current
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
- reconstructAbstractPath(endNode) {
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.abstractNode);
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 currentX = startX;
1868
- let currentY = startY;
1869
- for (let i = 0; i < abstractPath.length; i++) {
1870
- const node = abstractPath[i];
1871
- const targetX = i === abstractPath.length - 1 ? endX : node.position.x;
1872
- const targetY = i === abstractPath.length - 1 ? endY : node.position.y;
1873
- if (currentX !== targetX || currentY !== targetY) {
1874
- const segment = this.localPathfinder.findPath(currentX, currentY, targetX, targetY, opts);
1875
- if (!segment.found) {
1876
- if (fullPath.length > 0) {
1877
- return {
1878
- found: true,
1879
- path: fullPath,
1880
- cost: totalCost,
1881
- nodesSearched
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
- for (let j = fullPath.length === 0 ? 0 : 1; j < segment.path.length; j++) {
1887
- fullPath.push(segment.path[j]);
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 (currentX !== endX || currentY !== endY) {
1896
- const finalSegment = this.localPathfinder.findPath(currentX, currentY, endX, endY, opts);
1897
- if (finalSegment.found) {
1898
- for (let j = 1; j < finalSegment.path.length; j++) {
1899
- fullPath.push(finalSegment.path[j]);
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
- // 辅助方法 | Helper Methods
1914
- // =========================================================================
1915
- findLocalPath(startX, startY, endX, endY, opts) {
1916
- return this.localPathfinder.findPath(startX, startY, endX, endY, opts);
1917
- }
1918
- findInternalPath(startX, startY, endX, endY, cluster) {
1919
- const cacheKey = `${cluster.id}:${startX},${startY}->${endX},${endY}`;
1920
- if (this.config.cacheInternalPaths) {
1921
- const cached = this.internalPathCache.get(cacheKey);
1922
- if (cached) {
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
- const result = this.localPathfinder.findPath(startX, startY, endX, endY);
1927
- if (result.found && this.config.cacheInternalPaths) {
1928
- this.internalPathCache.set(cacheKey, [
1929
- ...result.path
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
- calculatePathCost(path) {
1937
- let cost = 0;
1938
- for (let i = 1; i < path.length; i++) {
1939
- const dx = Math.abs(path[i].x - path[i - 1].x);
1940
- const dy = Math.abs(path[i].y - path[i - 1].y);
1941
- cost += dx !== 0 && dy !== 0 ? Math.SQRT2 : 1;
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 _a3;
1960
- var NavMeshNode = (_a3 = class {
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(_a3, "NavMeshNode"), _a3);
2531
+ }, __name(_a5, "NavMeshNode"), _a5);
1974
2532
  var _NavMesh = class _NavMesh {
1975
2533
  constructor() {
1976
2534
  __publicField(this, "polygons", /* @__PURE__ */ new Map());