Compare commits

..

10 Commits

Author SHA1 Message Date
kadeshar
86390f90fd
- Fixed bug with not respecting InstantFlightPaths config (#1410) 2025-07-06 19:38:15 +08:00
kadeshar
3f39a57fe2
Merge pull request #1426 from NoxMax/delete-all-the-orphans
Fix: DeleteRandomBotAccounts sometimes leaves orphans. Also fix accumulating orphan pet data in DB
2025-07-06 09:51:31 +02:00
kadeshar
102aa24bb8
Merge pull request #1425 from liyunfan1223/rpg_gear_incremental
Added config to make rndbots only get gear from looting/quests
2025-07-06 09:50:48 +02:00
kadeshar
50792e5646
Merge pull request #1422 from ThePenguinMan96/Warlock-Ranged-Designation/DPS-Strat-cleanup
Warlock Ranged Designation/DPS Strategy Cleanup
2025-07-06 09:50:24 +02:00
NoxMax
78f4bd6d29
Ensuring data orphans are deleted 2025-07-05 18:59:30 -06:00
Yunfan Li
b558e86df0 correct typo in conf 2025-07-05 22:03:31 +08:00
Yunfan Li
f0c6aaff0b Random bots gear related enhancements 2025-07-05 20:29:34 +08:00
ThePenguinMan96
1a20d549fe Eureka!
I re-implemented the pet strategies into the "general" strategy area of the WarlockAiObjectContext, and it worked!!! Finally! The issue before was they were under the "Buff" area of the aiobjectcontext, which can only have 1 active at any given time - this is why the soulstone strategy and the pet strategy were cancelling out each other. Now, pets are summoned via a non-combat strategy that is assigned with aifactory by spec!
2025-07-04 22:44:17 -07:00
ThePenguinMan96
1c69490290 Added range checks for running away if too close
Added range checks for running away if too close - The warlocks were happily standing in cleave range and dieing. Now, they will backpedal if too close to an enemy. I also added custom logic to check if in demonology form before backpedaling. I also removed shadow cleave, as the dps increase with negligible (0.1%) and it was causing errors in the actions, resulting in the warlock standing idle in combat.
2025-07-04 11:33:46 -07:00
ThePenguinMan96
59af34809c Warlock Ranged Designation/DPS Strategy Cleanup
Hello community,

This PR focuses on 4 things:

Recognizing the Warlock as a "ranged" bot, so they will follow ranged commands and strategies, in GenericWarlockStrategy.h:
uint32 GetType() const override { return CombatStrategy::GetType() | STRATEGY_TYPE_RANGED | STRATEGY_TYPE_DPS; }

Cleanup/deletion of the DpsWarlockStrategy.cpp and .h (no longer used or referenced anywhere)

Fixes soulstone logic so multiple Warlocks don't soulstone the same target, and don't try to soulstone a target that is too far away or out of line of sight (WarlockActions.cpp)

Moved summoning of pets to the main non-combat strategy inittriggers:

// Pet-summoning triggers based on spec
    if (tab == 0)  // Affliction
    {
        triggers.push_back(new TriggerNode("no pet", NextAction::array(0, new NextAction("summon felhunter", 29.0f), nullptr)));
    }
    else if (tab == 1)  // Demonology
    {
        triggers.push_back(new TriggerNode("no pet", NextAction::array(0, new NextAction("summon felguard", 29.0f), nullptr)));
    }
    else if (tab == 2)  // Destruction
    {
        triggers.push_back(new TriggerNode("no pet", NextAction::array(0, new NextAction("summon imp", 29.0f), nullptr)));
    }
2025-07-04 03:32:29 -07:00
29 changed files with 374 additions and 268 deletions

View File

@ -656,6 +656,10 @@ AiPlayerbot.RandomGearQualityLimit = 3
# Default: 0 (no limit)
AiPlayerbot.RandomGearScoreLimit = 0
# If disabled, random bots can only upgrade equipment through looting and quests
# Default: 1 (enabled)
AiPlayerbot.IncrementalGearInit = 1
# Set minimum level of bots that will enchant their equipment (Maxlevel + 1 to disable)
# Default: 60
AiPlayerbot.MinEnchantingBotLevel = 60

View File

@ -80,13 +80,13 @@ uint8 AiFactory::GetPlayerSpecTab(Player* bot)
switch (bot->getClass())
{
case CLASS_MAGE:
tab = 1;
tab = MAGE_TAB_FROST;
break;
case CLASS_PALADIN:
tab = 2;
tab = PALADIN_TAB_RETRIBUTION;
break;
case CLASS_PRIEST:
tab = 1;
tab = PRIEST_TAB_HOLY;
break;
}

View File

@ -117,6 +117,8 @@ bool PlayerbotAIConfig::Initialize()
tellWhenAvoidAoe = sConfigMgr->GetOption<bool>("AiPlayerbot.TellWhenAvoidAoe", false);
randomGearLoweringChance = sConfigMgr->GetOption<float>("AiPlayerbot.RandomGearLoweringChance", 0.0f);
incrementalGearInit = sConfigMgr->GetOption<bool>("AiPlayerbot.IncrementalGearInit", true);
randomGearQualityLimit = sConfigMgr->GetOption<int32>("AiPlayerbot.RandomGearQualityLimit", 3);
randomGearScoreLimit = sConfigMgr->GetOption<int32>("AiPlayerbot.RandomGearScoreLimit", 0);

View File

@ -87,6 +87,7 @@ public:
std::vector<uint32> randomBotQuestIds;
uint32 randomBotTeleportDistance;
float randomGearLoweringChance;
bool incrementalGearInit;
int32 randomGearQualityLimit;
int32 randomGearScoreLimit;
float randomBotMinLevelChance, randomBotMaxLevelChance;

View File

@ -486,6 +486,13 @@ void RandomPlayerbotFactory::CreateRandomBots()
CharacterDatabase.Execute("DELETE FROM characters WHERE account IN (SELECT id FROM " + loginDBName + ".account WHERE username LIKE '{}%%')",
sPlayerbotAIConfig->randomBotAccountPrefix.c_str());
// Wait for the characters to be deleted before proceeding to dependent deletes
while (CharacterDatabase.QueueSize())
{
std::this_thread::sleep_for(1s);
}
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Extra 100ms fixed delay for safety.
// Clean up orphaned entries in playerbots_guild_tasks
PlayerbotsDatabase.Execute("DELETE FROM playerbots_guild_tasks WHERE owner NOT IN (SELECT guid FROM " + characterDBName + ".characters)");
@ -500,11 +507,13 @@ void RandomPlayerbotFactory::CreateRandomBots()
CharacterDatabase.Execute("DELETE FROM character_achievement WHERE guid NOT IN (SELECT guid FROM characters)");
CharacterDatabase.Execute("DELETE FROM character_achievement_progress WHERE guid NOT IN (SELECT guid FROM characters)");
CharacterDatabase.Execute("DELETE FROM character_action WHERE guid NOT IN (SELECT guid FROM characters)");
CharacterDatabase.Execute("DELETE FROM character_arena_stats WHERE guid NOT IN (SELECT guid FROM characters)");
CharacterDatabase.Execute("DELETE FROM character_aura WHERE guid NOT IN (SELECT guid FROM characters)");
CharacterDatabase.Execute("DELETE FROM character_entry_point WHERE guid NOT IN (SELECT guid FROM characters)");
CharacterDatabase.Execute("DELETE FROM character_glyphs WHERE guid NOT IN (SELECT guid FROM characters)");
CharacterDatabase.Execute("DELETE FROM character_homebind WHERE guid NOT IN (SELECT guid FROM characters)");
CharacterDatabase.Execute("DELETE FROM item_instance WHERE owner_guid NOT IN (SELECT guid FROM characters) AND owner_guid > 0");
CharacterDatabase.Execute("DELETE FROM character_inventory WHERE guid NOT IN (SELECT guid FROM characters)");
CharacterDatabase.Execute("DELETE FROM item_instance WHERE owner_guid NOT IN (SELECT guid FROM characters) AND owner_guid > 0");
// Clean up pet data
CharacterDatabase.Execute("DELETE FROM character_pet WHERE owner NOT IN (SELECT guid FROM characters)");
@ -559,11 +568,23 @@ void RandomPlayerbotFactory::CreateRandomBots()
uint32 timer = getMSTime();
// After ALL deletions, make sure data is commited to DB
LoginDatabase.Execute("COMMIT");
CharacterDatabase.Execute("COMMIT");
PlayerbotsDatabase.Execute("COMMIT");
// Wait for all pending database operations to complete
while (LoginDatabase.QueueSize() || CharacterDatabase.QueueSize() || PlayerbotsDatabase.QueueSize())
{
std::this_thread::sleep_for(1s);
}
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Extra 100ms fixed delay for safety.
// Flush tables to ensure all data in memory are written to disk
LoginDatabase.Execute("FLUSH TABLES");
CharacterDatabase.Execute("FLUSH TABLES");
PlayerbotsDatabase.Execute("FLUSH TABLES");
LOG_INFO("playerbots", ">> Random bot accounts and data deleted in {} ms", GetMSTimeDiffToNow(timer));
LOG_INFO("playerbots", "Please reset the AiPlayerbot.DeleteRandomBotAccounts to 0 and restart the server...");
World::StopNow(SHUTDOWN_EXIT_CODE);

View File

@ -181,6 +181,11 @@ RandomPlayerbotMgr::RandomPlayerbotMgr() : PlayerbotHolder(), processTicks(0)
BattlegroundData.clear(); // Clear here and here only.
// Cleanup on server start: orphaned pet data that's often left behind by bot pets that no longer exist in the DB
CharacterDatabase.Execute("DELETE FROM pet_aura WHERE guid NOT IN (SELECT id FROM character_pet)");
CharacterDatabase.Execute("DELETE FROM pet_spell WHERE guid NOT IN (SELECT id FROM character_pet)");
CharacterDatabase.Execute("DELETE FROM pet_spell_cooldown WHERE guid NOT IN (SELECT id FROM character_pet)");
for (int bracket = BG_BRACKET_ID_FIRST; bracket < MAX_BATTLEGROUND_BRACKETS; ++bracket)
{
for (int queueType = BATTLEGROUND_QUEUE_AV; queueType < MAX_BATTLEGROUND_QUEUE_TYPES; ++queueType)
@ -369,7 +374,7 @@ void RandomPlayerbotMgr::UpdateAIInternal(uint32 elapsed, bool /*minimal*/)
DelayLoginBotsTimer = time(nullptr) + sPlayerbotAIConfig->disabledWithoutRealPlayerLoginDelay;
}
}
else
else
{
if (DelayLoginBotsTimer)
{
@ -515,7 +520,7 @@ uint32 RandomPlayerbotMgr::AddRandomBots()
{
maxAllowedBotCount -= currentBots.size();
maxAllowedBotCount = std::min(sPlayerbotAIConfig->randomBotsPerInterval, maxAllowedBotCount);
uint32 totalRatio = sPlayerbotAIConfig->randomBotAllianceRatio + sPlayerbotAIConfig->randomBotHordeRatio;
uint32 allowedAllianceCount = maxAllowedBotCount * (sPlayerbotAIConfig->randomBotAllianceRatio) / totalRatio;
@ -536,7 +541,7 @@ uint32 RandomPlayerbotMgr::AddRandomBots()
{
// minus addclass bots account
int32 baseAccount = RandomPlayerbotFactory::CalculateTotalAccountCount() - sPlayerbotAIConfig->addClassAccountPoolSize;
if (baseAccount <= 0 || baseAccount > sPlayerbotAIConfig->randomBotAccounts.size())
{
LOG_ERROR("playerbots", "Account calculation error with PeriodicOnlineOffline");
@ -551,7 +556,7 @@ uint32 RandomPlayerbotMgr::AddRandomBots()
PreparedQueryResult result = CharacterDatabase.Query(stmt);
if (!result)
continue;
std::vector<GuidClassRaceInfo> allGuidInfos;
do {
@ -719,7 +724,7 @@ void RandomPlayerbotMgr::CheckBgQueue()
LOG_DEBUG("playerbots", "Checking BG Queue...");
// Initialize Battleground Data (do not clear here)
for (int bracket = BG_BRACKET_ID_FIRST; bracket < MAX_BATTLEGROUND_BRACKETS; ++bracket)
{
for (int queueType = BATTLEGROUND_QUEUE_AV; queueType < MAX_BATTLEGROUND_QUEUE_TYPES; ++queueType)
@ -793,11 +798,11 @@ void RandomPlayerbotMgr::CheckBgQueue()
{
std::vector<uint32>* instanceIds = nullptr;
uint32 instanceId = player->GetBattleground()->GetInstanceID();
instanceIds = &BattlegroundData[queueTypeId][bracketId].bgInstances;
if (instanceIds && std::find(instanceIds->begin(), instanceIds->end(), instanceId) == instanceIds->end())
instanceIds->push_back(instanceId);
BattlegroundData[queueTypeId][bracketId].bgInstanceCount = instanceIds->size();
}
}
@ -1478,7 +1483,7 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector<WorldLocation>&
z = 0.05f + ground;
PlayerInfo const* pInfo = sObjectMgr->GetPlayerInfo(bot->getRace(true), bot->getClass());
float dis = loc.GetExactDist(pInfo->positionX, pInfo->positionY, pInfo->positionZ);
// yunfan: distance check for low level
if (bot->GetLevel() <= 4 && (loc.GetMapId() != pInfo->mapId || dis > 500.0f))
{
@ -1606,8 +1611,8 @@ void RandomPlayerbotMgr::PrepareZone2LevelBracket()
zone2LevelBracket[2817] = {77, 80}; // Crystalsong Forest
zone2LevelBracket[3537] = {68, 75}; // Borean Tundra
zone2LevelBracket[3711] = {75, 80}; // Sholazar Basin
zone2LevelBracket[4197] = {79, 80}; // Wintergrasp
zone2LevelBracket[4197] = {79, 80}; // Wintergrasp
// Override with values from config
for (auto const& [zoneId, bracketPair] : sPlayerbotAIConfig->zoneBrackets) {
zone2LevelBracket[zoneId] = {bracketPair.first, bracketPair.second};
@ -1699,7 +1704,8 @@ void RandomPlayerbotMgr::PrepareTeleportCache()
"position_y, "
"position_z, "
"orientation, "
"t.faction "
"t.faction, "
"t.entry "
"FROM "
"creature c "
"INNER JOIN creature_template t on c.id1 = t.entry "
@ -1721,6 +1727,11 @@ void RandomPlayerbotMgr::PrepareTeleportCache()
float z = fields[3].Get<float>();
float orient = fields[4].Get<float>();
uint32 faction = fields[5].Get<uint32>();
uint32 tEntry = fields[6].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);
@ -1771,7 +1782,7 @@ void RandomPlayerbotMgr::PrepareTeleportCache()
}
LOG_INFO("playerbots", ">> {} innkeepers locations for level collected.", collected_locs);
}
results = WorldDatabase.Query(
"SELECT "
"map, "
@ -1788,13 +1799,8 @@ void RandomPlayerbotMgr::PrepareTeleportCache()
"AND t.npcflag != 135298 "
"AND t.minlevel != 55 "
"AND t.minlevel != 65 "
"AND t.faction != 35 "
"AND t.faction != 474 "
"AND t.faction != 69 "
"AND t.entry != 30606 "
"AND t.entry != 30608 "
"AND t.entry != 29282 "
"AND t.faction != 69 "
"AND t.faction not in (35, 474, 69, 57) "
"AND t.entry not in (30606, 30608, 29282) "
"AND map IN ({}) "
"ORDER BY "
"t.minlevel;",
@ -1877,12 +1883,12 @@ void RandomPlayerbotMgr::Init()
{
if (sPlayerbotAIConfig->addClassCommand)
sRandomPlayerbotMgr->PrepareAddclassCache();
if (sPlayerbotAIConfig->enabled)
{
sRandomPlayerbotMgr->PrepareTeleportCache();
}
if (sPlayerbotAIConfig->randomBotJoinBG)
sRandomPlayerbotMgr->LoadBattleMastersCache();
@ -2025,7 +2031,7 @@ void RandomPlayerbotMgr::RandomizeFirst(Player* bot)
if (sPlayerbotAIConfig->syncLevelWithPlayers)
maxLevel = std::max(sPlayerbotAIConfig->randomBotMinLevel,
std::min(playersLevel, sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)));
uint32 minLevel = sPlayerbotAIConfig->randomBotMinLevel;
if (bot->getClass() == CLASS_DEATH_KNIGHT)
{
@ -2622,7 +2628,7 @@ void RandomPlayerbotMgr::OnBotLoginInternal(Player* const bot)
{
LOG_INFO("playerbots", "{}/{} Bot {} logged in", playerBots.size(), sRandomPlayerbotMgr->GetMaxAllowedBotCount(),
bot->GetName().c_str());
if (playerBots.size() == sRandomPlayerbotMgr->GetMaxAllowedBotCount())
{
_isBotLogging = false;
@ -2757,14 +2763,19 @@ void RandomPlayerbotMgr::PrintStats()
std::map<uint8, uint32> perRace;
std::map<uint8, uint32> perClass;
std::map<uint8, uint32> lvlPerRace;
std::map<uint8, uint32> lvlPerClass;
for (uint8 race = RACE_HUMAN; race < MAX_RACES; ++race)
{
perRace[race] = 0;
lvlPerRace[race] = 0;
}
for (uint8 cls = CLASS_WARRIOR; cls < MAX_CLASSES; ++cls)
{
perClass[cls] = 0;
lvlPerClass[cls] = 0;
}
uint32 dps = 0;
@ -2802,6 +2813,9 @@ void RandomPlayerbotMgr::PrintStats()
++perRace[bot->getRace()];
++perClass[bot->getClass()];
lvlPerClass[bot->getClass()] += bot->GetLevel();
lvlPerRace[bot->getRace()] += bot->GetLevel();
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
if (botAI->AllowActivity())
++active;
@ -2858,7 +2872,7 @@ void RandomPlayerbotMgr::PrintStats()
++tank;
else
++dps;
zoneCount[bot->GetZoneId()]++;
if (sPlayerbotAIConfig->enableNewRpgStrategy)
@ -2869,7 +2883,7 @@ void RandomPlayerbotMgr::PrintStats()
}
}
LOG_INFO("playerbots", "Bots level:");
// uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL);
uint32_t currentAlliance = 0, currentHorde = 0;
@ -2894,15 +2908,21 @@ void RandomPlayerbotMgr::PrintStats()
LOG_INFO("playerbots", "Bots race:");
for (uint8 race = RACE_HUMAN; race < MAX_RACES; ++race)
{
if (perRace[race])
LOG_INFO("playerbots", " {}: {}", ChatHelper::FormatRace(race).c_str(), perRace[race]);
if (perRace[race]) {
uint32 lvl = lvlPerRace[race] * 10 / perRace[race];
float flvl = lvl / 10.0f;
LOG_INFO("playerbots", " {}: {}, avg lvl: {}", ChatHelper::FormatRace(race).c_str(), perRace[race], flvl);
}
}
LOG_INFO("playerbots", "Bots class:");
for (uint8 cls = CLASS_WARRIOR; cls < MAX_CLASSES; ++cls)
{
if (perClass[cls])
LOG_INFO("playerbots", " {}: {}", ChatHelper::FormatClass(cls).c_str(), perClass[cls]);
if (perClass[cls]) {
uint32 lvl = lvlPerClass[cls] * 10 / perClass[cls];
float flvl = lvl / 10.0f;
LOG_INFO("playerbots", " {}: {}, avg lvl: {}", ChatHelper::FormatClass(cls).c_str(), perClass[cls], flvl);
}
}
LOG_INFO("playerbots", "Bots role:");
@ -2925,7 +2945,7 @@ void RandomPlayerbotMgr::PrintStats()
LOG_INFO("playerbots", " In BG: {}", inBg);
LOG_INFO("playerbots", " In Rest: {}", rest);
LOG_INFO("playerbots", " Dead: {}", dead);
// LOG_INFO("playerbots", "Bots zone:");
// for (auto &[zond_id, counter] : zoneCount)
// {
@ -2933,7 +2953,7 @@ void RandomPlayerbotMgr::PrintStats()
// std::string name = PlayerbotAI::GetLocalizedAreaName(entry);
// LOG_INFO("playerbots", " {}: {}", name, counter);
// }
if (sPlayerbotAIConfig->enableNewRpgStrategy)
{
LOG_INFO("playerbots", "Bots rpg status:");

View File

@ -224,24 +224,22 @@ void PlayerbotFactory::Randomize(bool incremental)
{
bot->resetTalents(true);
}
// bot->SaveToDB(false, false);
ClearSkills();
// bot->SaveToDB(false, false);
ClearSpells();
// bot->SaveToDB(false, false);
if (!incremental)
{
ClearSkills();
ClearSpells();
ResetQuests();
if (!sPlayerbotAIConfig->equipmentPersistence || level < sPlayerbotAIConfig->equipmentPersistenceLevel)
{
ClearAllItems();
}
}
if (!sPlayerbotAIConfig->equipmentPersistence || level < sPlayerbotAIConfig->equipmentPersistenceLevel)
{
ClearAllItems();
}
ClearInventory();
bot->RemoveAllSpellCooldown();
UnbindInstance();
bot->GiveLevel(level);
bot->InitStatsForLevel();
bot->InitStatsForLevel(true);
CancelAuras();
// bot->SaveToDB(false, false);
if (pmo)
@ -280,7 +278,6 @@ void PlayerbotFactory::Randomize(bool incremental)
LOG_DEBUG("playerbots", "Initializing skills (step 1)...");
pmo = sPerformanceMonitor->start(PERF_MON_RNDBOT, "PlayerbotFactory_Skills1");
InitSkills();
// InitTradeSkills();
if (pmo)
pmo->finish();
@ -337,7 +334,8 @@ void PlayerbotFactory::Randomize(bool incremental)
if (!incremental || !sPlayerbotAIConfig->equipmentPersistence ||
bot->GetLevel() < sPlayerbotAIConfig->equipmentPersistenceLevel)
{
InitEquipment(incremental, incremental ? false : sPlayerbotAIConfig->twoRoundsGearInit);
if (sPlayerbotAIConfig->incrementalGearInit || !incremental)
InitEquipment(incremental, incremental ? false : sPlayerbotAIConfig->twoRoundsGearInit);
}
// bot->SaveToDB(false, false);
if (pmo)
@ -1024,9 +1022,21 @@ void PlayerbotFactory::InitTalentsTree(bool increment /*false*/, bool use_templa
/// @todo: match current talent with template
specTab = AiFactory::GetPlayerSpecTab(bot);
/// @todo: fix cat druid hardcode
if (bot->getClass() == CLASS_DRUID && specTab == DRUID_TAB_FERAL && bot->GetLevel() >= 20 &&
!bot->HasAura(16931))
specTab = 3;
if (bot->getClass() == CLASS_DRUID && specTab == DRUID_TAB_FERAL && bot->GetLevel() >= 20)
{
bool isCat = !bot->HasAura(16931);
if (!isCat && bot->GetLevel() == 20)
{
uint32 bearP = sPlayerbotAIConfig->randomClassSpecProb[cls][1];
uint32 catP = sPlayerbotAIConfig->randomClassSpecProb[cls][3];
if (urand(1, bearP + catP) <= catP)
isCat = true;
}
if (isCat)
{
specTab = 3;
}
}
}
else
{
@ -1599,9 +1609,51 @@ void Shuffle(std::vector<uint32>& items)
void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance)
{
if (incremental && !sPlayerbotAIConfig->incrementalGearInit)
return;
if (level < 5) {
// original items
if (CharStartOutfitEntry const* oEntry = GetCharStartOutfitEntry(bot->getRace(), bot->getClass(), bot->getGender()))
{
for (int j = 0; j < MAX_OUTFIT_ITEMS; ++j)
{
if (oEntry->ItemId[j] <= 0)
continue;
uint32 itemId = oEntry->ItemId[j];
// skip hearthstone
if (itemId == 6948)
continue;
// just skip, reported in ObjectMgr::LoadItemTemplates
ItemTemplate const* iProto = sObjectMgr->GetItemTemplate(itemId);
if (!iProto)
continue;
// BuyCount by default
uint32 count = iProto->BuyCount;
// special amount for food/drink
if (iProto->Class == ITEM_CLASS_CONSUMABLE && iProto->SubClass == ITEM_SUBCLASS_FOOD)
{
continue;
}
if (bot->HasItemCount(itemId, count)) {
continue;
}
bot->StoreNewItemInBestSlots(itemId, count);
}
}
return;
}
std::unordered_map<uint8, std::vector<uint32>> items;
// int tab = AiFactory::GetPlayerSpecTab(bot);
uint32 blevel = bot->GetLevel();
int32 delta = std::min(blevel, 10u);

View File

@ -29,12 +29,12 @@ void StatsCollector::CollectItemStats(ItemTemplate const* proto)
{
if (proto->IsRangedWeapon())
{
uint32 val = (proto->Damage[0].DamageMin + proto->Damage[0].DamageMax) * 1000 / 2 / proto->Delay;
float val = (proto->Damage[0].DamageMin + proto->Damage[0].DamageMax) * 1000 / 2 / proto->Delay;
stats[STATS_TYPE_RANGED_DPS] += val;
}
else if (proto->IsWeapon())
{
uint32 val = (proto->Damage[0].DamageMin + proto->Damage[0].DamageMax) * 1000 / 2 / proto->Delay;
float val = (proto->Damage[0].DamageMin + proto->Damage[0].DamageMax) * 1000 / 2 / proto->Delay;
stats[STATS_TYPE_MELEE_DPS] += val;
}
stats[STATS_TYPE_ARMOR] += proto->Armor;
@ -436,10 +436,10 @@ void StatsCollector::CollectByItemStatType(uint32 itemStatType, int32 val)
switch (itemStatType)
{
case ITEM_MOD_MANA:
stats[STATS_TYPE_MANA_REGENERATION] += val / 10;
stats[STATS_TYPE_MANA_REGENERATION] += (float)val / 10;
break;
case ITEM_MOD_HEALTH:
stats[STATS_TYPE_STAMINA] += val / 15;
stats[STATS_TYPE_STAMINA] += (float)val / 15;
break;
case ITEM_MOD_AGILITY:
stats[STATS_TYPE_AGILITY] += val;
@ -747,11 +747,11 @@ void StatsCollector::HandleApplyAura(const SpellEffectInfo& effectInfo, float mu
}
}
int32 StatsCollector::AverageValue(const SpellEffectInfo& effectInfo)
float StatsCollector::AverageValue(const SpellEffectInfo& effectInfo)
{
// float basePointsPerLevel = effectInfo.RealPointsPerLevel; //not used, line marked for removal.
int32 basePoints = effectInfo.BasePoints;
int32 randomPoints = int32(effectInfo.DieSides);
float basePoints = effectInfo.BasePoints;
int32 randomPoints = effectInfo.DieSides;
switch (randomPoints)
{
@ -761,7 +761,7 @@ int32 StatsCollector::AverageValue(const SpellEffectInfo& effectInfo)
basePoints += 1;
break;
default:
int32 randvalue = (1 + randomPoints) / 2;
float randvalue = (1 + randomPoints) / 2.0f;
basePoints += randvalue;
break;
}

View File

@ -71,7 +71,7 @@ public:
bool CheckSpellValidation(uint32 spellFamilyName, flag96 spelFalimyFlags, bool strict = true);
public:
int32 stats[STATS_TYPE_MAX];
float stats[STATS_TYPE_MAX];
private:
void CollectByItemStatType(uint32 itemStatType, int32 val);
@ -80,7 +80,7 @@ private:
void HandleApplyAura(const SpellEffectInfo& effectInfo, float multiplier, bool canNextTrigger,
uint32 triggerCooldown);
int32 AverageValue(const SpellEffectInfo& effectInfo);
float AverageValue(const SpellEffectInfo& effectInfo);
private:
CollectorType type_;

View File

@ -33,6 +33,7 @@ StatsWeightCalculator::StatsWeightCalculator(Player* player) : player_(player)
else
type_ = CollectorType::RANGED;
cls = player->getClass();
lvl = player->GetLevel();
tab = AiFactory::GetPlayerSpecTab(player);
collector_ = std::make_unique<StatsCollector>(type_, cls);
@ -70,7 +71,7 @@ float StatsWeightCalculator::CalculateItem(uint32 itemId, int32 randomPropertyId
Reset();
collector_->CollectItemStats(proto);
if (randomPropertyIds != 0)
CalculateRandomProperty(randomPropertyIds, itemId);
@ -181,6 +182,7 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player)
stats_weights_[STATS_TYPE_ARMOR] += 0.001f;
stats_weights_[STATS_TYPE_BONUS] += 1.0f;
stats_weights_[STATS_TYPE_MELEE_DPS] += 0.01f;
stats_weights_[STATS_TYPE_RANGED_DPS] += 0.01f;
if (cls == CLASS_HUNTER && (tab == HUNTER_TAB_BEASTMASTER || tab == HUNTER_TAB_SURVIVAL))
{
@ -529,13 +531,13 @@ void StatsWeightCalculator::CalculateItemTypePenalty(ItemTemplate const* proto)
// enhancement, rogue, ice dk, unholy dk, shield tank, fury warrior without titan's grip but with duel wield
if (((cls == CLASS_SHAMAN && tab == SHAMAN_TAB_ENHANCEMENT && player_->CanDualWield()) ||
(cls == CLASS_ROGUE) || (cls == CLASS_DEATH_KNIGHT && tab == DEATHKNIGHT_TAB_FROST) ||
(cls == CLASS_WARRIOR && tab == WARRIOR_TAB_FURY && !player_->CanTitanGrip() && player_->CanDualWield()) ||
(cls == CLASS_WARRIOR && tab == WARRIOR_TAB_FURY && !player_->CanTitanGrip() &&
player_->CanDualWield()) ||
(cls == CLASS_WARRIOR && tab == WARRIOR_TAB_PROTECTION) ||
(cls == CLASS_PALADIN && tab == PALADIN_TAB_PROTECTION)))
{
weight_ *= 0.1;
}
}
// spec with double hand
// fury without duel wield, arms, bear, retribution, blood dk
@ -551,15 +553,11 @@ void StatsWeightCalculator::CalculateItemTypePenalty(ItemTemplate const* proto)
weight_ *= 0.1;
}
// caster's main hand (cannot duel weapon but can equip two-hands stuff)
if (cls == CLASS_MAGE ||
cls == CLASS_PRIEST ||
cls == CLASS_WARLOCK ||
cls == CLASS_DRUID ||
if (cls == CLASS_MAGE || cls == CLASS_PRIEST || cls == CLASS_WARLOCK || cls == CLASS_DRUID ||
(cls == CLASS_SHAMAN && !player_->CanDualWield()))
{
weight_ *= 0.65;
}
}
// fury with titan's grip
if ((!isDoubleHand || proto->SubClass == ITEM_SUBCLASS_WEAPON_POLEARM ||
@ -568,16 +566,16 @@ void StatsWeightCalculator::CalculateItemTypePenalty(ItemTemplate const* proto)
{
weight_ *= 0.1;
}
if (cls == CLASS_HUNTER && proto->SubClass == ITEM_SUBCLASS_WEAPON_THROWN)
{
weight_ *= 0.1;
}
if (cls == CLASS_ROGUE && (tab == ROGUE_TAB_ASSASSINATION || tab == ROGUE_TAB_SUBTLETY) &&
proto->SubClass != ITEM_SUBCLASS_WEAPON_DAGGER)
if (lvl >= 10 && cls == CLASS_ROGUE && (tab == ROGUE_TAB_ASSASSINATION || tab == ROGUE_TAB_SUBTLETY) &&
proto->SubClass == ITEM_SUBCLASS_WEAPON_DAGGER)
{
weight_ *= 0.5;
weight_ *= 1.5;
}
if (cls == CLASS_ROGUE && player_->HasAura(13964) &&
@ -660,7 +658,7 @@ void StatsWeightCalculator::ApplyOverflowPenalty(Player* player)
else
validPoints = 0;
}
collector_->stats[STATS_TYPE_HIT] = std::min(collector_->stats[STATS_TYPE_HIT], (int)validPoints);
collector_->stats[STATS_TYPE_HIT] = std::min(collector_->stats[STATS_TYPE_HIT], validPoints);
}
{
@ -677,8 +675,7 @@ void StatsWeightCalculator::ApplyOverflowPenalty(Player* player)
else
validPoints = 0;
collector_->stats[STATS_TYPE_EXPERTISE] =
std::min(collector_->stats[STATS_TYPE_EXPERTISE], (int)validPoints);
collector_->stats[STATS_TYPE_EXPERTISE] = std::min(collector_->stats[STATS_TYPE_EXPERTISE], validPoints);
}
}
@ -695,7 +692,7 @@ void StatsWeightCalculator::ApplyOverflowPenalty(Player* player)
else
validPoints = 0;
collector_->stats[STATS_TYPE_DEFENSE] = std::min(collector_->stats[STATS_TYPE_DEFENSE], (int)validPoints);
collector_->stats[STATS_TYPE_DEFENSE] = std::min(collector_->stats[STATS_TYPE_DEFENSE], validPoints);
}
}
@ -714,7 +711,7 @@ void StatsWeightCalculator::ApplyOverflowPenalty(Player* player)
validPoints = 0;
collector_->stats[STATS_TYPE_ARMOR_PENETRATION] =
std::min(collector_->stats[STATS_TYPE_ARMOR_PENETRATION], (int)validPoints);
std::min(collector_->stats[STATS_TYPE_ARMOR_PENETRATION], validPoints);
}
}
}

View File

@ -57,6 +57,7 @@ private:
CollectorType hitOverflowType_;
std::unique_ptr<StatsCollector> collector_;
uint8 cls;
uint8 lvl;
int tab;
bool enable_overflow_penalty_;
bool enable_item_set_bonus_;

View File

@ -163,7 +163,8 @@ void AutoMaintenanceOnLevelupAction::AutoUpgradeEquip()
PlayerbotFactory factory(bot, bot->GetLevel());
if (!sPlayerbotAIConfig->equipmentPersistence || bot->GetLevel() < sPlayerbotAIConfig->equipmentPersistenceLevel)
{
factory.InitEquipment(true);
if (sPlayerbotAIConfig->incrementalGearInit)
factory.InitEquipment(true);
}
factory.InitAmmo();
return;

View File

@ -187,7 +187,8 @@ void EquipAction::EquipItem(Item* item)
// Priority 1: Replace main hand if the new weapon is strictly better
// and if conditions allow (e.g. no conflicting 2H logic)
bool betterThanMH = (newItemScore > mainHandScore);
bool mhConditionOK = ((invType != INVTYPE_2HWEAPON && !have2HWeaponEquipped) ||
// If a one-handed weapon is better, we can still use it instead of a two-handed weapon
bool mhConditionOK = (invType != INVTYPE_2HWEAPON ||
(isTwoHander && !canTitanGrip) ||
(canTitanGrip && isValidTGWeapon));

View File

@ -1801,7 +1801,6 @@ const Movement::PointsArray MovementAction::SearchForBestPath(float x, float y,
bool FleeAction::Execute(Event event)
{
// return Flee(AI_VALUE(Unit*, "current target"));
return MoveAway(AI_VALUE(Unit*, "current target"), sPlayerbotAIConfig->fleeDistance, true);
}
@ -1811,6 +1810,10 @@ bool FleeAction::isUseful()
{
return false;
}
Unit* target = AI_VALUE(Unit*, "current target");
if (target && target->IsInWorld() && !bot->IsWithinMeleeRange(target))
return false;
return true;
}

View File

@ -73,7 +73,7 @@ bool TaxiAction::Execute(Event event)
{
if (Creature* npcPtr = ObjectAccessor::GetCreature(*bot, npcGuid))
if (!movement.taxiNodes.empty())
bot->ActivateTaxiPathTo(movement.taxiNodes, npcPtr);
bot->ActivateTaxiPathTo(movement.taxiNodes, npcPtr, 0);
},
delay);
botAI->SetNextCheckDelay(delay + 50);
@ -114,7 +114,7 @@ bool TaxiAction::Execute(Event event)
return bot->ActivateTaxiPathTo({entry->from, entry->to}, npc, 0);
}
if (!movement.taxiNodes.empty() && !bot->ActivateTaxiPathTo(movement.taxiNodes, npc))
if (!movement.taxiNodes.empty() && !bot->ActivateTaxiPathTo(movement.taxiNodes, npc, 0))
{
movement.taxiNodes.clear();
movement.Set(nullptr);

View File

@ -60,7 +60,7 @@ ArcaneMageStrategy::ArcaneMageStrategy(PlayerbotAI* botAI) : GenericMageStrategy
NextAction** ArcaneMageStrategy::getDefaultActions()
{
return NextAction::array(0, new NextAction("arcane blast", ACTION_DEFAULT + 0.3f),
// new NextAction("arcane barrage", ACTION_DEFAULT + 0.2f), // cast during movement
new NextAction("frostbolt", ACTION_DEFAULT + 0.2f), // arcane immune target
new NextAction("fire blast", ACTION_DEFAULT + 0.1f), // cast during movement
new NextAction("shoot", ACTION_DEFAULT), nullptr);
}

View File

@ -10,7 +10,8 @@
NextAction** FireMageStrategy::getDefaultActions()
{
return NextAction::array(0, new NextAction("fireball", ACTION_DEFAULT + 0.2f),
return NextAction::array(0, new NextAction("fireball", ACTION_DEFAULT + 0.3f),
new NextAction("frostbolt", ACTION_DEFAULT + 0.2f), // fire immune target
new NextAction("fire blast", ACTION_DEFAULT + 0.1f), // cast during movement
new NextAction("shoot", ACTION_DEFAULT), NULL);
}

View File

@ -37,13 +37,6 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
botAI->rpgInfo.SetMoveFarTo(dest);
}
float dis = bot->GetExactDist(dest);
if (dis < pathFinderDis)
{
return MoveTo(dest.getMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), false, false,
false, true);
}
// performance optimization
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL))
{
@ -70,6 +63,13 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), dest.getMapId(), bot->GetZoneId(), zone_name);
return bot->TeleportTo(dest);
}
float dis = bot->GetExactDist(dest);
if (dis < pathFinderDis)
{
return MoveTo(dest.getMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), false, false,
false, true);
}
float minDelta = M_PI;
const float x = bot->GetPositionX();
@ -852,10 +852,18 @@ WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot)
float loRange = 2500.0f;
if (bot->GetLevel() < 5)
{
hiRange /= 10;
loRange /= 10;
hiRange /= 3;
loRange /= 3;
}
std::vector<WorldLocation> lo_prepared_locs, hi_prepared_locs;
bool inCity = false;
if (AreaTableEntry const* zone = sAreaTableStore.LookupEntry(bot->GetZoneId()))
{
if (zone->flags & AREA_FLAG_CAPITAL)
inCity = true;
}
for (auto& loc : locs)
{
if (bot->GetMapId() != loc.GetMapId())
@ -863,17 +871,17 @@ WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot)
if (bot->GetExactDist(loc) > 2500.0f)
continue;
if (bot->GetMap()->GetZoneId(bot->GetPhaseMask(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ()) !=
if (!inCity && bot->GetMap()->GetZoneId(bot->GetPhaseMask(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ()) !=
bot->GetZoneId())
continue;
if (bot->GetExactDist(loc) < 500.0f)
if (bot->GetExactDist(loc) < hiRange)
{
hi_prepared_locs.push_back(loc);
}
if (bot->GetExactDist(loc) < 2500.0f)
if (bot->GetExactDist(loc) < loRange)
{
lo_prepared_locs.push_back(loc);
}
@ -900,6 +908,15 @@ WorldPosition NewRpgBaseAction::SelectRandomInnKeeperPos(Player* bot)
const std::vector<WorldLocation>& locs = IsAlliance(bot->getRace())
? sRandomPlayerbotMgr->allianceStarterPerLevelCache[bot->GetLevel()]
: sRandomPlayerbotMgr->hordeStarterPerLevelCache[bot->GetLevel()];
bool inCity = false;
if (AreaTableEntry const* zone = sAreaTableStore.LookupEntry(bot->GetZoneId()))
{
if (zone->flags & AREA_FLAG_CAPITAL)
inCity = true;
}
std::vector<WorldLocation> prepared_locs;
for (auto& loc : locs)
{
@ -910,7 +927,7 @@ WorldPosition NewRpgBaseAction::SelectRandomInnKeeperPos(Player* bot)
if (bot->GetExactDist(loc) > range)
continue;
if (bot->GetMap()->GetZoneId(bot->GetPhaseMask(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ()) !=
if (!inCity && bot->GetMap()->GetZoneId(bot->GetPhaseMask(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ()) !=
bot->GetZoneId())
continue;

View File

@ -116,12 +116,11 @@ Unit* GrindTargetValue::FindTargetForGrinding(uint32 assistCount)
botAI->rpgInfo.status == RPG_GO_INNKEEPER ||
botAI->rpgInfo.status == RPG_DO_QUEST;
bool notHostile = !bot->IsHostileTo(unit); /*|| (unit->ToCreature() && unit->ToCreature()->IsCivilian());*/
float aggroRange = 30.0f;
if (unit->ToCreature())
aggroRange = std::min(30.0f, unit->ToCreature()->GetAggroRange(bot) + 10.0f);
bool outOfAggro = unit->ToCreature() && bot->GetDistance(unit) > aggroRange;
if (inactiveGrindStatus && (outOfAggro || notHostile))
if (inactiveGrindStatus && outOfAggro)
{
if (needForQuestMap.find(unit->GetEntry()) == needForQuestMap.end())
needForQuestMap[unit->GetEntry()] = needForQuest(unit);

View File

@ -84,6 +84,8 @@ void AfflictionWarlockStrategy::InitTriggers(std::vector<TriggerNode*>& triggers
// Life Tap glyph buff, and Life Tap as filler
triggers.push_back(new TriggerNode("life tap glyph buff", NextAction::array(0, new NextAction("life tap", 29.0f), nullptr)));
triggers.push_back(new TriggerNode("life tap", NextAction::array(0, new NextAction("life tap", 5.1f), nullptr)));
triggers.push_back(new TriggerNode("enemy too close for spell", NextAction::array(0, new NextAction("flee", 39.0f), nullptr)));
}
// ===== AoE Strategy, 3+ enemies =====

View File

@ -30,7 +30,6 @@ public:
creators["seed of corruption"] = &seed_of_corruption;
creators["rain of fire"] = &rain_of_fire;
creators["demon charge"] = &demon_charge;
creators["shadow cleave"] = &shadow_cleave;
}
private:
@ -52,7 +51,6 @@ private:
static ActionNode* seed_of_corruption(PlayerbotAI*) { return new ActionNode("seed of corruption", nullptr, nullptr, nullptr); }
static ActionNode* rain_of_fire(PlayerbotAI*) { return new ActionNode("rain of fire", nullptr, nullptr, nullptr); }
static ActionNode* demon_charge(PlayerbotAI*) { return new ActionNode("demon charge", nullptr, nullptr, nullptr); }
static ActionNode* shadow_cleave(PlayerbotAI*) { return new ActionNode("shadow cleave", nullptr, nullptr, nullptr); }
};
// ===== Single Target Strategy =====
@ -97,6 +95,8 @@ void DemonologyWarlockStrategy::InitTriggers(std::vector<TriggerNode*>& triggers
// Life Tap glyph buff, and Life Tap as filler
triggers.push_back(new TriggerNode("life tap glyph buff", NextAction::array(0, new NextAction("life tap", 29.0f), nullptr)));
triggers.push_back(new TriggerNode("life tap", NextAction::array(0, new NextAction("life tap", 5.1f), nullptr)));
triggers.push_back(new TriggerNode("meta melee flee check", NextAction::array(0, new NextAction("flee", 39.0f), nullptr)));
}
// ===== AoE Strategy, 3+ enemies =====
@ -122,6 +122,5 @@ void MetaMeleeAoeStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
triggers.push_back(new TriggerNode("immolation aura active", NextAction::array(0,
new NextAction("reach melee", 25.5f),
new NextAction("demon charge", 25.0f),
new NextAction("shadow cleave", 24.5f), nullptr)));
new NextAction("demon charge", 25.0f), nullptr)));
}

View File

@ -91,6 +91,8 @@ void DestructionWarlockStrategy::InitTriggers(std::vector<TriggerNode*>& trigger
// Life Tap glyph buff, and Life Tap as filler
triggers.push_back(new TriggerNode("life tap glyph buff", NextAction::array(0, new NextAction("life tap", 29.0f), nullptr)));
triggers.push_back(new TriggerNode("life tap", NextAction::array(0, new NextAction("life tap", 5.1f), nullptr)));
triggers.push_back(new TriggerNode("enemy too close for spell", NextAction::array(0, new NextAction("flee", 39.0f), nullptr)));
}
// ===== AoE Strategy, 3+ enemies =====

View File

@ -1,88 +0,0 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU GPL v2 license, you may redistribute it
* and/or modify it under version 2 of the License, or (at your option), any later version.
*/
#include "DpsWarlockStrategy.h"
#include "Playerbots.h"
// This strategy is designed for low-level Warlocks without talents.
// All of the important spells/cooldowns have been migrated to
// their respective specs.
// ===== Action Node Factory =====
class DpsWarlockStrategyActionNodeFactory : public NamedObjectFactory<ActionNode>
{
public:
DpsWarlockStrategyActionNodeFactory()
{
creators["corruption"] = &corruption;
creators["curse of agony"] = &curse_of_agony;
creators["immolate"] = &immolate;
creators["shadow bolt"] = &shadow_bolt;
creators["life tap"] = &life_tap;
creators["shadowflame"] = &shadowflame;
creators["seed of corruption"] = &seed_of_corruption;
creators["rain of fire"] = &rain_of_fire;
creators["drain soul"] = &drain_soul;
}
private:
static ActionNode* corruption(PlayerbotAI*) { return new ActionNode("corruption", nullptr, nullptr, nullptr); }
static ActionNode* curse_of_agony(PlayerbotAI*){return new ActionNode("curse of agony", nullptr, nullptr, nullptr);}
static ActionNode* immolate(PlayerbotAI*) { return new ActionNode("immolate", nullptr, nullptr, nullptr); }
static ActionNode* shadow_bolt(PlayerbotAI*) { return new ActionNode("shadow bolt", nullptr, nullptr, nullptr); }
static ActionNode* life_tap(PlayerbotAI*) { return new ActionNode("life tap", nullptr, nullptr, nullptr); }
static ActionNode* shadowflame(PlayerbotAI*) { return new ActionNode("shadowflame", nullptr, nullptr, nullptr); }
static ActionNode* seed_of_corruption(PlayerbotAI*){return new ActionNode("seed of corruption", nullptr, nullptr, nullptr);}
static ActionNode* rain_of_fire(PlayerbotAI*) { return new ActionNode("rain of fire", nullptr, nullptr, nullptr); }
static ActionNode* drain_soul(PlayerbotAI*) { return new ActionNode("drain soul", nullptr, nullptr, nullptr); }
};
// ===== Single Target Strategy =====
DpsWarlockStrategy::DpsWarlockStrategy(PlayerbotAI* botAI) : GenericWarlockStrategy(botAI)
{
actionNodeFactories.Add(new DpsWarlockStrategyActionNodeFactory());
}
// ===== Default Actions =====
NextAction** DpsWarlockStrategy::getDefaultActions()
{
return NextAction::array(0,
new NextAction("immolate", 5.5f),
new NextAction("corruption", 5.4f),
new NextAction("curse of agony", 5.3f),
new NextAction("shadow bolt", 5.2f),
new NextAction("shoot", 5.0f), nullptr);
}
// ===== Trigger Initialization ===
void DpsWarlockStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
GenericWarlockStrategy::InitTriggers(triggers);
// Main DoT triggers for high uptime
triggers.push_back(new TriggerNode("corruption on attacker", NextAction::array(0, new NextAction("corruption on attacker", 20.0f), nullptr)));
triggers.push_back(new TriggerNode("curse of agony on attacker", NextAction::array(0, new NextAction("curse of agony on attacker", 19.5f), nullptr)));
triggers.push_back(new TriggerNode("immolate on attacker", NextAction::array(0, new NextAction("immolate on attacker", 19.0f), nullptr)));
triggers.push_back(new TriggerNode("corruption", NextAction::array(0, new NextAction("corruption", 18.5f), nullptr)));
triggers.push_back(new TriggerNode("curse of agony", NextAction::array(0, new NextAction("curse of agony", 18.0f), nullptr)));
triggers.push_back(new TriggerNode("immolate", NextAction::array(0, new NextAction("immolate", 17.5f), nullptr)));
// Drain Soul as execute if target is low HP
triggers.push_back(new TriggerNode("target critical health", NextAction::array(0, new NextAction("drain soul", 17.0f), nullptr)));
// Cast during movement or to activate glyph buff
triggers.push_back(new TriggerNode("life tap", NextAction::array(0, new NextAction("life tap", ACTION_DEFAULT + 0.1f), nullptr)));
triggers.push_back(new TriggerNode("life tap glyph buff", NextAction::array(0, new NextAction("life tap", 28.0f), NULL)));
}
// ===== AoE Strategy, 3+ enemies =====
void DpsAoeWarlockStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
triggers.push_back(new TriggerNode("medium aoe", NextAction::array(0,
new NextAction("shadowflame", 22.5f),
new NextAction("seed of corruption on attacker", 22.0f),
new NextAction("seed of corruption", 21.5f),
new NextAction("rain of fire", 21.0f), nullptr)));
}

