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

Test staging
This commit is contained in:
Keleborn 2026-03-27 08:39:03 -07:00 committed by GitHub
commit c8dce882d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
73 changed files with 1224 additions and 1079 deletions

View File

@ -67,7 +67,7 @@ Bot messages have to be translatable, but you don't need to do the translations
the message is in a translatable format, and list in the table the message_key and the default English message.
Search for GetBotTextOrDefault in the codebase for examples.
-->
Does this change add bot messages to translate?
- Does this change add bot messages to translate?
- - [ ] No
- - [ ] Yes (**list messages in the table**)
@ -81,7 +81,7 @@ Does this change add bot messages to translate?
AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor.
We expect contributors to be honest about what they do and do not understand.
-->
Was AI assistance used while working on this change?
- Was AI assistance used while working on this change?
- - [ ] No
- - [ ] Yes (**explain below**)
<!--

View File

@ -34,11 +34,9 @@ We also have a **[Discord server](https://discord.gg/NQm5QShwf9)** where you can
Supported platforms are Ubuntu, Windows, and macOS. Other Linux distributions may work, but may not receive support.
**All `mod-playerbots` installations require a custom branch of AzerothCore: [mod-playerbots/azerothcore-wotlk/tree/Playerbot](https://github.com/mod-playerbots/azerothcore-wotlk/tree/Playerbot).** This branch allows the `mod-playerbots` module to build and function. Updates from the upstream are implemented regularly to this branch. Instructions for installing this required branch and this module are provided below.
> **Important:** All `mod-playerbots` installations require a custom fork of AzerothCore: [mod-playerbots/azerothcore-wotlk (Playerbot branch)](https://github.com/mod-playerbots/azerothcore-wotlk/tree/Playerbot). The standard AzerothCore repository will **not** work.
### Cloning the Repositories
To install both the required branch of AzerothCore and the `mod-playerbots` module from source, run the following:
### Quick Start
```bash
git clone https://github.com/mod-playerbots/azerothcore-wotlk.git --branch=Playerbot
@ -46,44 +44,18 @@ cd azerothcore-wotlk/modules
git clone https://github.com/mod-playerbots/mod-playerbots.git --branch=master
```
For more information, refer to the [AzerothCore Installation Guide](https://www.azerothcore.org/wiki/installation) and [Installing a Module](https://www.azerothcore.org/wiki/installing-a-module) pages.
Then build the server following the platform-specific instructions in our **[Installation Guide](https://github.com/mod-playerbots/mod-playerbots/wiki/Installation-Guide)**.
### Docker Installation
> **Testing branch:** A `test-staging` branch is available with the latest features and fixes before they are merged into `master`. To use it, clone with `--branch=test-staging` instead. Note that this branch may contain unstable or breaking changes — use it at your own risk and only if you are comfortable troubleshooting issues.
Docker installations are considered experimental (unofficial with limited support), and previous Docker experience is recommended. To install `mod-playerbots` on Docker, first clone the required branch of AzerothCore and this module:
### Detailed Guides
```bash
git clone https://github.com/mod-playerbots/azerothcore-wotlk.git --branch=Playerbot
cd azerothcore-wotlk/modules
git clone https://github.com/mod-playerbots/mod-playerbots.git --branch=master
```
| Guide | Description |
|---|---|
| **[Installation Guide](https://github.com/mod-playerbots/mod-playerbots/wiki/Installation-Guide)** | Full step-by-step instructions for clean installs, migrating from existing AzerothCore, Docker setup, adding modules, and updating |
| **[Troubleshooting](https://github.com/mod-playerbots/mod-playerbots/wiki/Troubleshooting)** | Solutions to the most common build errors, database issues, configuration mistakes, crashes, and platform-specific problems |
Afterwards, create a `docker-compose.override.yml` file in the `azerothcore-wotlk` directory. This override file allows for mounting the modules directory to the `ac-worldserver` service which is required for it to run. Put the following inside and save:
```yml
services:
ac-worldserver:
volumes:
- ./modules:/azerothcore/modules:ro
```
Additionally, this override file can be used to set custom configuration settings for `ac-worldserver` and any modules you install as environment variables:
```yml
services:
ac-worldserver:
environment:
AC_RATE_XP_KILL: "1"
AC_AI_PLAYERBOT_RANDOM_BOT_AUTOLOGIN: "1"
volumes:
- ./modules:/azerothcore/modules:ro
```
For example, to double the experience gain rate per kill, take the setting `Rate.XP.Kill = 1` from [woldserver.conf](https://github.com/mod-playerbots/azerothcore-wotlk/blob/Playerbot/src/server/apps/worldserver/worldserver.conf.dist), convert it to an environment variable, and change it to the desired setting in the override file to get `AC_RATE_XP_KILL: "2"`. If you wanted to disable random bots from logging in automatically, take the `AiPlayerbot.RandomBotAutologin = 1` setting from [playerbots.conf](https://github.com/mod-playerbots/mod-playerbots/blob/master/conf/playerbots.conf.dist) and do the same to get `AC_AI_PLAYERBOT_RANDOM_BOT_AUTOLOGIN: "0"`. For more information on how to configure Azerothcore, Playerbots, and other module settings as environment variables in Docker Compose, see the "Configuring AzerothCore in Containers" section in the [Install With Docker](https://www.azerothcore.org/wiki/install-with-docker) guide.
Before building, consider setting the database password. One way to do this is to create a `.env` file in the root `azerothcore-wotlk` directory using the [template](https://github.com/mod-playerbots/azerothcore-wotlk/blob/Playerbot/conf/dist/env.docker). This file also allows you to set the user and group Docker uses for the services in case you run into any permissions issues, which are the most common cause for Docker installation problems.
Use `docker compose up -d --build` to build and run the server. For more information, including how to create an account and taking backups, refer to the [Install With Docker](https://www.azerothcore.org/wiki/install-with-docker) page.
For additional references, see the [AzerothCore Installation Guide](https://www.azerothcore.org/wiki/installation) and [Installing a Module](https://www.azerothcore.org/wiki/installing-a-module) pages.
## Documentation

View File

@ -298,9 +298,24 @@ AiPlayerbot.TwoRoundsGearInit = 0
# Default: 0 (disabled)
AiPlayerbot.FreeMethodLoot = 0
# Bots' loot roll level (0 = pass, 1 = greed, 2 = need)
# Bots' Roll level bots will use for items they Need (0 = pass, 1 = greed, 2 = need)
# Default: 1 (greed)
AiPlayerbot.LootRollLevel = 1
AiPlayerbot.LootNeedRollLevel = 1
# Enable bots to roll GREED on items (global toggle)
# If disabled, bots will PASS instead of GREED on all items
# Default: 0 (disabled - bots only NEED or PASS)
AiPlayerbot.LootGreedRollLevel = 0
# Enable bots to roll on recipes. Will NEED on learnable profession recipes they don't already know
# Bots will roll GREED on BoE recipes they can't learn if LootRollGreed is enabled.
# Default: 0 (disabled)
AiPlayerbot.LootRollRecipe = 0
# Bots with enchanting will roll DISENCHANT instead of GREED on disenchantable items
# If disabled, bots will GREED on disenchantable items instead
# Default: 0 (disabled)
AiPlayerbot.LootRollDisenchant = 0
#
#
@ -1391,28 +1406,28 @@ AiPlayerbot.PremadeSpecLink.1.5.80 = 0502300123-3-250031220223012521332113321
#
AiPlayerbot.PremadeSpecName.2.0 = holy pve
AiPlayerbot.PremadeSpecGlyph.2.0 = 41106,43367,45741,43369,43365,41109
AiPlayerbot.PremadeSpecGlyph.2.0 = 41106,43367,45741,43368,43365,41109
AiPlayerbot.PremadeSpecLink.2.0.60 = 50350151020013053100515221
AiPlayerbot.PremadeSpecLink.2.0.80 = 50350152220013053100515221-503201312
AiPlayerbot.PremadeSpecName.2.1 = prot pve
AiPlayerbot.PremadeSpecGlyph.2.1 = 41099,43367,43869,43369,43365,45745
AiPlayerbot.PremadeSpecGlyph.2.1 = 41099,43367,43869,43368,43369,45745
AiPlayerbot.PremadeSpecLink.2.1.60 = -05005135203102311333112321
AiPlayerbot.PremadeSpecLink.2.1.80 = -05005135203102311333312321-502302012003
AiPlayerbot.PremadeSpecName.2.2 = ret pve
AiPlayerbot.PremadeSpecGlyph.2.2 = 41092,43367,41099,43369,43365,43869
AiPlayerbot.PremadeSpecGlyph.2.2 = 41092,43367,41099,43368,43369,43869
AiPlayerbot.PremadeSpecLink.2.2.60 = --05230051203331302133231131
AiPlayerbot.PremadeSpecLink.2.2.65 = -05-05230051203331302133231131
AiPlayerbot.PremadeSpecLink.2.2.80 = 050501-05-05232051203331302133231331
AiPlayerbot.PremadeSpecName.2.3 = holy pvp
AiPlayerbot.PremadeSpecGlyph.2.3 = 41110,43367,45746,43366,43365,45747
AiPlayerbot.PremadeSpecGlyph.2.3 = 41110,43367,45746,43369,43365,45747
AiPlayerbot.PremadeSpecLink.2.3.60 = 50332150300013050133215221
AiPlayerbot.PremadeSpecLink.2.3.80 = 50332150300013050133315221-5032013122
AiPlayerbot.PremadeSpecName.2.4 = prot pvp
AiPlayerbot.PremadeSpecGlyph.2.4 = 41092,43369,41101,43368,43365,45745
AiPlayerbot.PremadeSpecGlyph.2.4 = 41092,43367,41101,43369,43365,45745
AiPlayerbot.PremadeSpecLink.2.4.60 = -15320130223122311323311321
AiPlayerbot.PremadeSpecLink.2.4.80 = -15320130223122321333312321-052300502
AiPlayerbot.PremadeSpecName.2.5 = ret pvp
AiPlayerbot.PremadeSpecGlyph.2.5 = 41095,43369,41102,43368,43365,45747
AiPlayerbot.PremadeSpecGlyph.2.5 = 41095,43367,41102,43369,43365,45747
AiPlayerbot.PremadeSpecLink.2.5.60 = --05230250203331222133201321
AiPlayerbot.PremadeSpecLink.2.5.80 = -1532013022-05230250203331322133201321

View File

@ -163,6 +163,7 @@ public:
creators["war stomp"] = &ActionContext::war_stomp;
creators["blood fury"] = &ActionContext::blood_fury;
creators["berserking"] = &ActionContext::berserking;
creators["every man for himself"] = &ActionContext::every_man_for_himself;
creators["use trinket"] = &ActionContext::use_trinket;
creators["auto talents"] = &ActionContext::auto_talents;
creators["auto share quest"] = &ActionContext::auto_share_quest;
@ -357,6 +358,7 @@ private:
static Action* war_stomp(PlayerbotAI* botAI) { return new CastWarStompAction(botAI); }
static Action* blood_fury(PlayerbotAI* botAI) { return new CastBloodFuryAction(botAI); }
static Action* berserking(PlayerbotAI* botAI) { return new CastBerserkingAction(botAI); }
static Action* every_man_for_himself(PlayerbotAI* botAI) { return new CastEveryManForHimselfAction(botAI); }
static Action* use_trinket(PlayerbotAI* botAI) { return new UseTrinketAction(botAI); }
static Action* auto_talents(PlayerbotAI* botAI) { return new AutoSetTalentsAction(botAI); }
static Action* auto_share_quest(PlayerbotAI* ai) { return new AutoShareQuestAction(ai); }

View File

@ -311,6 +311,30 @@ bool CastVehicleSpellAction::Execute(Event /*event*/)
return botAI->CastVehicleSpell(spellId, GetTarget());
}
bool CastEveryManForHimselfAction::isPossible()
{
uint32 spellId = AI_VALUE2(uint32, "spell id", spell);
if (!spellId)
return false;
if (!bot->HasSpell(spellId))
return false;
if (bot->HasSpellCooldown(spellId))
return false;
return true;
}
bool CastEveryManForHimselfAction::isUseful()
{
return bot->HasAuraType(SPELL_AURA_MOD_STUN) ||
bot->HasAuraType(SPELL_AURA_MOD_FEAR) ||
bot->HasAuraType(SPELL_AURA_MOD_ROOT) ||
bot->HasAuraType(SPELL_AURA_MOD_CONFUSE) ||
bot->HasAuraType(SPELL_AURA_MOD_CHARM);
}
bool UseTrinketAction::Execute(Event /*event*/)
{
Item* trinket1 = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_TRINKET1);

View File

@ -284,6 +284,16 @@ public:
CastBerserkingAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "berserking") {}
};
class CastEveryManForHimselfAction : public CastSpellAction
{
public:
CastEveryManForHimselfAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "every man for himself") {}
std::string const GetTargetName() override { return "self target"; }
bool isPossible() override;
bool isUseful() override;
};
class UseTrinketAction : public Action
{
public:

View File

@ -22,10 +22,10 @@ bool LootRollAction::Execute(Event /*event*/)
std::vector<Roll*> rolls = group->GetRolls();
for (Roll*& roll : rolls)
{
if (roll->playerVote.find(bot->GetGUID())->second != NOT_EMITED_YET)
{
auto voteItr = roll->playerVote.find(bot->GetGUID());
if (voteItr == roll->playerVote.end() || voteItr->second != NOT_EMITED_YET)
continue;
}
ObjectGuid guid = roll->itemGUID;
uint32 itemId = roll->itemid;
int32 randomProperty = 0;
@ -41,27 +41,22 @@ bool LootRollAction::Execute(Event /*event*/)
std::string itemUsageParam;
if (randomProperty != 0)
{
itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty);
}
else
{
itemUsageParam = std::to_string(itemId);
}
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam);
// Armor Tokens are classed as MISC JUNK (Class 15, Subclass 0), luckily no other items I found have class bits and epic quality.
if (proto->Class == ITEM_CLASS_MISC && proto->SubClass == ITEM_SUBCLASS_JUNK && proto->Quality == ITEM_QUALITY_EPIC)
{
if (CanBotUseToken(proto, bot))
{
vote = NEED; // Eligible for "Need"
}
else
{
vote = GREED; // Not eligible, so "Greed"
}
}
else if (usage == ITEM_USAGE_DISENCHANT)
vote = sPlayerbotAIConfig.lootRollDisenchant ? DISENCHANT : GREED;
else
{
switch (proto->Class)
@ -69,40 +64,34 @@ bool LootRollAction::Execute(Event /*event*/)
case ITEM_CLASS_WEAPON:
case ITEM_CLASS_ARMOR:
if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP)
{
vote = NEED;
}
else if (usage != ITEM_USAGE_NONE)
{
vote = GREED;
}
break;
case ITEM_CLASS_RECIPE:
if (!sPlayerbotAIConfig.lootRollRecipe)
vote = PASS;
else if (usage == ITEM_USAGE_SKILL)
vote = NEED; // Bot can learn this recipe
else if (proto->Bonding != BIND_WHEN_PICKED_UP)
vote = GREED; // BoE recipe bot can't learn - GREED for AH/trade
break;
default:
if (StoreLootAction::IsLootAllowed(itemId, botAI))
vote = CalculateRollVote(proto); // Ensure correct Need/Greed behavior
vote = CalculateRollVote(proto, usage);
break;
}
}
if (sPlayerbotAIConfig.lootRollLevel == 0)
{
vote = PASS;
}
else if (sPlayerbotAIConfig.lootRollLevel == 1)
{
// Level 1 = "greed" mode: bots greed on useful items but never need
// Only downgrade NEED to GREED, preserve GREED votes as-is
if (vote == NEED)
{
if (RollUniqueCheck(proto, bot))
{
if (sPlayerbotAIConfig.lootNeedRollLevel == 0 || RollUniqueCheck(proto, bot))
vote = PASS;
}
else
{
else if (sPlayerbotAIConfig.lootNeedRollLevel == 1)
vote = GREED;
}
}
}
else if (vote == GREED && !sPlayerbotAIConfig.lootGreedRollLevel)
vote = PASS;
switch (group->GetLootMethod())
{
case MASTER_LOOT:
@ -120,11 +109,14 @@ bool LootRollAction::Execute(Event /*event*/)
return false;
}
RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto)
RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto, ItemUsage usage)
{
if (usage == ITEM_USAGE_NONE)
{
std::ostringstream out;
out << proto->ItemId;
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", out.str());
usage = AI_VALUE2(ItemUsage, "item usage", out.str());
}
RollVote needVote = PASS;
switch (usage)
@ -137,11 +129,13 @@ RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto)
break;
case ITEM_USAGE_SKILL:
case ITEM_USAGE_USE:
case ITEM_USAGE_DISENCHANT:
case ITEM_USAGE_AH:
case ITEM_USAGE_VENDOR:
needVote = GREED;
break;
case ITEM_USAGE_DISENCHANT:
needVote = sPlayerbotAIConfig.lootRollDisenchant ? DISENCHANT : GREED;
break;
default:
break;
}
@ -195,9 +189,7 @@ bool CanBotUseToken(ItemTemplate const* proto, Player* bot)
// Check if the bot's class is allowed to use the token
if (proto->AllowableClass & botClassMask)
{
return true; // Bot's class is eligible to use this token
}
return false; // Bot's class cannot use this token
}
@ -213,13 +205,9 @@ bool RollUniqueCheck(ItemTemplate const* proto, Player* bot)
// Determine if the unique item is already equipped
bool isEquipped = (totalItemCount > bagItemCount);
if (isEquipped && proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE))
{
return true; // Unique Item is already equipped
}
else if (proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE) && (bagItemCount > 1))
{
return true; // Unique item already in bag, don't roll for it
}
return false; // Item is not equipped or in bags, roll for it
}

