Core A* search loop. Expects buildNodeData() to have been called first. * Marks node.parent for path reconstruction. Returns true if endNode was * reached; false on disconnected goal or maxLoop exhaustion. * @param {PathFinderNode} startNode * @param {PathFinderNode} endNode
(startNode, endNode)
| 223 | * @returns {boolean} |
| 224 | * @private */ |
| 225 | aStarSearch(startNode, endNode) |
| 226 | { |
| 227 | ASSERT(startNode && endNode, 'aStarSearch needs both endpoints'); |
| 228 | ASSERT(startNode !== endNode, 'aStarSearch: start and end must differ — caller should handle trivial case'); |
| 229 | ASSERT(startNode.walkable && endNode.walkable, 'aStarSearch: endpoints must be walkable'); |
| 230 | |
| 231 | const openList = [startNode]; |
| 232 | startNode.isOpen = true; |
| 233 | let loopCount = 0; |
| 234 | |
| 235 | while (openList.length > 0) |
| 236 | { |
| 237 | // Find the open node with the smallest f score (linear scan). |
| 238 | // Same as the C++ — fine up to a few thousand nodes. |
| 239 | let bestIndex = 0; |
| 240 | let bestF = openList[0].f; |
| 241 | for (let i = 1; i < openList.length; ++i) |
| 242 | { |
| 243 | if (openList[i].f < bestF) |
| 244 | { |
| 245 | bestF = openList[i].f; |
| 246 | bestIndex = i; |
| 247 | } |
| 248 | } |
| 249 | const current = openList[bestIndex]; |
| 250 | |
| 251 | if (current === endNode) break; |
| 252 | if (++loopCount > this.maxLoop) break; |
| 253 | |
| 254 | // Move current from open to closed. |
| 255 | current.isOpen = false; |
| 256 | openList.splice(bestIndex, 1); |
| 257 | current.isClosed = true; |
| 258 | |
| 259 | if (this.debug && this.debugTime > 0) |
| 260 | debugRect(current.posWorld, PATHFINDER_TILE_VEC, rgb(1, 1, 1, 0.05), this.debugTime); |
| 261 | |
| 262 | // Expand all 8 neighbors. |
| 263 | for (let dy = -1; dy <= 1; ++dy) |
| 264 | for (let dx = -1; dx <= 1; ++dx) |
| 265 | { |
| 266 | if (dx === 0 && dy === 0) continue; |
| 267 | const neighbor = this.getNode(current.pos.x + dx, current.pos.y + dy); |
| 268 | if (!neighbor || !neighbor.walkable || neighbor.isClosed) continue; |
| 269 | |
| 270 | let stepCost = 1; |
| 271 | if (dx !== 0 && dy !== 0) |
| 272 | { |
| 273 | // Diagonal step: refuse if either cardinal neighbor is |
| 274 | // blocked. Prevents cutting through walls at corners. |
| 275 | // (Costed-but-walkable cardinals do not block — diagonal |
| 276 | // movement around expensive terrain is standard A*.) |
| 277 | const card1 = this.getNode(current.pos.x + dx, current.pos.y); |
| 278 | if (!card1 || !card1.walkable) continue; |
| 279 | const card2 = this.getNode(current.pos.x, current.pos.y + dy); |
| 280 | if (!card2 || !card2.walkable) continue; |
| 281 | stepCost = PATHFINDER_DIAGONAL_COST; |
| 282 | } |