mirror of
https://github.com/mod-playerbots/mod-playerbots.git
synced 2026-01-13 00:58:33 +00:00
This PR aims to achieve 4 main things: 1. Manual Curse Strategies - Each curse has it's own toggleable combat strategy, with curse of agony being the default for affliction and demonology, and curse of the elements being default for destro. The other curses that are available are curse of exhaustion (if specced for it), curse of doom, curse of weakness, and curse of tongues (6 total). You can add these by typing "co +curse of weakness", or similar. Note: Curses are part of the WarlockCurseStrategyFactoryInternal(), so there can only be one active. 2. Firestone/Spellstone Non-Combat Strategies - Players requested to me that they can decide if they want to use a spellstone or firestone for their weapon enchant, so I added them as non-combat strategies. Spellstone is the default for affliction and demonology, firestone is the default for destro. To add these, type "nc +firestone" or similar. 3. Soul Shard Replenishment - I noticed after hours of running a server (15+ hours) that altbots and rndbots would only cast imp and not use their soul shards. This is because they were actually running out of soul shards, without the ability to maintain themselves accordingly. I added a trigger (no soul shard) and action (create soul shard) that triggers if they are out of soul shards, creating only 1 soul shard (to not clog up the inventory). This way, you should never have a warlock using the wrong pet, or failing to cast shadowburn, failing to create soulstone/spellstone/firestone/healthstone, or failing to cast soul shatter. 4. Tidying up the code - I removed the built-in curse code from the DPS strategies, and migrated it to the manual curse strategies. I clumped the curse triggers and actions together in the associated files. I added logic for Curse of Weakness to check for conflicting debuffs. I moved the summoning strategies and curse strategies to their own strategy factories - WarlockPetStrategyFactoryInternal, and WarlockCurseStrategyFactoryInternal. This way they can only have one curse and one pet strategy active at once. I also renamed the "NonCombatBuffStrategyFactoryInternal" to "WarlockSoulstoneStrategyFactoryInternal", which was more accurate. I changed a single talent point in the Affliction Warlock PVE spec, taking one from destructive reach and adding it into nightfall for those sweet, sweet free shadowbolts. I added "ss self" as the default non-combat soulstone strategy, as before, they didn't have one assigned. The player can still of course remove that NC strategy and apply another, such as "ss master", "ss tank", or "ss healer".
325 lines
11 KiB
C++
325 lines
11 KiB
C++
/*
|
|
* 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 "WarlockActions.h"
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
#include "Event.h"
|
|
#include "Item.h"
|
|
#include "ObjectGuid.h"
|
|
#include "Player.h"
|
|
#include "PlayerbotAI.h"
|
|
#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; }
|
|
|
|
// Checks if the bot's health is above a certain threshold, and if so, allows casting Life Tap
|
|
bool CastLifeTapAction::isUseful() { return AI_VALUE2(uint8, "health", "self target") > sPlayerbotAIConfig->lowHealth; }
|
|
|
|
// Checks if the target marked with the moon icon can be banished
|
|
bool CastBanishOnCcAction::isPossible()
|
|
{
|
|
Unit* target = GetTarget();
|
|
if (!target)
|
|
return false;
|
|
|
|
// Only possible on elementals or demons
|
|
uint32 creatureType = target->GetCreatureType();
|
|
if (creatureType != CREATURE_TYPE_DEMON && creatureType != CREATURE_TYPE_ELEMENTAL)
|
|
return false;
|
|
|
|
// Use base class to check spell available, range, etc
|
|
return CastCrowdControlSpellAction::isPossible();
|
|
}
|
|
|
|
// Checks if the target marked with the moon icon can be feared
|
|
bool CastFearOnCcAction::isPossible()
|
|
{
|
|
Unit* target = GetTarget();
|
|
if (!target)
|
|
return false;
|
|
|
|
// Fear cannot be cast on mechanical or undead creatures
|
|
uint32 creatureType = target->GetCreatureType();
|
|
if (creatureType == CREATURE_TYPE_MECHANICAL || creatureType == CREATURE_TYPE_UNDEAD)
|
|
return false;
|
|
|
|
// Use base class to check spell available, range, etc
|
|
return CastCrowdControlSpellAction::isPossible();
|
|
}
|
|
|
|
// Checks if the enemies are close enough to use Shadowflame
|
|
bool CastShadowflameAction::isUseful()
|
|
{
|
|
Unit* target = AI_VALUE(Unit*, "current target");
|
|
if (!target)
|
|
return false;
|
|
bool facingTarget = AI_VALUE2(bool, "facing", "current target");
|
|
bool targetClose = bot->IsWithinCombatRange(target, 7.0f); // 7 yard cone
|
|
return facingTarget && targetClose;
|
|
}
|
|
|
|
// Checks if the bot knows Seed of Corruption, and prevents the use of Rain of Fire if it does
|
|
bool CastRainOfFireAction::isUseful()
|
|
{
|
|
Unit* target = GetTarget();
|
|
if (!target)
|
|
return false;
|
|
if (bot->HasSpell(27243) || bot->HasSpell(47835) || bot->HasSpell(47836)) // Seed of Corruption spell IDs
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
// Checks if the enemies are close enough to use Hellfire
|
|
bool CastHellfireAction::isUseful()
|
|
{
|
|
Unit* target = AI_VALUE(Unit*, "current target");
|
|
if (!target)
|
|
return false;
|
|
|
|
return bot->IsWithinCombatRange(target, 5.0f); // 5 yard AoE radius
|
|
}
|
|
|
|
// Checks if the "meta melee aoe" strategy is active, OR if the bot is in melee range of the target
|
|
bool CastImmolationAuraAction::isUseful()
|
|
{
|
|
if (botAI->HasStrategy("meta melee", BOT_STATE_COMBAT))
|
|
return true;
|
|
|
|
Unit* target = AI_VALUE(Unit*, "current target");
|
|
if (!target)
|
|
return false;
|
|
|
|
if (!bot->HasAura(47241)) // 47241 is Metamorphosis spell ID (WotLK)
|
|
return false;
|
|
|
|
return bot->IsWithinCombatRange(target, 5.0f); // 5 yard AoE radius
|
|
}
|
|
|
|
// Checks if the "warlock tank" strategy is active, and if so, prevents the use of Soulshatter
|
|
bool CastSoulshatterAction::isUseful()
|
|
{
|
|
if (botAI->HasStrategy("tank", BOT_STATE_COMBAT))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
// Checks if the bot has enough bag space to create a soul shard, then does so
|
|
bool CreateSoulShardAction::Execute(Event event)
|
|
{
|
|
Player* bot = botAI->GetBot();
|
|
if (!bot)
|
|
return false;
|
|
|
|
// Soul Shard item ID is 6265
|
|
uint32 soulShardId = 6265;
|
|
ItemPosCountVec dest;
|
|
uint32 count = 1;
|
|
if (bot->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, soulShardId, count) == EQUIP_ERR_OK)
|
|
{
|
|
bot->StoreNewItem(dest, soulShardId, true, Item::GenerateItemRandomPropertyId(soulShardId));
|
|
SQLTransaction trans = CharacterDatabase.BeginTransaction();
|
|
bot->SaveInventoryAndGoldToDB(trans);
|
|
CharacterDatabase.CommitTransaction(trans);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Checks if the target has a soulstone aura
|
|
static bool HasSoulstoneAura(Unit* unit)
|
|
{
|
|
static const std::vector<uint32> soulstoneAuraIds = {20707, 20762, 20763, 20764, 20765, 27239, 47883};
|
|
for (uint32 spellId : soulstoneAuraIds)
|
|
if (unit->HasAura(spellId))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
// Use the soulstone item on the bot itself with nc strategy "ss self"
|
|
bool UseSoulstoneSelfAction::Execute(Event event)
|
|
{
|
|
std::vector<Item*> items = AI_VALUE2(std::vector<Item*>, "inventory items", "soulstone");
|
|
if (items.empty())
|
|
return false;
|
|
|
|
if (HasSoulstoneAura(bot))
|
|
return false;
|
|
|
|
bot->SetSelection(bot->GetGUID());
|
|
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;
|
|
|
|
Player* master = botAI->GetMaster();
|
|
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);
|
|
}
|
|
|
|
// 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* 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) && botAI->IsMainTank(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 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 (!chosenTank)
|
|
return false;
|
|
|
|
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())
|
|
{
|
|
Player* member = gref->GetSource();
|
|
if (member && member->IsAlive() && botAI->IsHeal(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))
|
|
{
|
|
healer = member;
|
|
soulstoneReservations[healer->GetGUID()] = now + 2500; // Reserve for 2.5 seconds
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!healer)
|
|
return false;
|
|
|
|
bot->SetSelection(healer->GetGUID());
|
|
return UseItem(items[0], ObjectGuid::Empty, nullptr, healer);
|
|
}
|