View File

@ -22,7 +22,7 @@ public:
bool Execute(Event event) override;
protected:
RollVote CalculateRollVote(ItemTemplate const* proto);
RollVote CalculateRollVote(ItemTemplate const* proto, ItemUsage usage = ITEM_USAGE_NONE);
};
bool CanBotUseToken(ItemTemplate const* proto, Player* bot);

View File

@ -1387,8 +1387,8 @@ bool MovementAction::Flee(Unit* target)
}
}
HostileReference* ref = target->GetThreatMgr().getCurrentVictim();
if (ref && ref->getTarget() == bot) // bot is target - try to flee to tank or master
Unit* currentVictim = target->GetThreatMgr().GetCurrentVictim();
if (currentVictim && currentVictim == bot) // bot is target - try to flee to tank or master
{
if (Group* group = bot->GetGroup())
{

View File

@ -6,7 +6,8 @@
#include "TellTargetAction.h"
#include "Event.h"
#include "ThreatMgr.h"
#include "CombatManager.h"
#include "ThreatManager.h"
#include "AiObjectContext.h"
#include "PlayerbotAI.h"
@ -42,21 +43,21 @@ bool TellAttackersAction::Execute(Event /*event*/)
botAI->TellMaster("--- Threat ---");
HostileReference* ref = bot->getHostileRefMgr().getFirst();
if (!ref)
auto const& threatenedByMe = bot->GetThreatMgr().GetThreatenedByMeList();
if (threatenedByMe.empty())
return true;
while (ref)
for (auto const& [guid, ref] : threatenedByMe)
{
ThreatMgr* threatMgr = ref->GetSource();
Unit* unit = threatMgr->GetOwner();
Unit* unit = ref->GetOwner();
if (!unit)
continue;
float threat = ref->GetThreat();
std::ostringstream out;
out << unit->GetName() << " (" << threat << ")";
botAI->TellMaster(out);
ref = ref->next();
}
return true;

View File

@ -104,6 +104,13 @@ public:
creators["target"] = &ChatTriggerContext::target;
creators["formation"] = &ChatTriggerContext::formation;
creators["stance"] = &ChatTriggerContext::stance;
creators["cancel tree form"] = &ChatTriggerContext::cancel_tree_form;
creators["cancel travel form"] = &ChatTriggerContext::cancel_travel_form;
creators["cancel bear form"] = &ChatTriggerContext::cancel_bear_form;
creators["cancel dire bear form"] = &ChatTriggerContext::cancel_dire_bear_form;
creators["cancel cat form"] = &ChatTriggerContext::cancel_cat_form;
creators["cancel moonkin form"] = &ChatTriggerContext::cancel_moonkin_form;
creators["cancel aquatic form"] = &ChatTriggerContext::cancel_aquatic_form;
creators["sendmail"] = &ChatTriggerContext::sendmail;
creators["mail"] = &ChatTriggerContext::mail;
creators["outfit"] = &ChatTriggerContext::outfit;
@ -159,6 +166,13 @@ private:
static Trigger* sendmail(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "sendmail"); }
static Trigger* formation(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "formation"); }
static Trigger* stance(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "stance"); }
static Trigger* cancel_tree_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel tree form"); }
static Trigger* cancel_travel_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel travel form"); }
static Trigger* cancel_bear_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel bear form"); }
static Trigger* cancel_dire_bear_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel dire bear form"); }
static Trigger* cancel_cat_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel cat form"); }
static Trigger* cancel_moonkin_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel moonkin form"); }
static Trigger* cancel_aquatic_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel aquatic form"); }
static Trigger* attackers(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "attackers"); }
static Trigger* target(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "target"); }
static Trigger* max_dps(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "max dps"); }

View File

@ -160,6 +160,13 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas
supported.push_back("save mana");
supported.push_back("formation");
supported.push_back("stance");
supported.push_back("cancel tree form");
supported.push_back("cancel travel form");
supported.push_back("cancel bear form");
supported.push_back("cancel dire bear form");
supported.push_back("cancel cat form");
supported.push_back("cancel moonkin form");
supported.push_back("cancel aquatic form");
supported.push_back("sendmail");
supported.push_back("mail");
supported.push_back("outfit");

View File

@ -34,6 +34,9 @@ void RacialsStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
NextAction("berserking", ACTION_NORMAL + 5),
NextAction("use trinket", ACTION_NORMAL + 4) }));
triggers.push_back(new TriggerNode(
"loss of control", { NextAction("every man for himself", ACTION_EMERGENCY + 1) }));
}
RacialsStrategy::RacialsStrategy(PlayerbotAI* botAI) : Strategy(botAI)

View File

