Merge pull request #2359 from mod-playerbots/test-staging

Test staging
This commit is contained in:
Keleborn 2026-05-08 10:36:36 -07:00 committed by GitHub
commit 531282e4be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
59 changed files with 522 additions and 343 deletions

View File

@ -32,7 +32,7 @@
# LEVELS
# GEAR
# QUESTS
# ACTIVITIES
# ACTIVITY
# SPELLS
# STRATEGIES
# RPG STRATEGY

View File

@ -343,7 +343,7 @@ bool BGJoinAction::isUseful()
return false;
// check Deserter debuff
if (!bot->CanJoinToBattleground())
if (bot->IsDeserter())
return false;
// check if has free queue slots (pointless as already making sure not in queue)

View File

@ -213,13 +213,7 @@ bool BuyAction::Execute(Event event)
}
}
if (!vendored)
{
botAI->TellError("There are no vendors nearby");
return false;
}
return true;
return vendored;
}
bool BuyAction::BuyItem(VendorItemData const* tItems, ObjectGuid vendorguid, ItemTemplate const* proto)

View File

@ -296,7 +296,7 @@ bool PetitionTurnInAction::isUseful()
bool BuyTabardAction::Execute(Event /*event*/)
{
bool canBuy = botAI->DoSpecificAction("buy", Event("buy tabard", "Hitem:5976:"));
bool canBuy = botAI->DoSpecificAction("buy", Event("buy tabard", "Hitem:5976:"), true);
if (canBuy && AI_VALUE2(uint32, "item count", chat->FormatQItem(5976)))
return true;

View File

@ -0,0 +1,39 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#include "TellEmblemsAction.h"
#include <array>
#include "Event.h"
#include "Playerbots.h"
bool TellEmblemsAction::Execute(Event /*event*/)
{
static std::array<uint32, 6> const emblemIds = {
29434, // Badge of Justice
40752, // Emblem of Heroism
40753, // Emblem of Valor
45624, // Emblem of Conquest
47241, // Emblem of Triumph
49426 // Emblem of Frost
};
botAI->TellMaster("=== Emblems ===");
for (uint32 itemId : emblemIds)
{
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
if (!proto)
continue;
uint32 count = bot->GetItemCount(itemId, false);
std::ostringstream out;
out << chat->FormatItem(proto, count);
botAI->TellMaster(out);
}
return true;
}

View File

@ -0,0 +1,21 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#ifndef _PLAYERBOT_TELLEMBLEMSACTION_H
#define _PLAYERBOT_TELLEMBLEMSACTION_H
#include "InventoryAction.h"
class PlayerbotAI;
class TellEmblemsAction : public InventoryAction
{
public:
TellEmblemsAction(PlayerbotAI* botAI) : InventoryAction(botAI, "emblems") {}
bool Execute(Event event) override;
};
#endif

View File

@ -5,34 +5,23 @@
#include "TellReputationAction.h"
#include <algorithm>
#include "Event.h"
#include "PlayerbotAI.h"
#include "ReputationMgr.h"
bool TellReputationAction::Execute(Event /*event*/)
#include "SharedDefines.h"
std::string TellReputationAction::BuildReputationLine(FactionEntry const* entry)
{
Player* master = GetMaster();
if (!master)
return false;
ObjectGuid selection = master->GetTarget();
if (selection.IsEmpty())
return false;
Unit* unit = ObjectAccessor::GetUnit(*master, selection);
if (!unit)
return false;
FactionTemplateEntry const* factionTemplate = unit->GetFactionTemplateEntry();
uint32 faction = factionTemplate->faction;
FactionEntry const* entry = sFactionStore.LookupEntry(faction);
int32 reputation = bot->GetReputationMgr().GetReputation(faction);
ReputationMgr& repMgr = bot->GetReputationMgr();
ReputationRank rank = repMgr.GetRank(entry);
int32 reputation = repMgr.GetReputation(entry->ID);
std::ostringstream out;
out << entry->name[0] << ": ";
out << "|cff";
out << entry->name[0] << ": |cff";
ReputationRank rank = bot->GetReputationMgr().GetRank(entry);
switch (rank)
{
case REP_HATED:
@ -71,7 +60,65 @@ bool TellReputationAction::Execute(Event /*event*/)
base -= ReputationMgr::PointsInRank[i];
out << " (" << (reputation - base) << "/" << ReputationMgr::PointsInRank[rank] << ")";
botAI->TellMaster(out);
return out.str();
}
bool TellReputationAction::Execute(Event event)
{
std::string const param = event.getParam();
if (param == "all")
{
ReputationMgr& repMgr = bot->GetReputationMgr();
std::vector<std::string> lines;
FactionStateList const& stateList = repMgr.GetStateList();
lines.reserve(stateList.size());
for (auto const& itr : stateList)
{
FactionState const& faction = itr.second;
if (!(faction.Flags & FACTION_FLAG_VISIBLE))
continue;
if (faction.Flags & (FACTION_FLAG_HIDDEN | FACTION_FLAG_INVISIBLE_FORCED) &&
!(faction.Flags & FACTION_FLAG_SPECIAL))
continue;
FactionEntry const* entry = sFactionStore.LookupEntry(faction.ID);
if (!entry)
continue;
lines.push_back(BuildReputationLine(entry));
}
std::sort(lines.begin(), lines.end());
botAI->TellMaster("=== Reputations ===");
for (auto const& line : lines)
botAI->TellMaster(line);
return true;
}
Player* master = GetMaster();
if (!master)
return false;
ObjectGuid selection = master->GetTarget();
if (selection.IsEmpty())
return false;
Unit* unit = ObjectAccessor::GetUnit(*master, selection);
if (!unit)
return false;
FactionTemplateEntry const* factionTemplate = unit->GetFactionTemplateEntry();
FactionEntry const* entry = sFactionStore.LookupEntry(factionTemplate->faction);
if (!entry)
return false;
botAI->TellMaster(BuildReputationLine(entry));
return true;
}

View File

@ -6,8 +6,11 @@
#ifndef _PLAYERBOT_TELLREPUTATIONACTION_H
#define _PLAYERBOT_TELLREPUTATIONACTION_H
#include <string>
#include "Action.h"
struct FactionEntry;
class PlayerbotAI;
class TellReputationAction : public Action
@ -16,6 +19,9 @@ public:
TellReputationAction(PlayerbotAI* botAI) : Action(botAI, "reputation") {}
bool Execute(Event event) override;
private:
std::string BuildReputationLine(FactionEntry const* entry);
};
#endif

View File

@ -66,6 +66,7 @@
#include "TaxiAction.h"
#include "TeleportAction.h"
#include "TellCastFailedAction.h"
#include "TellEmblemsAction.h"
#include "TellItemCountAction.h"
#include "TellLosAction.h"
#include "TellReputationAction.h"
@ -120,6 +121,7 @@ public:
creators["teleport"] = &ChatActionContext::teleport;
creators["taxi"] = &ChatActionContext::taxi;
creators["repair"] = &ChatActionContext::repair;
creators["emblems"] = &ChatActionContext::emblems;
creators["use"] = &ChatActionContext::use;
creators["item count"] = &ChatActionContext::item_count;
creators["equip"] = &ChatActionContext::equip;
@ -276,6 +278,7 @@ private:
static Action* item_count(PlayerbotAI* botAI) { return new TellItemCountAction(botAI); }
static Action* use(PlayerbotAI* botAI) { return new UseItemAction(botAI); }
static Action* repair(PlayerbotAI* botAI) { return new RepairAllAction(botAI); }
static Action* emblems(PlayerbotAI* botAI) { return new TellEmblemsAction(botAI); }
static Action* taxi(PlayerbotAI* botAI) { return new TaxiAction(botAI); }
static Action* teleport(PlayerbotAI* botAI) { return new TeleportAction(botAI); }
static Action* release(PlayerbotAI* botAI) { return new ReleaseSpiritAction(botAI); }

View File

@ -41,6 +41,7 @@ public:
creators["teleport"] = &ChatTriggerContext::teleport;
creators["taxi"] = &ChatTriggerContext::taxi;
creators["repair"] = &ChatTriggerContext::repair;
creators["emblems"] = &ChatTriggerContext::emblems;
creators["u"] = &ChatTriggerContext::use;
creators["use"] = &ChatTriggerContext::use;
creators["c"] = &ChatTriggerContext::item_count;
@ -235,6 +236,7 @@ private:
static Trigger* item_count(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "c"); }
static Trigger* use(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "use"); }
static Trigger* repair(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "repair"); }
static Trigger* emblems(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "emblems"); }
static Trigger* taxi(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "taxi"); }
static Trigger* teleport(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "teleport"); }
static Trigger* q(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "q"); }

