From a0e21d9f388adaa2436b851173cf28fa40b7b7f0 Mon Sep 17 00:00:00 2001 From: bash Date: Sat, 30 May 2026 19:34:04 +0200 Subject: [PATCH] feat(Core/Travel): Re-enable area-trigger, static-portal, and teleport-spell nodes --- src/Ai/Base/Actions/MovementActions.cpp | 135 ++++++++++++++++++++++++ src/Mgr/Travel/TravelNode.cpp | 81 +++++++++++++- src/Mgr/Travel/TravelNode.h | 26 +++-- 3 files changed, 232 insertions(+), 10 deletions(-) diff --git a/src/Ai/Base/Actions/MovementActions.cpp b/src/Ai/Base/Actions/MovementActions.cpp index e9d0f0f8b..ad0a43830 100644 --- a/src/Ai/Base/Actions/MovementActions.cpp +++ b/src/Ai/Base/Actions/MovementActions.cpp @@ -3508,6 +3508,141 @@ bool MovementAction::ExecuteTravelPlan(TravelPlan& state) return true; } + case PathNodeType::NODE_AREA_TRIGGER: + { + // Pair: trigger (pointIdx) + dest (pointIdx+1). + // Bot walks into the area trigger volume; server teleports + // on entry. Bot may need quest/key prereqs to actually cross. + if (state.stepIdx + 1 >= state.steps.size()) + { + state.Reset(); + return false; + } + + const PathNodePoint& trigger = state.steps[state.stepIdx]; + const PathNodePoint& dst = state.steps[state.stepIdx + 1]; + + // Already on destination map — trigger fired, advance. + if (bot->GetMapId() == dst.point.GetMapId()) + { + state.stepIdx += 2; + return true; + } + + // Walk to the trigger position; collision with the trigger + // volume teleports us. + float dist = bot->GetExactDist(trigger.point.GetPositionX(), + trigger.point.GetPositionY(), + trigger.point.GetPositionZ()); + if (dist > INTERACTION_DISTANCE) + return MoveTo(trigger.point.GetMapId(), + trigger.point.GetPositionX(), + trigger.point.GetPositionY(), + trigger.point.GetPositionZ()); + + // At trigger but didn't teleport — likely missing quest/key. + // Abort; the do-quest yield-to-grind multiplier or next + // POI pick can reroute. + state.Reset(); + return false; + } + + case PathNodeType::NODE_STATIC_PORTAL: + { + // Pair: portal-GO position (pointIdx) + dest (pointIdx+1). + // Bot walks within interact range of the portal GameObject + // and sends CMSG_GAMEOBJ_USE to trigger its teleport spell. + if (state.stepIdx + 1 >= state.steps.size()) + { + state.Reset(); + return false; + } + + const PathNodePoint& portal = state.steps[state.stepIdx]; + const PathNodePoint& dst = state.steps[state.stepIdx + 1]; + + if (bot->GetMapId() == dst.point.GetMapId()) + { + state.stepIdx += 2; + return true; + } + + // Walk to portal GO position + float dist = bot->GetExactDist(portal.point.GetPositionX(), + portal.point.GetPositionY(), + portal.point.GetPositionZ()); + if (dist > INTERACTION_DISTANCE) + return MoveTo(portal.point.GetMapId(), + portal.point.GetPositionX(), + portal.point.GetPositionY(), + portal.point.GetPositionZ()); + + // In range — find the portal GameObject and interact + if (!portal.entry) + { + state.Reset(); + return false; + } + + if (bot->IsMounted()) + bot->Dismount(); + botAI->RemoveShapeshift(); + + GuidVector nearGOs = AI_VALUE(GuidVector, "nearest game objects"); + for (ObjectGuid const& guid : nearGOs) + { + GameObject* go = botAI->GetGameObject(guid); + if (!go || go->GetEntry() != portal.entry) + continue; + if (!bot->GetGameObjectIfCanInteractWith(guid, MAX_GAMEOBJECT_TYPE)) + continue; + + WorldPacket packet(CMSG_GAMEOBJ_USE); + packet << guid; + bot->GetSession()->QueuePacket(new WorldPacket(packet)); + return true; + } + + // GO not found nearby — abort and let next tick try again + state.Reset(); + return false; + } + + case PathNodeType::NODE_TELEPORT: + { + // Teleport-spell node: hearthstone (spell 8690) or class + // teleport spells (mage/druid). `entry` holds the spell ID. + uint32 spellId = pt.entry; + if (!spellId) + { + state.Reset(); + return false; + } + + if (bot->IsInFlight() || bot->IsNonMeleeSpellCast(false)) + return true; // wait + + if (bot->IsMounted()) + bot->Dismount(); + botAI->RemoveShapeshift(); + + bool cast = false; + if (spellId == 8690) + cast = botAI->DoSpecificAction("hearthstone", Event(), true); + else if (bot->HasSpell(spellId) && !bot->HasSpellCooldown(spellId)) + cast = botAI->CastSpell(spellId, bot); + + if (!cast) + { + state.Reset(); + return false; + } + + // Cast started — advance past the teleport step. + state.stepIdx++; + return true; + } + case PathNodeType::NODE_TRANSPORT: { if (state.stepIdx + 1 >= state.steps.size()) diff --git a/src/Mgr/Travel/TravelNode.cpp b/src/Mgr/Travel/TravelNode.cpp index 9a8addeb3..bc7c9ce47 100644 --- a/src/Mgr/Travel/TravelNode.cpp +++ b/src/Mgr/Travel/TravelNode.cpp @@ -875,7 +875,17 @@ TravelPath TravelNodeRoute::BuildPath(std::vector pathToStart, st continue; } - if (nodePath->getPathType() == TravelNodePathType::transport) + if (nodePath->getPathType() == TravelNodePathType::areaTrigger) + { + travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_AREA_TRIGGER, nodePath->getPathObject()); + travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_AREA_TRIGGER, nodePath->getPathObject()); + } + else if (nodePath->getPathType() == TravelNodePathType::staticPortal) + { + travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_STATIC_PORTAL, nodePath->getPathObject()); + travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_STATIC_PORTAL, nodePath->getPathObject()); + } + else if (nodePath->getPathType() == TravelNodePathType::transport) { // Emit the transport's full waypoint route, not just board+exit. // Intermediate points carry NODE_TRANSPORT type so the executor @@ -888,6 +898,11 @@ TravelPath TravelNodeRoute::BuildPath(std::vector pathToStart, st // Full taxi waypoint route; same reasoning as transport. travelPath.addPath(nodePath->GetPath(), PathNodeType::NODE_FLIGHTPATH, nodePath->getPathObject()); } + else if (nodePath->getPathType() == TravelNodePathType::teleportSpell) + { + travelPath.addPoint(*prevNode->getPosition(), PathNodeType::NODE_TELEPORT, nodePath->getPathObject()); + travelPath.addPoint(*node->getPosition(), PathNodeType::NODE_TELEPORT, nodePath->getPathObject()); + } else { std::vector path = nodePath->GetPath(); @@ -896,6 +911,11 @@ TravelPath TravelNodeRoute::BuildPath(std::vector pathToStart, st node != nodes.back()) // Remove the last point since that will also be the start of the next path. path.pop_back(); + if (path.size() > 1 && prevNode->isPortal() && + nodePath->getPathType() != TravelNodePathType::areaTrigger && + nodePath->getPathType() != TravelNodePathType::staticPortal) + path.erase(path.begin()); + if (path.size() > 1 && prevNode->isTransport() && nodePath->getPathType() != TravelNodePathType::transport) path.erase(path.begin()); @@ -1558,6 +1578,63 @@ void TravelNodeMap::generateStartNodes() } } +void TravelNodeMap::generateAreaTriggerNodes() +{ + // Entrance nodes + for (auto const& itr : sObjectMgr->GetAllAreaTriggerTeleports()) + { + AreaTriggerTeleport const& atEntry = itr.second; + AreaTrigger const* at = sObjectMgr->GetAreaTrigger(itr.first); + if (!at) + continue; + + WorldPosition inPos = WorldPosition(at->map, at->x, at->y, at->z, at->orientation); + WorldPosition outPos = WorldPosition(atEntry.target_mapId, atEntry.target_X, atEntry.target_Y, atEntry.target_Z, + atEntry.target_Orientation); + + std::string nodeName; + if (!outPos.isOverworld()) + nodeName = outPos.getAreaName(false) + " entrance"; + else if (!inPos.isOverworld()) + nodeName = inPos.getAreaName(false) + " exit"; + else + nodeName = inPos.getAreaName(false) + " portal"; + + TravelNodeMap::instance().addNode(inPos, nodeName, true, true); + } + + // Exit nodes + area-trigger link + for (auto const& itr : sObjectMgr->GetAllAreaTriggerTeleports()) + { + AreaTriggerTeleport const& atEntry = itr.second; + AreaTrigger const* at = sObjectMgr->GetAreaTrigger(itr.first); + if (!at) + continue; + + WorldPosition inPos = WorldPosition(at->map, at->x, at->y, at->z, at->orientation); + WorldPosition outPos = WorldPosition(atEntry.target_mapId, atEntry.target_X, atEntry.target_Y, atEntry.target_Z, + atEntry.target_Orientation); + + std::string nodeName; + if (!outPos.isOverworld()) + nodeName = outPos.getAreaName(false) + " entrance"; + else if (!inPos.isOverworld()) + nodeName = inPos.getAreaName(false) + " exit"; + else + nodeName = inPos.getAreaName(false) + " portal"; + + TravelNode* outNode = TravelNodeMap::instance().addNode(outPos, nodeName, true, true); + TravelNode* inNode = TravelNodeMap::instance().getNode(inPos, nullptr, 5.0f); + + if (outNode && inNode) + { + TravelNodePath travelPath(0.1f, 3.0f, (uint8)TravelNodePathType::areaTrigger, itr.first, true); + travelPath.setPath({*inNode->getPosition(), *outNode->getPosition()}); + inNode->setPathTo(outNode, travelPath); + } + } +} + void TravelNodeMap::generateTransportNodes() { for (auto const& itr : *sObjectMgr->GetGameObjectTemplates()) @@ -1661,6 +1738,8 @@ void TravelNodeMap::generateNodes() generateStartNodes(); LOG_INFO("playerbots", "-Generating npc nodes"); generateNpcNodes(); + LOG_INFO("playerbots", "-Generating area trigger nodes"); + generateAreaTriggerNodes(); LOG_INFO("playerbots", "-Generating transport nodes"); generateTransportNodes(); LOG_INFO("playerbots", "-Generating zone mean nodes"); diff --git a/src/Mgr/Travel/TravelNode.h b/src/Mgr/Travel/TravelNode.h index 2b02756d7..b7530b6ce 100644 --- a/src/Mgr/Travel/TravelNode.h +++ b/src/Mgr/Travel/TravelNode.h @@ -90,10 +90,11 @@ enum class TravelNodePathType : uint8 { none = 0, walk = 1, - // values 2 (areaTrigger), 5 (teleportSpell), 6 (staticPortal) - // reserved for future use — generation/execution not yet wired up. + areaTrigger = 2, transport = 3, - flightPath = 4 + flightPath = 4, + teleportSpell = 5, + staticPortal = 6 }; // A connection between two nodes. @@ -261,9 +262,14 @@ public: return false; } - // Portal-style link types (areaTrigger, staticPortal) are not - // generated in this PR. Stub retained so consumers compile. - bool isPortal() { return false; } + bool isPortal() + { + for (auto const& link : *getLinks()) + if (link.second->getPathType() == TravelNodePathType::areaTrigger || + link.second->getPathType() == TravelNodePathType::staticPortal) + return true; + return false; + } bool isWalking() { @@ -405,10 +411,11 @@ enum class PathNodeType : uint8 NODE_PREPATH = 0, NODE_PATH = 1, NODE_NODE = 2, - // values 3 (NODE_AREA_TRIGGER), 6 (NODE_TELEPORT), 7 (NODE_STATIC_PORTAL) - // reserved for future use — handlers not yet wired up. + NODE_AREA_TRIGGER = 3, NODE_TRANSPORT = 4, - NODE_FLIGHTPATH = 5 + NODE_FLIGHTPATH = 5, + NODE_TELEPORT = 6, + NODE_STATIC_PORTAL = 7 }; struct PathNodePoint @@ -662,6 +669,7 @@ public: void generateNpcNodes(); void generateStartNodes(); + void generateAreaTriggerNodes(); void generateNodes(); void generateTransportNodes(); void generateZoneMeanNodes();