View File

@ -1,33 +0,0 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU GPL v2 license, you may redistribute it
* and/or modify it under version 2 of the License, or (at your option), any later version.
*/
#ifndef _PLAYERBOT_DPSWARLOCKSTRATEGY_H
#define _PLAYERBOT_DPSWARLOCKSTRATEGY_H
#include "GenericWarlockStrategy.h"
#include "Strategy.h"
class PlayerbotAI;
class DpsWarlockStrategy : public GenericWarlockStrategy
{
public:
DpsWarlockStrategy(PlayerbotAI* botAI);
std::string const getName() override { return "dps"; }
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
NextAction** getDefaultActions() override;
uint32 GetType() const override { return GenericWarlockStrategy::GetType() | STRATEGY_TYPE_DPS; }
};
class DpsAoeWarlockStrategy : public CombatStrategy
{
public:
DpsAoeWarlockStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {}
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "aoe"; }
};
#endif

View File

@ -89,20 +89,18 @@ void GenericWarlockNonCombatStrategy::InitTriggers(std::vector<TriggerNode*>& tr
Player* bot = botAI->GetBot();
int tab = AiFactory::GetPlayerSpecTab(bot);
// Firestone/Spellstone triggers
if (tab == 2) // Destruction uses Firestone
{
triggers.push_back(
new TriggerNode("no firestone", NextAction::array(0, new NextAction("create firestone", 24.0f), nullptr)));
triggers.push_back(
new TriggerNode("firestone", NextAction::array(0, new NextAction("firestone", 24.0f), nullptr)));
triggers.push_back(new TriggerNode("no firestone", NextAction::array(0, new NextAction("create firestone", 24.0f), nullptr)));
triggers.push_back(new TriggerNode("firestone", NextAction::array(0, new NextAction("firestone", 24.0f), nullptr)));
}
else // Affliction and Demonology use Spellstone
{
triggers.push_back(new TriggerNode("no spellstone",
NextAction::array(0, new NextAction("create spellstone", 24.0f), nullptr)));
triggers.push_back(
new TriggerNode("spellstone", NextAction::array(0, new NextAction("spellstone", 24.0f), nullptr)));
triggers.push_back(new TriggerNode("no spellstone", NextAction::array(0, new NextAction("create spellstone", 24.0f), nullptr)));
triggers.push_back(new TriggerNode("spellstone", NextAction::array(0, new NextAction("spellstone", 24.0f), nullptr)));
}
}
// Non-combat strategy for summoning a Imp
@ -124,8 +122,7 @@ SummonVoidwalkerStrategy::SummonVoidwalkerStrategy(PlayerbotAI* ai) : NonCombatS
void SummonVoidwalkerStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
triggers.push_back(
new TriggerNode("no pet", NextAction::array(0, new NextAction("summon voidwalker", 29.0f), NULL)));
triggers.push_back(new TriggerNode("no pet", NextAction::array(0, new NextAction("summon voidwalker", 29.0f), NULL)));
}
// Non-combat strategy for summoning a Succubus
@ -147,8 +144,7 @@ SummonFelhunterStrategy::SummonFelhunterStrategy(PlayerbotAI* ai) : NonCombatStr
void SummonFelhunterStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
triggers.push_back(
new TriggerNode("no pet", NextAction::array(0, new NextAction("summon felhunter", 29.0f), NULL)));
triggers.push_back(new TriggerNode("no pet", NextAction::array(0, new NextAction("summon felhunter", 29.0f), NULL)));
}
// Non-combat strategy for summoning a Felguard