View File

@ -114,6 +114,7 @@ void ChatCommandHandlerStrategy::InitTriggers(std::vector<TriggerNode*>& trigger
triggers.push_back(new TriggerNode("pet attack", { NextAction("pet attack", relevance) }));
triggers.push_back(new TriggerNode("roll", { NextAction("roll", relevance) }));
triggers.push_back(new TriggerNode("focus heal", { NextAction("focus heal targets", relevance) }));
triggers.push_back(new TriggerNode("emblems", { NextAction("emblems", relevance) }));
}
ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI)
@ -138,6 +139,7 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas
supported.push_back("teleport");
supported.push_back("taxi");
supported.push_back("repair");
supported.push_back("emblems");
supported.push_back("talents");
supported.push_back("spells");
supported.push_back("co");
@ -202,8 +204,8 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas
supported.push_back("unlock items");
supported.push_back("unlock traded item");
supported.push_back("tame");
supported.push_back("glyphs"); // Added for custom Glyphs
supported.push_back("glyph equip"); // Added for custom Glyphs
supported.push_back("glyphs");
supported.push_back("glyph equip");
supported.push_back("pet");
supported.push_back("pet attack");
supported.push_back("wait for attack time");

View File

@ -297,7 +297,7 @@ bool PlayerWantsInBattlegroundTrigger::IsActive()
if (bot->GetBattleground() && bot->GetBattleground()->GetStatus() == STATUS_IN_PROGRESS)
return false;
if (!bot->CanJoinToBattleground())
if (bot->IsDeserter())
return false;
return true;

