diff --git a/src/Ai/Base/Actions/MovementActions.cpp b/src/Ai/Base/Actions/MovementActions.cpp index 5d0bd71d0..5546209e1 100644 --- a/src/Ai/Base/Actions/MovementActions.cpp +++ b/src/Ai/Base/Actions/MovementActions.cpp @@ -3327,16 +3327,26 @@ bool MovementAction::ExecuteTravelPlan(TravelPlan& state) } } - Creature* flightMaster = sTravelMgr.GetNearestFlightMaster(bot); - if (!flightMaster || !flightMaster->IsAlive()) + TravelMgr::FlightMasterInfo const* fmInfo = sTravelMgr.GetNearestFlightMasterInfo(bot); + if (!fmInfo) { state.route.clear(); state.stepIdx += 2; return true; } - if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE) - return MoveTo(flightMaster, INTERACTION_DISTANCE); + if (bot->GetDistance(fmInfo->pos) > INTERACTION_DISTANCE) + return MoveTo(fmInfo->pos.GetMapId(), fmInfo->pos.GetPositionX(), + fmInfo->pos.GetPositionY(), fmInfo->pos.GetPositionZ()); + + ObjectGuid fmGuid = ObjectGuid::Create(fmInfo->templateEntry, fmInfo->dbGuid); + Creature* flightMaster = ObjectAccessor::GetCreature(*bot, fmGuid); + if (!flightMaster || !flightMaster->IsAlive()) + { + state.route.clear(); + state.stepIdx += 2; + return true; + } botAI->RemoveShapeshift(); if (bot->IsMounted()) diff --git a/src/Ai/World/Rpg/Action/NewRpgAction.cpp b/src/Ai/World/Rpg/Action/NewRpgAction.cpp index 36e76f8f8..fe3aeeb58 100644 --- a/src/Ai/World/Rpg/Action/NewRpgAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgAction.cpp @@ -3,6 +3,7 @@ #include #include +#include "AreaDefines.h" #include "BroadcastHelper.h" #include "ChatHelper.h" #include "G3D/Vector2.h" @@ -119,17 +120,8 @@ bool NewRpgStatusUpdateAction::Execute(Event /*event*/) } break; } - case RPG_TRAVEL_FLIGHT: - { - auto& data = std::get(info.data); - if (data.inFlight && !bot->IsInFlight()) - { - // flight arrival - info.ChangeToIdle(); - return true; - } - break; - } + // RPG_TRAVEL_FLIGHT arrival is handled inside NewRpgTravelFlightAction + // so the flight action owns both take-off and landing transitions. case RPG_REST: { // REST -> IDLE @@ -463,25 +455,41 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/) return false; auto& data = *dataPtr; + + // Arrival: we had boarded a flight (data.inFlight) and we're no longer in + // it → we just landed. Special-case Rut'theran: walk to the portal GO so + // it teleports the bot into Darnassus, flipping the zone to AREA_DARNASSUS + // so this branch falls through to ChangeToIdle on the next tick. + if (data.inFlight && !bot->IsInFlight()) + { + if (bot->GetZoneId() == AREA_TELDRASSIL) + { + static WorldPosition const rutTheranPortalEntrance(1, 8799.41f, 969.787f, 26.2409f, 0.0f); + return MoveFarTo(rutTheranPortalEntrance); + } + info.ChangeToIdle(); + return true; + } + if (bot->IsInFlight()) { data.inFlight = true; return false; } - if (bot->GetDistance(data.fromPos) > INTERACTION_DISTANCE) - return MoveFarTo(data.fromPos); + if (bot->GetDistance(data.flightMasterPos) > INTERACTION_DISTANCE) + return MoveFarTo(data.flightMasterPos); - Creature* flightMaster = ObjectAccessor::GetCreature(*bot, data.fromFlightMaster ); + Creature* flightMaster = bot->FindNearestCreature(data.flightMasterEntry, INTERACTION_DISTANCE * 3); if (!flightMaster || !flightMaster->IsAlive()) { - botAI->rpgInfo.ChangeToIdle(); + info.ChangeToIdle(); return true; } if (!TakeFlight(data.path, flightMaster)) { - botAI->rpgInfo.ChangeToIdle(); + info.ChangeToIdle(); return true; } return true; diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp index 5ad288a82..a8fefe7b5 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp @@ -1072,19 +1072,21 @@ WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot) return dest; } -bool NewRpgBaseAction::SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector& path) +bool NewRpgBaseAction::SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector& path) { - flightMaster = sTravelMgr.GetNearestFlightMasterGuid(bot); - if (!flightMaster) + TravelMgr::FlightMasterInfo const* info = sTravelMgr.GetNearestFlightMasterInfo(bot); + if (!info) return false; std::vector> availablePaths = sTravelMgr.GetOptimalFlightDestinations(bot); if (availablePaths.empty()) return false; + flightMasterEntry = info->templateEntry; + flightMasterPos = info->pos; path = availablePaths[urand(0, availablePaths.size() - 1)]; LOG_DEBUG("playerbots", "[New RPG] Bot {} select random flight taxi node from:{} (node {}) to:{} ({} available)", - bot->GetName(), flightMaster.GetEntry(), path[0], path[path.size() - 1], availablePaths.size()); + bot->GetName(), flightMasterEntry, path[0], path[path.size() - 1], availablePaths.size()); return true; } @@ -1183,11 +1185,12 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector candidateSta } case RPG_TRAVEL_FLIGHT: { - ObjectGuid flightMasterGuid; + uint32 flightMasterEntry = 0; + WorldPosition flightMasterPos; std::vector path; - if (SelectRandomFlightTaxiNode(flightMasterGuid, path)) + if (SelectRandomFlightTaxiNode(flightMasterEntry, flightMasterPos, path)) { - botAI->rpgInfo.ChangeToTravelFlight(flightMasterGuid, path); + botAI->rpgInfo.ChangeToTravelFlight(flightMasterEntry, flightMasterPos, path); return true; } return false; @@ -1264,9 +1267,10 @@ bool NewRpgBaseAction::CheckRpgStatusAvailable(NewRpgStatus status) } case RPG_TRAVEL_FLIGHT: { - ObjectGuid flightMaster; + uint32 flightMasterEntry = 0; + WorldPosition flightMasterPos; std::vector path; - return SelectRandomFlightTaxiNode(flightMaster, path); + return SelectRandomFlightTaxiNode(flightMasterEntry, flightMasterPos, path); } case RPG_OUTDOOR_PVP: { diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h index 245d78ece..1a62d053c 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h @@ -55,7 +55,7 @@ protected: bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector& poiInfo, bool toComplete = false); static WorldPosition SelectRandomGrindPos(Player* bot); static WorldPosition SelectRandomCampPos(Player* bot); - bool SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector& path); + bool SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector& path); bool RandomChangeStatus(std::vector candidateStatus); bool CheckRpgStatusAvailable(NewRpgStatus status); diff --git a/src/Ai/World/Rpg/NewRpgInfo.cpp b/src/Ai/World/Rpg/NewRpgInfo.cpp index 4d757b7bf..150421752 100644 --- a/src/Ai/World/Rpg/NewRpgInfo.cpp +++ b/src/Ai/World/Rpg/NewRpgInfo.cpp @@ -37,11 +37,12 @@ void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest) data = do_quest; } -void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector path) +void NewRpgInfo::ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector path) { Reset(); TravelFlight flight; - flight.fromFlightMaster = fromFlightMaster; + flight.flightMasterEntry = flightMasterEntry; + flight.flightMasterPos = flightMasterPos; flight.path = std::move(path); flight.inFlight = false; data = flight; @@ -158,7 +159,7 @@ std::string NewRpgInfo::ToString() else if constexpr (std::is_same_v) { out << "TRAVEL_FLIGHT"; - out << "\nfromFlightMaster: " << arg.fromFlightMaster.GetEntry(); + out << "\nflightMasterEntry: " << arg.flightMasterEntry; out << "\nfromNode: " << arg.path[0]; out << "\ntoNode: " << arg.path[arg.path.size() - 1]; out << "\ninFlight: " << arg.inFlight; diff --git a/src/Ai/World/Rpg/NewRpgInfo.h b/src/Ai/World/Rpg/NewRpgInfo.h index 8a6e21d15..9b3f51485 100644 --- a/src/Ai/World/Rpg/NewRpgInfo.h +++ b/src/Ai/World/Rpg/NewRpgInfo.h @@ -50,8 +50,8 @@ struct NewRpgInfo // RPG_TRAVEL_FLIGHT struct TravelFlight { - ObjectGuid fromFlightMaster{}; - WorldPosition fromPos{}; + uint32 flightMasterEntry{0}; + WorldPosition flightMasterPos{}; std::vector path; bool inFlight{false}; }; @@ -101,7 +101,7 @@ struct NewRpgInfo void ChangeToWanderNpc(); void ChangeToWanderRandom(); void ChangeToDoQuest(uint32 questId, const Quest* quest); - void ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector path); + void ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector path); void ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId = 0); void ChangeToRest(); void ChangeToIdle(); diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp index 5d0e014f7..12fad2245 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -8,6 +8,7 @@ #include #include +#include "AreaDefines.h" #include "Creature.h" #include "Log.h" #include "ObjectAccessor.h" @@ -28,67 +29,60 @@ // Navigation data -enum class CityId : uint8 +struct Capital { - STORMWIND, - IRONFORGE, - DARNASSUS, - EXODAR, - ORGRIMMAR, - UNDERCITY, - THUNDER_BLUFF, - SILVERMOON_CITY, - SHATTRATH_CITY, - DALARAN + uint32 zoneId; + TeamId team; + char const* name; + std::vector bankers; }; -static const std::unordered_map> bankerToCity = { - {2455, {CityId::STORMWIND, TEAM_ALLIANCE}}, {2456, {CityId::STORMWIND, TEAM_ALLIANCE}}, {2457, {CityId::STORMWIND, TEAM_ALLIANCE}}, - {2460, {CityId::IRONFORGE, TEAM_ALLIANCE}}, {2461, {CityId::IRONFORGE, TEAM_ALLIANCE}}, {5099, {CityId::IRONFORGE, TEAM_ALLIANCE}}, - {4155, {CityId::DARNASSUS, TEAM_ALLIANCE}}, {4208, {CityId::DARNASSUS, TEAM_ALLIANCE}}, {4209, {CityId::DARNASSUS, TEAM_ALLIANCE}}, - {17773, {CityId::EXODAR, TEAM_ALLIANCE}}, {18350, {CityId::EXODAR, TEAM_ALLIANCE}}, {16710, {CityId::EXODAR, TEAM_ALLIANCE}}, - {3320, {CityId::ORGRIMMAR, TEAM_HORDE}}, {3309, {CityId::ORGRIMMAR, TEAM_HORDE}}, {3318, {CityId::ORGRIMMAR, TEAM_HORDE}}, - {4549, {CityId::UNDERCITY, TEAM_HORDE}}, {2459, {CityId::UNDERCITY, TEAM_HORDE}}, {2458, {CityId::UNDERCITY, TEAM_HORDE}}, {4550, {CityId::UNDERCITY, TEAM_HORDE}}, - {2996, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, {8356, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, {8357, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, - {17631, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {17632, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {17633, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, - {16615, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {16616, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {16617, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, - {19246, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, {19338, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, - {19034, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, {19318, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, - {30604, {CityId::DALARAN, TEAM_NEUTRAL}}, {30605, {CityId::DALARAN, TEAM_NEUTRAL}}, {30607, {CityId::DALARAN, TEAM_NEUTRAL}}, - {28675, {CityId::DALARAN, TEAM_NEUTRAL}}, {28676, {CityId::DALARAN, TEAM_NEUTRAL}}, {28677, {CityId::DALARAN, TEAM_NEUTRAL}} +static const std::vector capitals = { + { AREA_STORMWIND_CITY, TEAM_ALLIANCE, "Stormwind", {2455, 2456, 2457} }, + { AREA_IRONFORGE, TEAM_ALLIANCE, "Ironforge", {2460, 2461, 5099} }, + { AREA_DARNASSUS, TEAM_ALLIANCE, "Darnassus", {4155, 4208, 4209} }, + { AREA_THE_EXODAR, TEAM_ALLIANCE, "Exodar", {17773, 18350, 16710} }, + { AREA_ORGRIMMAR, TEAM_HORDE, "Orgrimmar", {3320, 3309, 3318} }, + { AREA_UNDERCITY, TEAM_HORDE, "Undercity", {4549, 2459, 2458, 4550} }, + { AREA_THUNDER_BLUFF, TEAM_HORDE, "Thunder Bluff", {2996, 8356, 8357} }, + { AREA_SILVERMOON_CITY, TEAM_HORDE, "Silvermoon", {17631, 17632, 17633, 16615, 16616, 16617} }, + { AREA_SHATTRATH_CITY, TEAM_NEUTRAL, "Shattrath", {19246, 19338, 19034, 19318} }, + { AREA_DALARAN, TEAM_NEUTRAL, "Dalaran", {30604, 30605, 30607, 28675, 28676, 28677, 29530} } }; -static const std::unordered_map> cityToBankers = { - {CityId::STORMWIND, {2455, 2456, 2457}}, - {CityId::IRONFORGE, {2460, 2461, 5099}}, - {CityId::DARNASSUS, {4155, 4208, 4209}}, - {CityId::EXODAR, {17773, 18350, 16710}}, - {CityId::ORGRIMMAR, {3320, 3309, 3318}}, - {CityId::UNDERCITY, {4549, 2459, 2458, 4550}}, - {CityId::THUNDER_BLUFF, {2996, 8356, 8357}}, - {CityId::SILVERMOON_CITY, {17631, 17632, 17633, 16615, 16616, 16617}}, - {CityId::SHATTRATH_CITY, {19246, 19338, 19034, 19318}}, - {CityId::DALARAN, {30604, 30605, 30607, 28675, 28676, 28677, 29530}} -}; - -static int GetCityWeight(CityId city) +static Capital const* FindCapitalByZone(uint32 zoneId) { - int weight = 0; - switch (city) + for (Capital const& capital : capitals) + if (capital.zoneId == zoneId) + return &capital; + return nullptr; +} + +static Capital const* FindCapitalByBanker(uint16 bankerEntry) +{ + for (Capital const& capital : capitals) + for (uint16 bankerId : capital.bankers) + if (bankerId == bankerEntry) + return &capital; + return nullptr; +} + +static int GetCityWeight(uint32 zoneId) +{ + switch (zoneId) { - case CityId::STORMWIND: weight = sPlayerbotAIConfig.weightTeleToStormwind; break; - case CityId::IRONFORGE: weight = sPlayerbotAIConfig.weightTeleToIronforge; break; - case CityId::DARNASSUS: weight = sPlayerbotAIConfig.weightTeleToDarnassus; break; - case CityId::EXODAR: weight = sPlayerbotAIConfig.weightTeleToExodar; break; - case CityId::ORGRIMMAR: weight = sPlayerbotAIConfig.weightTeleToOrgrimmar; break; - case CityId::UNDERCITY: weight = sPlayerbotAIConfig.weightTeleToUndercity; break; - case CityId::THUNDER_BLUFF: weight = sPlayerbotAIConfig.weightTeleToThunderBluff; break; - case CityId::SILVERMOON_CITY: weight = sPlayerbotAIConfig.weightTeleToSilvermoonCity; break; - case CityId::SHATTRATH_CITY: weight = sPlayerbotAIConfig.weightTeleToShattrathCity; break; - case CityId::DALARAN: weight = sPlayerbotAIConfig.weightTeleToDalaran; break; - default: weight = 0; break; + case AREA_STORMWIND_CITY: return sPlayerbotAIConfig.weightTeleToStormwind; + case AREA_IRONFORGE: return sPlayerbotAIConfig.weightTeleToIronforge; + case AREA_DARNASSUS: return sPlayerbotAIConfig.weightTeleToDarnassus; + case AREA_THE_EXODAR: return sPlayerbotAIConfig.weightTeleToExodar; + case AREA_ORGRIMMAR: return sPlayerbotAIConfig.weightTeleToOrgrimmar; + case AREA_UNDERCITY: return sPlayerbotAIConfig.weightTeleToUndercity; + case AREA_THUNDER_BLUFF: return sPlayerbotAIConfig.weightTeleToThunderBluff; + case AREA_SILVERMOON_CITY: return sPlayerbotAIConfig.weightTeleToSilvermoonCity; + case AREA_SHATTRATH_CITY: return sPlayerbotAIConfig.weightTeleToShattrathCity; + case AREA_DALARAN: return sPlayerbotAIConfig.weightTeleToDalaran; } - return weight; + return 0; } WorldPosition::WorldPosition(std::string const str) @@ -4295,76 +4289,117 @@ void TravelMgr::Init() sTravelNodeMap.Init(); } -Creature* TravelMgr::GetNearestFlightMaster(Player* bot) +TravelMgr::FlightMasterInfo const* TravelMgr::GetNearestFlightMasterInfo(Player* bot) const { - std::map& flightMasterCache = + auto const& flightMasterCache = (bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache; - Creature* nearestFlightMaster = nullptr; + FlightMasterInfo const* nearest = nullptr; float nearestDistance = std::numeric_limits::max(); - for (auto const& [entry, pos] : flightMasterCache) + for (auto const& [dbGuid, info] : flightMasterCache) { - if (pos.GetMapId() != bot->GetMapId()) + if (info.pos.GetMapId() != bot->GetMapId()) continue; - float distance = bot->GetExactDist2dSq(pos); - if (distance > nearestDistance) - continue; - - Creature* flightMaster = ObjectAccessor::GetSpawnedCreatureByDBGUID(bot->GetMapId(), entry); - if (flightMaster) + float distance = bot->GetExactDist2dSq(info.pos); + if (distance < nearestDistance) { nearestDistance = distance; - nearestFlightMaster = flightMaster; + nearest = &info; } } - return nearestFlightMaster; + return nearest; } -ObjectGuid TravelMgr::GetNearestFlightMasterGuid(Player* bot) +std::vector TravelMgr::GetFlightNodesInZone(uint32 zoneId, TeamId team, uint32 excludeNode) const { - Creature* nearestFlightMaster = GetNearestFlightMaster(bot); - if (!nearestFlightMaster) - return ObjectGuid::Empty; - - return nearestFlightMaster->GetGUID(); + auto const& cache = (team == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache; + std::unordered_set seen; + std::vector result; + for (auto const& [entry, info] : cache) + { + if (info.zoneId != zoneId || info.taxiNodeId == 0 || info.taxiNodeId == excludeNode) + continue; + if (seen.insert(info.taxiNodeId).second) + result.push_back(info.taxiNodeId); + } + return result; } std::vector> TravelMgr::GetOptimalFlightDestinations(Player* bot) { std::vector> validDestinations; - Creature* nearestFlightMaster = GetNearestFlightMaster(bot); - if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster) > 500.0f) + FlightMasterInfo const* nearestFlightMaster = GetNearestFlightMasterInfo(bot); + if (!nearestFlightMaster) return validDestinations; - uint32 fromNode = sObjectMgr->GetNearestTaxiNode(nearestFlightMaster->GetPositionX(), nearestFlightMaster->GetPositionY(), - nearestFlightMaster->GetPositionZ(), nearestFlightMaster->GetMapId(), - bot->GetTeamId()); + uint32 fromNode = nearestFlightMaster->taxiNodeId; if (!fromNode) return validDestinations; - std::vector candidateLocations; - if (bot->GetLevel() >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) - candidateLocations = GetCityLocations(bot); + TaxiNodesEntry const* startNode = sTaxiNodesStore.LookupEntry(fromNode); + if (!startNode) + return validDestinations; - std::vector hubLocations = GetTravelHubs(bot); - candidateLocations.insert(candidateLocations.end(), hubLocations.begin(), hubLocations.end()); + uint32 botLevel = bot->GetLevel(); - for (auto const& loc : candidateLocations) + // Bots already in a capital shouldn't have another capital picked as a + // flight destination — that just shuffles them between cities. + bool botInCapital = false; + if (AreaTableEntry const* area = sAreaTableStore.LookupEntry(bot->GetZoneId())) + botInCapital = (area->flags & AREA_FLAG_CAPITAL) != 0; + + std::vector candidateZones; + if (botLevel >= 10 && !botInCapital && + urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) { - uint32 candidateNode = sObjectMgr->GetNearestTaxiNode(loc.GetPositionX(), loc.GetPositionY(), - loc.GetPositionZ(), loc.GetMapId(), - bot->GetTeamId()); - if (!candidateNode) - continue; - - std::vector path = sTravelNodeMap.FindTaxiPath(fromNode, candidateNode); - if (!path.empty()) - validDestinations.push_back(path); + TeamId botTeam = bot->GetTeamId(); + for (Capital const& capital : capitals) + { + if (capital.team != TEAM_NEUTRAL && capital.team != botTeam) + continue; + candidateZones.push_back(capital.zoneId); + } } + if (candidateZones.empty()) + { + for (auto const& [zoneId, bracket] : zone2LevelBracket) + { + if (botLevel < bracket.low || botLevel > bracket.high) + continue; + if (GetFlightNodesInZone(zoneId, bot->GetTeamId(), fromNode).empty()) + continue; + candidateZones.push_back(zoneId); + } + } + + if (candidateZones.empty()) + return validDestinations; + + while (!candidateZones.empty()) + { + uint32 zoneIndex = urand(0, candidateZones.size() - 1); + uint32 pickedZone = candidateZones[zoneIndex]; + + std::vector usableNodes = GetFlightNodesInZone(pickedZone, bot->GetTeamId(), fromNode); + + if (!usableNodes.empty()) + { + uint32 pickedNode = usableNodes[urand(0, usableNodes.size() - 1)]; + std::vector path = sTravelNodeMap.FindTaxiPath(fromNode, pickedNode); + if (!path.empty()) + { + validDestinations.push_back(std::move(path)); + return validDestinations; + } + } + + candidateZones.erase(candidateZones.begin() + zoneIndex); + } + return validDestinations; } @@ -4398,34 +4433,34 @@ std::vector TravelMgr::GetCityLocations(Player* bot) return fallbackLocations; TeamId botTeamId = bot->GetTeamId(); - std::unordered_set validBankerCities; + std::unordered_set validBankerCities; for (auto& loc : bankerLocsPerLevelCache[level]) { - auto cityIt = bankerToCity.find(loc.entry); - if (cityIt == bankerToCity.end()) + Capital const* capital = FindCapitalByBanker(loc.entry); + if (!capital) continue; - TeamId cityTeamId = cityIt->second.second; + TeamId cityTeamId = capital->team; if (cityTeamId == botTeamId || (cityTeamId == TEAM_NEUTRAL) ) - validBankerCities.insert(cityIt->second.first); + validBankerCities.insert(capital->zoneId); } // Fallback if no valid cities if (validBankerCities.empty()) return fallbackLocations; // Apply weights to valid cities - std::vector weightedCities; - for (CityId city : validBankerCities) + std::vector weightedCities; + for (uint32 zoneId : validBankerCities) { - int weight = GetCityWeight(city); + int weight = GetCityWeight(zoneId); if (weight <= 0) continue; for (int i = 0; i < weight; ++i) - weightedCities.push_back(city); + weightedCities.push_back(zoneId); } // Fallback if no valid cities @@ -4433,9 +4468,11 @@ std::vector TravelMgr::GetCityLocations(Player* bot) return fallbackLocations; // Pick a weighted city randomly, then a random banker in that city - CityId selectedCity = weightedCities[urand(0, weightedCities.size() - 1)]; - - auto const& bankers = cityToBankers.at(selectedCity); + uint32 selectedCity = weightedCities[urand(0, weightedCities.size() - 1)]; + Capital const* selectedCapital = FindCapitalByZone(selectedCity); + if (!selectedCapital) + return fallbackLocations; + auto const& bankers = selectedCapital->bankers; uint32 selectedBankerEntry = bankers[urand(0, bankers.size() - 1)]; auto locIt = bankerEntryToLocation.find(selectedBankerEntry); if (locIt != bankerEntryToLocation.end()) @@ -4474,78 +4511,78 @@ bool TravelMgr::SelectAuctioneerByMap(Player* bot, NpcLocation& outAuctioneer) void TravelMgr::PrepareZone2LevelBracket() { - // Classic WoW - Low - level zones - zone2LevelBracket[1] = {5, 12}; // Dun Morogh - zone2LevelBracket[12] = {5, 12}; // Elwynn Forest - zone2LevelBracket[14] = {5, 12}; // Durotar - zone2LevelBracket[85] = {5, 12}; // Tirisfal Glades - zone2LevelBracket[141] = {5, 12}; // Teldrassil - zone2LevelBracket[215] = {5, 12}; // Mulgore - zone2LevelBracket[3430] = {5, 12}; // Eversong Woods - zone2LevelBracket[3524] = {5, 12}; // Azuremyst Isle + // Classic WoW - starter zones + zone2LevelBracket[AREA_DUN_MOROGH] = {5, 12}; + zone2LevelBracket[AREA_ELWYNN_FOREST] = {5, 12}; + zone2LevelBracket[AREA_DUROTAR] = {5, 12}; + zone2LevelBracket[AREA_TIRISFAL_GLADES] = {5, 12}; + zone2LevelBracket[AREA_TELDRASSIL] = {5, 12}; + zone2LevelBracket[AREA_MULGORE] = {5, 12}; + zone2LevelBracket[AREA_EVERSONG_WOODS] = {5, 12}; + zone2LevelBracket[AREA_AZUREMYST_ISLE] = {5, 12}; - // Classic WoW - Mid - level zones - zone2LevelBracket[17] = {10, 25}; // Barrens - zone2LevelBracket[38] = {10, 20}; // Loch Modan - zone2LevelBracket[40] = {10, 21}; // Westfall - zone2LevelBracket[130] = {10, 23}; // Silverpine Forest - zone2LevelBracket[148] = {10, 21}; // Darkshore - zone2LevelBracket[3433] = {10, 22}; // Ghostlands - zone2LevelBracket[3525] = {10, 21}; // Bloodmyst Isle + // Classic WoW - low level zones + zone2LevelBracket[AREA_THE_BARRENS] = {10, 25}; + zone2LevelBracket[AREA_LOCH_MODAN] = {10, 20}; + zone2LevelBracket[AREA_WESTFALL] = {10, 21}; + zone2LevelBracket[AREA_SILVERPINE_FOREST] = {10, 23}; + zone2LevelBracket[AREA_DARKSHORE] = {10, 21}; + zone2LevelBracket[AREA_GHOSTLANDS] = {10, 22}; + zone2LevelBracket[AREA_BLOODMYST_ISLE] = {10, 21}; - // Classic WoW - High - level zones - zone2LevelBracket[10] = {19, 33}; // Deadwind Pass - zone2LevelBracket[11] = {21, 30}; // Wetlands - zone2LevelBracket[44] = {16, 28}; // Redridge Mountains - zone2LevelBracket[267] = {20, 34}; // Hillsbrad Foothills - zone2LevelBracket[331] = {18, 33}; // Ashenvale - zone2LevelBracket[400] = {24, 36}; // Thousand Needles - zone2LevelBracket[406] = {16, 29}; // Stonetalon Mountains + // Classic WoW - mid-level zones + zone2LevelBracket[AREA_DUSKWOOD] = {19, 33}; + zone2LevelBracket[AREA_WETLANDS] = {21, 30}; + zone2LevelBracket[AREA_REDRIDGE_MOUNTAINS] = {16, 28}; + zone2LevelBracket[AREA_HILLSBRAD_FOOTHILLS] = {20, 34}; + zone2LevelBracket[AREA_ASHENVALE] = {18, 33}; + zone2LevelBracket[AREA_THOUSAND_NEEDLES] = {24, 36}; + zone2LevelBracket[AREA_STONETALON_MOUNTAINS] = {16, 29}; - // Classic WoW - Higher - level zones - zone2LevelBracket[3] = {36, 46}; // Badlands - zone2LevelBracket[8] = {36, 46}; // Swamp of Sorrows - zone2LevelBracket[15] = {35, 46}; // Dustwallow Marsh - zone2LevelBracket[16] = {45, 52}; // Azshara - zone2LevelBracket[33] = {32, 47}; // Stranglethorn Vale - zone2LevelBracket[45] = {30, 42}; // Arathi Highlands - zone2LevelBracket[47] = {42, 51}; // Hinterlands - zone2LevelBracket[51] = {45, 51}; // Searing Gorge - zone2LevelBracket[357] = {40, 52}; // Feralas - zone2LevelBracket[405] = {30, 41}; // Desolace - zone2LevelBracket[440] = {41, 52}; // Tanaris + // Classic WoW - 30-52 zones + zone2LevelBracket[AREA_BADLANDS] = {36, 46}; + zone2LevelBracket[AREA_SWAMP_OF_SORROWS] = {36, 46}; + zone2LevelBracket[AREA_DUSTWALLOW_MARSH] = {35, 46}; + zone2LevelBracket[AREA_AZSHARA] = {45, 52}; + zone2LevelBracket[AREA_STRANGLETHORN_VALE] = {32, 47}; + zone2LevelBracket[AREA_ARATHI_HIGHLANDS] = {30, 42}; + zone2LevelBracket[AREA_THE_HINTERLANDS] = {42, 51}; + zone2LevelBracket[AREA_SEARING_GORGE] = {45, 51}; + zone2LevelBracket[AREA_FERALAS] = {40, 52}; + zone2LevelBracket[AREA_DESOLACE] = {30, 41}; + zone2LevelBracket[AREA_TANARIS] = {41, 52}; - // Classic WoW - Top - level zones - zone2LevelBracket[4] = {52, 57}; // Blasted Lands - zone2LevelBracket[28] = {50, 60}; // Western Plaguelands - zone2LevelBracket[46] = {51, 60}; // Burning Steppes - zone2LevelBracket[139] = {54, 62}; // Eastern Plaguelands - zone2LevelBracket[361] = {47, 57}; // Felwood - zone2LevelBracket[490] = {49, 56}; // Un'Goro Crater - zone2LevelBracket[618] = {54, 61}; // Winterspring - zone2LevelBracket[1377] = {54, 63}; // Silithus + // Classic WoW - top level zones + zone2LevelBracket[AREA_BLASTED_LANDS] = {52, 57}; + zone2LevelBracket[AREA_WESTERN_PLAGUELANDS] = {50, 60}; + zone2LevelBracket[AREA_BURNING_STEPPES] = {51, 60}; + zone2LevelBracket[AREA_EASTERN_PLAGUELANDS] = {54, 62}; + zone2LevelBracket[361] = {47, 57}; // Felwood (no AREA_ define) + zone2LevelBracket[490] = {49, 56}; // Un'Goro Crater (no AREA_ define) + zone2LevelBracket[AREA_WINTERSPRING] = {54, 61}; + zone2LevelBracket[AREA_SILITHUS] = {54, 63}; - // The Burning Crusade - Zones - zone2LevelBracket[3483] = {58, 66}; // Hellfire Peninsula - zone2LevelBracket[3518] = {64, 70}; // Nagrand - zone2LevelBracket[3519] = {62, 73}; // Terokkar Forest - zone2LevelBracket[3520] = {66, 73}; // Shadowmoon Valley - zone2LevelBracket[3521] = {60, 67}; // Zangarmarsh - zone2LevelBracket[3522] = {64, 73}; // Blade's Edge Mountains - zone2LevelBracket[3523] = {67, 73}; // Netherstorm - zone2LevelBracket[4080] = {68, 73}; // Isle of Quel'Danas + // The Burning Crusade zones + zone2LevelBracket[AREA_HELLFIRE_PENINSULA] = {58, 66}; + zone2LevelBracket[AREA_NAGRAND] = {64, 70}; + zone2LevelBracket[AREA_TEROKKAR_FOREST] = {62, 73}; + zone2LevelBracket[AREA_SHADOWMOON_VALLEY] = {66, 73}; + zone2LevelBracket[AREA_ZANGARMARSH] = {60, 67}; + zone2LevelBracket[AREA_BLADES_EDGE_MOUNTAINS] = {64, 73}; + zone2LevelBracket[AREA_NETHERSTORM] = {67, 73}; + zone2LevelBracket[AREA_ISLE_OF_QUEL_DANAS] = {68, 73}; - // Wrath of the Lich King - Zones - zone2LevelBracket[65] = {71, 77}; // Dragonblight - zone2LevelBracket[66] = {74, 80}; // Zul'Drak - zone2LevelBracket[67] = {77, 80}; // Storm Peaks - zone2LevelBracket[210] = {77, 80}; // Icecrown Glacier - zone2LevelBracket[394] = {72, 78}; // Grizzly Hills - zone2LevelBracket[495] = {68, 74}; // Howling Fjord - zone2LevelBracket[2817] = {77, 80}; // Crystalsong Forest - zone2LevelBracket[3537] = {68, 75}; // Borean Tundra - zone2LevelBracket[3711] = {75, 80}; // Sholazar Basin - zone2LevelBracket[4197] = {79, 80}; // Wintergrasp + // Wrath of the Lich King zones + zone2LevelBracket[AREA_DRAGONBLIGHT] = {71, 77}; + zone2LevelBracket[AREA_ZUL_DRAK] = {74, 80}; + zone2LevelBracket[AREA_THE_STORM_PEAKS] = {77, 80}; + zone2LevelBracket[210] = {77, 80}; // Icecrown Glacier (no AREA_ define) + zone2LevelBracket[AREA_GRIZZLY_HILLS] = {72, 78}; + zone2LevelBracket[AREA_HOWLING_FJORD] = {68, 74}; + zone2LevelBracket[AREA_CRYSTALSONG_FOREST] = {77, 80}; + zone2LevelBracket[AREA_BOREAN_TUNDRA] = {68, 75}; + zone2LevelBracket[AREA_SHOLAZAR_BASIN] = {75, 80}; + zone2LevelBracket[AREA_WINTERGRASP] = {79, 80}; // Override with values from config for (auto const& [zoneId, bracketPair] : sPlayerbotAIConfig.zoneBrackets) @@ -4605,16 +4642,18 @@ void TravelMgr::PrepareDestinationCache() (creatureTemplate->unit_flags & 4096) == 0 && creatureTemplate->rank == 0) { - uint32 roundX = (x / 50.0f) * 10.0f; - uint32 roundY = (y / 50.0f) * 10.0f; - uint32 roundZ = (z / 50.0f) * 10.0f; + uint32 roundX = static_cast(std::round(x / 50.0f)); + uint32 roundY = static_cast(std::round(y / 50.0f)); + uint32 roundZ = static_cast(std::round(z / 50.0f)); tempLocsCache[std::make_tuple(mapId, roundX, roundY, roundZ)].push_back(creatureData); tempCreatureCache[templateEntry][areaId].push_back(WorldLocation(mapId, x, y, z)); } // FLIGHT MASTERS + // Entry 29480 is Grimwing (Storm Peaks) — has FLIGHTMASTER flag but + // isn't a real usable flight master; skip it. else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER || creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) && - creatureTemplate->Entry != 3838 && creatureTemplate->Entry != 29480) + creatureTemplate->Entry != 29480) { FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction); bool forHorde = !(factionEntry->hostileMask & 4); @@ -4624,23 +4663,39 @@ void TravelMgr::PrepareDestinationCache() { WorldPosition pos(mapId, x, y, z, orient); if (forHorde) - hordeFlightMasterCache[guid] = pos; + { + FlightMasterInfo info; + info.pos = pos; + info.zoneId = areaId; + info.taxiNodeId = sObjectMgr->GetNearestTaxiNode(x, y, z, mapId, TEAM_HORDE); + info.templateEntry = templateEntry; + info.dbGuid = guid; + hordeFlightMasterCache[guid] = info; + } if (forAlliance) - allianceFlightMasterCache[guid] = pos; + { + FlightMasterInfo info; + info.pos = pos; + info.zoneId = areaId; + info.taxiNodeId = sObjectMgr->GetNearestTaxiNode(x, y, z, mapId, TEAM_ALLIANCE); + info.templateEntry = templateEntry; + info.dbGuid = guid; + allianceFlightMasterCache[guid] = info; + } flightMastersCount++; // Zones that have flight masters but no innkeepers — use flight master as hub static const std::set zonesWithoutInnkeeper = { - 4, // Blasted Lands (52-57) - 16, // Azshara (45-52) - 28, // Western Plaguelands (50-60) - 46, // Burning Steppes (51-60) - 51, // Searing Gorge (45-51) + AREA_BLASTED_LANDS, + AREA_AZSHARA, + AREA_WESTERN_PLAGUELANDS, + AREA_BURNING_STEPPES, + AREA_SEARING_GORGE, 361, // Felwood (47-57) 490, // Un'Goro Crater (49-56) - 2817, // Crystalsong Forest (77-80) - 4197 // Wintergrasp (79-80) + AREA_CRYSTALSONG_FOREST, + AREA_WINTERGRASP }; if (zonesWithoutInnkeeper.count(areaId)) { @@ -4736,7 +4791,7 @@ void TravelMgr::PrepareDestinationCache() // Process temporary caches for (auto const& [gridTuple, creatureDataList] : tempLocsCache) { - if (creatureDataList.size() > 2) + if (creatureDataList.size() >= 2) { CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id1); uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2; diff --git a/src/Mgr/Travel/TravelMgr.h b/src/Mgr/Travel/TravelMgr.h index 98d86c36a..00dac5968 100644 --- a/src/Mgr/Travel/TravelMgr.h +++ b/src/Mgr/Travel/TravelMgr.h @@ -852,6 +852,15 @@ public: uint32 entry; }; + struct FlightMasterInfo + { + WorldPosition pos; + uint32 zoneId; // resolved once at cache load + uint32 taxiNodeId; // DBC taxi node nearest to this flight master + uint32 templateEntry; // creature template ID (for ObjectGuid construction) + uint32 dbGuid; // DB spawn GUID (for ObjectGuid construction) + }; + static TravelMgr& instance() { static TravelMgr instance; @@ -864,12 +873,13 @@ public: // Navigation void Init(); - Creature* GetNearestFlightMaster(Player* bot); - ObjectGuid GetNearestFlightMasterGuid(Player* bot); + + FlightMasterInfo const* GetNearestFlightMasterInfo(Player* bot) const; std::vector> GetOptimalFlightDestinations(Player* bot); const std::vector GetTeleportLocations(Player* bot); const std::vector GetTravelHubs(Player* bot); std::vector GetCityLocations(Player* bot); + std::vector GetFlightNodesInZone(uint32 zoneId, TeamId team, uint32 excludeNode = 0) const; bool SelectAuctioneerByMap(Player* bot, NpcLocation& outAuctioneer); const std::vector& GetLocsPerLevelCache(uint8 level) { return locsPerLevelCache[level]; } @@ -976,8 +986,8 @@ private: }; // Navigation caches - std::map allianceFlightMasterCache; - std::map hordeFlightMasterCache; + std::map allianceFlightMasterCache; + std::map hordeFlightMasterCache; std::map> allianceHubsPerLevelCache; std::map> hordeHubsPerLevelCache; std::map> bankerLocsPerLevelCache; diff --git a/src/Mgr/Travel/TravelNode.cpp b/src/Mgr/Travel/TravelNode.cpp index a825bbc5f..64a305ea9 100644 --- a/src/Mgr/Travel/TravelNode.cpp +++ b/src/Mgr/Travel/TravelNode.cpp @@ -2429,7 +2429,7 @@ std::vector TravelNodeMap::FindTaxiPath(uint32 fromNode, uint32 toNode) TaxiNodesEntry const* startNode = sTaxiNodesStore.LookupEntry(fromNode); TaxiNodesEntry const* endNode = sTaxiNodesStore.LookupEntry(toNode); - if (!startNode || !endNode || startNode->map_id != endNode->map_id) + if (!startNode || !endNode) return {}; auto cacheItr = m_taxiPathCache.find(fromNode);