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. 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. 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 - - [ ] No
- - [ ] Yes (**list messages in the table**) - - [ ] 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. 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. 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 - - [ ] No
- - [ ] Yes (**explain below**) - - [ ] 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. 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 ### Quick Start
To install both the required branch of AzerothCore and the `mod-playerbots` module from source, run the following:
```bash ```bash
git clone https://github.com/mod-playerbots/azerothcore-wotlk.git --branch=Playerbot 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 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 | Guide | Description |
git clone https://github.com/mod-playerbots/azerothcore-wotlk.git --branch=Playerbot |---|---|
cd azerothcore-wotlk/modules | **[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 |
git clone https://github.com/mod-playerbots/mod-playerbots.git --branch=master | **[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: 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.
```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.
## Documentation ## Documentation

View File

@ -298,9 +298,24 @@ AiPlayerbot.TwoRoundsGearInit = 0
# Default: 0 (disabled) # Default: 0 (disabled)
AiPlayerbot.FreeMethodLoot = 0 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) # 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.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.60 = 50350151020013053100515221
AiPlayerbot.PremadeSpecLink.2.0.80 = 50350152220013053100515221-503201312 AiPlayerbot.PremadeSpecLink.2.0.80 = 50350152220013053100515221-503201312
AiPlayerbot.PremadeSpecName.2.1 = prot pve 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.60 = -05005135203102311333112321
AiPlayerbot.PremadeSpecLink.2.1.80 = -05005135203102311333312321-502302012003 AiPlayerbot.PremadeSpecLink.2.1.80 = -05005135203102311333312321-502302012003
AiPlayerbot.PremadeSpecName.2.2 = ret pve 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.60 = --05230051203331302133231131
AiPlayerbot.PremadeSpecLink.2.2.65 = -05-05230051203331302133231131 AiPlayerbot.PremadeSpecLink.2.2.65 = -05-05230051203331302133231131
AiPlayerbot.PremadeSpecLink.2.2.80 = 050501-05-05232051203331302133231331 AiPlayerbot.PremadeSpecLink.2.2.80 = 050501-05-05232051203331302133231331
AiPlayerbot.PremadeSpecName.2.3 = holy pvp 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.60 = 50332150300013050133215221
AiPlayerbot.PremadeSpecLink.2.3.80 = 50332150300013050133315221-5032013122 AiPlayerbot.PremadeSpecLink.2.3.80 = 50332150300013050133315221-5032013122
AiPlayerbot.PremadeSpecName.2.4 = prot pvp 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.60 = -15320130223122311323311321
AiPlayerbot.PremadeSpecLink.2.4.80 = -15320130223122321333312321-052300502 AiPlayerbot.PremadeSpecLink.2.4.80 = -15320130223122321333312321-052300502
AiPlayerbot.PremadeSpecName.2.5 = ret pvp 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.60 = --05230250203331222133201321
AiPlayerbot.PremadeSpecLink.2.5.80 = -1532013022-05230250203331322133201321 AiPlayerbot.PremadeSpecLink.2.5.80 = -1532013022-05230250203331322133201321

View File

@ -163,6 +163,7 @@ public:
creators["war stomp"] = &ActionContext::war_stomp; creators["war stomp"] = &ActionContext::war_stomp;
creators["blood fury"] = &ActionContext::blood_fury; creators["blood fury"] = &ActionContext::blood_fury;
creators["berserking"] = &ActionContext::berserking; creators["berserking"] = &ActionContext::berserking;
creators["every man for himself"] = &ActionContext::every_man_for_himself;
creators["use trinket"] = &ActionContext::use_trinket; creators["use trinket"] = &ActionContext::use_trinket;
creators["auto talents"] = &ActionContext::auto_talents; creators["auto talents"] = &ActionContext::auto_talents;
creators["auto share quest"] = &ActionContext::auto_share_quest; 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* war_stomp(PlayerbotAI* botAI) { return new CastWarStompAction(botAI); }
static Action* blood_fury(PlayerbotAI* botAI) { return new CastBloodFuryAction(botAI); } static Action* blood_fury(PlayerbotAI* botAI) { return new CastBloodFuryAction(botAI); }
static Action* berserking(PlayerbotAI* botAI) { return new CastBerserkingAction(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* use_trinket(PlayerbotAI* botAI) { return new UseTrinketAction(botAI); }
static Action* auto_talents(PlayerbotAI* botAI) { return new AutoSetTalentsAction(botAI); } static Action* auto_talents(PlayerbotAI* botAI) { return new AutoSetTalentsAction(botAI); }
static Action* auto_share_quest(PlayerbotAI* ai) { return new AutoShareQuestAction(ai); } 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()); 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*/) bool UseTrinketAction::Execute(Event /*event*/)
{ {
Item* trinket1 = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_TRINKET1); Item* trinket1 = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_TRINKET1);

View File

@ -284,6 +284,16 @@ public:
CastBerserkingAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "berserking") {} 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 class UseTrinketAction : public Action
{ {
public: public:

View File

@ -22,10 +22,10 @@ bool LootRollAction::Execute(Event /*event*/)
std::vector<Roll*> rolls = group->GetRolls(); std::vector<Roll*> rolls = group->GetRolls();
for (Roll*& roll : rolls) 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; continue;
}
ObjectGuid guid = roll->itemGUID; ObjectGuid guid = roll->itemGUID;
uint32 itemId = roll->itemid; uint32 itemId = roll->itemid;
int32 randomProperty = 0; int32 randomProperty = 0;
@ -41,27 +41,22 @@ bool LootRollAction::Execute(Event /*event*/)
std::string itemUsageParam; std::string itemUsageParam;
if (randomProperty != 0) if (randomProperty != 0)
{
itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty); itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty);
}
else else
{
itemUsageParam = std::to_string(itemId); itemUsageParam = std::to_string(itemId);
}
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam); 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. // 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 (proto->Class == ITEM_CLASS_MISC && proto->SubClass == ITEM_SUBCLASS_JUNK && proto->Quality == ITEM_QUALITY_EPIC)
{ {
if (CanBotUseToken(proto, bot)) if (CanBotUseToken(proto, bot))
{
vote = NEED; // Eligible for "Need" vote = NEED; // Eligible for "Need"
}
else else
{
vote = GREED; // Not eligible, so "Greed" vote = GREED; // Not eligible, so "Greed"
}
} }
else if (usage == ITEM_USAGE_DISENCHANT)
vote = sPlayerbotAIConfig.lootRollDisenchant ? DISENCHANT : GREED;
else else
{ {
switch (proto->Class) switch (proto->Class)
@ -69,40 +64,34 @@ bool LootRollAction::Execute(Event /*event*/)
case ITEM_CLASS_WEAPON: case ITEM_CLASS_WEAPON:
case ITEM_CLASS_ARMOR: case ITEM_CLASS_ARMOR:
if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP) if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP)
{
vote = NEED; vote = NEED;
}
else if (usage != ITEM_USAGE_NONE) else if (usage != ITEM_USAGE_NONE)
{
vote = GREED; 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; break;
default: default:
if (StoreLootAction::IsLootAllowed(itemId, botAI)) if (StoreLootAction::IsLootAllowed(itemId, botAI))
vote = CalculateRollVote(proto); // Ensure correct Need/Greed behavior vote = CalculateRollVote(proto, usage);
break; break;
} }
} }
if (sPlayerbotAIConfig.lootRollLevel == 0) if (vote == NEED)
{ {
if (sPlayerbotAIConfig.lootNeedRollLevel == 0 || RollUniqueCheck(proto, bot))
vote = PASS;
else if (sPlayerbotAIConfig.lootNeedRollLevel == 1)
vote = GREED;
}
else if (vote == GREED && !sPlayerbotAIConfig.lootGreedRollLevel)
vote = PASS; 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))
{
vote = PASS;
}
else
{
vote = GREED;
}
}
}
switch (group->GetLootMethod()) switch (group->GetLootMethod())
{ {
case MASTER_LOOT: case MASTER_LOOT:
@ -120,11 +109,14 @@ bool LootRollAction::Execute(Event /*event*/)
return false; return false;
} }
RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto) RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto, ItemUsage usage)
{ {
std::ostringstream out; if (usage == ITEM_USAGE_NONE)
out << proto->ItemId; {
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", out.str()); std::ostringstream out;
out << proto->ItemId;
usage = AI_VALUE2(ItemUsage, "item usage", out.str());
}
RollVote needVote = PASS; RollVote needVote = PASS;
switch (usage) switch (usage)
@ -137,11 +129,13 @@ RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto)
break; break;
case ITEM_USAGE_SKILL: case ITEM_USAGE_SKILL:
case ITEM_USAGE_USE: case ITEM_USAGE_USE:
case ITEM_USAGE_DISENCHANT:
case ITEM_USAGE_AH: case ITEM_USAGE_AH:
case ITEM_USAGE_VENDOR: case ITEM_USAGE_VENDOR:
needVote = GREED; needVote = GREED;
break; break;
case ITEM_USAGE_DISENCHANT:
needVote = sPlayerbotAIConfig.lootRollDisenchant ? DISENCHANT : GREED;
break;
default: default:
break; break;
} }
@ -195,9 +189,7 @@ bool CanBotUseToken(ItemTemplate const* proto, Player* bot)
// Check if the bot's class is allowed to use the token // Check if the bot's class is allowed to use the token
if (proto->AllowableClass & botClassMask) if (proto->AllowableClass & botClassMask)
{
return true; // Bot's class is eligible to use this token return true; // Bot's class is eligible to use this token
}
return false; // Bot's class cannot 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 // Determine if the unique item is already equipped
bool isEquipped = (totalItemCount > bagItemCount); bool isEquipped = (totalItemCount > bagItemCount);
if (isEquipped && proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE)) if (isEquipped && proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE))
{
return true; // Unique Item is already equipped return true; // Unique Item is already equipped
}
else if (proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE) && (bagItemCount > 1)) else if (proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE) && (bagItemCount > 1))
{
return true; // Unique item already in bag, don't roll for it return true; // Unique item already in bag, don't roll for it
}
return false; // Item is not equipped or in bags, 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; bool Execute(Event event) override;
protected: protected:
RollVote CalculateRollVote(ItemTemplate const* proto); RollVote CalculateRollVote(ItemTemplate const* proto, ItemUsage usage = ITEM_USAGE_NONE);
}; };
bool CanBotUseToken(ItemTemplate const* proto, Player* bot); bool CanBotUseToken(ItemTemplate const* proto, Player* bot);

View File

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

View File

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

View File

@ -104,6 +104,13 @@ public:
creators["target"] = &ChatTriggerContext::target; creators["target"] = &ChatTriggerContext::target;
creators["formation"] = &ChatTriggerContext::formation; creators["formation"] = &ChatTriggerContext::formation;
creators["stance"] = &ChatTriggerContext::stance; 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["sendmail"] = &ChatTriggerContext::sendmail;
creators["mail"] = &ChatTriggerContext::mail; creators["mail"] = &ChatTriggerContext::mail;
creators["outfit"] = &ChatTriggerContext::outfit; creators["outfit"] = &ChatTriggerContext::outfit;
@ -159,6 +166,13 @@ private:
static Trigger* sendmail(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "sendmail"); } static Trigger* sendmail(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "sendmail"); }
static Trigger* formation(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "formation"); } static Trigger* formation(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "formation"); }
static Trigger* stance(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "stance"); } 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* attackers(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "attackers"); }
static Trigger* target(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "target"); } static Trigger* target(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "target"); }
static Trigger* max_dps(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "max dps"); } 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("save mana");
supported.push_back("formation"); supported.push_back("formation");
supported.push_back("stance"); 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("sendmail");
supported.push_back("mail"); supported.push_back("mail");
supported.push_back("outfit"); supported.push_back("outfit");

View File

@ -34,6 +34,9 @@ void RacialsStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
NextAction("berserking", ACTION_NORMAL + 5), NextAction("berserking", ACTION_NORMAL + 5),
NextAction("use trinket", ACTION_NORMAL + 4) })); 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) RacialsStrategy::RacialsStrategy(PlayerbotAI* botAI) : Strategy(botAI)

View File

@ -16,7 +16,7 @@
#include "PositionValue.h" #include "PositionValue.h"
#include "SharedDefines.h" #include "SharedDefines.h"
#include "TemporarySummon.h" #include "TemporarySummon.h"
#include "ThreatMgr.h" #include "ThreatManager.h"
#include "Timer.h" #include "Timer.h"
#include "PlayerbotAI.h" #include "PlayerbotAI.h"
#include "Player.h" #include "Player.h"
@ -217,7 +217,7 @@ bool LowTankThreatTrigger::IsActive()
if (!current_target) if (!current_target)
return false; return false;
ThreatMgr& mgr = current_target->GetThreatMgr(); ThreatManager& mgr = current_target->GetThreatMgr();
float threat = mgr.GetThreat(bot); float threat = mgr.GetThreat(bot);
float tankThreat = mgr.GetThreat(mt); float tankThreat = mgr.GetThreat(mt);
return tankThreat == 0.0f || threat > tankThreat * 0.5f; 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 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() bool HasAuraStackTrigger::IsActive()
{ {
Aura* aura = botAI->GetAura(getName(), GetTarget(), false, true, stack); Aura* aura = botAI->GetAura(getName(), GetTarget(), false, true, stack);

View File

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

View File

@ -59,6 +59,7 @@ public:
creators["party member almost full health"] = &TriggerContext::PartyMemberAlmostFullHealth; creators["party member almost full health"] = &TriggerContext::PartyMemberAlmostFullHealth;
creators["generic boost"] = &TriggerContext::generic_boost; creators["generic boost"] = &TriggerContext::generic_boost;
creators["loss of control"] = &TriggerContext::loss_of_control;
creators["protect party member"] = &TriggerContext::protect_party_member; creators["protect party member"] = &TriggerContext::protect_party_member;
@ -103,7 +104,8 @@ public:
creators["enemy within melee"] = &TriggerContext::enemy_within_melee; 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["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["combo points 3 available"] = &TriggerContext::ComboPoints3Available;
creators["target with combo points almost dead"] = &TriggerContext::target_with_combo_points_almost_dead; creators["target with combo points almost dead"] = &TriggerContext::target_with_combo_points_almost_dead;
creators["combo points not full"] = &TriggerContext::ComboPointsNotFull; creators["combo points not full"] = &TriggerContext::ComboPointsNotFull;
@ -338,7 +340,8 @@ private:
{ {
return new PartyMemberToHealOutOfSpellRangeTrigger(botAI); 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* ComboPoints3Available(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI, 3); }
static Trigger* target_with_combo_points_almost_dead(PlayerbotAI* ai) static Trigger* target_with_combo_points_almost_dead(PlayerbotAI* ai)
{ {
@ -361,6 +364,7 @@ private:
return new PartyMemberAlmostFullHealthTrigger(botAI); return new PartyMemberAlmostFullHealthTrigger(botAI);
} }
static Trigger* generic_boost(PlayerbotAI* botAI) { return new GenericBoostTrigger(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) static Trigger* PartyMemberCriticalHealth(PlayerbotAI* botAI)
{ {
return new PartyMemberCriticalHealthTrigger(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()) if (!player || !player->IsInWorld() || player->IsBeingTeleported())
return; return;
HostileRefMgr& refManager = player->getHostileRefMgr(); for (auto const& [guid, ref] : player->GetThreatMgr().GetThreatenedByMeList())
HostileReference* ref = refManager.getFirst();
if (!ref)
return;
while (ref)
{ {
ThreatMgr* threatMgr = ref->GetSource(); Unit* attacker = ref->GetOwner();
Unit* attacker = threatMgr->GetOwner(); if (!attacker)
continue;
if (player->IsValidAttackTarget(attacker) && if (player->IsValidAttackTarget(attacker) &&
player->GetDistance2d(attacker) < sPlayerbotAIConfig.sightDistance) player->GetDistance2d(attacker) < sPlayerbotAIConfig.sightDistance)
targets.insert(attacker); targets.insert(attacker);
ref = ref->next();
} }
} }
@ -131,7 +125,6 @@ bool AttackersValue::hasRealThreat(Unit* attacker)
return attacker && attacker->IsInWorld() && attacker->IsAlive() && !attacker->IsPolymorphed() && return attacker && attacker->IsInWorld() && attacker->IsAlive() && !attacker->IsPolymorphed() &&
// !attacker->isInRoots() && // !attacker->isInRoots() &&
!attacker->IsFriendlyTo(bot); !attacker->IsFriendlyTo(bot);
(attacker->GetThreatMgr().getCurrentVictim() || dynamic_cast<Player*>(attacker));
} }
bool AttackersValue::IsPossibleTarget(Unit* attacker, Player* bot, float /*range*/) 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) bool AttackersValue::IsValidTarget(Unit* attacker, Player* bot)
{ {
return IsPossibleTarget(attacker, bot) && bot->IsWithinLOSInMap(attacker); 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() bool PossibleAddsValue::Calculate()
@ -255,27 +245,24 @@ bool PossibleAddsValue::Calculate()
{ {
if (find(attackers.begin(), attackers.end(), guid) != attackers.end()) if (find(attackers.begin(), attackers.end(), guid) != attackers.end())
continue; continue;
Unit* add = botAI->GetUnit(guid);
if (!add || !add->IsInWorld() || add->IsDuringRemoveFromWorld())
continue;
if (Unit* add = botAI->GetUnit(guid)) if (!add->GetTarget() && !add->GetThreatMgr().GetLastVictim() && add->IsHostileTo(bot))
{ {
if (!add->IsInWorld() || add->IsDuringRemoveFromWorld()) for (ObjectGuid const attackerGUID : attackers)
continue;
if (!add->GetTarget() && !add->GetThreatMgr().getCurrentVictim() && add->IsHostileTo(bot))
{ {
for (ObjectGuid const attackerGUID : attackers) Unit* attacker = botAI->GetUnit(attackerGUID);
{ if (!attacker)
Unit* attacker = botAI->GetUnit(attackerGUID); continue;
if (!attacker)
continue;
float dist = ServerFacade::instance().GetDistance2d(attacker, add); float dist = ServerFacade::instance().GetDistance2d(attacker, add);
if (ServerFacade::instance().IsDistanceLessOrEqualThan(dist, sPlayerbotAIConfig.aoeRadius * 1.5f)) if (ServerFacade::instance().IsDistanceLessOrEqualThan(dist, sPlayerbotAIConfig.aoeRadius * 1.5f))
continue; continue;
if (ServerFacade::instance().IsDistanceLessOrEqualThan(dist, sPlayerbotAIConfig.aggroDistance)) if (ServerFacade::instance().IsDistanceLessOrEqualThan(dist, sPlayerbotAIConfig.aggroDistance))
return true; return true;
}
} }
} }
} }

View File

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

View File

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

View File

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

View File

@ -153,9 +153,8 @@ ItemUsage ItemUsageValue::Calculate()
// Need to add something like free bagspace or item value. // Need to add something like free bagspace or item value.
if (proto->SellPrice > 0) 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; return ITEM_USAGE_AH;
else else
return ITEM_USAGE_VENDOR; return ITEM_USAGE_VENDOR;
} }

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@
#include "Value.h" #include "Value.h"
class PlayerbotAI; class PlayerbotAI;
class ThreatMgr; class ThreatManager;
class Unit; class Unit;
class FindTargetStrategy class FindTargetStrategy
@ -20,7 +20,7 @@ public:
FindTargetStrategy(PlayerbotAI* botAI) : result(nullptr), botAI(botAI) {} FindTargetStrategy(PlayerbotAI* botAI) : result(nullptr), botAI(botAI) {}
Unit* GetResult(); 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); void GetPlayerCount(Unit* creature, uint32* tankCount, uint32* dpsCount);
bool IsHighPriority(Unit* attacker); bool IsHighPriority(Unit* attacker);
@ -129,7 +129,7 @@ class FindBossTargetStrategy : public FindTargetStrategy
{ {
public: public:
FindBossTargetStrategy(PlayerbotAI* ai) : FindTargetStrategy(ai) {} 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 class BossTargetValue : public TargetValue, public Qualified

View File

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

View File

@ -44,13 +44,13 @@ bool CastCasterFormAction::isUseful()
AI_VALUE2(uint8, "mana", "self target") > sPlayerbotAIConfig.mediumHealth; 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; return true;
} }
bool CastCancelTreeFormAction::isUseful() { return botAI->HasAura(33891, bot); } bool CastCancelDruidAction::isUseful() { return botAI->HasAura(auraId, bot); }
bool CastTreeFormAction::isUseful() bool CastTreeFormAction::isUseful()
{ {

View File

@ -71,14 +71,78 @@ public:
bool isPossible() override { return true; } bool isPossible() override { return true; }
}; };
class CastCancelTreeFormAction : public CastBuffSpellAction class CastCancelDruidAction : public CastBuffSpellAction
{ {
public: 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 Execute(Event event) override;
bool isUseful() override; bool isUseful() override;
bool isPossible() override { return true; } 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 #endif

View File

@ -170,6 +170,12 @@ public:
creators["aquatic form"] = &DruidAiObjectContextInternal::aquatic_form; creators["aquatic form"] = &DruidAiObjectContextInternal::aquatic_form;
creators["caster form"] = &DruidAiObjectContextInternal::caster_form; creators["caster form"] = &DruidAiObjectContextInternal::caster_form;
creators["cancel tree form"] = &DruidAiObjectContextInternal::cancel_tree_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["mangle (bear)"] = &DruidAiObjectContextInternal::mangle_bear;
creators["maul"] = &DruidAiObjectContextInternal::maul; creators["maul"] = &DruidAiObjectContextInternal::maul;
creators["bash"] = &DruidAiObjectContextInternal::bash; creators["bash"] = &DruidAiObjectContextInternal::bash;
@ -258,6 +264,12 @@ private:
static Action* aquatic_form(PlayerbotAI* botAI) { return new CastAquaticFormAction(botAI); } static Action* aquatic_form(PlayerbotAI* botAI) { return new CastAquaticFormAction(botAI); }
static Action* caster_form(PlayerbotAI* botAI) { return new CastCasterFormAction(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_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* mangle_bear(PlayerbotAI* botAI) { return new CastMangleBearAction(botAI); }
static Action* maul(PlayerbotAI* botAI) { return new CastMaulAction(botAI); } static Action* maul(PlayerbotAI* botAI) { return new CastMaulAction(botAI); }
static Action* bash(PlayerbotAI* botAI) { return new CastBashAction(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( triggers.push_back(
new TriggerNode( new TriggerNode(
"combo points available", "combo points 5 available",
{ {
NextAction("rip", ACTION_HIGH + 6) NextAction("rip", ACTION_HIGH + 6)
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -15,9 +15,6 @@ public:
{ {
creators["sanctity aura"] = &sanctity_aura; creators["sanctity aura"] = &sanctity_aura;
creators["retribution aura"] = &retribution_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["blessing of might"] = &blessing_of_might;
creators["crusader strike"] = &crusader_strike; creators["crusader strike"] = &crusader_strike;
creators["repentance"] = &repentance; creators["repentance"] = &repentance;
@ -27,36 +24,6 @@ public:
} }
private: 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) static ActionNode* blessing_of_might([[maybe_unused]] PlayerbotAI* botAI)
{ {
return new ActionNode( return new ActionNode(

View File

@ -19,14 +19,15 @@ void GenericPaladinNonCombatStrategy::InitTriggers(std::vector<TriggerNode*>& tr
NonCombatStrategy::InitTriggers(triggers); NonCombatStrategy::InitTriggers(triggers);
triggers.push_back(new TriggerNode("party member dead", { NextAction("redemption", ACTION_CRITICAL_HEAL + 10) })); 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 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", 26.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", 27.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", 28.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()); int specTab = AiFactory::GetPlayerSpecTab(botAI->GetBot());
if (specTab == 0 || specTab == 1) // Holy or Protection if (specTab == PALADIN_TAB_HOLY || specTab == PALADIN_TAB_PROTECTION)
triggers.push_back(new TriggerNode("often", { NextAction("apply oil", 1.0f) })); triggers.push_back(new TriggerNode("often", { NextAction("apply oil", ACTION_IDLE + 1.0f) }));
if (specTab == 2) // Retribution if (specTab == PALADIN_TAB_RETRIBUTION)
triggers.push_back(new TriggerNode("often", { NextAction("apply stone", 1.0f) })); 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 magic"] = &cleanse_magic;
creators["cleanse poison on party"] = &cleanse_poison_on_party; creators["cleanse poison on party"] = &cleanse_poison_on_party;
creators["cleanse disease on party"] = &cleanse_disease_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 wisdom"] = &seal_of_wisdom;
creators["seal of justice"] = &seal_of_justice; creators["seal of justice"] = &seal_of_justice;
creators["hand of reckoning"] = &hand_of_reckoning; 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 wisdom on party"] = &blessing_of_wisdom_on_party;
creators["blessing of sanctuary on party"] = &blessing_of_sanctuary_on_party; creators["blessing of sanctuary on party"] = &blessing_of_sanctuary_on_party;
creators["blessing of sanctuary"] = &blessing_of_sanctuary; creators["blessing of sanctuary"] = &blessing_of_sanctuary;
creators["seal of command"] = &seal_of_command;
creators["taunt spell"] = &hand_of_reckoning; creators["taunt spell"] = &hand_of_reckoning;
creators["righteous defense"] = &righteous_defense; creators["righteous defense"] = &righteous_defense;
creators["avenger's shield"] = &avengers_shield; creators["avenger's shield"] = &avengers_shield;
@ -155,18 +157,39 @@ private:
/*A*/ { NextAction("purify disease on party") }, /*A*/ { NextAction("purify disease on party") },
/*C*/ {}); /*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 */) static ActionNode* seal_of_wisdom(PlayerbotAI* /* ai */)
{ {
return new ActionNode ("seal of wisdom", return new ActionNode ("seal of wisdom",
/*P*/ {}, /*P*/ {},
/*A*/ { NextAction("seal of righteousness") }, /*A*/ { NextAction("seal of corruption") },
/*C*/ {}); /*C*/ {});
} }
static ActionNode* seal_of_justice(PlayerbotAI* /* ai */) static ActionNode* seal_of_justice(PlayerbotAI* /* ai */)
{ {
return new ActionNode("seal of justice", return new ActionNode("seal of justice",
/*P*/ {}, /*P*/ {},
/*A*/ { NextAction("seal of righteousness") }, /*A*/ { NextAction("seal of corruption") },
/*C*/ {}); /*C*/ {});
} }
static ActionNode* hand_of_reckoning(PlayerbotAI* /* ai */) static ActionNode* hand_of_reckoning(PlayerbotAI* /* ai */)
@ -246,13 +269,6 @@ private:
/*A*/ {}, /*A*/ {},
/*C*/ {}); /*C*/ {});
} }
static ActionNode* seal_of_command(PlayerbotAI* /* ai */)
{
return new ActionNode("seal of command",
/*P*/ {},
/*A*/ { NextAction("seal of righteousness") },
/*C*/ {});
}
}; };
#endif #endif

View File

@ -30,7 +30,7 @@ void HealPaladinStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
new TriggerNode( new TriggerNode(
"seal", "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", return SpellTrigger::IsActive() && !botAI->HasAnyAuraOf(target, "blessing of might", "blessing of wisdom",
"blessing of kings", "blessing of sanctuary", nullptr); "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: public:
BlessingOnPartyTrigger(PlayerbotAI* botAI) 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 class BlessingTrigger : public BuffTrigger
@ -93,7 +91,8 @@ public:
class HammerOfJusticeInterruptSpellTrigger : public InterruptSpellTrigger class HammerOfJusticeInterruptSpellTrigger : public InterruptSpellTrigger
{ {
public: public:
HammerOfJusticeInterruptSpellTrigger(PlayerbotAI* botAI) : InterruptSpellTrigger(botAI, "hammer of justice") {} HammerOfJusticeInterruptSpellTrigger(PlayerbotAI* botAI)
: InterruptSpellTrigger(botAI, "hammer of justice") {}
}; };
class HammerOfJusticeSnareTrigger : public SnareTargetTrigger class HammerOfJusticeSnareTrigger : public SnareTargetTrigger
@ -144,9 +143,7 @@ class CleanseCurePartyMemberDiseaseTrigger : public PartyMemberNeedCureTrigger
{ {
public: public:
CleanseCurePartyMemberDiseaseTrigger(PlayerbotAI* botAI) CleanseCurePartyMemberDiseaseTrigger(PlayerbotAI* botAI)
: PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_DISEASE) : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_DISEASE) {}
{
}
}; };
class CleanseCurePoisonTrigger : public NeedCureTrigger class CleanseCurePoisonTrigger : public NeedCureTrigger
@ -159,9 +156,7 @@ class CleanseCurePartyMemberPoisonTrigger : public PartyMemberNeedCureTrigger
{ {
public: public:
CleanseCurePartyMemberPoisonTrigger(PlayerbotAI* botAI) CleanseCurePartyMemberPoisonTrigger(PlayerbotAI* botAI)
: PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_POISON) : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_POISON) {}
{
}
}; };
class CleanseCureMagicTrigger : public NeedCureTrigger class CleanseCureMagicTrigger : public NeedCureTrigger
@ -173,15 +168,15 @@ public:
class CleanseCurePartyMemberMagicTrigger : public PartyMemberNeedCureTrigger class CleanseCurePartyMemberMagicTrigger : public PartyMemberNeedCureTrigger
{ {
public: public:
CleanseCurePartyMemberMagicTrigger(PlayerbotAI* botAI) : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_MAGIC) CleanseCurePartyMemberMagicTrigger(PlayerbotAI* botAI)
{ : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_MAGIC) {}
}
}; };
class HammerOfJusticeEnemyHealerTrigger : public InterruptEnemyHealerTrigger class HammerOfJusticeEnemyHealerTrigger : public InterruptEnemyHealerTrigger
{ {
public: public:
HammerOfJusticeEnemyHealerTrigger(PlayerbotAI* botAI) : InterruptEnemyHealerTrigger(botAI, "hammer of justice") {} HammerOfJusticeEnemyHealerTrigger(PlayerbotAI* botAI)
: InterruptEnemyHealerTrigger(botAI, "hammer of justice") {}
}; };
class DivineFavorTrigger : public BuffTrigger class DivineFavorTrigger : public BuffTrigger
@ -190,6 +185,14 @@ public:
DivineFavorTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "divine favor") {} 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 class TurnUndeadTrigger : public HasCcTargetTrigger
{ {
public: public:
@ -201,7 +204,8 @@ DEBUFF_TRIGGER(AvengerShieldTrigger, "avenger's shield");
class BeaconOfLightOnMainTankTrigger : public BuffOnMainTankTrigger class BeaconOfLightOnMainTankTrigger : public BuffOnMainTankTrigger
{ {
public: public:
BeaconOfLightOnMainTankTrigger(PlayerbotAI* ai) : BuffOnMainTankTrigger(ai, "beacon of light", true) {} BeaconOfLightOnMainTankTrigger(PlayerbotAI* ai)
: BuffOnMainTankTrigger(ai, "beacon of light", true) {}
}; };
class SacredShieldOnMainTankTrigger : public BuffOnMainTankTrigger class SacredShieldOnMainTankTrigger : public BuffOnMainTankTrigger
@ -213,34 +217,29 @@ public:
class BlessingOfKingsOnPartyTrigger : public BuffOnPartyTrigger class BlessingOfKingsOnPartyTrigger : public BuffOnPartyTrigger
{ {
public: 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 class BlessingOfWisdomOnPartyTrigger : public BuffOnPartyTrigger
{ {
public: public:
BlessingOfWisdomOnPartyTrigger(PlayerbotAI* botAI) 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 class BlessingOfMightOnPartyTrigger : public BuffOnPartyTrigger
{ {
public: public:
BlessingOfMightOnPartyTrigger(PlayerbotAI* botAI) 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 class BlessingOfSanctuaryOnPartyTrigger : public BuffOnPartyTrigger
{ {
public: public:
BlessingOfSanctuaryOnPartyTrigger(PlayerbotAI* botAI) BlessingOfSanctuaryOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000) : BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000) {}
{
}
}; };
class AvengingWrathTrigger : public BoostTrigger class AvengingWrathTrigger : public BoostTrigger
@ -248,4 +247,5 @@ class AvengingWrathTrigger : public BoostTrigger
public: public:
AvengingWrathTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "avenging wrath") {} AvengingWrathTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "avenging wrath") {}
}; };
#endif #endif

View File

@ -61,7 +61,7 @@ bool CastEnvenomAction::isUseful()
bool CastEnvenomAction::isPossible() bool CastEnvenomAction::isPossible()
{ {
// alternate to eviscerate if talents unlearned // 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() bool CastTricksOfTheTradeOnMainTankAction::isUseful()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,6 @@
#include "BroadcastHelper.h" #include "BroadcastHelper.h"
#include "ChatHelper.h" #include "ChatHelper.h"
#include "Creature.h" #include "Creature.h"
#include "FlightMasterCache.h"
#include "G3D/Vector2.h" #include "G3D/Vector2.h"
#include "GameObject.h" #include "GameObject.h"
#include "GossipDef.h" #include "GossipDef.h"
@ -856,7 +855,7 @@ bool NewRpgBaseAction::GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector
WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot) 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 hiRange = 500.0f;
float loRange = 2500.0f; float loRange = 2500.0f;
if (bot->GetLevel() < 5) if (bot->GetLevel() < 5)
@ -914,9 +913,7 @@ WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot)
WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot) WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot)
{ {
const std::vector<WorldLocation>& locs = IsAlliance(bot->getRace()) const std::vector<WorldLocation> locs = sTravelMgr.GetTravelHubs(bot);
? sRandomPlayerbotMgr.allianceStarterPerLevelCache[bot->GetLevel()]
: sRandomPlayerbotMgr.hordeStarterPerLevelCache[bot->GetLevel()];
bool inCity = false; bool inCity = false;
@ -957,70 +954,19 @@ WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot)
return dest; 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); flightMaster = sTravelMgr.GetNearestFlightMasterGuid(bot);
if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster) > 500.0f) if (!flightMaster)
return false; return false;
fromNode = sObjectMgr->GetNearestTaxiNode(nearestFlightMaster->GetPositionX(), nearestFlightMaster->GetPositionY(), std::vector<std::vector<uint32>> availablePaths = sTravelMgr.GetOptimalFlightDestinations(bot);
nearestFlightMaster->GetPositionZ(), nearestFlightMaster->GetMapId(), if (availablePaths.empty())
bot->GetTeamId());
if (!fromNode)
return false; return false;
std::vector<uint32> availableToNodes; path = availablePaths[urand(0, availablePaths.size() - 1)];
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)];
LOG_DEBUG("playerbots", "[New RPG] Bot {} select random flight taxi node from:{} (node {}) to:{} ({} available)", 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; return true;
} }
@ -1121,10 +1067,10 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector<NewRpgStatus> candidateSta
case RPG_TRAVEL_FLIGHT: case RPG_TRAVEL_FLIGHT:
{ {
ObjectGuid flightMaster; ObjectGuid flightMaster;
uint32 fromNode, toNode; std::vector<uint32> path;
if (SelectRandomFlightTaxiNode(flightMaster, fromNode, toNode)) if (SelectRandomFlightTaxiNode(flightMaster, path))
{ {
botAI->rpgInfo.ChangeToTravelFlight(flightMaster, fromNode, toNode); botAI->rpgInfo.ChangeToTravelFlight(flightMaster, path);
return true; return true;
} }
return false; return false;
@ -1197,8 +1143,8 @@ bool NewRpgBaseAction::CheckRpgStatusAvailable(NewRpgStatus status)
case RPG_TRAVEL_FLIGHT: case RPG_TRAVEL_FLIGHT:
{ {
ObjectGuid flightMaster; ObjectGuid flightMaster;
uint32 fromNode, toNode; std::vector<uint32> path;
return SelectRandomFlightTaxiNode(flightMaster, fromNode, toNode); return SelectRandomFlightTaxiNode(flightMaster, path);
} }
default: default:
return false; return false;

View File

@ -54,7 +54,7 @@ protected:
bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo>& poiInfo, bool toComplete = false); bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo>& poiInfo, bool toComplete = false);
static WorldPosition SelectRandomGrindPos(Player* bot); static WorldPosition SelectRandomGrindPos(Player* bot);
static WorldPosition SelectRandomCampPos(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 RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
bool CheckRpgStatusAvailable(NewRpgStatus status); bool CheckRpgStatusAvailable(NewRpgStatus status);

View File

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

View File

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

View File

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

View File

@ -1119,6 +1119,9 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet)
if (guid1.IsEmpty() || p.size() > p.DEFAULT_SIZE) if (guid1.IsEmpty() || p.size() > p.DEFAULT_SIZE)
return; return;
if (lang == LANG_ADDON)
return;
if (p.GetOpcode() == SMSG_GM_MESSAGECHAT) if (p.GetOpcode() == SMSG_GM_MESSAGECHAT)
{ {
p >> textLen; p >> textLen;
@ -1168,8 +1171,6 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet)
if (HasRealPlayerMaster() && guid1 != GetMaster()->GetGUID()) if (HasRealPlayerMaster() && guid1 != GetMaster()->GetGUID())
return; return;
if (lang == LANG_ADDON)
return;
if (message.starts_with(sPlayerbotAIConfig.toxicLinksPrefix) && if (message.starts_with(sPlayerbotAIConfig.toxicLinksPrefix) &&
(GetChatHelper()->ExtractAllItemIds(message).size() > 0 || (GetChatHelper()->ExtractAllItemIds(message).size() > 0 ||
@ -1249,17 +1250,10 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet)
p >> guid.ReadAsPacked() >> counter >> vcos >> vsin >> horizontalSpeed >> verticalSpeed; p >> guid.ReadAsPacked() >> counter >> vcos >> vsin >> horizontalSpeed >> verticalSpeed;
if (horizontalSpeed <= 0.1f) if (horizontalSpeed <= 0.1f)
{
horizontalSpeed = 0.11f; horizontalSpeed = 0.11f;
}
verticalSpeed = -verticalSpeed; 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->StopMoving();
bot->GetMotionMaster()->Clear(); bot->GetMotionMaster()->Clear();
@ -6486,7 +6480,7 @@ ChatChannelSource PlayerbotAI::GetChatChannelSource(Player* bot, uint32 type, st
return ChatChannelSource::SRC_UNDEFINED; 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) if (player->GetLevel() > 16)
return true; return true;

View File

@ -556,7 +556,7 @@ public:
bool IsSafe(WorldObject* obj); bool IsSafe(WorldObject* obj);
ChatChannelSource GetChatChannelSource(Player* bot, uint32 type, std::string channelName); 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) bool HasCheat(BotCheatMask mask)
{ {

View File

@ -23,7 +23,6 @@
#include "DatabaseEnv.h" #include "DatabaseEnv.h"
#include "Define.h" #include "Define.h"
#include "FleeManager.h" #include "FleeManager.h"
#include "FlightMasterCache.h"
#include "GridNotifiers.h" #include "GridNotifiers.h"
#include "LFGMgr.h" #include "LFGMgr.h"
#include "MapMgr.h" #include "MapMgr.h"
@ -47,9 +46,7 @@
#include "World.h" #include "World.h"
#include "Cell.h" #include "Cell.h"
#include "GridNotifiers.h" #include "GridNotifiers.h"
// Required for Cell because of poor AC implementation
#include "CellImpl.h" #include "CellImpl.h"
// Required for GridNotifiers because of poor AC implementation
#include "GridNotifiersImpl.h" #include "GridNotifiersImpl.h"
struct GuidClassRaceInfo struct GuidClassRaceInfo
@ -59,48 +56,6 @@ struct GuidClassRaceInfo
uint32 rRace; 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 PrintStatsThread() { sRandomPlayerbotMgr.PrintStats(); }
void activatePrintStatsThread() void activatePrintStatsThread()
@ -1718,7 +1673,7 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector<WorldLocation>&
z = 0.05f + ground; z = 0.05f + ground;
if (!botAI->CheckLocationDistanceByLevel(bot, loc, true)) if (!botAI->StarterLevelDistanceCheck(bot, loc, true))
continue; continue;
const LocaleConstant& locale = sWorld->GetDefaultDbcLocale(); const LocaleConstant& locale = sWorld->GetDefaultDbcLocale();
@ -1762,329 +1717,6 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector<WorldLocation>&
// tlocs.size()); // 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() void RandomPlayerbotMgr::PrepareAddclassCache()
{ {
// Using accounts marked as type 2 (AddClass) // Using accounts marked as type 2 (AddClass)
@ -2125,11 +1757,6 @@ void RandomPlayerbotMgr::Init()
if (sPlayerbotAIConfig.addClassCommand) if (sPlayerbotAIConfig.addClassCommand)
sRandomPlayerbotMgr.PrepareAddclassCache(); sRandomPlayerbotMgr.PrepareAddclassCache();
if (sPlayerbotAIConfig.enabled)
{
sRandomPlayerbotMgr.PrepareTeleportCache();
}
if (sPlayerbotAIConfig.randomBotJoinBG) if (sPlayerbotAIConfig.randomBotJoinBG)
sRandomPlayerbotMgr.LoadBattleMastersCache(); sRandomPlayerbotMgr.LoadBattleMastersCache();
@ -2141,103 +1768,17 @@ void RandomPlayerbotMgr::RandomTeleportForLevel(Player* bot)
if (bot->InBattleground()) if (bot->InBattleground())
return; return;
uint32 level = bot->GetLevel(); std::vector<WorldLocation> locs = sTravelMgr.GetCityLocations(bot);
uint8 race = bot->getRace(); if (!locs.empty())
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> fallbackLocs; RandomTeleport(bot, locs, true);
for (auto& bLoc : bankerLocsPerLevelCache[level]) return;
fallbackLocs.push_back(bLoc.loc);
if (!sPlayerbotAIConfig.enableWeightTeleToCityBankers)
{
RandomTeleport(bot, fallbackLocs, true);
return;
}
// Collect valid cities based on bot faction.
std::unordered_set<CityId> validBankerCities;
for (auto& loc : bankerLocsPerLevelCache[level])
{
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);
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 locs = sTravelMgr.GetTeleportLocations(bot);
if (!locs.empty())
{ {
RandomTeleport(bot, *locs); RandomTeleport(bot, locs, false);
return;
} }
} }
@ -2246,17 +1787,11 @@ void RandomPlayerbotMgr::RandomTeleportGrindForLevel(Player* bot)
if (bot->InBattleground()) if (bot->InBattleground())
return; return;
uint32 level = bot->GetLevel(); std::vector<WorldLocation> locs = sTravelMgr.GetTeleportLocations(bot);
uint8 race = bot->getRace();
std::vector<WorldLocation>* locs = nullptr;
if (sPlayerbotAIConfig.enableNewRpgStrategy)
locs = IsAlliance(race) ? &allianceStarterPerLevelCache[level] : &hordeStarterPerLevelCache[level];
else
locs = &locsPerLevelCache[level];
LOG_DEBUG("playerbots", "Random teleporting bot {} for level {} ({} locations available)", bot->GetName().c_str(), 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) void RandomPlayerbotMgr::RandomTeleport(Player* bot)

View File

@ -164,25 +164,8 @@ public:
static uint8 GetTeamClassIdx(bool isAlliance, uint8 claz) { return isAlliance * 20 + claz; } static uint8 GetTeamClassIdx(bool isAlliance, uint8 claz) { return isAlliance * 20 + claz; }
void PrepareAddclassCache(); void PrepareAddclassCache();
void PrepareZone2LevelBracket();
void PrepareTeleportCache();
void Init(); void Init();
std::map<uint8, std::unordered_set<ObjectGuid>> addclassCache; 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 // Account type management
void AssignAccountTypes(); 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; 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 class FindItemsByQualityVisitor : public IterateItemsVisitor
{ {
public: public:

View File

@ -8,6 +8,10 @@
#include <iomanip> #include <iomanip>
#include <numeric> #include <numeric>
#include "Creature.h"
#include "Log.h"
#include "ObjectAccessor.h"
#include "TravelNode.h"
#include "Talentspec.h" #include "Talentspec.h"
#include "ChatHelper.h" #include "ChatHelper.h"
#include "MMapFactory.h" #include "MMapFactory.h"
@ -22,6 +26,71 @@
#include "Corpse.h" #include "Corpse.h"
#include "CellImpl.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) WorldPosition::WorldPosition(std::string const str)
{ {
std::vector<std::string> tokens = split(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 #define _PLAYERBOT_TRAVELMGR_H
#include <boost/functional/hash.hpp> #include <boost/functional/hash.hpp>
#include <map>
#include <random> #include <random>
#include "AiObject.h" #include "AiObject.h"
@ -15,6 +16,7 @@
#include "GridDefines.h" #include "GridDefines.h"
#include "PlayerbotAIConfig.h" #include "PlayerbotAIConfig.h"
class Creature;
class GuidPosition; class GuidPosition;
class ObjectGuid; class ObjectGuid;
class Quest; class Quest;
@ -854,6 +856,16 @@ public:
void Clear(); void Clear();
void LoadQuestTravelTable(); 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> template <class D, class W, class URBG>
void weighted_shuffle(D first, D last, W first_weight, W last_weight, URBG&& g) void weighted_shuffle(D first, D last, W first_weight, W last_weight, URBG&& g)
{ {
@ -943,6 +955,37 @@ private:
TravelMgr(TravelMgr&&) = delete; TravelMgr(TravelMgr&&) = delete;
TravelMgr& operator=(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 #endif

View File

@ -7,6 +7,7 @@
#include <iomanip> #include <iomanip>
#include <regex> #include <regex>
#include <unordered_set>
#include "BudgetValues.h" #include "BudgetValues.h"
#include "PathGenerator.h" #include "PathGenerator.h"
@ -2447,3 +2448,127 @@ WorldPosition TravelNodeMap::getMapOffset(uint32 mapId)
return WorldPosition(mapId, 0, 0, 0, 0); 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(); void calcMapOffset();
WorldPosition getMapOffset(uint32 mapId); 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::shared_timed_mutex m_nMapMtx;
std::unordered_map<ObjectGuid, std::unordered_map<uint32, TravelNode*>> teleportNodes; std::unordered_map<ObjectGuid, std::unordered_map<uint32, TravelNode*>> teleportNodes;
@ -593,6 +597,16 @@ private:
TravelNodeMap(TravelNodeMap&&) = delete; TravelNodeMap(TravelNodeMap&&) = delete;
TravelNodeMap& operator=(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<TravelNode*> m_nodes;
std::vector<std::pair<uint32, WorldPosition>> mapOffsets; std::vector<std::pair<uint32, WorldPosition>> mapOffsets;

View File

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

View File

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

View File

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