@ -16,7 +16,7 @@
#include "PositionValue.h"
#include "SharedDefines.h"
#include "TemporarySummon.h"
#include "ThreatMgr.h"
#include "ThreatManager.h"
#include "Timer.h"
#include "PlayerbotAI.h"
#include "Player.h"
@ -217,7 +217,7 @@ bool LowTankThreatTrigger::IsActive()
if (!current_target)
return false;
ThreatMgr& mgr = current_target->GetThreatMgr();
ThreatManager& mgr = current_target->GetThreatMgr();
float threat = mgr.GetThreat(bot);
float tankThreat = mgr.GetThreat(mt);
return tankThreat == 0.0f || threat > tankThreat * 0.5f;
@ -464,6 +464,15 @@ bool AttackerCountTrigger::IsActive() { return AI_VALUE(uint8, "attacker count")
bool HasAuraTrigger::IsActive() { return botAI->HasAura(getName(), GetTarget(), false, false, -1, true); }
bool LossOfControlTrigger::IsActive()
{
return bot->HasAuraType(SPELL_AURA_MOD_STUN) ||
bot->HasAuraType(SPELL_AURA_MOD_FEAR) ||
bot->HasAuraType(SPELL_AURA_MOD_ROOT) ||
bot->HasAuraType(SPELL_AURA_MOD_CONFUSE) ||
bot->HasAuraType(SPELL_AURA_MOD_CHARM);
}
bool HasAuraStackTrigger::IsActive()
{
Aura* aura = botAI->GetAura(getName(), GetTarget(), false, true, stack);

View File

@ -746,6 +746,14 @@ public:
bool IsActive() override;
};
class LossOfControlTrigger : public Trigger
{
public:
LossOfControlTrigger(PlayerbotAI* botAI) : Trigger(botAI, "loss of control", 1) {}
bool IsActive() override;
};
class IsSwimmingTrigger : public Trigger
{
public:

View File

@ -59,6 +59,7 @@ public:
creators["party member almost full health"] = &TriggerContext::PartyMemberAlmostFullHealth;
creators["generic boost"] = &TriggerContext::generic_boost;
creators["loss of control"] = &TriggerContext::loss_of_control;
creators["protect party member"] = &TriggerContext::protect_party_member;
@ -103,7 +104,8 @@ public:
creators["enemy within melee"] = &TriggerContext::enemy_within_melee;
creators["party member to heal out of spell range"] = &TriggerContext::party_member_to_heal_out_of_spell_range;
creators["combo points available"] = &TriggerContext::ComboPointsAvailable;
creators["combo points 5 available"] = &TriggerContext::ComboPoints5Available;
creators["combo points 4 available"] = &TriggerContext::ComboPoints4Available;
creators["combo points 3 available"] = &TriggerContext::ComboPoints3Available;
creators["target with combo points almost dead"] = &TriggerContext::target_with_combo_points_almost_dead;
creators["combo points not full"] = &TriggerContext::ComboPointsNotFull;
@ -338,7 +340,8 @@ private:
{
return new PartyMemberToHealOutOfSpellRangeTrigger(botAI);
}
static Trigger* ComboPointsAvailable(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI); }
static Trigger* ComboPoints5Available(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI, 5); }
static Trigger* ComboPoints4Available(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI, 4); }
static Trigger* ComboPoints3Available(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI, 3); }
static Trigger* target_with_combo_points_almost_dead(PlayerbotAI* ai)
{
@ -361,6 +364,7 @@ private:
return new PartyMemberAlmostFullHealthTrigger(botAI);
}
static Trigger* generic_boost(PlayerbotAI* botAI) { return new GenericBoostTrigger(botAI); }
static Trigger* loss_of_control(PlayerbotAI* botAI) { return new LossOfControlTrigger(botAI); }
static Trigger* PartyMemberCriticalHealth(PlayerbotAI* botAI)
{
return new PartyMemberCriticalHealthTrigger(botAI);

View File

@ -92,21 +92,15 @@ void AttackersValue::AddAttackersOf(Player* player, std::unordered_set<Unit*>& t
if (!player || !player->IsInWorld() || player->IsBeingTeleported())
return;
HostileRefMgr& refManager = player->getHostileRefMgr();
HostileReference* ref = refManager.getFirst();
if (!ref)
return;
while (ref)
for (auto const& [guid, ref] : player->GetThreatMgr().GetThreatenedByMeList())
{
ThreatMgr* threatMgr = ref->GetSource();
Unit* attacker = threatMgr->GetOwner();
Unit* attacker = ref->GetOwner();
if (!attacker)
continue;
if (player->IsValidAttackTarget(attacker) &&
player->GetDistance2d(attacker) < sPlayerbotAIConfig.sightDistance)
targets.insert(attacker);
ref = ref->next();
}
}
@ -131,7 +125,6 @@ bool AttackersValue::hasRealThreat(Unit* attacker)
return attacker && attacker->IsInWorld() && attacker->IsAlive() && !attacker->IsPolymorphed() &&
// !attacker->isInRoots() &&
!attacker->IsFriendlyTo(bot);
(attacker->GetThreatMgr().getCurrentVictim() || dynamic_cast<Player*>(attacker));
}
bool AttackersValue::IsPossibleTarget(Unit* attacker, Player* bot, float /*range*/)
@ -241,9 +234,6 @@ bool AttackersValue::IsPossibleTarget(Unit* attacker, Player* bot, float /*range
bool AttackersValue::IsValidTarget(Unit* attacker, Player* bot)
{
return IsPossibleTarget(attacker, bot) && bot->IsWithinLOSInMap(attacker);
// (attacker->GetThreatMgr().getCurrentVictim() || attacker->GetGuidValue(UNIT_FIELD_TARGET) ||
// attacker->GetGUID().IsPlayer() || attacker->GetGUID() ==
// GET_PLAYERBOT_AI(bot)->GetAiObjectContext()->GetValue<ObjectGuid>("pull target")->Get());
}
bool PossibleAddsValue::Calculate()
@ -255,13 +245,11 @@ bool PossibleAddsValue::Calculate()
{
if (find(attackers.begin(), attackers.end(), guid) != attackers.end())
continue;
if (Unit* add = botAI->GetUnit(guid))
{
if (!add->IsInWorld() || add->IsDuringRemoveFromWorld())
Unit* add = botAI->GetUnit(guid);
if (!add || !add->IsInWorld() || add->IsDuringRemoveFromWorld())
continue;
if (!add->GetTarget() && !add->GetThreatMgr().getCurrentVictim() && add->IsHostileTo(bot))
if (!add->GetTarget() && !add->GetThreatMgr().GetLastVictim() && add->IsHostileTo(bot))
{
for (ObjectGuid const attackerGUID : attackers)
{
@ -278,7 +266,6 @@ bool PossibleAddsValue::Calculate()
}
}
}
}
return false;
}

View File

@ -20,7 +20,7 @@ public:
}
public:
void CheckAttacker(Unit* creature, ThreatMgr* threatMgr) override
void CheckAttacker(Unit* creature, ThreatManager* threatMgr) override
{
Player* bot = botAI->GetBot();
if (!botAI->CanCastSpell(spell, creature))

View File

@ -13,7 +13,7 @@ public:
{
}
void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override
void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override
{
if (botAI->HasAura(spell, attacker))
result = attacker;

View File

@ -13,16 +13,14 @@ class FindMaxThreatGapTargetStrategy : public FindTargetStrategy
public:
FindMaxThreatGapTargetStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI), minThreat(0) {}
void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override
void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override
{
if (!attacker->IsAlive())
{
return;
}
if (foundHighPriority)
{
return;
}
if (IsHighPriority(attacker))
{
result = attacker;
@ -32,7 +30,7 @@ public:
if (!result || CalcThreatGap(attacker, threatMgr) > CalcThreatGap(result, &result->GetThreatMgr()))
result = attacker;
}
float CalcThreatGap(Unit* attacker, ThreatMgr* threatMgr)
float CalcThreatGap(Unit* attacker, ThreatManager* threatMgr)
{
Unit* victim = attacker->GetVictim();
return threatMgr->GetThreat(victim) - threatMgr->GetThreat(attacker);
@ -52,7 +50,7 @@ public:
result = nullptr;
}
void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override
void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override
{
if (Group* group = botAI->GetBot()->GetGroup())
{
@ -61,13 +59,11 @@ public:
return;
}
if (!attacker->IsAlive())
{
return;
}
if (foundHighPriority)
{
return;
}
if (IsHighPriority(attacker))
{
result = attacker;
@ -90,24 +86,19 @@ public:
int new_level = GetIntervalLevel(new_unit);
int old_level = GetIntervalLevel(old_unit);
if (new_level != old_level)
{
return new_level > old_level;
}
int32_t level = new_level;
if (level % 10 == 2 || level % 10 == 0)
{
return new_time < old_time;
}
// dont switch targets when all of them with low health
Unit* currentTarget = botAI->GetAiObjectContext()->GetValue<Unit*>("current target")->Get();
if (currentTarget == new_unit)
{
return true;
}
if (currentTarget == old_unit)
{
return false;
}
return new_time > old_time;
}
int32_t GetIntervalLevel(Unit* unit)
@ -119,13 +110,11 @@ public:
attackRange += 5.0f;
int level = dis < attackRange ? 10 : 0;
if (time >= 5 && time <= 30)
{
return level + 2;
}
if (time > 30)
{
return level;
}
return level + 1;
}
@ -143,7 +132,7 @@ public:
{
}
void CheckAttacker(Unit* attacker, ThreatMgr*) override
void CheckAttacker(Unit* attacker, ThreatManager*) override
{
if (Group* group = botAI->GetBot()->GetGroup())
{
@ -152,13 +141,11 @@ public:
return;
}
if (!attacker->IsAlive())
{
return;
}
if (foundHighPriority)
{
return;
}
if (IsHighPriority(attacker))
{
result = attacker;
@ -186,9 +173,8 @@ public:
// attack enemy in range and with lowest health
int level = new_level;
if (level == 10)
{
return new_time < old_time;
}
// all targets are far away, choose the closest one
return botAI->GetBot()->GetDistance(new_unit) < botAI->GetBot()->GetDistance(old_unit);
}
@ -216,7 +202,7 @@ public:
{
}
void CheckAttacker(Unit* attacker, ThreatMgr*) override
void CheckAttacker(Unit* attacker, ThreatManager*) override
{
if (Group* group = botAI->GetBot()->GetGroup())
{
@ -225,13 +211,11 @@ public:
return;
}
if (!attacker->IsAlive())
{
return;
}
if (foundHighPriority)
{
return;
}
if (IsHighPriority(attacker))
{
result = attacker;
@ -254,9 +238,8 @@ public:
int new_level = GetIntervalLevel(new_unit);
int old_level = GetIntervalLevel(old_unit);
if (new_level != old_level)
{
return new_level > old_level;
}
// attack enemy in range and with lowest health
int level = new_level;
Player* bot = botAI->GetBot();
@ -264,9 +247,8 @@ public:
{
Unit* combo_unit = bot->GetComboTarget();
if (new_unit == combo_unit)
{
return true;
}
return new_time < old_time;
}
// all targets are far away, choose the closest one
@ -319,7 +301,7 @@ class FindMaxHpTargetStrategy : public FindTargetStrategy
public:
FindMaxHpTargetStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI), maxHealth(0) {}
void CheckAttacker(Unit* attacker, ThreatMgr*) override
void CheckAttacker(Unit* attacker, ThreatManager*) override
{
if (Group* group = botAI->GetBot()->GetGroup())
{

View File

@ -5,6 +5,7 @@
#include "EnemyPlayerValue.h"
#include "CombatManager.h"
#include "Playerbots.h"
#include "ServerFacade.h"
#include "Vehicle.h"
@ -51,35 +52,22 @@ Unit* EnemyPlayerValue::Calculate()
controllingVehicle = true;
}
// 1. Check units we are currently in combat with.
// 1. Check units we are currently in PvP combat with.
std::vector<Unit*> targets;
Unit* pVictim = bot->GetVictim();
HostileReference* pReference = bot->getHostileRefMgr().getFirst();
while (pReference)
for (auto const& [guid, combatRef] : bot->GetCombatManager().GetPvPCombatRefs())
{
ThreatMgr* threatMgr = pReference->GetSource();
if (Unit* pTarget = threatMgr->GetOwner())
{
if (pTarget != pVictim && pTarget->IsPlayer() && pTarget->CanSeeOrDetect(bot) &&
bot->IsWithinDist(pTarget, VISIBILITY_DISTANCE_NORMAL))
{
if (bot->GetTeamId() == TEAM_HORDE)
{
if (pTarget->HasAura(23333))
Unit* pTarget = combatRef->GetOther(bot);
if (!pTarget || pTarget == pVictim || !pTarget->IsPlayer() || !pTarget->CanSeeOrDetect(bot) ||
!bot->IsWithinDist(pTarget, VISIBILITY_DISTANCE_NORMAL))
continue;
if ((bot->GetTeamId() == TEAM_HORDE && pTarget->HasAura(23333)) ||
(bot->GetTeamId() == TEAM_ALLIANCE && pTarget->HasAura(23335)))
return pTarget;
}
else
{
if (pTarget->HasAura(23335))
return pTarget;
}
targets.push_back(pTarget);
}
}
pReference = pReference->next();
}
if (!targets.empty())
{

View File

@ -153,9 +153,8 @@ ItemUsage ItemUsageValue::Calculate()
// Need to add something like free bagspace or item value.
if (proto->SellPrice > 0)
{
if (proto->Quality >= ITEM_QUALITY_NORMAL && !isSoulbound)
if (proto->Quality >= ITEM_QUALITY_NORMAL && !isSoulbound && proto->Bonding != BIND_WHEN_PICKED_UP)
return ITEM_USAGE_AH;
else
return ITEM_USAGE_VENDOR;
}

View File

@ -13,7 +13,7 @@ class FindLeastHpTargetStrategy : public FindNonCcTargetStrategy
public:
FindLeastHpTargetStrategy(PlayerbotAI* botAI) : FindNonCcTargetStrategy(botAI), minHealth(0) {}
void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override
void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override
{
if (IsCcTarget(attacker))
return;

View File

@ -15,12 +15,11 @@ class FindTargetForTankStrategy : public FindNonCcTargetStrategy
public:
FindTargetForTankStrategy(PlayerbotAI* botAI) : FindNonCcTargetStrategy(botAI), minThreat(0) {}
void CheckAttacker(Unit* creature, ThreatMgr* threatMgr) override
void CheckAttacker(Unit* creature, ThreatManager* threatMgr) override
{
if (!creature || !creature->IsAlive())
{
return;
}
Player* bot = botAI->GetBot();
float threat = threatMgr->GetThreat(bot);
if (!result)
@ -29,15 +28,11 @@ public:
result = creature;
}
// neglect if victim is main tank, or no victim (for untauntable target)
if (threatMgr->getCurrentVictim())
{
// float max_threat = threatMgr->GetThreat(threatMgr->getCurrentVictim()->getTarget());
Unit* victim = threatMgr->getCurrentVictim()->getTarget();
if (victim && victim->ToPlayer() && botAI->IsMainTank(victim->ToPlayer()))
if (Unit* victim = threatMgr->GetCurrentVictim())
{
if (victim->ToPlayer() && botAI->IsMainTank(victim->ToPlayer()))
return;
}
}
if (minThreat >= threat)
{
minThreat = threat;
@ -54,7 +49,7 @@ class FindTankTargetSmartStrategy : public FindTargetStrategy
public:
FindTankTargetSmartStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI) {}
void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override
void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override
{
if (Group* group = botAI->GetBot()->GetGroup())
{
@ -63,14 +58,11 @@ public:
return;
}
if (!attacker->IsAlive())
{
return;
}
if (!result || IsBetter(attacker, result))
{
result = attacker;
}
}
bool IsBetter(Unit* new_unit, Unit* old_unit)
{
Player* bot = botAI->GetBot();
@ -80,6 +72,7 @@ public:
{
if (old_unit == currentTarget)
return false;
if (new_unit == currentTarget)
return true;
}
@ -89,26 +82,22 @@ public:
float old_dis = bot->GetDistance(old_unit);
// hasAggro? -> withinMelee? -> threat
if (GetIntervalLevel(new_unit) != GetIntervalLevel(old_unit))
{
return GetIntervalLevel(new_unit) > GetIntervalLevel(old_unit);
}
int32_t interval = GetIntervalLevel(new_unit);
if (interval == 2)
{
return new_dis < old_dis;
}
return new_threat < old_threat;
}
int32_t GetIntervalLevel(Unit* unit)
{
if (!botAI->HasAggro(unit))
{
return 2;
}
if (botAI->GetBot()->IsWithinMeleeRange(unit))
{
return 1;
}
return 0;
}
};

View File

@ -5,12 +5,13 @@
#include "TargetValue.h"
#include "CombatManager.h"
#include "LastMovementValue.h"
#include "ObjectGuid.h"
#include "Playerbots.h"
#include "RtiTargetValue.h"
#include "ScriptedCreature.h"
#include "ThreatMgr.h"
#include "ThreatManager.h"
Unit* FindTargetStrategy::GetResult() { return result; }
@ -23,8 +24,8 @@ Unit* TargetValue::FindTarget(FindTargetStrategy* strategy)
if (!unit)
continue;
ThreatMgr& ThreatMgr = unit->GetThreatMgr();
strategy->CheckAttacker(unit, &ThreatMgr);
ThreatManager& threatMgr = unit->GetThreatMgr();
strategy->CheckAttacker(unit, &threatMgr);
}
return strategy->GetResult();
@ -144,24 +145,23 @@ Unit* FindTargetValue::Calculate()
{
return nullptr;
}
HostileReference* ref = bot->getHostileRefMgr().getFirst();
while (ref)
for (auto const& [guid, ref] : bot->GetThreatMgr().GetThreatenedByMeList())
{
ThreatMgr* threatManager = ref->GetSource();
Unit* unit = threatManager->GetOwner();
Unit* unit = ref->GetOwner();
if (!unit)
continue;
std::wstring wnamepart;
Utf8toWStr(unit->GetName(), wnamepart);
wstrToLower(wnamepart);
if (!qualifier.empty() && qualifier.length() == wnamepart.length() && Utf8FitTo(qualifier, wnamepart))
{
return unit;
}
ref = ref->next();
}
return nullptr;
}
void FindBossTargetStrategy::CheckAttacker(Unit* attacker, ThreatMgr* threatManager)
void FindBossTargetStrategy::CheckAttacker(Unit* attacker, ThreatManager* threatManager)
{
UnitAI* unitAI = attacker->GetAI();
BossAI* bossAI = dynamic_cast<BossAI*>(unitAI);

View File

@ -11,7 +11,7 @@
#include "Value.h"
class PlayerbotAI;
class ThreatMgr;
class ThreatManager;
class Unit;
class FindTargetStrategy
@ -20,7 +20,7 @@ public:
FindTargetStrategy(PlayerbotAI* botAI) : result(nullptr), botAI(botAI) {}
Unit* GetResult();
virtual void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) = 0;
virtual void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) = 0;
void GetPlayerCount(Unit* creature, uint32* tankCount, uint32* dpsCount);
bool IsHighPriority(Unit* attacker);
@ -129,7 +129,7 @@ class FindBossTargetStrategy : public FindTargetStrategy
{
public:
FindBossTargetStrategy(PlayerbotAI* ai) : FindTargetStrategy(ai) {}
virtual void CheckAttacker(Unit* attacker, ThreatMgr* threatManager);
virtual void CheckAttacker(Unit* attacker, ThreatManager* threatManager);
};
class BossTargetValue : public TargetValue, public Qualified

View File

@ -6,7 +6,7 @@
#include "ThreatValues.h"
#include "Playerbots.h"
#include "ThreatMgr.h"
#include "ThreatManager.h"
uint8 ThreatValue::Calculate()
{

View File

@ -44,13 +44,13 @@ bool CastCasterFormAction::isUseful()
AI_VALUE2(uint8, "mana", "self target") > sPlayerbotAIConfig.mediumHealth;
}
bool CastCancelTreeFormAction::Execute(Event /*event*/)
bool CastCancelDruidAction::Execute(Event /*event*/)
{
botAI->RemoveAura("tree of life");
botAI->RemoveAura(auraName);
return true;
}
bool CastCancelTreeFormAction::isUseful() { return botAI->HasAura(33891, bot); }
bool CastCancelDruidAction::isUseful() { return botAI->HasAura(auraId, bot); }
bool CastTreeFormAction::isUseful()
{

View File

@ -71,14 +71,78 @@ public:
bool isPossible() override { return true; }
};
class CastCancelTreeFormAction : public CastBuffSpellAction
class CastCancelDruidAction : public CastBuffSpellAction
{
public:
CastCancelTreeFormAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "cancel tree form") {}
CastCancelDruidAction(PlayerbotAI* botAI, std::string const& actionName, std::string const& auraName, uint32 auraId)
: CastBuffSpellAction(botAI, actionName), auraName(auraName), auraId(auraId)
{
}
bool Execute(Event event) override;
bool isUseful() override;
bool isPossible() override { return true; }
private:
std::string auraName;
uint32 auraId;
};
class CastCancelTreeFormAction : public CastCancelDruidAction
{
public:
CastCancelTreeFormAction(PlayerbotAI* botAI)
: CastCancelDruidAction(botAI, "cancel tree form", "tree of life", 33891)
{
}
};
class CastCancelTravelFormAction : public CastCancelDruidAction
{
public:
CastCancelTravelFormAction(PlayerbotAI* botAI)
: CastCancelDruidAction(botAI, "cancel travel form", "travel form", 783)
{
}
};
class CastCancelBearFormAction : public CastCancelDruidAction
{
public:
CastCancelBearFormAction(PlayerbotAI* botAI) : CastCancelDruidAction(botAI, "cancel bear form", "bear form", 5487) {}
};
class CastCancelDireBearFormAction : public CastCancelDruidAction
{
public:
CastCancelDireBearFormAction(PlayerbotAI* botAI)
: CastCancelDruidAction(botAI, "cancel dire bear form", "dire bear form", 9634)
{
}
};
class CastCancelCatFormAction : public CastCancelDruidAction
{
public:
CastCancelCatFormAction(PlayerbotAI* botAI) : CastCancelDruidAction(botAI, "cancel cat form", "cat form", 768) {}
};
class CastCancelMoonkinFormAction : public CastCancelDruidAction
{
public:
CastCancelMoonkinFormAction(PlayerbotAI* botAI)
: CastCancelDruidAction(botAI, "cancel moonkin form", "moonkin form", 24858)
{
}
};
class CastCancelAquaticFormAction : public CastCancelDruidAction
{
public:
CastCancelAquaticFormAction(PlayerbotAI* botAI)
: CastCancelDruidAction(botAI, "cancel aquatic form", "aquatic form", 1066)
{
}
};
#endif

View File

@ -170,6 +170,12 @@ public:
creators["aquatic form"] = &DruidAiObjectContextInternal::aquatic_form;
creators["caster form"] = &DruidAiObjectContextInternal::caster_form;
creators["cancel tree form"] = &DruidAiObjectContextInternal::cancel_tree_form;
creators["cancel travel form"] = &DruidAiObjectContextInternal::cancel_travel_form;
creators["cancel bear form"] = &DruidAiObjectContextInternal::cancel_bear_form;
creators["cancel dire bear form"] = &DruidAiObjectContextInternal::cancel_dire_bear_form;
creators["cancel cat form"] = &DruidAiObjectContextInternal::cancel_cat_form;
creators["cancel moonkin form"] = &DruidAiObjectContextInternal::cancel_moonkin_form;
creators["cancel aquatic form"] = &DruidAiObjectContextInternal::cancel_aquatic_form;
creators["mangle (bear)"] = &DruidAiObjectContextInternal::mangle_bear;
creators["maul"] = &DruidAiObjectContextInternal::maul;
creators["bash"] = &DruidAiObjectContextInternal::bash;
@ -258,6 +264,12 @@ private:
static Action* aquatic_form(PlayerbotAI* botAI) { return new CastAquaticFormAction(botAI); }
static Action* caster_form(PlayerbotAI* botAI) { return new CastCasterFormAction(botAI); }
static Action* cancel_tree_form(PlayerbotAI* botAI) { return new CastCancelTreeFormAction(botAI); }
static Action* cancel_travel_form(PlayerbotAI* botAI) { return new CastCancelTravelFormAction(botAI); }
static Action* cancel_bear_form(PlayerbotAI* botAI) { return new CastCancelBearFormAction(botAI); }
static Action* cancel_dire_bear_form(PlayerbotAI* botAI) { return new CastCancelDireBearFormAction(botAI); }
static Action* cancel_cat_form(PlayerbotAI* botAI) { return new CastCancelCatFormAction(botAI); }
static Action* cancel_moonkin_form(PlayerbotAI* botAI) { return new CastCancelMoonkinFormAction(botAI); }
static Action* cancel_aquatic_form(PlayerbotAI* botAI) { return new CastCancelAquaticFormAction(botAI); }
static Action* mangle_bear(PlayerbotAI* botAI) { return new CastMangleBearAction(botAI); }
static Action* maul(PlayerbotAI* botAI) { return new CastMaulAction(botAI); }
static Action* bash(PlayerbotAI* botAI) { return new CastBashAction(botAI); }

View File