View File

@ -180,19 +180,11 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
delete pItem;
if (result != EQUIP_ERR_OK && result != EQUIP_ERR_CANT_CARRY_MORE_OF_THIS)
{
return ITEM_USAGE_NONE;
}
// Check is unique items are equipped or not
bool needToCheckUnique = false;
if (result == EQUIP_ERR_CANT_CARRY_MORE_OF_THIS)
{
needToCheckUnique = true;
}
else if (itemProto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE))
{
needToCheckUnique = true;
}
// Check if unique items are equipped or not
bool needToCheckUnique = result == EQUIP_ERR_CANT_CARRY_MORE_OF_THIS ||
itemProto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE);
if (needToCheckUnique)
{
@ -206,14 +198,11 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
bool isEquipped = (totalItemCount > bagItemCount);
if (isEquipped)
{
return ITEM_USAGE_NONE; // Item is already equipped
}
// If not equipped, continue processing
}
if (itemProto->Class == ITEM_CLASS_QUIVER)
if (bot->getClass() != CLASS_HUNTER)
if (itemProto->Class == ITEM_CLASS_QUIVER && bot->getClass() != CLASS_HUNTER)
return ITEM_USAGE_NONE;
if (itemProto->Class == ITEM_CLASS_CONTAINER)
@ -221,13 +210,15 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
if (itemProto->SubClass != ITEM_SUBCLASS_CONTAINER)
return ITEM_USAGE_NONE; // Todo add logic for non-bag containers. We want to look at professions/class and
// only replace if non-bag is larger than bag.
if (GetSmallestBagSize() >= itemProto->ContainerSlots)
return ITEM_USAGE_NONE;
return ITEM_USAGE_EQUIP;
}
if (itemProto->Class == ITEM_CLASS_WEAPON && itemProto->SubClass == ITEM_SUBCLASS_WEAPON_MISC)
return ITEM_USAGE_NONE;
bool shouldEquip = false;
// uint32 statWeight = sRandomItemMgr.GetLiveStatWeight(bot, itemProto->ItemId);
StatsWeightCalculator calculator(bot);
@ -254,19 +245,14 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
uint8 dstSlot = botAI->FindEquipSlot(itemProto, NULL_SLOT, true);
// Check if dest wasn't set correctly by CanEquipItem and use FindEquipSlot instead
// This occurs with unique items that are already in the bots bags when CanEquipItem is called
if (dest == 0)
{
if (dstSlot != NULL_SLOT)
if (dest == 0 && dstSlot != NULL_SLOT)
{
// Construct dest from dstSlot
dest = (INVENTORY_SLOT_BAG_0 << 8) | dstSlot;
}
}
if (dstSlot == EQUIPMENT_SLOT_FINGER1 || dstSlot == EQUIPMENT_SLOT_TRINKET1)
{
possibleSlots = 2;
}
// Check weapon case separately to keep things a bit cleaner
bool have2HWeapon = false;
@ -283,14 +269,9 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
itemProto->SubClass == ITEM_SUBCLASS_WEAPON_SWORD2);
// If the bot can Titan Grip, ignore any 2H weapon that isn't a 2H sword, mace, or axe.
if (bot->CanTitanGrip())
{
// If this weapon is 2H but not one of the valid TG weapon types, do not equip it at all.
if (itemProto->InventoryType == INVTYPE_2HWEAPON && !isValidTGWeapon)
{
if (bot->CanTitanGrip() && itemProto->InventoryType == INVTYPE_2HWEAPON && !isValidTGWeapon)
return ITEM_USAGE_NONE;
}
}
// Now handle the logic for equipping and possible offhand slots
// If the bot can Dual Wield and:
@ -317,10 +298,8 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
if (shouldEquipInSlot)
return ITEM_USAGE_EQUIP;
else
{
return ITEM_USAGE_BAD_EQUIP;
}
}
ItemTemplate const* oldItemProto = oldItem->GetTemplate();
float oldScore = calculator.CalculateItem(oldItemProto->ItemId, oldItem->GetInt32Value(ITEM_FIELD_RANDOM_PROPERTIES_ID));
@ -328,23 +307,17 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
{
// uint32 oldStatWeight = sRandomItemMgr.GetLiveStatWeight(bot, oldItemProto->ItemId);
if (itemScore || oldScore)
{
shouldEquipInSlot = itemScore > oldScore * sPlayerbotAIConfig.equipUpgradeThreshold;
}
}
// Bigger quiver
if (itemProto->Class == ITEM_CLASS_QUIVER)
{
if (!oldItem || oldItemProto->ContainerSlots < itemProto->ContainerSlots)
{
return ITEM_USAGE_EQUIP;
}
else
{
return ITEM_USAGE_NONE;
}
}
bool existingShouldEquip = true;
if (oldItemProto->Class == ITEM_CLASS_WEAPON && !sRandomItemMgr.CanEquipWeapon(bot->getClass(), oldItemProto))

