From 33aa553b719b8f5c44347f48c678226d2b3a6adf Mon Sep 17 00:00:00 2001 From: bash Date: Sat, 30 May 2026 23:39:31 +0200 Subject: [PATCH] feat(Core/Travel): Inject hearthstone + mage teleport spells into A* via PortalNode --- src/Mgr/Travel/TravelNode.cpp | 99 +++++++++++++++++++++++++++++++++-- src/Mgr/Travel/TravelNode.h | 41 +++++++++++++++ 2 files changed, 137 insertions(+), 3 deletions(-) diff --git a/src/Mgr/Travel/TravelNode.cpp b/src/Mgr/Travel/TravelNode.cpp index 86c2e0b7e..346fc26e2 100644 --- a/src/Mgr/Travel/TravelNode.cpp +++ b/src/Mgr/Travel/TravelNode.cpp @@ -1413,6 +1413,8 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal, std::vector open, closed; + std::vector portNodes; // synthetic teleport/portal edges + if (bot) { PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); @@ -1425,20 +1427,95 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal, AiObjectContext* context = botAI->GetAiObjectContext(); startStub->currentGold = AI_VALUE2(uint32, "free money for", (uint32)NeedMoneyFor::travel); } + + // Hearthstone (item 6948 / spell 8690): inject a synthetic + // teleport edge from start to the node nearest the bot's + // home bind, so A* can pick hearthing over walking. + if (bot->IsAlive() && bot->HasItemCount(6948, 1)) + { + WorldPosition homePos = AI_VALUE(WorldPosition, "home bind"); + std::vector dummy; + TravelNode* homeNode = sTravelNodeMap.getNode(homePos, dummy, nullptr, 50.0f); + if (homeNode && homeNode != start) + { + PortalNode* portNode = new PortalNode(start); + portNode->SetPortal(start, homeNode, 8690); + + TravelNodeStub* hsStub = &m_stubs.insert(std::make_pair( + static_cast(portNode), TravelNodeStub(portNode))).first->second; + + // Cost: ~10 minutes minus death count (cap at 2 min min) + // so a recently-died bot prefers the hearth. + uint32 deathCount = AI_VALUE(uint32, "death count"); + hsStub->costFromStart = std::max(2, (10 - std::min(8, deathCount))) * MINUTE; + hsStub->heuristic = hsStub->dataNode->fDist(goal) / botSpeed; + hsStub->totalCost = hsStub->costFromStart + hsStub->heuristic; + + open.push_back(hsStub); + hsStub->open = true; + portNodes.push_back(portNode); + } + } + + // Mage teleport spells: 3561 Stormwind, 3562 Ironforge, 3563 Undercity, + // 3565 Darnassus, 3566 Thunder Bluff, 3567 Orgrimmar, 18960 Moonglade. + // Inject one synthetic teleport edge per known + ready spell. + static const uint32 teleSpells[] = {3561, 3562, 3563, 3565, 3566, 3567, 18960}; + for (uint32 spellId : teleSpells) + { + if (!bot->IsAlive() || bot->IsInCombat()) + break; + if (!bot->HasSpell(spellId)) + continue; + if (bot->HasSpellCooldown(spellId)) + continue; + + SpellTargetPosition const* stp = + sSpellMgr->GetSpellTargetPosition(spellId, EFFECT_0); + if (!stp) + continue; + + WorldPosition telePos(stp->target_mapId, stp->target_X, + stp->target_Y, stp->target_Z, 0.0f); + std::vector dummy; + TravelNode* destNode = sTravelNodeMap.getNode(telePos, dummy, nullptr, 10.0f); + if (!destNode || destNode == start) + continue; + + PortalNode* portNode = new PortalNode(start); + portNode->SetPortal(start, destNode, spellId); + + TravelNodeStub* tsStub = &m_stubs.insert(std::make_pair( + static_cast(portNode), TravelNodeStub(portNode))).first->second; + + tsStub->costFromStart = MINUTE; // cheaper than ~1-min walk + tsStub->heuristic = tsStub->dataNode->fDist(goal) / botSpeed; + tsStub->totalCost = tsStub->costFromStart + tsStub->heuristic; + + open.push_back(tsStub); + tsStub->open = true; + portNodes.push_back(portNode); + } } else startStub->currentGold = bot->GetMoney(); } - if (!start->hasRouteTo(goal)) + if (open.empty() && !start->hasRouteTo(goal)) + { + for (auto* p : portNodes) + delete p; return TravelNodeRoute(); + } // Min-heap: smallest f at front auto heapComp = [](TravelNodeStub* i, TravelNodeStub* j) { return i->totalCost > j->totalCost; }; open.push_back(startStub); - std::push_heap(open.begin(), open.end(), heapComp); startStub->open = true; + // Heapify all of open in one pass — covers both startStub and any + // PortalNode stubs injected above. + std::make_heap(open.begin(), open.end(), heapComp); constexpr uint32 MAX_A_STAR_EXPLORED = 500; uint32 nodesExplored = 0; @@ -1446,7 +1523,11 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal, while (!open.empty()) { if (++nodesExplored > MAX_A_STAR_EXPLORED) + { + for (auto* p : portNodes) + delete p; return TravelNodeRoute(); + } std::pop_heap(open.begin(), open.end(), heapComp); currentNode = open.back(); @@ -1471,7 +1552,11 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal, reverse(path.begin(), path.end()); - return TravelNodeRoute(path); + // Successful route: hand off ownership of any synthetic + // PortalNodes injected at the head. Caller (GetFullPath) + // is expected to call cleanTempNodes() when done with the + // route — see the call site for the lifecycle. + return TravelNodeRoute(path, portNodes); } for (auto const& link : *currentNode->dataNode->getLinks()) // for each successor n' of n @@ -1510,6 +1595,9 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal, } } + // A* exhausted open without reaching goal. Clean up synthetic nodes. + for (auto* p : portNodes) + delete p; return TravelNodeRoute(); } @@ -1719,6 +1807,11 @@ TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos, uint32 botZoneId, path = route.BuildPath(pathToStart, pathToEnd, nullptr); + // Release any synthetic PortalNodes the A* injected (hearthstone / + // mage teleports). They served their purpose in route assembly and + // their points are now baked into `path`. + route.cleanTempNodes(); + return path; } diff --git a/src/Mgr/Travel/TravelNode.h b/src/Mgr/Travel/TravelNode.h index f6ccf1d72..192642a87 100644 --- a/src/Mgr/Travel/TravelNode.h +++ b/src/Mgr/Travel/TravelNode.h @@ -409,6 +409,26 @@ protected: // uint32 transportId = 0; }; +// Synthetic A* node injected at search start to represent a teleport-spell +// (hearthstone, mage portal, etc.) as an alternative travel edge. Owned +// by GetNodeRoute caller; deleted after the route is built. +class PortalNode : public TravelNode +{ +public: + PortalNode(TravelNode* baseNode) : TravelNode(baseNode) {} + + void SetPortal(TravelNode* baseNode, TravelNode* endNode, uint32 portalSpell) + { + nodeName = baseNode->getName(); + point = *baseNode->getPosition(); + paths.clear(); + links.clear(); + TravelNodePath path(0.1f, 0.1f, (uint8)TravelNodePathType::teleportSpell, + portalSpell, true); + setPathTo(endNode, path); + } +}; + // Route step type enum class PathNodeType : uint8 { @@ -552,6 +572,13 @@ public: { nodes = nodes1; } + TravelNodeRoute(std::vector nodes1, + std::vector const& tempNodes_) + { + nodes = nodes1; + if (!tempNodes_.empty()) + addTempNodes(tempNodes_); + } bool isEmpty() { return nodes.empty(); } @@ -563,6 +590,19 @@ public: std::vector getNodes() { return nodes; } + // Take ownership of synthetic A* nodes (PortalNode etc.). Must call + // cleanTempNodes() to delete them when the route is no longer needed. + void addTempNodes(std::vector const& tempNodes_) + { + tempNodes.insert(tempNodes.end(), tempNodes_.begin(), tempNodes_.end()); + } + void cleanTempNodes() + { + for (auto* n : tempNodes) + delete n; + tempNodes.clear(); + } + TravelPath BuildPath( std::vector pathToStart = {}, std::vector pathToEnd = {}, @@ -576,6 +616,7 @@ private: return std::find(nodes.begin(), nodes.end(), node); } std::vector nodes; + std::vector tempNodes; // owned synthetic nodes (PortalNode etc.) }; // A node container to aid A* calculations with nodes.