@ -228,7 +228,7 @@ void CatDpsDruidStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
);
triggers.push_back(
new TriggerNode(
"combo points available",
"combo points 5 available",
{
NextAction("rip", ACTION_HIGH + 6)
}

View File

@ -176,7 +176,7 @@ void OffhealDruidCatStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
);
triggers.push_back(
new TriggerNode(
"combo points available",
"combo points 5 available",
{
NextAction("rip", ACTION_HIGH + 6)
}
@ -257,7 +257,7 @@ void OffhealDruidCatStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
);
triggers.push_back(
new TriggerNode(
"low energy",
"tiger's fury",
{
NextAction("tiger's fury", ACTION_NORMAL + 1)
}

View File

@ -472,9 +472,8 @@ Unit* CastRighteousDefenseAction::GetTarget()
{
Unit* current_target = AI_VALUE(Unit*, "current target");
if (!current_target)
{
return NULL;
}
return nullptr;
return current_target->GetVictim();
}

View File

@ -91,9 +91,8 @@ public:
class CastBlessingOnPartyAction : public BuffOnPartyAction
{
public:
CastBlessingOnPartyAction(PlayerbotAI* botAI, std::string const name) : BuffOnPartyAction(botAI, name), name(name)
{
}
CastBlessingOnPartyAction(PlayerbotAI* botAI, std::string const name)
: BuffOnPartyAction(botAI, name), name(name) {}
Value<Unit*>* GetTargetValue() override;
@ -154,9 +153,7 @@ public:
class CastBlessingOfSanctuaryOnPartyAction : public BuffOnPartyAction
{
public:
CastBlessingOfSanctuaryOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of sanctuary")
{
}
CastBlessingOfSanctuaryOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of sanctuary") {}
std::string const getName() override { return "blessing of sanctuary on party"; }
Value<Unit*>* GetTargetValue() override;
@ -173,18 +170,14 @@ class CastHolyShockOnPartyAction : public HealPartyMemberAction
{
public:
CastHolyShockOnPartyAction(PlayerbotAI* botAI)
: HealPartyMemberAction(botAI, "holy shock", 25.0f, HealingManaEfficiency::LOW)
{
}
: HealPartyMemberAction(botAI, "holy shock", 25.0f, HealingManaEfficiency::LOW) {}
};
class CastHolyLightOnPartyAction : public HealPartyMemberAction
{
public:
CastHolyLightOnPartyAction(PlayerbotAI* botAI)
: HealPartyMemberAction(botAI, "holy light", 50.0f, HealingManaEfficiency::MEDIUM)
{
}
: HealPartyMemberAction(botAI, "holy light", 50.0f, HealingManaEfficiency::MEDIUM) {}
};
class CastFlashOfLightAction : public CastHealingSpellAction
@ -197,9 +190,7 @@ class CastFlashOfLightOnPartyAction : public HealPartyMemberAction
{
public:
CastFlashOfLightOnPartyAction(PlayerbotAI* botAI)
: HealPartyMemberAction(botAI, "flash of light", 15.0f, HealingManaEfficiency::HIGH)
{
}
: HealPartyMemberAction(botAI, "flash of light", 15.0f, HealingManaEfficiency::HIGH) {}
};
class CastLayOnHandsAction : public CastHealingSpellAction
@ -357,9 +348,7 @@ class CastHammerOfJusticeOnEnemyHealerAction : public CastSpellOnEnemyHealerActi
{
public:
CastHammerOfJusticeOnEnemyHealerAction(PlayerbotAI* botAI)
: CastSpellOnEnemyHealerAction(botAI, "hammer of justice")
{
}
: CastSpellOnEnemyHealerAction(botAI, "hammer of justice") {}
};
class CastHammerOfJusticeSnareAction : public CastSnareSpellAction
@ -368,6 +357,12 @@ public:
CastHammerOfJusticeSnareAction(PlayerbotAI* botAI) : CastSnareSpellAction(botAI, "hammer of justice") {}
};
class CastSenseUndeadAction : public CastBuffSpellAction
{
public:
CastSenseUndeadAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "sense undead") {}
};
class CastTurnUndeadAction : public CastBuffSpellAction
{
public:
@ -381,25 +376,25 @@ PROTECT_ACTION(CastBlessingOfProtectionProtectAction, "blessing of protection");
class CastDivinePleaAction : public CastBuffSpellAction
{
public:
CastDivinePleaAction(PlayerbotAI* ai) : CastBuffSpellAction(ai, "divine plea") {}
CastDivinePleaAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "divine plea") {}
};
class ShieldOfRighteousnessAction : public CastMeleeSpellAction
{
public:
ShieldOfRighteousnessAction(PlayerbotAI* ai) : CastMeleeSpellAction(ai, "shield of righteousness") {}
ShieldOfRighteousnessAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "shield of righteousness") {}
};
class CastBeaconOfLightOnMainTankAction : public BuffOnMainTankAction
{
public:
CastBeaconOfLightOnMainTankAction(PlayerbotAI* ai) : BuffOnMainTankAction(ai, "beacon of light", true) {}
CastBeaconOfLightOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "beacon of light", true) {}
};
class CastSacredShieldOnMainTankAction : public BuffOnMainTankAction
{
public:
CastSacredShieldOnMainTankAction(PlayerbotAI* ai) : BuffOnMainTankAction(ai, "sacred shield", false) {}
CastSacredShieldOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "sacred shield", false) {}
};
class CastAvengingWrathAction : public CastBuffSpellAction
@ -428,4 +423,5 @@ public:
bool Execute(Event event) override;
bool isUseful() override;
};
#endif

View File

@ -132,6 +132,7 @@ public:
&PaladinTriggerFactoryInternal::hammer_of_justice_on_enemy_target;
creators["hammer of justice on snare target"] =
&PaladinTriggerFactoryInternal::hammer_of_justice_on_snare_target;
creators["not sensing undead"] = &PaladinTriggerFactoryInternal::not_sensing_undead;
creators["divine favor"] = &PaladinTriggerFactoryInternal::divine_favor;
creators["turn undead"] = &PaladinTriggerFactoryInternal::turn_undead;
creators["avenger's shield"] = &PaladinTriggerFactoryInternal::avenger_shield;
@ -151,6 +152,7 @@ public:
}
private:
static Trigger* not_sensing_undead(PlayerbotAI* botAI) { return new NotSensingUndeadTrigger(botAI); }
static Trigger* turn_undead(PlayerbotAI* botAI) { return new TurnUndeadTrigger(botAI); }
static Trigger* divine_favor(PlayerbotAI* botAI) { return new DivineFavorTrigger(botAI); }
static Trigger* holy_shield(PlayerbotAI* botAI) { return new HolyShieldTrigger(botAI); }
@ -288,6 +290,7 @@ public:
creators["hammer of justice on snare target"] =
&PaladinAiObjectContextInternal::hammer_of_justice_on_snare_target;
creators["divine favor"] = &PaladinAiObjectContextInternal::divine_favor;
creators["sense undead"] = &PaladinAiObjectContextInternal::sense_undead;
creators["turn undead"] = &PaladinAiObjectContextInternal::turn_undead;
creators["blessing of protection on party"] = &PaladinAiObjectContextInternal::blessing_of_protection_on_party;
creators["righteous defense"] = &PaladinAiObjectContextInternal::righteous_defense;
@ -312,6 +315,7 @@ private:
{
return new CastBlessingOfProtectionProtectAction(botAI);
}
static Action* sense_undead(PlayerbotAI* botAI) { return new CastSenseUndeadAction(botAI); }
static Action* turn_undead(PlayerbotAI* botAI) { return new CastTurnUndeadAction(botAI); }
static Action* divine_favor(PlayerbotAI* botAI) { return new CastDivineFavorAction(botAI); }
static Action* righteous_fury(PlayerbotAI* botAI) { return new CastRighteousFuryAction(botAI); }

View File

@ -15,9 +15,6 @@ public:
{
creators["sanctity aura"] = &sanctity_aura;
creators["retribution aura"] = &retribution_aura;
creators["seal of corruption"] = &seal_of_corruption;
creators["seal of vengeance"] = &seal_of_vengeance;
creators["seal of command"] = &seal_of_command;
creators["blessing of might"] = &blessing_of_might;
creators["crusader strike"] = &crusader_strike;
creators["repentance"] = &repentance;
@ -27,36 +24,6 @@ public:
}
private:
static ActionNode* seal_of_corruption([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"seal of corruption",
/*P*/ {},
/*A*/ { NextAction("seal of vengeance") },
/*C*/ {}
);
}
static ActionNode* seal_of_vengeance([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"seal of vengeance",
/*P*/ {},
/*A*/ { NextAction("seal of command") },
/*C*/ {}
);
}
static ActionNode* seal_of_command([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"seal of command",
/*P*/ {},
/*A*/ { NextAction("seal of righteousness") },
/*C*/ {}
);
}
static ActionNode* blessing_of_might([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(

View File

@ -19,14 +19,15 @@ void GenericPaladinNonCombatStrategy::InitTriggers(std::vector<TriggerNode*>& tr
NonCombatStrategy::InitTriggers(triggers);
triggers.push_back(new TriggerNode("party member dead", { NextAction("redemption", ACTION_CRITICAL_HEAL + 10) }));
triggers.push_back(new TriggerNode("party member almost full health", { NextAction("flash of light on party", 25.0f) }));
triggers.push_back(new TriggerNode("party member medium health", { NextAction("flash of light on party", 26.0f) }));
triggers.push_back(new TriggerNode("party member low health", { NextAction("holy light on party", 27.0f) }));
triggers.push_back(new TriggerNode("party member critical health", { NextAction("holy light on party", 28.0f) }));
triggers.push_back(new TriggerNode("party member almost full health", { NextAction("flash of light on party", ACTION_MEDIUM_HEAL + 5.0f) }));
triggers.push_back(new TriggerNode("party member medium health", { NextAction("flash of light on party", ACTION_MEDIUM_HEAL + 6.0f) }));
triggers.push_back(new TriggerNode("party member low health", { NextAction("holy light on party", ACTION_MEDIUM_HEAL + 7.0f) }));
triggers.push_back(new TriggerNode("party member critical health", { NextAction("holy light on party", ACTION_MEDIUM_HEAL + 8.0f) }));
triggers.push_back(new TriggerNode("not sensing undead", { NextAction("sense undead", ACTION_IDLE + 1.0f) }));
int specTab = AiFactory::GetPlayerSpecTab(botAI->GetBot());
if (specTab == 0 || specTab == 1) // Holy or Protection
triggers.push_back(new TriggerNode("often", { NextAction("apply oil", 1.0f) }));
if (specTab == 2) // Retribution
triggers.push_back(new TriggerNode("often", { NextAction("apply stone", 1.0f) }));
if (specTab == PALADIN_TAB_HOLY || specTab == PALADIN_TAB_PROTECTION)
triggers.push_back(new TriggerNode("often", { NextAction("apply oil", ACTION_IDLE + 1.0f) }));
if (specTab == PALADIN_TAB_RETRIBUTION)
triggers.push_back(new TriggerNode("often", { NextAction("apply stone", ACTION_IDLE + 1.0f) }));
}

View File

@ -22,6 +22,9 @@ public:
creators["cleanse magic"] = &cleanse_magic;
creators["cleanse poison on party"] = &cleanse_poison_on_party;
creators["cleanse disease on party"] = &cleanse_disease_on_party;
creators["seal of corruption"] = &seal_of_corruption;
creators["seal of vengeance"] = &seal_of_vengeance;
creators["seal of command"] = &seal_of_command;
creators["seal of wisdom"] = &seal_of_wisdom;
creators["seal of justice"] = &seal_of_justice;
creators["hand of reckoning"] = &hand_of_reckoning;
@ -41,7 +44,6 @@ public:
creators["blessing of wisdom on party"] = &blessing_of_wisdom_on_party;
creators["blessing of sanctuary on party"] = &blessing_of_sanctuary_on_party;
creators["blessing of sanctuary"] = &blessing_of_sanctuary;
creators["seal of command"] = &seal_of_command;
creators["taunt spell"] = &hand_of_reckoning;
creators["righteous defense"] = &righteous_defense;
creators["avenger's shield"] = &avengers_shield;
@ -155,18 +157,39 @@ private:
/*A*/ { NextAction("purify disease on party") },
/*C*/ {});
}
static ActionNode* seal_of_corruption(PlayerbotAI* /* ai */)
{
return new ActionNode("seal of corruption",
/*P*/ {},
/*A*/ { NextAction("seal of vengeance") },
/*C*/ {});
}
static ActionNode* seal_of_vengeance(PlayerbotAI* /* ai */)
{
return new ActionNode("seal of vengeance",
/*P*/ {},
/*A*/ { NextAction("seal of command") },
/*C*/ {});
}
static ActionNode* seal_of_command(PlayerbotAI* /* ai */)
{
return new ActionNode("seal of command",
/*P*/ {},
/*A*/ { NextAction("seal of righteousness") },
/*C*/ {});
}
static ActionNode* seal_of_wisdom(PlayerbotAI* /* ai */)
{
return new ActionNode ("seal of wisdom",
/*P*/ {},
/*A*/ { NextAction("seal of righteousness") },
/*A*/ { NextAction("seal of corruption") },
/*C*/ {});
}
static ActionNode* seal_of_justice(PlayerbotAI* /* ai */)
{
return new ActionNode("seal of justice",
/*P*/ {},
/*A*/ { NextAction("seal of righteousness") },
/*A*/ { NextAction("seal of corruption") },
/*C*/ {});
}
static ActionNode* hand_of_reckoning(PlayerbotAI* /* ai */)
@ -246,13 +269,6 @@ private:
/*A*/ {},
/*C*/ {});
}
static ActionNode* seal_of_command(PlayerbotAI* /* ai */)
{
return new ActionNode("seal of command",
/*P*/ {},
/*A*/ { NextAction("seal of righteousness") },
/*C*/ {});
}
};
#endif

View File

@ -30,7 +30,7 @@ void HealPaladinStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
new TriggerNode(
"seal",
{
NextAction("seal of wisdom", ACTION_HIGH)
NextAction("seal of wisdom", ACTION_HIGH),
}
)
);

View File

@ -30,3 +30,8 @@ bool BlessingTrigger::IsActive()
return SpellTrigger::IsActive() && !botAI->HasAnyAuraOf(target, "blessing of might", "blessing of wisdom",
"blessing of kings", "blessing of sanctuary", nullptr);
}
bool NotSensingUndeadTrigger::IsActive()
{
return !botAI->HasAura("sense undead", bot);
}

View File