View File

@ -11,6 +11,22 @@
#include "AoeValues.h"
#include "TargetValue.h"
namespace
{
bool PrepareThornsTarget(PlayerbotAI* botAI, Unit* target)
{
if (!target)
return false;
Aura* existingThorns = botAI->GetAura("thorns", target, true);
if (!existingThorns)
return true;
target->RemoveOwnedAura(existingThorns, AURA_REMOVE_BY_CANCEL);
return true;
}
}
std::vector<NextAction> CastAbolishPoisonAction::getAlternatives()
{
return NextAction::merge({ NextAction("cure poison") },
@ -33,6 +49,21 @@ bool CastLifebloomOnMainTankAction::isUseful()
return !lifebloom || lifebloom->GetStackAmount() < 3 || lifebloom->GetDuration() < 2000;
}
bool CastThornsAction::Execute(Event event)
{
return PrepareThornsTarget(botAI, GetTarget()) && CastBuffSpellAction::Execute(event);
}
bool CastThornsOnPartyAction::Execute(Event event)
{
return PrepareThornsTarget(botAI, GetTarget()) && BuffOnPartyAction::Execute(event);
}
bool CastThornsOnMainTankAction::Execute(Event event)
{
return PrepareThornsTarget(botAI, GetTarget()) && BuffOnMainTankAction::Execute(event);
}
Value<Unit*>* CastEntanglingRootsCcAction::GetTargetValue()
{
return context->GetValue<Unit*>("cc target", "entangling roots");

View File

@ -114,18 +114,24 @@ class CastThornsAction : public CastBuffSpellAction
{
public:
CastThornsAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "thorns") {}
bool Execute(Event event) override;
};
class CastThornsOnPartyAction : public BuffOnPartyAction
{
public:
CastThornsOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "thorns") {}
bool Execute(Event event) override;
};
class CastThornsOnMainTankAction : public BuffOnMainTankAction
{
public:
CastThornsOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "thorns", false) {}
bool Execute(Event event) override;
};
class CastLifebloomOnMainTankAction : public BuffOnMainTankAction

View File

@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDAQ20TRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDAQ20TRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidAq20Triggers.h"

View File

@ -1,8 +1,6 @@
#ifndef _PLAYERBOT_RAIDAQ20STRATEGY_H
#define _PLAYERBOT_RAIDAQ20STRATEGY_H
#include "AiObjectContext.h"
#include "Multiplier.h"
#include "Strategy.h"
class RaidAq20Strategy : public Strategy

View File

@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDBWLTRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDBWLTRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidBwlTriggers.h"

View File

@ -2,8 +2,6 @@
#ifndef _PLAYERBOT_RAIDBWLSTRATEGY_H
#define _PLAYERBOT_RAIDBWLSTRATEGY_H
#include "AiObjectContext.h"
#include "Multiplier.h"
#include "Strategy.h"
class RaidBwlStrategy : public Strategy

View File

@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDEOETRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDEOETRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidEoETriggers.h"

View File

@ -1,8 +1,6 @@
#ifndef _PLAYERBOT_RAIDEOESTRATEGY_H
#define _PLAYERBOT_RAIDEOESTRATEGY_H
#include "AiObjectContext.h"
#include "Multiplier.h"
#include "Strategy.h"
class RaidEoEStrategy : public Strategy

View File

