Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/runtime/components/navigation/Pathfinder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,10 @@ int32_t WeightedF(int32_t g, int32_t h, double hWeight) {
// portal centroid when cross-level.
// * `hWeight > 1` trades optimality for speed. 1.5 is balanced, 3.0 is the
// production default (~60x speedup vs 1.0 with <0.1% distance penalty).
// * Any-angle (Theta*-style): a node is linked to its parent's cast origin
// whenever that is within range, so the 8-way walk steps the search falls
// back to in a dead end cost nothing until they outgrow one cast. The
// emitted chain is the cast sequence; it needs no post-reduction.
std::vector<Position> CastCountTeleportSearch(CollisionLookup& coll, Position start, Position end, int32_t radius,
const std::stop_token& cancelToken, double hWeight) {
// Ring offsets and A* neighbor math stay in signed Point; convert the
Expand Down Expand Up @@ -731,6 +735,16 @@ std::vector<Position> CastCountTeleportSearch(CollisionLookup& coll, Position st
auto pushNeighbor = [&](Point np, int32_t parentG, Point parent) {
if (closed.contains(np))
return;
// Theta*: cast to np straight from the parent's own cast origin when that
// is still in range. Without it every walk step out of a dead end counts
// as a cast, and the path keeps them as single-cell teleports.
if (const auto parentIt = nodes.find(parent); parentIt != nodes.end() && parentIt->second.hasParent) {
const Point origin = parentIt->second.parent;
if (EuclideanDist(origin, np) < range) {
parent = origin;
parentG = nodes.at(origin).g;
}
}
auto [npIdx, npNode] = getOrCreate(np);
const int32_t newG = parentG + range + EuclideanDist(parent, np);
if (newG >= npNode.g)
Expand Down
29 changes: 29 additions & 0 deletions tests/runtime/pathfinding/tests/TeleportAStarTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,32 @@ TEST_CASE("Teleport A* navigates around wall") {
CHECK(path.front() == (Position{.x = 25, .y = 50}));
CHECK(path.back() == (Position{.x = 75, .y = 50}));
}

TEST_CASE("Teleport A* does not emit single-cell steps along a wall") {
CollisionLookup coll;
coll.primary = LevelGrid({.size = {.width = 150, .height = 150}});

// A wall thicker than the teleport radius, so no ring jump clears it and the
// search has to creep along its face to the gap at y >= 131.
for (uint32_t x = 50; x < 80; x++) {
for (uint32_t y = 0; y < 131; y++) {
coll.primary.Set({.x = x, .y = y}, CollisionFlag::Wall);
}
}

auto path = FindPathOnGrid(coll, {.x = 25, .y = 50}, {.x = 120, .y = 50}, ReductionType::Teleport, 20, {});
REQUIRE_FALSE(path.empty());
CHECK(path.front() == (Position{.x = 25, .y = 50}));
CHECK(path.back() == (Position{.x = 120, .y = 50}));

// The last hop may be short: the search reaches a ring point near the goal first.
size_t shortSteps = 0;
for (size_t i = 1; i + 1 < path.size(); i++) {
const int32_t dx = static_cast<int32_t>(path[i].x) - static_cast<int32_t>(path[i - 1].x);
const int32_t dy = static_cast<int32_t>(path[i].y) - static_cast<int32_t>(path[i - 1].y);
if ((dx * dx) + (dy * dy) <= 4) {
++shortSteps;
}
}
CHECK(shortSteps == 0);
}
Loading