@ -77,9 +77,7 @@ class BlessingOnPartyTrigger : public BuffOnPartyTrigger
{
public:
BlessingOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "blessing of kings,blessing of might,blessing of wisdom", 2 * 2000)
{
}
: BuffOnPartyTrigger(botAI, "blessing of kings,blessing of might,blessing of wisdom", 2 * 2000) {}
};
class BlessingTrigger : public BuffTrigger
@ -93,7 +91,8 @@ public:
class HammerOfJusticeInterruptSpellTrigger : public InterruptSpellTrigger
{
public:
HammerOfJusticeInterruptSpellTrigger(PlayerbotAI* botAI) : InterruptSpellTrigger(botAI, "hammer of justice") {}
HammerOfJusticeInterruptSpellTrigger(PlayerbotAI* botAI)
: InterruptSpellTrigger(botAI, "hammer of justice") {}
};
class HammerOfJusticeSnareTrigger : public SnareTargetTrigger
@ -144,9 +143,7 @@ class CleanseCurePartyMemberDiseaseTrigger : public PartyMemberNeedCureTrigger
{
public:
CleanseCurePartyMemberDiseaseTrigger(PlayerbotAI* botAI)
: PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_DISEASE)
{
}
: PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_DISEASE) {}
};
class CleanseCurePoisonTrigger : public NeedCureTrigger
@ -159,9 +156,7 @@ class CleanseCurePartyMemberPoisonTrigger : public PartyMemberNeedCureTrigger
{
public:
CleanseCurePartyMemberPoisonTrigger(PlayerbotAI* botAI)
: PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_POISON)
{
}
: PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_POISON) {}
};
class CleanseCureMagicTrigger : public NeedCureTrigger
@ -173,15 +168,15 @@ public:
class CleanseCurePartyMemberMagicTrigger : public PartyMemberNeedCureTrigger
{
public:
CleanseCurePartyMemberMagicTrigger(PlayerbotAI* botAI) : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_MAGIC)
{
}
CleanseCurePartyMemberMagicTrigger(PlayerbotAI* botAI)
: PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_MAGIC) {}
};
class HammerOfJusticeEnemyHealerTrigger : public InterruptEnemyHealerTrigger
{
public:
HammerOfJusticeEnemyHealerTrigger(PlayerbotAI* botAI) : InterruptEnemyHealerTrigger(botAI, "hammer of justice") {}
HammerOfJusticeEnemyHealerTrigger(PlayerbotAI* botAI)
: InterruptEnemyHealerTrigger(botAI, "hammer of justice") {}
};
class DivineFavorTrigger : public BuffTrigger
@ -190,6 +185,14 @@ public:
DivineFavorTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "divine favor") {}
};
class NotSensingUndeadTrigger : public BuffTrigger
{
public:
NotSensingUndeadTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "not sensing undead") {}
bool IsActive() override;
};
class TurnUndeadTrigger : public HasCcTargetTrigger
{
public:
@ -201,7 +204,8 @@ DEBUFF_TRIGGER(AvengerShieldTrigger, "avenger's shield");
class BeaconOfLightOnMainTankTrigger : public BuffOnMainTankTrigger
{
public:
BeaconOfLightOnMainTankTrigger(PlayerbotAI* ai) : BuffOnMainTankTrigger(ai, "beacon of light", true) {}
BeaconOfLightOnMainTankTrigger(PlayerbotAI* ai)
: BuffOnMainTankTrigger(ai, "beacon of light", true) {}
};
class SacredShieldOnMainTankTrigger : public BuffOnMainTankTrigger
@ -213,34 +217,29 @@ public:
class BlessingOfKingsOnPartyTrigger : public BuffOnPartyTrigger
{
public:
BlessingOfKingsOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "blessing of kings", 2 * 2000) {}
BlessingOfKingsOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "blessing of kings", 2 * 2000) {}
};
class BlessingOfWisdomOnPartyTrigger : public BuffOnPartyTrigger
{
public:
BlessingOfWisdomOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000)
{
}
: BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) {}
};
class BlessingOfMightOnPartyTrigger : public BuffOnPartyTrigger
{
public:
BlessingOfMightOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000)
{
}
: BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) {}
};
class BlessingOfSanctuaryOnPartyTrigger : public BuffOnPartyTrigger
{
public:
BlessingOfSanctuaryOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000)
{
}
: BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000) {}
};
class AvengingWrathTrigger : public BoostTrigger
@ -248,4 +247,5 @@ class AvengingWrathTrigger : public BoostTrigger
public:
AvengingWrathTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "avenging wrath") {}
};
#endif

View File

@ -61,7 +61,7 @@ bool CastEnvenomAction::isUseful()
bool CastEnvenomAction::isPossible()
{
// alternate to eviscerate if talents unlearned
return botAI->HasAura(58410, bot) /* Master Poisoner */;
return botAI->HasAura(58410, bot) /* Master Poisoner Rank 3 */;
}
bool CastTricksOfTheTradeOnMainTankAction::isUseful()

View File

@ -78,6 +78,12 @@ public:
CastFeintAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "feint") {}
};
class CastColdBloodAction : public CastBuffSpellAction
{
public:
CastColdBloodAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "cold blood") {}
};
class CastDismantleAction : public CastSpellAction
{
public:

View File

@ -143,6 +143,7 @@ public:
creators["use instant poison on off hand"] = &RogueAiObjectContextInternal::use_instant_poison_off_hand;
creators["fan of knives"] = &RogueAiObjectContextInternal::fan_of_knives;
creators["killing spree"] = &RogueAiObjectContextInternal::killing_spree;
creators["cold blood"] = &RogueAiObjectContextInternal::cold_blood;
}
private:
@ -184,6 +185,7 @@ private:
static Action* use_instant_poison_off_hand(PlayerbotAI* ai) { return new UseInstantPoisonOffHandAction(ai); }
static Action* fan_of_knives(PlayerbotAI* ai) { return new FanOfKnivesAction(ai); }
static Action* killing_spree(PlayerbotAI* ai) { return new CastKillingSpreeAction(ai); }
static Action* cold_blood(PlayerbotAI* ai) { return new CastColdBloodAction(ai); }
};
SharedNamedObjectContextList<Strategy> RogueAiObjectContext::sharedStrategyContexts;

View File

@ -29,7 +29,7 @@ private:
return new ActionNode(
"envenom",
/*P*/ {},
/*A*/ { NextAction("rupture") },
/*A*/ { NextAction("eviscerate") },
/*C*/ {}
);
}
@ -108,10 +108,10 @@ void AssassinationRogueStrategy::InitTriggers(std::vector<TriggerNode*>& trigger
triggers.push_back(
new TriggerNode(
"combo points 3 available",
"combo points 4 available",
{
NextAction("envenom", ACTION_HIGH + 5),
NextAction("eviscerate", ACTION_HIGH + 3)
NextAction("cold blood", ACTION_HIGH + 6),
NextAction("envenom", ACTION_HIGH + 5)
}
)
);
@ -120,8 +120,7 @@ void AssassinationRogueStrategy::InitTriggers(std::vector<TriggerNode*>& trigger
new TriggerNode(
"target with combo points almost dead",
{
NextAction("envenom", ACTION_HIGH + 4),
NextAction("eviscerate", ACTION_HIGH + 2)
NextAction("envenom", ACTION_HIGH + 4)
}
)
);

View File

@ -12,36 +12,14 @@ class DpsRogueStrategyActionNodeFactory : public NamedObjectFactory<ActionNode>
public:
DpsRogueStrategyActionNodeFactory()
{
creators["mutilate"] = &mutilate;
creators["sinister strike"] = &sinister_strike;
creators["kick"] = &kick;
creators["kidney shot"] = &kidney_shot;
creators["backstab"] = &backstab;
creators["melee"] = &melee;
creators["rupture"] = &rupture;
}
private:
static ActionNode* melee([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"melee",
/*P*/ {},
/*A*/ {
NextAction("mutilate") },
/*C*/ {}
);
}
static ActionNode* mutilate([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"mutilate",
/*P*/ {},
/*A*/ {
NextAction("sinister strike") },
/*C*/ {}
);
}
static ActionNode* sinister_strike([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
@ -77,7 +55,7 @@ private:
"backstab",
/*P*/ {},
/*A*/ {
NextAction("mutilate") },
NextAction("sinister strike") },
/*C*/ {}
);
}
@ -140,7 +118,7 @@ void DpsRogueStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
triggers.push_back(
new TriggerNode(
"combo points available",
"combo points 5 available",
{
NextAction("rupture", ACTION_HIGH + 1),
NextAction("eviscerate", ACTION_HIGH)
@ -335,7 +313,7 @@ void StealthedRogueStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
triggers.push_back(
new TriggerNode(
"combo points available",
"combo points 5 available",
{
NextAction("eviscerate", ACTION_HIGH)
}

View File

@ -231,7 +231,6 @@ bool NewRpgDoQuestAction::Execute(Event /*event*/)
return false;
auto& data = *dataPtr;
uint32 questId = data.questId;
const Quest* quest = data.quest;
uint8 questStatus = bot->GetQuestStatus(questId);
switch (questStatus)
{
@ -438,7 +437,7 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE)
return MoveFarTo(flightMaster);
std::vector<uint32> nodes = {data.fromNode, data.toNode};
std::vector<uint32> nodes = data.path;
botAI->RemoveShapeshift();
if (bot->IsMounted())
@ -447,7 +446,7 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
if (!bot->ActivateTaxiPathTo(nodes, flightMaster, 0))
{
LOG_DEBUG("playerbots", "[New RPG] {} active taxi path {} (from {} to {}) failed", bot->GetName(),
flightMaster->GetEntry(), nodes[0], nodes[1]);
flightMaster->GetEntry(), nodes[0], nodes[nodes.size() - 1]);
botAI->rpgInfo.ChangeToIdle();
}
return true;

View File

@ -3,7 +3,6 @@
#include "BroadcastHelper.h"
#include "ChatHelper.h"
#include "Creature.h"
#include "FlightMasterCache.h"
#include "G3D/Vector2.h"
#include "GameObject.h"
#include "GossipDef.h"
@ -856,7 +855,7 @@ bool NewRpgBaseAction::GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector
WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot)
{
const std::vector<WorldLocation>& locs = sRandomPlayerbotMgr.locsPerLevelCache[bot->GetLevel()];
const std::vector<WorldLocation>& locs = sTravelMgr.GetLocsPerLevelCache(bot->GetLevel());
float hiRange = 500.0f;
float loRange = 2500.0f;
if (bot->GetLevel() < 5)
@ -914,9 +913,7 @@ WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot)
WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot)
{
const std::vector<WorldLocation>& locs = IsAlliance(bot->getRace())
? sRandomPlayerbotMgr.allianceStarterPerLevelCache[bot->GetLevel()]
: sRandomPlayerbotMgr.hordeStarterPerLevelCache[bot->GetLevel()];
const std::vector<WorldLocation> locs = sTravelMgr.GetTravelHubs(bot);
bool inCity = false;
@ -957,70 +954,19 @@ WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot)
return dest;
}
bool NewRpgBaseAction::SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, uint32& fromNode, uint32& toNode)
bool NewRpgBaseAction::SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector<uint32>& path)
{
Creature* nearestFlightMaster = FlightMasterCache::Instance().GetNearestFlightMaster(bot);
if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster) > 500.0f)
flightMaster = sTravelMgr.GetNearestFlightMasterGuid(bot);
if (!flightMaster)
return false;
fromNode = sObjectMgr->GetNearestTaxiNode(nearestFlightMaster->GetPositionX(), nearestFlightMaster->GetPositionY(),
nearestFlightMaster->GetPositionZ(), nearestFlightMaster->GetMapId(),
bot->GetTeamId());
if (!fromNode)
std::vector<std::vector<uint32>> availablePaths = sTravelMgr.GetOptimalFlightDestinations(bot);
if (availablePaths.empty())
return false;
std::vector<uint32> availableToNodes;
for (uint32 i = 1; i < sTaxiNodesStore.GetNumRows(); ++i)
{
if (fromNode == i)
continue;
TaxiNodesEntry const* node = sTaxiNodesStore.LookupEntry(i);
// check map
if (!node || node->map_id != bot->GetMapId() ||
(!node->MountCreatureID[bot->GetTeamId() == TEAM_ALLIANCE ? 1 : 0])) // dk flight
continue;
// check taxi node known
if (!bot->isTaxiCheater() && !bot->m_taxi.IsTaximaskNodeKnown(i))
continue;
// check distance by level
if (!botAI->CheckLocationDistanceByLevel(bot, WorldLocation(node->map_id, node->x, node->y, node->z), false))
continue;
// check path
uint32 path, cost;
sObjectMgr->GetTaxiPath(fromNode, i, path, cost);
if (!path)
continue;
// check area level
uint32 nodeZoneId = bot->GetMap()->GetZoneId(bot->GetPhaseMask(), node->x, node->y, node->z);
bool capital = false;
if (AreaTableEntry const* zone = sAreaTableStore.LookupEntry(nodeZoneId))
{
capital = zone->flags & AREA_FLAG_CAPITAL;
}
auto itr = sRandomPlayerbotMgr.zone2LevelBracket.find(nodeZoneId);
if (!capital && itr == sRandomPlayerbotMgr.zone2LevelBracket.end())
continue;
if (!capital && (bot->GetLevel() < itr->second.low || bot->GetLevel() > itr->second.high))
continue;
availableToNodes.push_back(i);
}
if (availableToNodes.empty())
return false;
flightMaster = nearestFlightMaster->GetGUID();
toNode = availableToNodes[urand(0, availableToNodes.size() - 1)];
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(), fromNode, toNode, availableToNodes.size());
bot->GetName(), flightMaster.GetEntry(), path[0], path[path.size() - 1], availablePaths.size());
return true;
}
@ -1121,10 +1067,10 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector<NewRpgStatus> candidateSta
case RPG_TRAVEL_FLIGHT:
{
ObjectGuid flightMaster;
uint32 fromNode, toNode;
if (SelectRandomFlightTaxiNode(flightMaster, fromNode, toNode))
std::vector<uint32> path;
if (SelectRandomFlightTaxiNode(flightMaster, path))
{
botAI->rpgInfo.ChangeToTravelFlight(flightMaster, fromNode, toNode);
botAI->rpgInfo.ChangeToTravelFlight(flightMaster, path);
return true;
}
return false;
@ -1197,8 +1143,8 @@ bool NewRpgBaseAction::CheckRpgStatusAvailable(NewRpgStatus status)
case RPG_TRAVEL_FLIGHT:
{
ObjectGuid flightMaster;
uint32 fromNode, toNode;
return SelectRandomFlightTaxiNode(flightMaster, fromNode, toNode);
std::vector<uint32> path;
return SelectRandomFlightTaxiNode(flightMaster, path);
}
default:
return false;

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, uint32& fromNode, uint32& toNode);
bool SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector<uint32>& path);
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
bool CheckRpgStatusAvailable(NewRpgStatus status);

View File

@ -37,13 +37,12 @@ void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest)
data = do_quest;
}
void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, uint32 fromNode, uint32 toNode)
void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector<uint32> path)
{
startT = getMSTime();
TravelFlight flight;
flight.fromFlightMaster = fromFlightMaster;
flight.fromNode = fromNode;
flight.toNode = toNode;
flight.path = std::move(path);
flight.inFlight = false;
data = flight;
}
@ -150,8 +149,8 @@ std::string NewRpgInfo::ToString()
{
out << "TRAVEL_FLIGHT";
out << "\nfromFlightMaster: " << arg.fromFlightMaster.GetEntry();
out << "\nfromNode: " << arg.fromNode;
out << "\ntoNode: " << arg.toNode;
out << "\nfromNode: " << arg.path[0];
out << "\ntoNode: " << arg.path[arg.path.size() - 1];
out << "\ninFlight: " << arg.inFlight;
}
else

View File

@ -50,8 +50,7 @@ struct NewRpgInfo
struct TravelFlight
{
ObjectGuid fromFlightMaster{};
uint32 fromNode{0};
uint32 toNode{0};
std::vector<uint32> path;
bool inFlight{false};
};
// RPG_REST
@ -91,7 +90,7 @@ struct NewRpgInfo
void ChangeToWanderNpc();
void ChangeToWanderRandom();
void ChangeToDoQuest(uint32 questId, const Quest* quest);
void ChangeToTravelFlight(ObjectGuid fromFlightMaster, uint32 fromNode, uint32 toNode);
void ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector<uint32> path);
void ChangeToRest();
void ChangeToIdle();
bool CanChangeTo(NewRpgStatus status);

View File

