refactor(Core/Travel): Remove teleportSpell + NODE_TELEPORT + PortalNode + hearthstone/mage A* injection (staticPortal kept)

This commit is contained in:
bash 2026-05-31 15:48:57 +02:00
parent 6079863fce
commit 45a3189d4b
3 changed files with 5 additions and 237 deletions

View File

@ -3342,46 +3342,6 @@ bool MovementAction::HandleSpecialMovement(TravelPath& path)
return bot->ActivateTaxiPathTo(route, flightMaster, 0); return bot->ActivateTaxiPathTo(route, flightMaster, 0);
} }
case PathNodeType::NODE_TELEPORT:
{
if (!next.entry)
return false;
// Can't cast while flying — let the bot land first.
bool const canCastNow = !bot->IsFlying();
if (next.entry == 8690) // Hearthstone
{
if (canCastNow)
{
bool const ok = botAI->DoSpecificAction("hearthstone",
Event("move action"), true);
if (ok)
return true;
}
}
else if (canCastNow)
{
// Mage city portal / similar spell — dismount, drop
// shapeshift, queue cast. We don't gate on reagents (no
// "has reagents for" value on AC); the server-side cast
// attempt will fail cleanly if reagents are missing.
if (bot->IsMounted())
bot->Dismount();
botAI->RemoveShapeshift();
if (botAI->DoSpecificAction(
"cast",
Event("rpg action", std::to_string(next.entry)), true))
return true;
}
// Cast didn't happen or failed — clear the cached path so
// the next tick re-resolves cleanly instead of retrying the
// same teleport edge that just failed.
AI_VALUE(LastMovement&, "last movement").setPath(TravelPath());
return false;
}
default: default:
return false; return false;
} }

View File

@ -912,13 +912,6 @@ bool TravelPath::UpcommingSpecialMovement(WorldPosition startPos,
return true; return true;
} }
// Teleport spell (hearthstone et al.): fire on the next-step marker.
if (nextP->type == PathNodeType::NODE_TELEPORT)
{
cutTo(*nextP, false);
return true;
}
// Flight path: interact with flight master when in range. // Flight path: interact with flight master when in range.
if (startP->type == PathNodeType::NODE_FLIGHTPATH && if (startP->type == PathNodeType::NODE_FLIGHTPATH &&
startPos.distance(startP->point) < INTERACTION_DISTANCE) startPos.distance(startP->point) < INTERACTION_DISTANCE)
@ -1239,14 +1232,6 @@ TravelPath TravelNodeRoute::BuildPath(std::vector<WorldPosition> pathToStart, st
// Full taxi waypoint route; same reasoning as transport. // Full taxi waypoint route; same reasoning as transport.
travelPath.addPath(nodePath->GetPath(), PathNodeType::NODE_FLIGHTPATH, nodePath->getPathObject()); travelPath.addPath(nodePath->GetPath(), PathNodeType::NODE_FLIGHTPATH, nodePath->getPathObject());
} }
else if (nodePath->getPathType() == TravelNodePathType::teleportSpell)
{
// Hearthstone or spell-cast teleport edge: emit a paired
// NODE_TELEPORT (entry = exit) so HandleSpecialMovement can
// dispatch the cast when the head reaches the entry point.
travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_TELEPORT, nodePath->getPathObject());
travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_TELEPORT, nodePath->getPathObject());
}
else else
{ {
std::vector<WorldPosition> path = nodePath->GetPath(); std::vector<WorldPosition> path = nodePath->GetPath();
@ -1435,8 +1420,6 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal,
std::vector<TravelNodeStub*> open, closed; std::vector<TravelNodeStub*> open, closed;
std::vector<TravelNode*> portNodes; // synthetic teleport/portal edges
if (bot) if (bot)
{ {
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
@ -1473,97 +1456,20 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal,
PAI_VALUE2(uint32, "free money for", (uint32)NeedMoneyFor::travel)); PAI_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<WorldPosition> 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<TravelNode*>(portNode), TravelNodeStub(portNode))).first->second;
// Cost: max(2, (10 - deathCount) * MINUTE) — matches
// reference exactly, including the uint32 underflow at
// deathCount > 10 (which makes hearthstone prohibitive
// for very-dead bots — apparently intentional).
hsStub->costFromStart = std::max<uint32>(2,
(10 - AI_VALUE(uint32, "death count")) * 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<WorldPosition> 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<TravelNode*>(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 else
startStub->currentGold = bot->GetMoney(); startStub->currentGold = bot->GetMoney();
} }
if (open.empty() && !start->hasRouteTo(goal)) if (!start->hasRouteTo(goal))
{
for (auto* p : portNodes)
delete p;
return TravelNodeRoute(); return TravelNodeRoute();
}
// Min-heap: smallest f at front // Min-heap: smallest f at front
auto heapComp = [](TravelNodeStub* i, TravelNodeStub* j) { return i->totalCost > j->totalCost; }; auto heapComp = [](TravelNodeStub* i, TravelNodeStub* j) { return i->totalCost > j->totalCost; };
open.push_back(startStub); open.push_back(startStub);
startStub->open = true; startStub->open = true;
// Heapify all of open in one pass — covers both startStub and any std::push_heap(open.begin(), open.end(), heapComp);
// PortalNode stubs injected above.
std::make_heap(open.begin(), open.end(), heapComp);
while (!open.empty()) while (!open.empty())
{ {
@ -1589,12 +1495,7 @@ TravelNodeRoute TravelNodeMap::GetNodeRoute(TravelNode* start, TravelNode* goal,
} }
reverse(path.begin(), path.end()); 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 for (auto const& link : *currentNode->dataNode->getLinks()) // for each successor n' of n
@ -1633,9 +1534,6 @@ 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(); return TravelNodeRoute();
} }
@ -1828,7 +1726,6 @@ TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos, [[maybe_unused]] uin
if (transportEntry) if (transportEntry)
{ {
path = route.BuildPath({botPos}, endProbe, bot); path = route.BuildPath({botPos}, endProbe, bot);
route.cleanTempNodes();
return path; return path;
} }
@ -1852,7 +1749,6 @@ TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos, [[maybe_unused]] uin
if (!startPathOk) if (!startPathOk)
{ {
badStartNodes.push_back(s); badStartNodes.push_back(s);
route.cleanTempNodes();
continue; continue;
} }
@ -1861,50 +1757,10 @@ TravelPath TravelNodeMap::GetFullPath(WorldPosition botPos, [[maybe_unused]] uin
// ResolveMovePath cycles can reuse it. // ResolveMovePath cycles can reuse it.
beginPath = pathToStart; beginPath = pathToStart;
path = route.BuildPath(pathToStart, endProbe, bot); path = route.BuildPath(pathToStart, endProbe, bot);
route.cleanTempNodes();
return path; return path;
} }
} }
// No graph route found. Last-resort hearthstone fallback (reference
// also does this): if bot has hearthstone item and is alive, treat
// the bot's current position as a one-off node and try routing from
// it to each endCandidate via the hearthstone PortalNode edge.
if (Player* player = dynamic_cast<Player*>(bot))
{
if (player->IsAlive() && player->HasItemCount(6948, 1))
{
TravelNode* botNode = new TravelNode(botPos, "Bot Pos", false);
botNode->setPoint(botPos);
for (TravelNode* e : endCandidates)
{
if (!e || std::find(badEndNodes.begin(), badEndNodes.end(), e) != badEndNodes.end())
continue;
TravelNodeRoute route = GetNodeRoute(botNode, e, player);
if (route.isEmpty())
continue;
// Build the end-side path again for this candidate.
WorldPosition endNodePos = *e->getPosition();
std::vector<WorldPosition> endProbe;
if (endNodePos.GetMapId() == destination.GetMapId())
{
Unit* pathBot = (bot && bot->GetMapId() == destination.GetMapId()) ? bot : nullptr;
endProbe = endNodePos.getPathTo(destination, pathBot);
}
else
endProbe = {endNodePos, destination};
route.addTempNodes({botNode}); // transfer ownership of botNode
path = route.BuildPath({botPos}, endProbe, bot);
route.cleanTempNodes();
return path;
}
delete botNode;
}
}
return path; // empty return path; // empty
} }