View File

@ -18,6 +18,7 @@ public:
std::string const getName() override { return "warlock"; }
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
NextAction** getDefaultActions() override;
uint32 GetType() const override { return CombatStrategy::GetType() | STRATEGY_TYPE_RANGED | STRATEGY_TYPE_DPS; }
};
class WarlockBoostStrategy : public Strategy

View File

@ -15,6 +15,9 @@
#include "Playerbots.h"
#include "ServerFacade.h"
#include "Unit.h"
#include "Timer.h"
#include <unordered_map>
#include <mutex>
// Checks if the bot has less than 32 soul shards, and if so, allows casting Drain Soul
bool CastDrainSoulAction::isUseful() { return AI_VALUE2(uint32, "item count", "soul shard") < 32; }
@ -134,9 +137,29 @@ bool UseSoulstoneSelfAction::Execute(Event event)
return UseItem(items[0], ObjectGuid::Empty, nullptr, bot);
}
// Reservation map for soulstone targets (GUID -> reservation expiry in ms)
static std::unordered_map<ObjectGuid, uint32> soulstoneReservations;
static std::mutex soulstoneReservationsMutex;
// Helper to clean up expired reservations
void CleanupSoulstoneReservations()
{
uint32 now = getMSTime();
std::lock_guard<std::mutex> lock(soulstoneReservationsMutex);
for (auto it = soulstoneReservations.begin(); it != soulstoneReservations.end();)
{
if (it->second <= now)
it = soulstoneReservations.erase(it);
else
++it;
}
}
// Use the soulstone item on the bot's master with nc strategy "ss master"
bool UseSoulstoneMasterAction::Execute(Event event)
{
CleanupSoulstoneReservations();
std::vector<Item*> items = AI_VALUE2(std::vector<Item*>, "inventory items", "soulstone");
if (items.empty())
return false;
@ -145,6 +168,23 @@ bool UseSoulstoneMasterAction::Execute(Event event)
if (!master || HasSoulstoneAura(master))
return false;
uint32 now = getMSTime();
{
std::lock_guard<std::mutex> lock(soulstoneReservationsMutex);
if (soulstoneReservations.count(master->GetGUID()) && soulstoneReservations[master->GetGUID()] > now)
return false; // Already being soulstoned
soulstoneReservations[master->GetGUID()] = now + 2500; // Reserve for 2.5 seconds
}
float distance = sServerFacade->GetDistance2d(bot, master);
if (distance >= 30.0f)
return false;
if (!bot->IsWithinLOSInMap(master))
return false;
bot->SetSelection(master->GetGUID());
return UseItem(items[0], ObjectGuid::Empty, nullptr, master);
}
@ -152,41 +192,83 @@ bool UseSoulstoneMasterAction::Execute(Event event)
// Use the soulstone item on a tank in the group with nc strategy "ss tank"
bool UseSoulstoneTankAction::Execute(Event event)
{
CleanupSoulstoneReservations();
std::vector<Item*> items = AI_VALUE2(std::vector<Item*>, "inventory items", "soulstone");
if (items.empty())
return false;
Player* tank = nullptr;
Player* chosenTank = nullptr;
Group* group = bot->GetGroup();
uint32 now = getMSTime();
// First: Try to soulstone the main tank
if (group)
{
for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
{
Player* member = gref->GetSource();
if (member && member->IsAlive() && botAI->IsTank(member) && !HasSoulstoneAura(member))
if (member && member->IsAlive() && botAI->IsTank(member) && botAI->IsMainTank(member) &&
!HasSoulstoneAura(member))
{
tank = member;
break;
std::lock_guard<std::mutex> lock(soulstoneReservationsMutex);
if (soulstoneReservations.count(member->GetGUID()) && soulstoneReservations[member->GetGUID()] > now)
continue; // Already being soulstoned
float distance = sServerFacade->GetDistance2d(bot, member);
if (distance < 30.0f && bot->IsWithinLOSInMap(member))
{
chosenTank = member;
soulstoneReservations[chosenTank->GetGUID()] = now + 2500; // Reserve for 2.5 seconds
break;
}
}
}
// If no main tank found, soulstone another tank
if (!chosenTank)
{
for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
{
Player* member = gref->GetSource();
if (member && member->IsAlive() && botAI->IsTank(member) && !HasSoulstoneAura(member))
{
std::lock_guard<std::mutex> lock(soulstoneReservationsMutex);
if (soulstoneReservations.count(member->GetGUID()) &&
soulstoneReservations[member->GetGUID()] > now)
continue; // Already being soulstoned
float distance = sServerFacade->GetDistance2d(bot, member);
if (distance < 30.0f && bot->IsWithinLOSInMap(member))
{
chosenTank = member;
soulstoneReservations[chosenTank->GetGUID()] = now + 2500; // Reserve for 2.5 seconds
break;
}
}
}
}
}
if (!tank)
if (!chosenTank)
return false;
bot->SetSelection(tank->GetGUID());
return UseItem(items[0], ObjectGuid::Empty, nullptr, tank);
bot->SetSelection(chosenTank->GetGUID());
return UseItem(items[0], ObjectGuid::Empty, nullptr, chosenTank);
}
// Use the soulstone item on a healer in the group with nc strategy "ss healer"
bool UseSoulstoneHealerAction::Execute(Event event)
{
CleanupSoulstoneReservations();
std::vector<Item*> items = AI_VALUE2(std::vector<Item*>, "inventory items", "soulstone");
if (items.empty())
return false;
Player* healer = nullptr;
Group* group = bot->GetGroup();
uint32 now = getMSTime();
if (group)
{
for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
@ -194,8 +276,20 @@ bool UseSoulstoneHealerAction::Execute(Event event)
Player* member = gref->GetSource();
if (member && member->IsAlive() && botAI->IsHeal(member) && !HasSoulstoneAura(member))
{
healer = member;
break;
{
std::lock_guard<std::mutex> lock(soulstoneReservationsMutex);
if (soulstoneReservations.count(member->GetGUID()) &&
soulstoneReservations[member->GetGUID()] > now)
continue; // Already being soulstoned
float distance = sServerFacade->GetDistance2d(bot, member);
if (distance < 30.0f && bot->IsWithinLOSInMap(member))
{
healer = member;
soulstoneReservations[healer->GetGUID()] = now + 2500; // Reserve for 2.5 seconds
break;
}
}
}
}
}