@ -762,7 +762,7 @@ void PlayerbotFactory::InitPetTalents()
// pet_family->petTalentType);
return;
}
std::unordered_map<uint32, std::vector<TalentEntry const*>> spells;
std::map<uint32, std::vector<TalentEntry const*>> spells;
bool diveTypePet = (1LL << ci->family) & diveMask;
for (uint32 i = 0; i < sTalentStore.GetNumRows(); ++i)
@ -2553,17 +2553,15 @@ void PlayerbotFactory::InitClassSpells()
bot->learnSpell(7386, false); // Sunder Armor
}
if (level >= 30)
{
bot->learnSpell(2458, false); // Berserker Stance
}
break;
case CLASS_PALADIN:
bot->learnSpell(21084, true);
bot->learnSpell(635, true);
if (level >= 12)
{
bot->learnSpell(7328, false); // Redemption
}
if (level >= 20)
bot->learnSpell(5502, false); // Sense Undead
break;
case CLASS_ROGUE:
bot->learnSpell(1752, true);
@ -2605,17 +2603,11 @@ void PlayerbotFactory::InitClassSpells()
bot->learnSpell(686, true);
bot->learnSpell(688, false); // summon imp
if (level >= 10)
{
bot->learnSpell(697, false); // summon voidwalker
}
if (level >= 20)
{
bot->learnSpell(712, false); // summon succubus
}
if (level >= 30)
{
bot->learnSpell(691, false); // summon felhunter
}
break;
case CLASS_DRUID:
bot->learnSpell(5176, true);
@ -2632,17 +2624,11 @@ void PlayerbotFactory::InitClassSpells()
bot->learnSpell(331, true);
// bot->learnSpell(66747, true); // Totem of the Earthen Ring
if (level >= 4)
{
bot->learnSpell(8071, false); // stoneskin totem
}
if (level >= 10)
{
bot->learnSpell(3599, false); // searing totem
}
if (level >= 20)
{
bot->learnSpell(5394, false); // healing stream totem
}
break;
default:
break;
@ -2667,7 +2653,7 @@ void PlayerbotFactory::InitSpecialSpells()
void PlayerbotFactory::InitTalents(uint32 specNo)
{
uint32 classMask = bot->getClassMask();
std::unordered_map<uint32, std::vector<TalentEntry const*>> spells;
std::map<uint32, std::vector<TalentEntry const*>> spells;
for (uint32 i = 0; i < sTalentStore.GetNumRows(); ++i)
{
TalentEntry const* talentInfo = sTalentStore.LookupEntry(i);
@ -3342,18 +3328,36 @@ void PlayerbotFactory::InitReagents()
items.push_back({44615, 40}); // Devout Candle
break;
case CLASS_SHAMAN:
{
HasRelicBySubclassVisitor relicVisitor(ITEM_SUBCLASS_ARMOR_TOTEM);
IterateItems(&relicVisitor, (IterateItemsMask)(ITERATE_ITEMS_IN_BAGS | ITERATE_ITEMS_IN_EQUIP));
bool hasRelic = relicVisitor.found;
if (!hasRelic)
{
if (level >= 4)
items.push_back({5175, 1}); // Earth Totem
if (level >= 10)
items.push_back({5176, 1}); // Flame Totem
if (level >= 20)
items.push_back({5177, 1}); // Water Totem
}
else
{
ItemIds totemIds = {5175, 5176, 5177, 5178};
FindItemByIdsVisitor totemVisitor(totemIds);
IterateItems(&totemVisitor, (IterateItemsMask)(ITERATE_ITEMS_IN_BAGS | ITERATE_ITEMS_IN_EQUIP | ITERATE_ITEMS_IN_BANK));
for (Item* item : totemVisitor.GetResult())
bot->DestroyItem(item->GetBagSlot(), item->GetSlot(), true);
}
if (level >= 30)
{
if (!hasRelic)
items.push_back({5178, 1}); // Air Totem
items.push_back({17030, 20}); // Ankh
}
break;
}
case CLASS_WARLOCK:
items.push_back({6265, 5}); // Soul Shard
break;

View File

@ -1119,6 +1119,9 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet)
if (guid1.IsEmpty() || p.size() > p.DEFAULT_SIZE)
return;
if (lang == LANG_ADDON)
return;
if (p.GetOpcode() == SMSG_GM_MESSAGECHAT)
{
p >> textLen;
@ -1168,8 +1171,6 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet)
if (HasRealPlayerMaster() && guid1 != GetMaster()->GetGUID())
return;
if (lang == LANG_ADDON)
return;
if (message.starts_with(sPlayerbotAIConfig.toxicLinksPrefix) &&
(GetChatHelper()->ExtractAllItemIds(message).size() > 0 ||
@ -1249,17 +1250,10 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet)
p >> guid.ReadAsPacked() >> counter >> vcos >> vsin >> horizontalSpeed >> verticalSpeed;
if (horizontalSpeed <= 0.1f)
{
horizontalSpeed = 0.11f;
}
verticalSpeed = -verticalSpeed;
// high vertical may result in stuck as bot can not handle gravity
if (verticalSpeed > 35.0f)
break;
// stop casting
InterruptSpell();
// stop movement
InterruptSpell();
bot->StopMoving();
bot->GetMotionMaster()->Clear();
@ -6486,7 +6480,7 @@ ChatChannelSource PlayerbotAI::GetChatChannelSource(Player* bot, uint32 type, st
return ChatChannelSource::SRC_UNDEFINED;
}
bool PlayerbotAI::CheckLocationDistanceByLevel(Player* player, const WorldLocation& loc, bool fromStartUp)
bool PlayerbotAI::StarterLevelDistanceCheck(Player* player, const WorldLocation& loc, bool fromStartUp)
{
if (player->GetLevel() > 16)
return true;

View File

@ -556,7 +556,7 @@ public:
bool IsSafe(WorldObject* obj);
ChatChannelSource GetChatChannelSource(Player* bot, uint32 type, std::string channelName);
bool CheckLocationDistanceByLevel(Player* player, const WorldLocation &loc, bool fromStartUp = false);
bool StarterLevelDistanceCheck(Player* player, const WorldLocation &loc, bool fromStartUp = false);
bool HasCheat(BotCheatMask mask)
{

View File

@ -23,7 +23,6 @@
#include "DatabaseEnv.h"
#include "Define.h"
#include "FleeManager.h"
#include "FlightMasterCache.h"
#include "GridNotifiers.h"
#include "LFGMgr.h"
#include "MapMgr.h"
@ -47,9 +46,7 @@
#include "World.h"
#include "Cell.h"
#include "GridNotifiers.h"
// Required for Cell because of poor AC implementation
#include "CellImpl.h"
// Required for GridNotifiers because of poor AC implementation
#include "GridNotifiersImpl.h"
struct GuidClassRaceInfo
@ -59,48 +56,6 @@ struct GuidClassRaceInfo
uint32 rRace;
};
enum class CityId : uint8 {
STORMWIND, IRONFORGE, DARNASSUS, EXODAR,
ORGRIMMAR, UNDERCITY, THUNDER_BLUFF, SILVERMOON_CITY,
SHATTRATH_CITY, DALARAN
};
enum class FactionId : uint8 { ALLIANCE, HORDE, NEUTRAL };
// Map of banker entry → city + faction
static const std::unordered_map<uint16, std::pair<CityId, FactionId>> bankerToCity = {
{2455, {CityId::STORMWIND, FactionId::ALLIANCE}}, {2456, {CityId::STORMWIND, FactionId::ALLIANCE}}, {2457, {CityId::STORMWIND, FactionId::ALLIANCE}},
{2460, {CityId::IRONFORGE, FactionId::ALLIANCE}}, {2461, {CityId::IRONFORGE, FactionId::ALLIANCE}}, {5099, {CityId::IRONFORGE, FactionId::ALLIANCE}},
{4155, {CityId::DARNASSUS, FactionId::ALLIANCE}}, {4208, {CityId::DARNASSUS, FactionId::ALLIANCE}}, {4209, {CityId::DARNASSUS, FactionId::ALLIANCE}},
{17773, {CityId::EXODAR, FactionId::ALLIANCE}}, {18350, {CityId::EXODAR, FactionId::ALLIANCE}}, {16710, {CityId::EXODAR, FactionId::ALLIANCE}},
{3320, {CityId::ORGRIMMAR, FactionId::HORDE}}, {3309, {CityId::ORGRIMMAR, FactionId::HORDE}}, {3318, {CityId::ORGRIMMAR, FactionId::HORDE}},
{4549, {CityId::UNDERCITY, FactionId::HORDE}}, {2459, {CityId::UNDERCITY, FactionId::HORDE}}, {2458, {CityId::UNDERCITY, FactionId::HORDE}}, {4550, {CityId::UNDERCITY, FactionId::HORDE}},
{2996, {CityId::THUNDER_BLUFF, FactionId::HORDE}}, {8356, {CityId::THUNDER_BLUFF, FactionId::HORDE}}, {8357, {CityId::THUNDER_BLUFF, FactionId::HORDE}},
{17631, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {17632, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {17633, {CityId::SILVERMOON_CITY, FactionId::HORDE}},
{16615, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {16616, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {16617, {CityId::SILVERMOON_CITY, FactionId::HORDE}},
{19246, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, {19338, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}},
{19034, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, {19318, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}},
{30604, {CityId::DALARAN, FactionId::NEUTRAL}}, {30605, {CityId::DALARAN, FactionId::NEUTRAL}}, {30607, {CityId::DALARAN, FactionId::NEUTRAL}},
{28675, {CityId::DALARAN, FactionId::NEUTRAL}}, {28676, {CityId::DALARAN, FactionId::NEUTRAL}}, {28677, {CityId::DALARAN, FactionId::NEUTRAL}}
};
// Map of city → available banker entries
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}}
};
// Quick lookup map: banker entry → location
static std::unordered_map<uint32, WorldLocation> bankerEntryToLocation;
void PrintStatsThread() { sRandomPlayerbotMgr.PrintStats(); }
void activatePrintStatsThread()
@ -1718,7 +1673,7 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector<WorldLocation>&
z = 0.05f + ground;
if (!botAI->CheckLocationDistanceByLevel(bot, loc, true))
if (!botAI->StarterLevelDistanceCheck(bot, loc, true))
continue;
const LocaleConstant& locale = sWorld->GetDefaultDbcLocale();
@ -1762,329 +1717,6 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector<WorldLocation>&
// tlocs.size());
}
void RandomPlayerbotMgr::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 - 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 - High - level zones
zone2LevelBracket[10] = {19, 33}; // Duskwood
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 - 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 - 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
// 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
// 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
// Override with values from config
for (auto const& [zoneId, bracketPair] : sPlayerbotAIConfig.zoneBrackets)
{
zone2LevelBracket[zoneId] = {bracketPair.first, bracketPair.second};
}
}
void RandomPlayerbotMgr::PrepareTeleportCache()
{
uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL);
LOG_INFO("playerbots", "Preparing random teleport caches for {} levels...", maxLevel);
QueryResult results = WorldDatabase.Query(
"SELECT "
"g.map, "
"position_x, "
"position_y, "
"position_z, "
"t.minlevel, "
"t.maxlevel "
"FROM "
"(SELECT "
"map, "
"MIN( c.guid ) guid "
"FROM "
"creature c "
"INNER JOIN creature_template t ON c.id1 = t.entry "
"WHERE "
"t.npcflag = 0 "
"AND t.lootid != 0 "
"AND t.maxlevel - t.minlevel < 3 "
"AND map IN ({}) "
"AND t.entry not in (32820, 24196, 30627, 30617) "
"AND c.spawntimesecs < 1000 "
"AND t.faction not in (11, 71, 79, 85, 188, 1575) "
"AND (t.unit_flags & 256) = 0 "
"AND (t.unit_flags & 4096) = 0 "
"AND t.rank = 0 "
// "AND (t.flags_extra & 32768) = 0 "
"GROUP BY "
"map, "
"ROUND(position_x / 50), "
"ROUND(position_y / 50), "
"ROUND(position_z / 50) "
"HAVING "
"count(*) >= 2) "
"AS g "
"INNER JOIN creature c ON g.guid = c.guid "
"INNER JOIN creature_template t on c.id1 = t.entry "
"ORDER BY "
"t.minlevel;",
sPlayerbotAIConfig.randomBotMapsAsString.c_str());
uint32 collected_locs = 0;
if (results)
{
do
{
Field* fields = results->Fetch();
uint16 mapId = fields[0].Get<uint16>();
float x = fields[1].Get<float>();
float y = fields[2].Get<float>();
float z = fields[3].Get<float>();
uint32 min_level = fields[4].Get<uint32>();
uint32 max_level = fields[5].Get<uint32>();
uint32 level = (min_level + max_level + 1) / 2;
WorldLocation loc(mapId, x, y, z, 0);
collected_locs++;
for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel;
l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++)
{
if (l < 1 || l > maxLevel)
{
continue;
}
locsPerLevelCache[(uint8)l].push_back(loc);
}
} while (results->NextRow());
}
LOG_INFO("playerbots", ">> {} locations for level collected.", collected_locs);
if (sPlayerbotAIConfig.enableNewRpgStrategy)
{
PrepareZone2LevelBracket();
LOG_INFO("playerbots", "Preparing innkeepers / flightmasters locations for level...");
results = WorldDatabase.Query(
"SELECT "
"map, "
"position_x, "
"position_y, "
"position_z, "
"orientation, "
"t.faction, "
"t.entry, "
"t.npcflag, "
"c.guid "
"FROM "
"creature c "
"INNER JOIN creature_template t on c.id1 = t.entry "
"WHERE "
"t.npcflag & 73728 "
"AND map IN ({}) "
"ORDER BY "
"t.minlevel;",
sPlayerbotAIConfig.randomBotMapsAsString.c_str());
collected_locs = 0;
if (results)
{
do
{
Field* fields = results->Fetch();
uint16 mapId = fields[0].Get<uint16>();
float x = fields[1].Get<float>();
float y = fields[2].Get<float>();
float z = fields[3].Get<float>();
float orient = fields[4].Get<float>();
uint32 faction = fields[5].Get<uint32>();
uint32 tEntry = fields[6].Get<uint32>();
uint32 tNpcflag = fields[7].Get<uint32>();
uint32 guid = fields[8].Get<uint32>();
if (tEntry == 3838 || tEntry == 29480)
continue;
const FactionTemplateEntry* entry = sFactionTemplateStore.LookupEntry(faction);
WorldLocation loc(mapId, x + cos(orient) * 5.0f, y + sin(orient) * 5.0f, z + 0.5f, orient + M_PI);
collected_locs++;
Map* map = sMapMgr->FindMap(loc.GetMapId(), 0);
if (!map)
continue;
bool forHorde = !(entry->hostileMask & 4);
bool forAlliance = !(entry->hostileMask & 2);
if (tNpcflag & UNIT_NPC_FLAG_FLIGHTMASTER)
{
WorldPosition pos(mapId, x, y, z, orient);
if (forHorde)
FlightMasterCache::Instance().AddHordeFlightMaster(guid, pos);
if (forAlliance)
FlightMasterCache::Instance().AddAllianceFlightMaster(guid, pos);
}
const AreaTableEntry* area = sAreaTableStore.LookupEntry(map->GetAreaId(PHASEMASK_NORMAL, x, y, z));
uint32 zoneId = area->zone ? area->zone : area->ID;
if (zone2LevelBracket.find(zoneId) == zone2LevelBracket.end())
continue;
LevelBracket bracket = zone2LevelBracket[zoneId];
for (int i = bracket.low; i <= bracket.high; i++)
{
if (forHorde)
hordeStarterPerLevelCache[i].push_back(loc);
if (forAlliance)
allianceStarterPerLevelCache[i].push_back(loc);
}
} while (results->NextRow());
}
// add all initial position
for (uint32 i = 1; i < sRaceMgr->GetMaxRaces(); i++)
{
for (uint32 j = 1; j < MAX_CLASSES; j++)
{
PlayerInfo const* info = sObjectMgr->GetPlayerInfo(i, j);
if (!info)
continue;
WorldPosition pos(info->mapId, info->positionX, info->positionY, info->positionZ, info->orientation);
for (int32 l = 1; l <= 5; l++)
{
if ((1 << (i - 1)) & sRaceMgr->GetAllianceRaceMask())
allianceStarterPerLevelCache[(uint8)l].push_back(pos);
else
hordeStarterPerLevelCache[(uint8)l].push_back(pos);
}
break;
}
}
LOG_INFO("playerbots", ">> {} innkeepers locations for level collected.", collected_locs);
}
results = WorldDatabase.Query(
"SELECT "
"map, "
"position_x, "
"position_y, "
"position_z, "
"orientation, "
"t.minlevel, "
"t.entry "
"FROM "
"creature c "
"INNER JOIN creature_template t on c.id1 = t.entry "
"WHERE "
"t.npcflag & 131072 "
"AND t.npcflag != 135298 "
"AND t.minlevel != 55 "
"AND t.minlevel != 65 "
"AND t.faction not in (35, 474, 69, 57) "
"AND t.entry not in (30606, 30608, 29282) "
"AND map IN ({}) "
"ORDER BY "
"t.minlevel;",
sPlayerbotAIConfig.randomBotMapsAsString.c_str());
collected_locs = 0;
if (results)
{
do
{
Field* fields = results->Fetch();
uint16 mapId = fields[0].Get<uint16>();
float x = fields[1].Get<float>();
float y = fields[2].Get<float>();
float z = fields[3].Get<float>();
float orient = fields[4].Get<float>();
uint32 level = fields[5].Get<uint32>();
uint32 entry = fields[6].Get<uint32>();
BankerLocation bLoc;
bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI);
bLoc.entry = entry;
collected_locs++;
for (int32 l = 1; l <= maxLevel; l++)
{
// Bots 1-60 go to base game bankers (all have minlevel 30 or 45)
if (l <=60 && level > 45)
{
continue;
}
// Bots 61-70 go to Shattrath bankers (all have minlevel 60 or 70)
if ((l >=61 && l <=70) && (level < 60 || level > 70))
{
continue;
}
// Bots 71+ go to Dalaran bankers (all have minlevel 75)
if ((l >=71) && level != 75)
{
continue;
}
bankerLocsPerLevelCache[(uint8)l].push_back(bLoc);
bankerEntryToLocation[bLoc.entry] = bLoc.loc;
}
} while (results->NextRow());
}
LOG_INFO("playerbots", ">> {} banker locations for level collected.", collected_locs);
}
void RandomPlayerbotMgr::PrepareAddclassCache()
{
// Using accounts marked as type 2 (AddClass)
@ -2125,11 +1757,6 @@ void RandomPlayerbotMgr::Init()
if (sPlayerbotAIConfig.addClassCommand)
sRandomPlayerbotMgr.PrepareAddclassCache();
if (sPlayerbotAIConfig.enabled)
{
sRandomPlayerbotMgr.PrepareTeleportCache();
}
if (sPlayerbotAIConfig.randomBotJoinBG)
sRandomPlayerbotMgr.LoadBattleMastersCache();
@ -2141,104 +1768,18 @@ void RandomPlayerbotMgr::RandomTeleportForLevel(Player* bot)
if (bot->InBattleground())
return;
uint32 level = bot->GetLevel();
uint8 race = bot->getRace();
std::vector<WorldLocation>* locs = nullptr;
if (sPlayerbotAIConfig.enableNewRpgStrategy)
locs = IsAlliance(race) ? &allianceStarterPerLevelCache[level] : &hordeStarterPerLevelCache[level];
else
locs = &locsPerLevelCache[level];
if (level >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100)
std::vector<WorldLocation> locs = sTravelMgr.GetCityLocations(bot);
if (!locs.empty())
{
std::vector<WorldLocation> fallbackLocs;
for (auto& bLoc : bankerLocsPerLevelCache[level])
fallbackLocs.push_back(bLoc.loc);
if (!sPlayerbotAIConfig.enableWeightTeleToCityBankers)
{
RandomTeleport(bot, fallbackLocs, true);
RandomTeleport(bot, locs, true);
return;
}
// Collect valid cities based on bot faction.
std::unordered_set<CityId> validBankerCities;
for (auto& loc : bankerLocsPerLevelCache[level])
locs = sTravelMgr.GetTeleportLocations(bot);
if (!locs.empty())
{
auto cityIt = bankerToCity.find(loc.entry);
if (cityIt == bankerToCity.end()) continue;
CityId cityId = cityIt->second.first;
FactionId cityFactionId = cityIt->second.second;
if ((IsAlliance(bot->getRace()) && cityFactionId == FactionId::ALLIANCE) ||
(!IsAlliance(bot->getRace()) && cityFactionId == FactionId::HORDE) ||
(cityFactionId == FactionId::NEUTRAL))
{
validBankerCities.insert(cityId);
}
}
// Fallback if no valid cities
if (validBankerCities.empty())
{
RandomTeleport(bot, fallbackLocs, true);
RandomTeleport(bot, locs, false);
return;
}
// Apply weights to valid cities
std::vector<CityId> weightedCities;
for (CityId city : validBankerCities)
{
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;
}
if (weight <= 0) continue;
for (int i = 0; i < weight; ++i)
{
weightedCities.push_back(city);
}
}
// Fallback if no valid cities
if (weightedCities.empty())
{
RandomTeleport(bot, fallbackLocs, true);
return;
}
// Pick a weighted city randomly, then a random banker in that city
// then teleport to that banker
CityId selectedCity = weightedCities[urand(0, weightedCities.size() - 1)];
auto const& bankers = cityToBankers.at(selectedCity);
uint32 selectedBankerEntry = bankers[urand(0, bankers.size() - 1)];
auto locIt = bankerEntryToLocation.find(selectedBankerEntry);
if (locIt != bankerEntryToLocation.end())
{
std::vector<WorldLocation> teleportTarget = { locIt->second };
RandomTeleport(bot, teleportTarget, true);
return;
}
// Fallback if something went wrong
RandomTeleport(bot, *locs);
}
else
{
RandomTeleport(bot, *locs);
}
}
void RandomPlayerbotMgr::RandomTeleportGrindForLevel(Player* bot)
@ -2246,17 +1787,11 @@ void RandomPlayerbotMgr::RandomTeleportGrindForLevel(Player* bot)
if (bot->InBattleground())
return;
uint32 level = bot->GetLevel();
uint8 race = bot->getRace();
std::vector<WorldLocation>* locs = nullptr;
if (sPlayerbotAIConfig.enableNewRpgStrategy)
locs = IsAlliance(race) ? &allianceStarterPerLevelCache[level] : &hordeStarterPerLevelCache[level];
else
locs = &locsPerLevelCache[level];
std::vector<WorldLocation> locs = sTravelMgr.GetTeleportLocations(bot);
LOG_DEBUG("playerbots", "Random teleporting bot {} for level {} ({} locations available)", bot->GetName().c_str(),
bot->GetLevel(), locs->size());
bot->GetLevel(), locs.size());
RandomTeleport(bot, *locs);
RandomTeleport(bot, locs);
}
void RandomPlayerbotMgr::RandomTeleport(Player* bot)