View File

@ -95,10 +95,7 @@ enum class TravelNodePathType : uint8
areaTrigger = 2, areaTrigger = 2,
transport = 3, transport = 3,
flightPath = 4, flightPath = 4,
// Teleport-spell edges (hearthstone, mage portals). Generated at A* // value 5 reserved (was teleportSpell — removed)
// search start via PortalNode injection; consumed by
// HandleSpecialMovement's NODE_TELEPORT case.
teleportSpell = 5,
staticPortal = 6 staticPortal = 6
}; };
@ -410,26 +407,6 @@ protected:
// uint32 transportId = 0; // 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 // Route step type
enum class PathNodeType : uint8 enum class PathNodeType : uint8
{ {
@ -439,10 +416,7 @@ enum class PathNodeType : uint8
NODE_AREA_TRIGGER = 3, NODE_AREA_TRIGGER = 3,
NODE_TRANSPORT = 4, NODE_TRANSPORT = 4,
NODE_FLIGHTPATH = 5, NODE_FLIGHTPATH = 5,
// Teleport-spell endpoint (hearthstone, mage portal). Emitted by // value 6 reserved (was NODE_TELEPORT — removed with teleportSpell)
// TravelNodeRoute::BuildPath when traversing a teleportSpell-type
// edge; consumed by HandleSpecialMovement.
NODE_TELEPORT = 6,
NODE_STATIC_PORTAL = 7 NODE_STATIC_PORTAL = 7
}; };
@ -575,14 +549,6 @@ public:
{ {
nodes = nodes1; nodes = nodes1;
} }
TravelNodeRoute(std::vector<TravelNode*> nodes1,
std::vector<TravelNode*> const& tempNodes_)
{
nodes = nodes1;
if (!tempNodes_.empty())
addTempNodes(tempNodes_);
}
bool isEmpty() { return nodes.empty(); } bool isEmpty() { return nodes.empty(); }
bool hasNode(TravelNode* node) bool hasNode(TravelNode* node)
@ -593,19 +559,6 @@ public:
std::vector<TravelNode*> getNodes() { return nodes; } std::vector<TravelNode*> 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<TravelNode*> const& tempNodes_)
{
tempNodes.insert(tempNodes.end(), tempNodes_.begin(), tempNodes_.end());
}
void cleanTempNodes()
{
for (auto* n : tempNodes)
delete n;
tempNodes.clear();
}
TravelPath BuildPath( TravelPath BuildPath(
std::vector<WorldPosition> pathToStart = {}, std::vector<WorldPosition> pathToStart = {},
std::vector<WorldPosition> pathToEnd = {}, std::vector<WorldPosition> pathToEnd = {},
@ -619,7 +572,6 @@ private:
return std::find(nodes.begin(), nodes.end(), node); return std::find(nodes.begin(), nodes.end(), node);
} }
std::vector<TravelNode*> nodes; std::vector<TravelNode*> nodes;
std::vector<TravelNode*> tempNodes; // owned synthetic nodes (PortalNode etc.)
}; };
// A node container to aid A* calculations with nodes. // A node container to aid A* calculations with nodes.