View File

@ -8,7 +8,6 @@
#include "DemonologyWarlockStrategy.h"
#include "DestructionWarlockStrategy.h"
#include "TankWarlockStrategy.h"
#include "DpsWarlockStrategy.h"
#include "GenericTriggers.h"
#include "GenericWarlockNonCombatStrategy.h"
#include "NamedObjectContext.h"
@ -29,16 +28,16 @@ public:
creators["boost"] = &WarlockStrategyFactoryInternal::boost;
creators["cc"] = &WarlockStrategyFactoryInternal::cc;
creators["pet"] = &WarlockStrategyFactoryInternal::pet;
creators["affli"] = &WarlockStrategyFactoryInternal::affliction;
creators["affli aoe"] = &WarlockStrategyFactoryInternal::affliction_aoe;
creators["demo"] = &WarlockStrategyFactoryInternal::demonology;
creators["demo aoe"] = &WarlockStrategyFactoryInternal::demonology_aoe;
creators["destro"] = &WarlockStrategyFactoryInternal::destruction;
creators["destro aoe"] = &WarlockStrategyFactoryInternal::destruction_aoe;
creators["meta melee"] = &WarlockStrategyFactoryInternal::meta_melee_aoe;
creators["dps"] = &WarlockStrategyFactoryInternal::dps;
creators["aoe"] = &WarlockStrategyFactoryInternal::aoe;
creators["curse of elements"] = &WarlockStrategyFactoryInternal::curse_of_elements;
creators["imp"] = &WarlockStrategyFactoryInternal::imp;
creators["voidwalker"] = &WarlockStrategyFactoryInternal::voidwalker;
creators["succubus"] = &WarlockStrategyFactoryInternal::succubus;
creators["felhunter"] = &WarlockStrategyFactoryInternal::felhunter;
creators["felguard"] = &WarlockStrategyFactoryInternal::felguard;
}
private:
@ -47,16 +46,16 @@ private:
static Strategy* pull(PlayerbotAI* botAI) { return new PullStrategy(botAI, "shoot"); }
static Strategy* boost(PlayerbotAI* botAI) { return new WarlockBoostStrategy(botAI); }
static Strategy* cc(PlayerbotAI* botAI) { return new WarlockCcStrategy(botAI); }
static Strategy* affliction(PlayerbotAI* botAI) { return new AfflictionWarlockStrategy(botAI); }
static Strategy* affliction_aoe(PlayerbotAI* botAI) { return new AfflictionWarlockAoeStrategy(botAI); }
static Strategy* demonology(PlayerbotAI* botAI) { return new DemonologyWarlockStrategy(botAI); }
static Strategy* demonology_aoe(PlayerbotAI* botAI) { return new DemonologyWarlockAoeStrategy(botAI); }
static Strategy* destruction(PlayerbotAI* botAI) { return new DestructionWarlockStrategy(botAI); }
static Strategy* destruction_aoe(PlayerbotAI* botAI) { return new DestructionWarlockAoeStrategy(botAI); }
static Strategy* meta_melee_aoe(PlayerbotAI* botAI) { return new MetaMeleeAoeStrategy(botAI); }
static Strategy* dps(PlayerbotAI* botAI) { return new DpsWarlockStrategy(botAI); }
static Strategy* aoe(PlayerbotAI* botAI) { return new DpsAoeWarlockStrategy(botAI); }
static Strategy* curse_of_elements(PlayerbotAI* botAI) { return new WarlockCurseOfTheElementsStrategy(botAI); }
static Strategy* imp(PlayerbotAI* ai) { return new SummonImpStrategy(ai); }
static Strategy* voidwalker(PlayerbotAI* ai) { return new SummonVoidwalkerStrategy(ai); }
static Strategy* succubus(PlayerbotAI* ai) { return new SummonSuccubusStrategy(ai); }
static Strategy* felhunter(PlayerbotAI* ai) { return new SummonFelhunterStrategy(ai); }
static Strategy* felguard(PlayerbotAI* ai) { return new SummonFelguardStrategy(ai); }
};
class WarlockCombatStrategyFactoryInternal : public NamedObjectContext<Strategy>
@ -65,10 +64,16 @@ public:
WarlockCombatStrategyFactoryInternal() : NamedObjectContext<Strategy>(false, true)
{
creators["tank"] = &WarlockCombatStrategyFactoryInternal::tank;
creators["affli"] = &WarlockCombatStrategyFactoryInternal::affliction;
creators["demo"] = &WarlockCombatStrategyFactoryInternal::demonology;
creators["destro"] = &WarlockCombatStrategyFactoryInternal::destruction;
}
private:
static Strategy* tank(PlayerbotAI* botAI) { return new TankWarlockStrategy(botAI); }
static Strategy* affliction(PlayerbotAI* botAI) { return new AfflictionWarlockStrategy(botAI); }
static Strategy* demonology(PlayerbotAI* botAI) { return new DemonologyWarlockStrategy(botAI); }
static Strategy* destruction(PlayerbotAI* botAI) { return new DestructionWarlockStrategy(botAI); }
};
class NonCombatBuffStrategyFactoryInternal : public NamedObjectContext<Strategy>
@ -76,11 +81,6 @@ class NonCombatBuffStrategyFactoryInternal : public NamedObjectContext<Strategy>
public:
NonCombatBuffStrategyFactoryInternal() : NamedObjectContext<Strategy>(false, true)
{
creators["imp"] = &NonCombatBuffStrategyFactoryInternal::imp;
creators["voidwalker"] = &NonCombatBuffStrategyFactoryInternal::voidwalker;
creators["succubus"] = &NonCombatBuffStrategyFactoryInternal::succubus;
creators["felhunter"] = &NonCombatBuffStrategyFactoryInternal::felhunter;
creators["felguard"] = &NonCombatBuffStrategyFactoryInternal::felguard;
creators["ss self"] = &NonCombatBuffStrategyFactoryInternal::soulstone_self;
creators["ss master"] = &NonCombatBuffStrategyFactoryInternal::soulstone_master;
creators["ss tank"] = &NonCombatBuffStrategyFactoryInternal::soulstone_tank;
@ -88,11 +88,6 @@ public:
}
private:
static Strategy* imp(PlayerbotAI* ai) { return new SummonImpStrategy(ai); }
static Strategy* voidwalker(PlayerbotAI* ai) { return new SummonVoidwalkerStrategy(ai); }
static Strategy* succubus(PlayerbotAI* ai) { return new SummonSuccubusStrategy(ai); }
static Strategy* felhunter(PlayerbotAI* ai) { return new SummonFelhunterStrategy(ai); }
static Strategy* felguard(PlayerbotAI* ai) { return new SummonFelguardStrategy(ai); }
static Strategy* soulstone_self(PlayerbotAI* ai) { return new SoulstoneSelfStrategy(ai); }
static Strategy* soulstone_master(PlayerbotAI* ai) { return new SoulstoneMasterStrategy(ai); }
static Strategy* soulstone_tank(PlayerbotAI* ai) { return new SoulstoneTankStrategy(ai); }
@ -137,6 +132,8 @@ public:
creators["metamorphosis"] = &WarlockTriggerFactoryInternal::metamorphosis;
creators["demonic empowerment"] = &WarlockTriggerFactoryInternal::demonic_empowerment;
creators["immolation aura active"] = &WarlockTriggerFactoryInternal::immolation_aura_active;
creators["metamorphosis not active"] = &WarlockTriggerFactoryInternal::metamorphosis_not_active;
creators["meta melee flee check"] = &WarlockTriggerFactoryInternal::meta_melee_flee_check;
}
private:
@ -173,6 +170,8 @@ private:
static Trigger* metamorphosis(PlayerbotAI* ai) { return new MetamorphosisTrigger(ai); }
static Trigger* demonic_empowerment(PlayerbotAI* ai) { return new DemonicEmpowermentTrigger(ai); }
static Trigger* immolation_aura_active(PlayerbotAI* ai) { return new ImmolationAuraActiveTrigger(ai); }
static Trigger* metamorphosis_not_active(PlayerbotAI* ai) { return new MetamorphosisNotActiveTrigger(ai); }
static Trigger* meta_melee_flee_check(PlayerbotAI* ai) { return new MetaMeleeEnemyTooCloseForSpellTrigger(ai); }
};
class WarlockAiObjectContextInternal : public NamedObjectContext<Action>

View File

@ -271,4 +271,18 @@ class MoltenCoreTrigger : public HasAuraTrigger
public:
MoltenCoreTrigger(PlayerbotAI* ai) : HasAuraTrigger(ai, "molten core") {}
};
class MetamorphosisNotActiveTrigger : public HasNoAuraTrigger
{
public:
MetamorphosisNotActiveTrigger(PlayerbotAI* ai) : HasNoAuraTrigger(ai, "metamorphosis") {}
};
class MetaMeleeEnemyTooCloseForSpellTrigger : public TwoTriggers
{
public:
MetaMeleeEnemyTooCloseForSpellTrigger(PlayerbotAI* ai)
: TwoTriggers(ai, "enemy too close for spell", "metamorphosis not active") {}
};
#endif