View File

@ -164,25 +164,8 @@ public:
static uint8 GetTeamClassIdx(bool isAlliance, uint8 claz) { return isAlliance * 20 + claz; }
void PrepareAddclassCache();
void PrepareZone2LevelBracket();
void PrepareTeleportCache();
void Init();
std::map<uint8, std::unordered_set<ObjectGuid>> addclassCache;
std::map<uint8, std::vector<WorldLocation>> locsPerLevelCache;
std::map<uint8, std::vector<WorldLocation>> allianceStarterPerLevelCache;
std::map<uint8, std::vector<WorldLocation>> hordeStarterPerLevelCache;
struct LevelBracket {
uint32 low;
uint32 high;
bool InsideBracket(uint32 val) { return val >= low && val <= high; }
};
std::map<uint32, LevelBracket> zone2LevelBracket;
struct BankerLocation {
WorldLocation loc;
uint32 entry;
};
std::map<uint8, std::vector<BankerLocation>> bankerLocsPerLevelCache;
// Account type management
void AssignAccountTypes();

View File

@ -1,39 +0,0 @@
#include "FlightMasterCache.h"
void FlightMasterCache::AddHordeFlightMaster(uint32 entry, WorldPosition pos)
{
hordeFlightMasterCache[entry] = pos;
}
void FlightMasterCache::AddAllianceFlightMaster(uint32 entry, WorldPosition pos)
{
allianceFlightMasterCache[entry] = pos;
}
Creature* FlightMasterCache::GetNearestFlightMaster(Player* bot)
{
std::map<uint32, WorldPosition>& flightMasterCache =
(bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache;
Creature* nearestFlightMaster = nullptr;
float nearestDistance = std::numeric_limits<float>::max();
for (auto const& [entry, pos] : flightMasterCache)
{
if (pos.GetMapId() == bot->GetMapId())
{
float distance = bot->GetExactDist2dSq(pos);
if (distance < nearestDistance)
{
Creature* flightMaster = ObjectAccessor::GetSpawnedCreatureByDBGUID(bot->GetMapId(), entry);
if (flightMaster)
{
nearestDistance = distance;
nearestFlightMaster = flightMaster;
}
}
}
}
return nearestFlightMaster;
}

View File

@ -1,36 +0,0 @@
#ifndef _PLAYERBOT_FLIGHTMASTER_H
#define _PLAYERBOT_FLIGHTMASTER_H
#include "Creature.h"
#include "Player.h"
#include "TravelMgr.h"
class FlightMasterCache
{
public:
static FlightMasterCache& Instance()
{
static FlightMasterCache instance;
return instance;
}
Creature* GetNearestFlightMaster(Player* bot);
void AddHordeFlightMaster(uint32 entry, WorldPosition pos);
void AddAllianceFlightMaster(uint32 entry, WorldPosition pos);
private:
FlightMasterCache() = default;
~FlightMasterCache() = default;
FlightMasterCache(const FlightMasterCache&) = delete;
FlightMasterCache& operator=(const FlightMasterCache&) = delete;
FlightMasterCache(FlightMasterCache&&) = delete;
FlightMasterCache& operator=(FlightMasterCache&&) = delete;
std::map<uint32, WorldPosition> allianceFlightMasterCache;
std::map<uint32, WorldPosition> hordeFlightMasterCache;
};
#endif

View File

@ -66,6 +66,29 @@ private:
Player* bot;
};
class HasRelicBySubclassVisitor : public IterateItemsVisitor
{
public:
HasRelicBySubclassVisitor(uint32 subClass) : subClass(subClass) {}
bool Visit(Item* item) override
{
ItemTemplate const* proto = item->GetTemplate();
if (proto && proto->InventoryType == INVTYPE_RELIC && proto->SubClass == subClass)
{
found = true;
return false;
}
return true;
}
bool found = false;
private:
uint32 subClass;
};
class FindItemsByQualityVisitor : public IterateItemsVisitor
{
public:

View File

@ -8,6 +8,10 @@
#include <iomanip>
#include <numeric>
#include "Creature.h"
#include "Log.h"
#include "ObjectAccessor.h"
#include "TravelNode.h"
#include "Talentspec.h"
#include "ChatHelper.h"
#include "MMapFactory.h"
@ -22,6 +26,71 @@
#include "Corpse.h"
#include "CellImpl.h"
// Navigation data
enum class CityId : uint8
{
STORMWIND,
IRONFORGE,
DARNASSUS,
EXODAR,
ORGRIMMAR,
UNDERCITY,
THUNDER_BLUFF,
SILVERMOON_CITY,
SHATTRATH_CITY,
DALARAN
};
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::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)
{
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;
}
return weight;
}
WorldPosition::WorldPosition(std::string const str)
{
std::vector<std::string> tokens = split(str, '|');
@ -4287,3 +4356,434 @@ void TravelMgr::printObj(WorldObject* obj, std::string const type)
}
}
}
void TravelMgr::Init()
{
if (sPlayerbotAIConfig.enabled)
{
PrepareZone2LevelBracket();
PrepareDestinationCache();
}
sTravelNodeMap.InitTaxiGraph();
LOG_INFO("playerbots", "Playerbots Taxi graph and destination cache built.");
}
Creature* TravelMgr::GetNearestFlightMaster(Player* bot)
{
std::map<uint32, WorldPosition>& flightMasterCache =
(bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache;
Creature* nearestFlightMaster = nullptr;
float nearestDistance = std::numeric_limits<float>::max();
for (auto const& [entry, pos] : flightMasterCache)
{
if (pos.GetMapId() != bot->GetMapId())
continue;
float distance = bot->GetExactDist2dSq(pos);
if (distance > nearestDistance)
continue;
Creature* flightMaster = ObjectAccessor::GetSpawnedCreatureByDBGUID(bot->GetMapId(), entry);
if (flightMaster)
{
nearestDistance = distance;
nearestFlightMaster = flightMaster;
}
}
return nearestFlightMaster;
}
ObjectGuid TravelMgr::GetNearestFlightMasterGuid(Player* bot)
{
Creature* nearestFlightMaster = GetNearestFlightMaster(bot);
if (!nearestFlightMaster)
return ObjectGuid::Empty;
return nearestFlightMaster->GetGUID();
}
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)
return validDestinations;
uint32 fromNode = sObjectMgr->GetNearestTaxiNode(nearestFlightMaster->GetPositionX(), nearestFlightMaster->GetPositionY(),
nearestFlightMaster->GetPositionZ(), nearestFlightMaster->GetMapId(),
bot->GetTeamId());
if (!fromNode)
return validDestinations;
std::vector<WorldLocation> candidateLocations;
if (bot->GetLevel() >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100)
candidateLocations = GetCityLocations(bot);
std::vector<WorldLocation> hubLocations = GetTravelHubs(bot);
candidateLocations.insert(candidateLocations.end(), hubLocations.begin(), hubLocations.end());
for (auto const& loc : candidateLocations)
{
uint32 candidateNode = sObjectMgr->GetNearestTaxiNode(loc.GetPositionX(), loc.GetPositionY(),
loc.GetPositionZ(), loc.GetMapId(),
bot->GetTeamId());
if (!candidateNode)
continue;
std::vector<uint32> path = sTravelNodeMap.FindTaxiPath(fromNode, candidateNode);
if (!path.empty())
validDestinations.push_back(path);
}
return validDestinations;
}
const std::vector<WorldLocation> TravelMgr::GetTeleportLocations(Player* bot)
{
uint32 level = bot->GetLevel();
uint8 isAlliance = bot->GetTeamId() == TEAM_ALLIANCE;
if (sPlayerbotAIConfig.enableNewRpgStrategy)
return isAlliance ? allianceHubsPerLevelCache[level] : hordeHubsPerLevelCache[level];
return locsPerLevelCache[level];
}
const std::vector<WorldLocation> TravelMgr::GetTravelHubs(Player* bot)
{
std::vector<WorldLocation> locs = bot->GetTeamId() == TEAM_ALLIANCE
? allianceHubsPerLevelCache[bot->GetLevel()]
: hordeHubsPerLevelCache[bot->GetLevel()];
return locs;
}
std::vector<WorldLocation> TravelMgr::GetCityLocations(Player* bot)
{
uint32 level = bot->GetLevel();
std::vector<WorldLocation> fallbackLocations;
for (auto& bLoc : bankerLocsPerLevelCache[level])
fallbackLocations.push_back(bLoc.loc);
if (!sPlayerbotAIConfig.enableWeightTeleToCityBankers)
return fallbackLocations;
TeamId botTeamId = bot->GetTeamId();
std::unordered_set<CityId> validBankerCities;
for (auto& loc : bankerLocsPerLevelCache[level])
{
auto cityIt = bankerToCity.find(loc.entry);
if (cityIt == bankerToCity.end())
continue;
TeamId cityTeamId = cityIt->second.second;
if (cityTeamId == botTeamId ||
(cityTeamId == TEAM_NEUTRAL)
)
validBankerCities.insert(cityIt->second.first);
}
// Fallback if no valid cities
if (validBankerCities.empty())
return fallbackLocations;
// Apply weights to valid cities
std::vector<CityId> weightedCities;
for (CityId city : validBankerCities)
{
int weight = GetCityWeight(city);
if (weight <= 0)
continue;
for (int i = 0; i < weight; ++i)
weightedCities.push_back(city);
}
// Fallback if no valid cities
if (weightedCities.empty())
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 selectedBankerEntry = bankers[urand(0, bankers.size() - 1)];
auto locIt = bankerEntryToLocation.find(selectedBankerEntry);
if (locIt != bankerEntryToLocation.end())
return { locIt->second };
// Fallback if something went wrong
return fallbackLocations;
}
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 - 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 - 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 - 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 - 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
// 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
// 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
// Override with values from config
for (auto const& [zoneId, bracketPair] : sPlayerbotAIConfig.zoneBrackets)
zone2LevelBracket[zoneId] = {bracketPair.first, bracketPair.second};
}
void TravelMgr::PrepareDestinationCache()
{
uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL);
uint32 flightMastersCount = 0;
uint32 innkeepersCount = 0;
uint32 bankerCount = 0;
LOG_INFO("playerbots", "Preparing destination caches for {} levels...", maxLevel);
// Temporary map to group creatures by entry and area
std::map<std::tuple<uint16, int32, int32, int32>, std::vector<CreatureData>> tempLocsCache;
std::map<uint32, std::map<uint32, std::vector<WorldLocation>>> tempCreatureCache;
for (auto const& [guid, creatureData] : sObjectMgr->GetAllCreatureData())
{
CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureData.id1);
if (!creatureTemplate)
continue;
uint16 mapId = creatureData.mapid;
if (std::find(sPlayerbotAIConfig.randomBotMaps.begin(), sPlayerbotAIConfig.randomBotMaps.end(), mapId)
== sPlayerbotAIConfig.randomBotMaps.end())
continue;
float x = creatureData.posX;
float y = creatureData.posY;
float z = creatureData.posZ;
float orient = creatureData.orientation;
uint32 templateEntry = creatureData.id1;
Map* map = sMapMgr->FindMap(mapId, 0);
if (!map)
continue;
AreaTableEntry const* area = sAreaTableStore.LookupEntry(map->GetAreaId(PHASEMASK_NORMAL, x, y, z));
if (!area)
continue;
uint32 areaId = area->zone ? area->zone : area->ID;
// CREATURES
if (creatureTemplate->npcflag == 0 &&
creatureTemplate->lootid != 0 &&
creatureTemplate->maxlevel - creatureTemplate->minlevel < 3 &&
creatureTemplate->Entry != 32820 && creatureTemplate->Entry != 24196 &&
creatureTemplate->Entry != 30627 && creatureTemplate->Entry != 30617 &&
creatureData.spawntimesecs < 1000 &&
creatureTemplate->faction != 11 && creatureTemplate->faction != 71 &&
creatureTemplate->faction != 79 && creatureTemplate->faction != 85 &&
creatureTemplate->faction != 188 && creatureTemplate->faction != 1575 &&
(creatureTemplate->unit_flags & 256) == 0 &&
(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;
tempLocsCache[std::make_tuple(mapId, roundX, roundY, roundZ)].push_back(creatureData);
tempCreatureCache[templateEntry][areaId].push_back(WorldLocation(mapId, x, y, z));
}
// FLIGHT MASTERS
else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER ||
creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) &&
creatureTemplate->Entry != 3838 && creatureTemplate->Entry != 29480)
{
FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction);
bool forHorde = !(factionEntry->hostileMask & 4);
bool forAlliance = !(factionEntry->hostileMask & 2);
if (creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER)
{
WorldPosition pos(mapId, x, y, z, orient);
if (forHorde)
hordeFlightMasterCache[guid] = pos;
if (forAlliance)
allianceFlightMasterCache[guid] = pos;
flightMastersCount++;
}
else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER)
{
if (zone2LevelBracket.find(areaId) == zone2LevelBracket.end())
continue;
LevelBracket bracket = zone2LevelBracket[areaId];
WorldPosition loc(mapId, x + cos(orient) * 5.0f, y + sin(orient) * 5.0f, z + 0.5f, orient + M_PI);
for (int i = bracket.low; i <= bracket.high; i++)
{
if (forHorde)
hordeHubsPerLevelCache[i].push_back(loc);
if (forAlliance)
allianceHubsPerLevelCache[i].push_back(loc);
innkeepersCount++;
}
}
}
// === BANKERS ===
else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_BANKER &&
creatureTemplate->npcflag != 135298 &&
creatureTemplate->minlevel != 55 &&
creatureTemplate->minlevel != 65 &&
creatureTemplate->faction != 35 && creatureTemplate->faction != 474 &&
creatureTemplate->faction != 69 && creatureTemplate->faction != 57 &&
creatureTemplate->Entry != 30606 && creatureTemplate->Entry != 30608 &&
creatureTemplate->Entry != 29282)
{
BankerLocation bLoc;
bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI);
bLoc.entry = templateEntry;
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;
for (int32 l = 1; l <= maxLevel; l++)
{
// Bots 1-60 go to base game bankers (all have minlevel 30 or 45)
if (l <=60 && level > 45)
continue;
// Bots 61-70 go to Shattrath bankers (all have minlevel 60 or 70)
if ((l >=61 && l <=70) && (level < 60 || level > 70))
continue;
// Bots 71+ go to Dalaran bankers (all have minlevel 75)
if ((l >=71) && level != 75)
continue;
bankerLocsPerLevelCache[(uint8)l].push_back(bLoc);
bankerEntryToLocation[bLoc.entry] = bLoc.loc;
}
bankerCount++;
}
}
// Process temporary caches
for (auto const& [gridTuple, creatureDataList] : tempLocsCache)
{
if (creatureDataList.size() > 2)
{
CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id1);
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;
for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel;
l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++)
{
if (l < 1 || l > maxLevel)
continue;
locsPerLevelCache[(uint8)l].push_back(WorldLocation(std::get<0>(gridTuple)));
}
}
}
for (auto const& [entry, areaMap] : tempCreatureCache)
{
for (auto const& [area, locList] : areaMap)
{
if (locList.size() > 3)
continue;
float totalX = 0, totalY = 0, totalZ = 0;
for (auto const& loc : locList)
{
totalX += loc.GetPositionX();
totalY += loc.GetPositionY();
totalZ += loc.GetPositionZ();
}
float avgX = totalX / locList.size();
float avgY = totalY / locList.size();
float avgZ = totalZ / locList.size();
creatureSpawnsByTemplate[entry].push_back(WorldLocation(locList[0].GetMapId(), avgX, avgY, avgZ, 0));
}
}
// Add travel hubs based on player start locations
for (uint32 i = 1; i < sRaceMgr->GetMaxRaces(); i++)
{
for (uint32 j = 1; j < MAX_CLASSES; j++)
{
PlayerInfo const* info = sObjectMgr->GetPlayerInfo(i, j);
if (!info)
continue;
WorldPosition pos(info->mapId, info->positionX, info->positionY, info->positionZ, info->orientation);
for (int32 l = 1; l <= 5; l++)
{
if ((1 << (i - 1)) & sRaceMgr->GetAllianceRaceMask())
allianceHubsPerLevelCache[(uint8)l].push_back(pos);
else
hordeHubsPerLevelCache[(uint8)l].push_back(pos);
}
break;
}
}
LOG_INFO("playerbots", ">> {} flight masters and {} innkeepers and {} banker locations for level collected.", flightMastersCount, innkeepersCount, bankerCount);
}