@ -2,7 +2,7 @@
#define _PLAYERBOT_RAIDGRUULSLAIRTRIGGERCONTEXT_H
#include "RaidGruulsLairTriggers.h"
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
class RaidGruulsLairTriggerContext : public NamedObjectContext<Trigger>
{

View File

@ -2,7 +2,6 @@
#define _PLAYERBOT_RAIDGRUULSLAIRSTRATEGY_H
#include "Strategy.h"
#include "Multiplier.h"
class RaidGruulsLairStrategy : public Strategy
{

View File

@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDICCTRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDICCTRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidIccTriggers.h"

View File

@ -1,10 +1,7 @@
#ifndef _PLAYERBOT_RAIDICCSTRATEGY_H
#define _PLAYERBOT_RAIDICCSTRATEGY_H
#include "AiObjectContext.h"
#include "Multiplier.h"
#include "Strategy.h"
#include "RaidIccMultipliers.h"
class RaidIccStrategy : public Strategy
{

View File

@ -2,7 +2,7 @@
#define _PLAYERBOT_RAIDKARAZHANTRIGGERCONTEXT_H
#include "RaidKarazhanTriggers.h"
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
class RaidKarazhanTriggerContext : public NamedObjectContext<Trigger>
{

View File

@ -2,7 +2,6 @@
#define _PLAYERBOT_RAIDKARAZHANSTRATEGY_H_
#include "Strategy.h"
#include "Multiplier.h"
class RaidKarazhanStrategy : public Strategy
{

View File

@ -2,7 +2,7 @@
#define _PLAYERBOT_RAIDMAGTHERIDONTRIGGERCONTEXT_H
#include "RaidMagtheridonTriggers.h"
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
class RaidMagtheridonTriggerContext : public NamedObjectContext<Trigger>
{

View File

@ -2,7 +2,6 @@
#define _PLAYERBOT_RAIDMAGTHERIDONSTRATEGY_H
#include "Strategy.h"
#include "Multiplier.h"
class RaidMagtheridonStrategy : public Strategy
{

View File

@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDMCTRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDMCTRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "BossAuraTriggers.h"
#include "NamedObjectContext.h"
#include "RaidMcTriggers.h"

View File

@ -1,8 +1,6 @@
#ifndef _PLAYERBOT_RAIDMCSTRATEGY_H
#define _PLAYERBOT_RAIDMCSTRATEGY_H
#include "AiObjectContext.h"
#include "Multiplier.h"
#include "Strategy.h"
class RaidMcStrategy : public Strategy

View File

@ -6,7 +6,6 @@
#ifndef _PLAYERBOT_RAIDNAXXTRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDNAXXTRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidNaxxTriggers.h"

View File

@ -2,8 +2,6 @@
#ifndef _PLAYERBOT_RAIDNAXXSTRATEGY_H
#define _PLAYERBOT_RAIDNAXXSTRATEGY_H
#include "AiObjectContext.h"
#include "Multiplier.h"
#include "Strategy.h"
class RaidNaxxStrategy : public Strategy

View File

@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDOSTRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDOSTRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidOsTriggers.h"

View File

@ -1,8 +1,6 @@
#ifndef _PLAYERBOT_RAIDOSSTRATEGY_H
#define _PLAYERBOT_RAIDOSSTRATEGY_H
#include "AiObjectContext.h"
#include "Multiplier.h"
#include "Strategy.h"
class RaidOsStrategy : public Strategy

View File

@ -99,8 +99,12 @@ bool RaidOnyxiaMoveToSafeZoneAction::Execute(Event /*event*/)
if (bot->IsWithinDist2d(bestZone->pos.GetPositionX(), bestZone->pos.GetPositionY(), bestZone->radius))
return false; // Already safe
// Stop current spell first
bot->AttackStop();
bot->InterruptNonMeleeSpells(false);
// bot->Yell("Moving to Safe Zone!", LANG_UNIVERSAL);
return MoveTo(bot->GetMapId(), bestZone->pos.GetPositionX(), bestZone->pos.GetPositionY(), bot->GetPositionZ(),
return MoveTo(bot->GetMapId(), bestZone->pos.GetPositionX(), bestZone->pos.GetPositionY(), bestZone->pos.GetPositionZ(),
false, false, false, false, MovementPriority::MOVEMENT_COMBAT);
}

View File

@ -2,7 +2,6 @@
#ifndef _PLAYERBOT_RAIDONYXIAACTIONS_H_
#define _PLAYERBOT_RAIDONYXIAACTIONS_H_
#include "Action.h"
#include "AttackAction.h"
#include "GenericSpellActions.h"
#include "MovementActions.h"
@ -45,42 +44,45 @@ public:
bool Execute(Event event) override;
private:
std::vector<SafeZone> GetSafeZonesForBreath(uint32 spellId)
static std::vector<SafeZone> GetSafeZonesForBreath(uint32 spellId)
{
// Define your safe zone coordinates based on the map
// Example assumes Onyxia's lair map coordinates
float z = bot->GetPositionZ(); // Stay at current height
// Safe zone coordinates based on the map
// Assumes Onyxia's lair map coordinates
switch (spellId)
{
case 17086: // N to S
case 18351: // S to N
return {SafeZone{Position(-10.0f, -180.0f, z), 5.0f},
SafeZone{Position(-20.0f, -250.0f, z), 5.0f}}; // Bottom Safe Zone
return {
SafeZone{Position(-10.0f, -180.0f, -87.0f), 5.0f},
SafeZone{Position(-20.0f, -250.0f, -88.0f), 5.0f}
}; // Bottom Safe Zone
case 18576: // E to W
case 18609: // W to E
return {
SafeZone{Position(20.0f, -210.0f, z), 5.0f},
SafeZone{Position(-75.0f, -210.0f, z), 5.0f},
SafeZone{Position(20.0f, -210.0f, -85.5f), 5.0f},
SafeZone{Position(-75.0f, -210.0f, -83.4f), 5.0f},
}; // Left Safe Zone
case 18564: // SE to NW
case 18584: // NW to SE
return {
SafeZone{Position(-60.0f, -195.0f, z), 5.0f},
SafeZone{Position(10.0f, -240.0f, z), 5.0f},
SafeZone{Position(-60.0f, -195.0f, -85.0f), 5.0f},
SafeZone{Position(10.0f, -240.0f, -85.9f), 5.0f},
}; // NW Safe Zone
case 18596: // SW to NE
case 18617: // NE to SW
return {
SafeZone{Position(7.0f, -185.0f, z), 5.0f},
SafeZone{Position(-60.0f, -240.0f, z), 5.0f},
SafeZone{Position(7.0f, -185.0f, -86.2f), 5.0f},
SafeZone{Position(-60.0f, -240.0f, -85.2f), 5.0f},
}; // NE Safe Zone
default:
return {SafeZone{Position(0.0f, 0.0f, z), 5.0f}}; // Fallback center - shouldn't ever happen
return {
SafeZone{Position(-40.0f, -214.0f, -86.6f), 5.0f}
}; // Fallback center - shouldn't ever happen
}
}
};

View File

@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDONYXIATRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDONYXIATRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidOnyxiaTriggers.h"

View File

@ -7,7 +7,7 @@
#define _PLAYERBOT_RAIDSSCTRIGGERCONTEXT_H
#include "RaidSSCTriggers.h"
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
class RaidSSCTriggerContext : public NamedObjectContext<Trigger>
{

View File

@ -7,7 +7,6 @@
#define _PLAYERBOT_RAIDSSCSTRATEGY_H_
#include "Strategy.h"
#include "Multiplier.h"
class RaidSSCStrategy : public Strategy
{

View File

@ -2,7 +2,7 @@
#define _PLAYERBOT_RAIDTEMPESTKEEPTRIGGERCONTEXT_H
#include "RaidTempestKeepTriggers.h"
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
class RaidTempestKeepTriggerContext : public NamedObjectContext<Trigger>
{

View File

@ -2,7 +2,6 @@
#define _PLAYERBOT_RAIDTEMPESTKEEPSTRATEGY_H_
#include "Strategy.h"
#include "Multiplier.h"
class RaidTempestKeepStrategy : public Strategy
{

View File

@ -6,7 +6,6 @@
#ifndef _PLAYERBOT_RAIDULDUARTRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDULDUARTRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidUlduarTriggers.h"
#include "BossAuraTriggers.h"

View File

@ -2,7 +2,6 @@
#ifndef _PLAYERBOT_RAIDULDUARSTRATEGY_H
#define _PLAYERBOT_RAIDULDUARSTRATEGY_H
#include "AiObjectContext.h"
#include "Strategy.h"
class RaidUlduarStrategy : public Strategy

View File

@ -6,7 +6,6 @@
#ifndef _PLAYERBOT_RAIDVOATRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDVOATRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "BossAuraTriggers.h"
#include "NamedObjectContext.h"
#include "RaidVoATriggers.h"

View File

@ -3,10 +3,6 @@
#define _PLAYERBOT_RAIDVOASTRATEGY_H
#include "Strategy.h"
#include "PlayerbotAI.h"
#include "string"
#include "Trigger.h"
#include "vector"
class RaidVoAStrategy : public Strategy
{

View File

@ -7,7 +7,7 @@
#define _PLAYERBOT_RAIDZULAMANTRIGGERCONTEXT_H
#include "RaidZulAmanTriggers.h"
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
class RaidZulAmanTriggerContext : public NamedObjectContext<Trigger>
{

View File

@ -7,7 +7,6 @@
#define _PLAYERBOT_RAIDZULAMANSTRATEGY_H_
#include "Strategy.h"
#include "Multiplier.h"
class RaidZulAmanStrategy : public Strategy
{

View File

@ -3,6 +3,7 @@
#include <cmath>
#include <cstdlib>
#include "AreaDefines.h"
#include "BroadcastHelper.h"
#include "ChatHelper.h"
#include "G3D/Vector2.h"
@ -468,10 +469,14 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
data.inFlight = true;
return false;
}
Creature* flightMaster = ObjectAccessor::GetCreature(*bot, data.fromFlightMaster);
if (bot->GetDistance(data.flightMasterPos) > INTERACTION_DISTANCE)
return MoveFarTo(data.flightMasterPos);
Creature* flightMaster = bot->FindNearestCreature(data.flightMasterEntry, INTERACTION_DISTANCE * 3);
if (!flightMaster || !flightMaster->IsAlive())
{
botAI->rpgInfo.ChangeToIdle();
info.ChangeToIdle();
return true;
}
if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE)
@ -487,7 +492,8 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
{
LOG_DEBUG("playerbots", "[New RPG] {} active taxi path {} (from {} to {}) failed", bot->GetName(),
flightMaster->GetEntry(), nodes[0], nodes[nodes.size() - 1]);
botAI->rpgInfo.ChangeToIdle();
info.ChangeToIdle();
return true;
}
return true;
}

View File

@ -1027,19 +1027,21 @@ WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot)
return dest;
}
bool NewRpgBaseAction::SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector<uint32>& path)
bool NewRpgBaseAction::SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector<uint32>& path)
{
flightMaster = sTravelMgr.GetNearestFlightMasterGuid(bot);
if (!flightMaster)
TravelMgr::FlightMasterInfo const* info = sTravelMgr.GetNearestFlightMasterInfo(bot);
if (!info)
return false;
std::vector<std::vector<uint32>> 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;
}
@ -1139,11 +1141,12 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector<NewRpgStatus> candidateSta
}
case RPG_TRAVEL_FLIGHT:
{
ObjectGuid flightMaster;
uint32 flightMasterEntry = 0;
WorldPosition flightMasterPos;
std::vector<uint32> path;
if (SelectRandomFlightTaxiNode(flightMaster, path))
if (SelectRandomFlightTaxiNode(flightMasterEntry, flightMasterPos, path))
{
botAI->rpgInfo.ChangeToTravelFlight(flightMaster, path);
botAI->rpgInfo.ChangeToTravelFlight(flightMasterEntry, flightMasterPos, path);
return true;
}
return false;
@ -1220,9 +1223,10 @@ bool NewRpgBaseAction::CheckRpgStatusAvailable(NewRpgStatus status)
}
case RPG_TRAVEL_FLIGHT:
{
ObjectGuid flightMaster;
uint32 flightMasterEntry = 0;
WorldPosition flightMasterPos;
std::vector<uint32> path;
return SelectRandomFlightTaxiNode(flightMaster, path);
return SelectRandomFlightTaxiNode(flightMasterEntry, flightMasterPos, path);
}
case RPG_OUTDOOR_PVP:
{

View File

@ -54,7 +54,7 @@ protected:
bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo>& poiInfo, bool toComplete = false);
static WorldPosition SelectRandomGrindPos(Player* bot);
static WorldPosition SelectRandomCampPos(Player* bot);
bool SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector<uint32>& path);
bool SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector<uint32>& path);
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
bool CheckRpgStatusAvailable(NewRpgStatus status);

View File

@ -37,11 +37,12 @@ void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest)
data = do_quest;
}
void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector<uint32> path)
void NewRpgInfo::ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector<uint32> path)
{
startT = getMSTime();
TravelFlight flight;
flight.fromFlightMaster = fromFlightMaster;
flight.flightMasterEntry = flightMasterEntry;
flight.flightMasterPos = flightMasterPos;
flight.path = std::move(path);
flight.inFlight = false;
data = flight;
@ -157,7 +158,7 @@ std::string NewRpgInfo::ToString()
else if constexpr (std::is_same_v<T, TravelFlight>)
{
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;

View File

@ -49,7 +49,8 @@ struct NewRpgInfo
// RPG_TRAVEL_FLIGHT
struct TravelFlight
{
ObjectGuid fromFlightMaster{};
uint32 flightMasterEntry{0};
WorldPosition flightMasterPos{};
std::vector<uint32> path;
bool inFlight{false};
};
@ -96,7 +97,7 @@ struct NewRpgInfo
void ChangeToWanderNpc();
void ChangeToWanderRandom();
void ChangeToDoQuest(uint32 questId, const Quest* quest);
void ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector<uint32> path);
void ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector<uint32> path);
void ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId = 0);
void ChangeToRest();
void ChangeToIdle();