View File

@ -7,6 +7,7 @@
#define _PLAYERBOT_TRAVELMGR_H
#include <boost/functional/hash.hpp>
#include <map>
#include <random>
#include "AiObject.h"
@ -15,6 +16,7 @@
#include "GridDefines.h"
#include "PlayerbotAIConfig.h"
class Creature;
class GuidPosition;
class ObjectGuid;
class Quest;
@ -854,6 +856,16 @@ public:
void Clear();
void LoadQuestTravelTable();
// Navigation
void Init();
Creature* GetNearestFlightMaster(Player* bot);
ObjectGuid GetNearestFlightMasterGuid(Player* bot);
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);
const std::vector<WorldLocation>& GetLocsPerLevelCache(uint8 level) { return locsPerLevelCache[level]; }
template <class D, class W, class URBG>
void weighted_shuffle(D first, D last, W first_weight, W last_weight, URBG&& g)
{
@ -943,6 +955,37 @@ private:
TravelMgr(TravelMgr&&) = delete;
TravelMgr& operator=(TravelMgr&&) = delete;
// Navigation initialization
void PrepareZone2LevelBracket();
void PrepareDestinationCache();
// Internal types
struct LevelBracket
{
uint32 low;
uint32 high;
bool InsideBracket(uint32 val) const { return val >= low && val <= high; }
};
struct BankerLocation
{
WorldLocation loc;
uint32 entry;
};
// Navigation caches
std::map<uint32, WorldPosition> allianceFlightMasterCache;
std::map<uint32, WorldPosition> hordeFlightMasterCache;
std::map<uint8, std::vector<WorldLocation>> allianceHubsPerLevelCache;
std::map<uint8, std::vector<WorldLocation>> hordeHubsPerLevelCache;
std::map<uint8, std::vector<BankerLocation>> bankerLocsPerLevelCache;
std::unordered_map<uint32, WorldLocation> bankerEntryToLocation;
std::map<uint8, std::vector<WorldLocation>> locsPerLevelCache;
std::unordered_map<uint32, std::vector<WorldLocation>> creatureSpawnsByTemplate;
std::map<uint32, LevelBracket> zone2LevelBracket;
};
#define sTravelMgr TravelMgr::instance()
#endif

View File

@ -7,6 +7,7 @@
#include <iomanip>
#include <regex>
#include <unordered_set>
#include "BudgetValues.h"
#include "PathGenerator.h"
@ -2447,3 +2448,127 @@ WorldPosition TravelNodeMap::getMapOffset(uint32 mapId)
return WorldPosition(mapId, 0, 0, 0, 0);
}
// ============================================================
// TravelNodeMap taxi graph (BFS-based flight path lookup)
// ============================================================
void TravelNodeMap::InitTaxiGraph()
{
BuildTaxiGraph();
ComputeAllPaths();
}
std::vector<uint32> TravelNodeMap::FindTaxiPath(uint32 fromNode, uint32 toNode)
{
if (fromNode == toNode)
return {};
TaxiNodesEntry const* startNode = sTaxiNodesStore.LookupEntry(fromNode);
TaxiNodesEntry const* endNode = sTaxiNodesStore.LookupEntry(toNode);
if (!startNode || !endNode || startNode->map_id != endNode->map_id)
return {};
auto cacheItr = taxiPathCache.find(fromNode);
if (cacheItr == taxiPathCache.end())
return {};
auto toNodeItr = cacheItr->second.find(toNode);
if (toNodeItr == cacheItr->second.end())
return {};
return toNodeItr->second;
}
void TravelNodeMap::BuildTaxiGraph()
{
taxiGraph.clear();
std::unordered_map<uint32, std::unordered_set<uint32>> tempGraph;
for (uint32 i = 0; i < sTaxiPathStore.GetNumRows(); ++i)
{
TaxiPathEntry const* path = sTaxiPathStore.LookupEntry(i);
if (!path)
continue;
if (path->to == 0 || path->to == uint32(-1))
continue;
tempGraph[path->from].insert(path->to);
tempGraph[path->to].insert(path->from);
}
for (auto const& [node, neighbors] : tempGraph)
taxiGraph[node] = std::vector<uint32>(neighbors.begin(), neighbors.end());
}
void TravelNodeMap::ComputeAllPaths()
{
std::set<uint32> allNodes;
for (auto const& [source, neighbors] : taxiGraph)
allNodes.insert(source);
for (uint32 source : allNodes)
{
auto parentMap = BFS(source);
for (uint32 target : allNodes)
{
if (source == target)
continue;
auto path = BuildPath(source, target, parentMap);
if (!path.empty())
taxiPathCache[source][target] = path;
}
}
}
std::unordered_map<uint32, uint32> TravelNodeMap::BFS(uint32 fromNode)
{
std::queue<uint32> workQueue;
std::unordered_set<uint32> visited;
std::unordered_map<uint32, uint32> parentMap;
workQueue.push(fromNode);
visited.insert(fromNode);
parentMap[fromNode] = 0;
while (!workQueue.empty())
{
uint32 current = workQueue.front();
workQueue.pop();
for (uint32 next : taxiGraph.at(current))
{
if (visited.count(next))
continue;
visited.insert(next);
parentMap[next] = current;
workQueue.push(next);
}
}
return parentMap;
}
std::vector<uint32> TravelNodeMap::BuildPath(uint32 fromNode, uint32 toNode,
const std::unordered_map<uint32, uint32>& parentMap)
{
if (!parentMap.count(toNode))
return {}; // unreachable
std::vector<uint32> path;
uint32 current = toNode;
while (current != fromNode)
{
path.push_back(current);
auto it = parentMap.find(current);
if (it == parentMap.end() || it->second == 0)
break;
current = it->second;
}
path.push_back(fromNode);
std::reverse(path.begin(), path.end());
return path;
}

View File

@ -580,6 +580,10 @@ public:
void calcMapOffset();
WorldPosition getMapOffset(uint32 mapId);
// Taxi graph (BFS-based path lookup between taxi nodes)
void InitTaxiGraph();
std::vector<uint32> FindTaxiPath(uint32 fromNode, uint32 toNode);
std::shared_timed_mutex m_nMapMtx;
std::unordered_map<ObjectGuid, std::unordered_map<uint32, TravelNode*>> teleportNodes;
@ -593,6 +597,16 @@ private:
TravelNodeMap(TravelNodeMap&&) = delete;
TravelNodeMap& operator=(TravelNodeMap&&) = delete;
// Taxi graph internals
void BuildTaxiGraph();
void ComputeAllPaths();
std::unordered_map<uint32, uint32> BFS(uint32 startNode);
std::vector<uint32> BuildPath(uint32 fromNode, uint32 toNode,
const std::unordered_map<uint32, uint32>& parentMap);
std::unordered_map<uint32, std::vector<uint32>> taxiGraph;
std::map<uint32, std::map<uint32, std::vector<uint32>>> taxiPathCache;
std::vector<TravelNode*> m_nodes;
std::vector<std::pair<uint32, WorldPosition>> mapOffsets;

View File

@ -15,6 +15,7 @@
#include "RandomPlayerbotFactory.h"
#include "RandomPlayerbotMgr.h"
#include "Talentspec.h"
#include "TravelMgr.h"
template <class T>
void LoadList(std::string const value, T& list)
@ -620,7 +621,10 @@ bool PlayerbotAIConfig::Initialize()
// SPP automation
freeMethodLoot = sConfigMgr->GetOption<bool>("AiPlayerbot.FreeMethodLoot", false);
lootRollLevel = sConfigMgr->GetOption<int32>("AiPlayerbot.LootRollLevel", 1);
lootNeedRollLevel = sConfigMgr->GetOption<int32>("AiPlayerbot.LootNeedRollLevel", 1);
lootRollRecipe = sConfigMgr->GetOption<bool>("AiPlayerbot.LootRollRecipe", false);
lootRollDisenchant = sConfigMgr->GetOption<bool>("AiPlayerbot.LootRollDisenchant", false);
lootGreedRollLevel = sConfigMgr->GetOption<bool>("AiPlayerbot.LootGreedRollLevel", false);
autoPickReward = sConfigMgr->GetOption<std::string>("AiPlayerbot.AutoPickReward", "yes");
autoEquipUpgradeLoot = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoEquipUpgradeLoot", true);
equipUpgradeThreshold = sConfigMgr->GetOption<float>("AiPlayerbot.EquipUpgradeThreshold", 1.1f);
@ -688,6 +692,7 @@ bool PlayerbotAIConfig::Initialize()
{
PlayerbotDungeonRepository::instance().LoadDungeonSuggestions();
}
sTravelMgr.Init();
excludedHunterPetFamilies.clear();
LoadList<std::vector<uint32>>(sConfigMgr->GetOption<std::string>("AiPlayerbot.ExcludedHunterPetFamilies", ""), excludedHunterPetFamilies);

View File

@ -346,7 +346,10 @@ public:
uint32 botActiveAloneSmartScaleWhenMaxLevel;
bool freeMethodLoot;
int32 lootRollLevel;
int32 lootNeedRollLevel;
bool lootGreedRollLevel;
bool lootRollRecipe;
bool lootRollDisenchant;
std::string autoPickReward;
bool autoEquipUpgradeLoot;
float equipUpgradeThreshold;

View File

@ -112,13 +112,10 @@ public:
if (sPlayerbotAIConfig.enabled || sPlayerbotAIConfig.randomBotAutologin)
{
std::string roundedTime =
std::to_string(std::ceil((sPlayerbotAIConfig.maxRandomBots * 0.11 / 60) * 10) / 10.0);
roundedTime = roundedTime.substr(0, roundedTime.find('.') + 2);
std::string maxAllowedBotCount = std::to_string(sRandomPlayerbotMgr.GetMaxAllowedBotCount());
ChatHandler(player->GetSession()).SendSysMessage(
"|cff00ff00Playerbots:|r bot initialization at server startup takes about '"
+ roundedTime + "' minutes.");
"|cff00ff00Playerbots:|r The server is configured with " + maxAllowedBotCount + " bots.");
}
}
}