View File

@ -619,7 +619,7 @@ void RandomPlayerbotFactory::CreateRandomBots()
else
password = accountName;
AccountMgr::CreateAccount(accountName, password);
sAccountMgr->CreateAccount(accountName, password);
LOG_DEBUG("playerbots", "Account {} created for random bots", accountName.c_str());
}

View File

@ -54,9 +54,9 @@
#include "Unit.h"
#include "UpdateTime.h"
#include "Vehicle.h"
#include "../../../../src/server/scripts/Spells/spell_dk.cpp"
const int SPELL_TITAN_GRIP = 49152;
constexpr uint32 SPELL_TITAN_GRIP = 49152;
constexpr uint32 SPELL_DK_FROST_PRESENCE = 48263;
std::vector<std::string> PlayerbotAI::dispel_whitelist = {
"mutating injection",

View File

@ -2077,7 +2077,7 @@ void RandomPlayerbotMgr::Refresh(Player* bot)
bot->DurabilityRepairAll(false, 1.0f, false);
bot->SetFullHealth();
bot->SetPvP(true);
bot->SetPvP(sWorld->IsPvPRealm());
PlayerbotFactory factory(bot, bot->GetLevel());
factory.Refresh();
@ -2642,6 +2642,7 @@ void RandomPlayerbotMgr::OnPlayerLogin(Player* player)
{
// ObjectGuid::LowType guid = player->GetGUID().GetCounter(); //not used, conditional could be rewritten for
// simplicity. line marked for removal.
player->SetPvP(sWorld->IsPvPRealm());
}
else
{

View File

@ -8,6 +8,7 @@
#include <iomanip>
#include <numeric>
#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<uint16> bankers;
};
static const std::unordered_map<uint16, std::pair<CityId, TeamId>> 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<Capital> 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<CityId, std::vector<uint16>> 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)
{
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;
for (Capital const& capital : capitals)
if (capital.zoneId == zoneId)
return &capital;
return nullptr;
}
return weight;
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 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 0;
}
WorldPosition::WorldPosition(std::string const str)
@ -4369,76 +4363,117 @@ void TravelMgr::Init()
LOG_INFO("playerbots", "Playerbots Taxi graph and destination cache built.");
}
Creature* TravelMgr::GetNearestFlightMaster(Player* bot)
TravelMgr::FlightMasterInfo const* TravelMgr::GetNearestFlightMasterInfo(Player* bot) const
{
std::map<uint32, WorldPosition>& flightMasterCache =
auto const& flightMasterCache =
(bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache;
Creature* nearestFlightMaster = nullptr;
FlightMasterInfo const* nearest = nullptr;
float nearestDistance = std::numeric_limits<float>::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<uint32> 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<uint32> seen;
std::vector<uint32> 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<std::vector<uint32>> TravelMgr::GetOptimalFlightDestinations(Player* bot)
{
std::vector<std::vector<uint32>> validDestinations;
Creature* nearestFlightMaster = GetNearestFlightMaster(bot);
if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster) > 500.0f)
FlightMasterInfo const* nearestFlightMaster = GetNearestFlightMasterInfo(bot);
if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster->pos) > 500.0f)
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<WorldLocation> 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<WorldLocation> 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;
//Simplify destination delection. Its either target cities (Based on config value) or target world.
std::vector<uint32> 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)
TeamId botTeam = bot->GetTeamId();
for (Capital const& capital : capitals)
{
if (capital.team != TEAM_NEUTRAL && capital.team != botTeam)
continue;
std::vector<uint32> path = sTravelNodeMap.FindTaxiPath(fromNode, candidateNode);
if (!path.empty())
validDestinations.push_back(path);
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<uint32> usableNodes = GetFlightNodesInZone(pickedZone, bot->GetTeamId(), fromNode);
if (!usableNodes.empty())
{
uint32 pickedNode = usableNodes[urand(0, usableNodes.size() - 1)];
std::vector<uint32> path = sTravelNodeMap.FindTaxiPath(fromNode, pickedNode);
if (!path.empty())
{
validDestinations.push_back(std::move(path));
return validDestinations;
}
}
candidateZones.erase(candidateZones.begin() + zoneIndex);
}
return validDestinations;
}
@ -4472,34 +4507,34 @@ std::vector<WorldLocation> TravelMgr::GetCityLocations(Player* bot)
return fallbackLocations;
TeamId botTeamId = bot->GetTeamId();
std::unordered_set<CityId> validBankerCities;
std::unordered_set<uint32> 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<CityId> weightedCities;
for (CityId city : validBankerCities)
std::vector<uint32> 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
@ -4507,9 +4542,11 @@ std::vector<WorldLocation> 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())
@ -4520,78 +4557,78 @@ std::vector<WorldLocation> TravelMgr::GetCityLocations(Player* bot)
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)
@ -4650,13 +4687,15 @@ 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<uint32>(std::round(x / 50.0f));
uint32 roundY = static_cast<uint32>(std::round(y / 50.0f));
uint32 roundZ = static_cast<uint32>(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)
// Entry 3838 is Vesprystus in Rut'Theran. Need Travel Node system to resolve this one.
else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER ||
creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) &&
creatureTemplate->Entry != 3838 && creatureTemplate->Entry != 29480)
@ -4669,23 +4708,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<uint32> 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))
{
@ -4756,7 +4811,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;

View File

@ -846,6 +846,21 @@ protected:
class TravelMgr
{
public:
struct NpcLocation
{
WorldLocation loc;
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;
@ -858,12 +873,14 @@ public:
// Navigation
void Init();
Creature* GetNearestFlightMaster(Player* bot);
ObjectGuid GetNearestFlightMasterGuid(Player* bot);
FlightMasterInfo const* GetNearestFlightMasterInfo(Player* bot) const;
std::vector<std::vector<uint32>> GetOptimalFlightDestinations(Player* bot);
const std::vector<WorldLocation> GetTeleportLocations(Player* bot);
const std::vector<WorldLocation> GetTravelHubs(Player* bot);
std::vector<WorldLocation> GetCityLocations(Player* bot);
std::vector<uint32> GetFlightNodesInZone(uint32 zoneId, TeamId team, uint32 excludeNode = 0) const;
bool SelectAuctioneerByMap(Player* bot, NpcLocation& outAuctioneer);
const std::vector<WorldLocation>& GetLocsPerLevelCache(uint8 level) { return locsPerLevelCache[level]; }
template <class D, class W, class URBG>
@ -975,8 +992,8 @@ private:
};
// Navigation caches
std::map<uint32, WorldPosition> allianceFlightMasterCache;
std::map<uint32, WorldPosition> hordeFlightMasterCache;
std::map<uint32, FlightMasterInfo> allianceFlightMasterCache;
std::map<uint32, FlightMasterInfo> hordeFlightMasterCache;
std::map<uint8, std::vector<WorldLocation>> allianceHubsPerLevelCache;
std::map<uint8, std::vector<WorldLocation>> hordeHubsPerLevelCache;
std::map<uint8, std::vector<BankerLocation>> bankerLocsPerLevelCache;

View File

@ -2467,7 +2467,7 @@ std::vector<uint32> 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 = taxiPathCache.find(fromNode);