diff --git a/sql/playerbots/base/playerbots_text.sql b/sql/playerbots/base/ai_playerbot_texts.sql similarity index 99% rename from sql/playerbots/base/playerbots_text.sql rename to sql/playerbots/base/ai_playerbot_texts.sql index 881e4671..e1c49907 100644 --- a/sql/playerbots/base/playerbots_text.sql +++ b/sql/playerbots/base/ai_playerbot_texts.sql @@ -464,7 +464,7 @@ INSERT INTO `ai_playerbot_texts` (`name`, `text`, `say_type`, `reply_type`, `tex -- %my_level ('suggest_something', 'Wanna party in %zone_name.', 0, 0, '', 'Je veux faire la fête dans %zone_name.', '', '', '', '¡Vamos a perrear a %zone_name!', '', 'Ищу группу в %zone_name.'), ('suggest_something', 'Anyone is looking for %my_role?', 0, 0, '', 'Quelqu\'un cherche un %my_role ?', '', '', '', '¿Alguien está buscando %my_role?', '', 'Кто-нибудь ищет %my_role?'), -('suggest_something', '%my_role is looking for quild.', 0, 0, '', '%my_role recherche une guilde.', '', '', '', '%my_role está buscando hermandad.', '', '%my_role ищу гильдию.'), +('suggest_something', '%my_role is looking for guild.', 0, 0, '', '%my_role recherche une guilde.', '', '', '', '%my_role está buscando hermandad.', '', '%my_role ищу гильдию.'), ('suggest_something', 'Looking for gold.', 0, 0, '', 'A la recherche de l\'or.', '', '', '', 'Buscando oro.', '', 'Дайте голды'), ('suggest_something', '%my_role wants to join a good guild.', 0, 0, '', '%my_role veut rejoindre une bonne guilde.', '', '', '', '%my_role quiere unirse a una buen hermandad.', '', '%my_role хочу в хорошую гильдию.'), ('suggest_something', 'Need a friend.', 0, 0, '', 'Besoin d\'un ami.', '', '', '', 'Necesito un amigo...', '', 'Ищу друга.'), @@ -1457,19 +1457,3 @@ INSERT INTO `ai_playerbot_texts` (`name`, `text`, `say_type`, `reply_type`, `tex ('dummy_end', 'dummy', 0, 0, '', '', '', '', '', '', '', ''); - -DROP TABLE IF EXISTS `ai_playerbot_texts_chance`; - -CREATE TABLE IF NOT EXISTS `ai_playerbot_texts_chance` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT, - `name` varchar(255) NOT NULL, - `probability` bigint(20) NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=UTF8; - -/*!40000 ALTER TABLE `ai_playerbot_texts_chance` DISABLE KEYS */; -INSERT INTO `ai_playerbot_texts_chance` (`id`, `name`, `probability`) VALUES - (1, 'taunt', 30), - (2, 'aoe', 75), - (3, 'loot', 20); -/*!40000 ALTER TABLE `ai_playerbot_texts_chance` ENABLE KEYS */; \ No newline at end of file diff --git a/sql/playerbots/base/ai_playerbot_texts_chance.sql b/sql/playerbots/base/ai_playerbot_texts_chance.sql new file mode 100644 index 00000000..81218dba --- /dev/null +++ b/sql/playerbots/base/ai_playerbot_texts_chance.sql @@ -0,0 +1,14 @@ +DROP TABLE IF EXISTS `ai_playerbot_texts_chance`; +CREATE TABLE IF NOT EXISTS `ai_playerbot_texts_chance` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `probability` bigint(20) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=UTF8; + +/*!40000 ALTER TABLE `ai_playerbot_texts_chance` DISABLE KEYS */; +INSERT INTO `ai_playerbot_texts_chance` (`id`, `name`, `probability`) VALUES + (1, 'taunt', 30), + (2, 'aoe', 75), + (3, 'loot', 20); +/*!40000 ALTER TABLE `ai_playerbot_texts_chance` ENABLE KEYS */; diff --git a/sql/playerbots/base/playerbots_preferred_mounts.sql b/sql/playerbots/base/playerbots_preferred_mounts.sql new file mode 100644 index 00000000..6d904fb3 --- /dev/null +++ b/sql/playerbots/base/playerbots_preferred_mounts.sql @@ -0,0 +1,10 @@ +DROP TABLE IF EXISTS `playerbots_preferred_mounts`; +CREATE TABLE `playerbots_preferred_mounts` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `guid` INT(11) NOT NULL, + `type` TINYINT(3) NOT NULL COMMENT '0: Ground, 1: Flying', + `spellid` INT(11) NOT NULL, + PRIMARY KEY (`id`), + KEY `guid` (`guid`), + KEY `type` (`type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/src/ChatHelper.cpp b/src/ChatHelper.cpp index 8f464d5d..7bd8e590 100644 --- a/src/ChatHelper.cpp +++ b/src/ChatHelper.cpp @@ -304,9 +304,13 @@ ItemIds ChatHelper::parseItems(std::string const text) std::string const ChatHelper::FormatQuest(Quest const* quest) { + if (!quest) + { + return "Invalid quest"; + } + std::ostringstream out; - out << "|cFFFFFF00|Hquest:" << quest->GetQuestId() << ':' << quest->GetQuestLevel() << "|h[" << quest->GetTitle() - << "]|h|r"; + out << "|cFFFFFF00|Hquest:" << quest->GetQuestId() << ':' << quest->GetQuestLevel() << "|h[" << quest->GetTitle() << "]|h|r"; return out.str(); } diff --git a/src/PlayerbotMgr.cpp b/src/PlayerbotMgr.cpp index 34e107d1..444401a1 100644 --- a/src/PlayerbotMgr.cpp +++ b/src/PlayerbotMgr.cpp @@ -100,6 +100,8 @@ void PlayerbotHolder::HandlePlayerBotLoginCallback(PlayerbotLoginQueryHolder con Player* bot = botSession->GetPlayer(); if (!bot) { + // Debug log + LOG_DEBUG("mod-playerbots", "Bot player could not be loaded for account ID: {}", botAccountId); botSession->LogoutPlayer(true); delete botSession; botLoading.erase(holder.GetGuid()); @@ -108,6 +110,14 @@ void PlayerbotHolder::HandlePlayerBotLoginCallback(PlayerbotLoginQueryHolder con uint32 masterAccount = holder.GetMasterAccountId(); WorldSession* masterSession = masterAccount ? sWorld->FindSession(masterAccount) : nullptr; + + // Check if masterSession->GetPlayer() is valid + Player* masterPlayer = masterSession ? masterSession->GetPlayer() : nullptr; + if (masterSession && !masterPlayer) + { + LOG_DEBUG("mod-playerbots", "Master session found but no player is associated for master account ID: {}", masterAccount); + } + std::ostringstream out; bool allowed = false; if (botAccountId == masterAccount) @@ -115,7 +125,7 @@ void PlayerbotHolder::HandlePlayerBotLoginCallback(PlayerbotLoginQueryHolder con allowed = true; } else if (masterSession && sPlayerbotAIConfig->allowGuildBots && bot->GetGuildId() != 0 && - bot->GetGuildId() == masterSession->GetPlayer()->GetGuildId()) + bot->GetGuildId() == masterPlayer->GetGuildId()) { allowed = true; } @@ -129,10 +139,14 @@ void PlayerbotHolder::HandlePlayerBotLoginCallback(PlayerbotLoginQueryHolder con out << "Failure: You are not allowed to control bot " << bot->GetName().c_str(); } - if (allowed && masterSession) + if (allowed && masterSession && masterPlayer) { - Player* player = masterSession->GetPlayer(); - PlayerbotMgr* mgr = GET_PLAYERBOT_MGR(player); + PlayerbotMgr* mgr = GET_PLAYERBOT_MGR(masterPlayer); + if (!mgr) + { + LOG_DEBUG("mod-playerbots", "PlayerbotMgr not found for master player with GUID: {}", masterPlayer->GetGUID().GetRawValue()); + } + uint32 count = mgr->GetPlayerbotsCount(); uint32 cls_count = mgr->GetPlayerbotsCountByClass(bot->getClass()); if (count >= sPlayerbotAIConfig->maxAddedBots) @@ -428,14 +442,17 @@ void PlayerbotHolder::OnBotLogin(Player* const bot) PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); if (!botAI) { + // Log a warning here to indicate that the botAI is null + LOG_DEBUG("mod-playerbots", "PlayerbotAI is null for bot with GUID: {}", bot->GetGUID().GetRawValue()); return; } + Player* master = botAI->GetMaster(); - if (master) + if (!master) { - ObjectGuid masterGuid = master->GetGUID(); - if (master->GetGroup() && !master->GetGroup()->IsLeader(masterGuid)) - master->GetGroup()->ChangeLeader(masterGuid); + // Log a warning to indicate that the master is null + LOG_DEBUG("mod-playerbots", "Master is null for bot with GUID: {}", bot->GetGUID().GetRawValue()); + return; } Group* group = bot->GetGroup(); diff --git a/src/strategy/AiObjectContext.cpp b/src/strategy/AiObjectContext.cpp index c874f577..b8121320 100644 --- a/src/strategy/AiObjectContext.cpp +++ b/src/strategy/AiObjectContext.cpp @@ -58,6 +58,7 @@ AiObjectContext::AiObjectContext(PlayerbotAI* botAI) : PlayerbotAIAware(botAI) actionContexts.Add(new WotlkDungeonVHActionContext()); actionContexts.Add(new WotlkDungeonGDActionContext()); actionContexts.Add(new WotlkDungeonHoSActionContext()); + actionContexts.Add(new WotlkDungeonHoLActionContext()); triggerContexts.Add(new TriggerContext()); triggerContexts.Add(new ChatTriggerContext()); @@ -76,6 +77,7 @@ AiObjectContext::AiObjectContext(PlayerbotAI* botAI) : PlayerbotAIAware(botAI) triggerContexts.Add(new WotlkDungeonVHTriggerContext()); triggerContexts.Add(new WotlkDungeonGDTriggerContext()); triggerContexts.Add(new WotlkDungeonHoSTriggerContext()); + triggerContexts.Add(new WotlkDungeonHoLTriggerContext()); valueContexts.Add(new ValueContext()); diff --git a/src/strategy/actions/CheckMountStateAction.cpp b/src/strategy/actions/CheckMountStateAction.cpp index ce223d16..7ae52f74 100644 --- a/src/strategy/actions/CheckMountStateAction.cpp +++ b/src/strategy/actions/CheckMountStateAction.cpp @@ -239,9 +239,16 @@ bool CheckMountStateAction::Mount() // continue; uint32 index = (spellInfo->Effects[1].ApplyAuraName == SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED || - spellInfo->Effects[2].ApplyAuraName == SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED) - ? 1 - : 0; + spellInfo->Effects[2].ApplyAuraName == SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED || + // Winged Steed of the Ebon Blade + // This mount is meant to autoscale from a 150% flyer + // up to a 280% as you train your flying skill up. + // This incorrectly gets categorised as a ground mount, force this to flyer only. + // TODO: Add other scaling mounts here if they have the same issue, or adjust above + // checks so that they are all correctly detected. + spellInfo->Id == 54729) + ? 1 // Flying Mount + : 0; // Ground Mount if (index == 0 && std::max(spellInfo->Effects[EFFECT_1].BasePoints, spellInfo->Effects[EFFECT_2].BasePoints) > 59) @@ -263,6 +270,42 @@ bool CheckMountStateAction::Mount() : 0; } + // Check for preferred mounts table in db + QueryResult checkTable = PlayerbotsDatabase.Query( + "SELECT EXISTS(SELECT * FROM information_schema.tables WHERE table_schema = 'acore_playerbots' AND table_name = 'playerbots_preferred_mounts')"); + + if (checkTable) + { + uint32 tableExists = checkTable->Fetch()[0].Get(); + if (tableExists == 1) + { + // Check for preferred mount entry + QueryResult result = PlayerbotsDatabase.Query( + "SELECT spellid FROM playerbots_preferred_mounts WHERE guid = {} AND type = {}", + bot->GetGUID().GetCounter(), masterMountType); + + if (result) + { + std::vector mounts; + do + { + Field* fields = result->Fetch(); + uint32 spellId = fields[0].Get(); + mounts.push_back(spellId); + } while (result->NextRow()); + + uint32 index = urand(0, mounts.size() - 1); + // Validate spell ID + if (index < mounts.size() && sSpellMgr->GetSpellInfo(mounts[index])) + { + // TODO: May want to do checks for 'bot riding skill > skill required to ride the mount' + return botAI->CastSpell(mounts[index], bot); + } + } + } + } + + // No preferred mount found (or invalid), continue with random mount selection std::map>& spells = allSpells[masterMountType]; if (hasSwiftMount) { diff --git a/src/strategy/actions/LeaveGroupAction.cpp b/src/strategy/actions/LeaveGroupAction.cpp index 39d0451d..b2594e0f 100644 --- a/src/strategy/actions/LeaveGroupAction.cpp +++ b/src/strategy/actions/LeaveGroupAction.cpp @@ -30,6 +30,8 @@ bool PartyCommandAction::Execute(Event event) Player* master = GetMaster(); if (master && member == master->GetName()) return Leave(bot); + + botAI->Reset(); return false; } @@ -62,6 +64,8 @@ bool UninviteAction::Execute(Event event) if (bot->GetGUID() == guid) return Leave(bot); } + + botAI->Reset(); return false; } @@ -160,6 +164,8 @@ bool LeaveFarAwayAction::isUseful() { return true; } + + botAI->Reset(); return false; } diff --git a/src/strategy/dungeons/DungeonStrategyContext.h b/src/strategy/dungeons/DungeonStrategyContext.h index fa0d31c2..5ed70f6c 100644 --- a/src/strategy/dungeons/DungeonStrategyContext.h +++ b/src/strategy/dungeons/DungeonStrategyContext.h @@ -10,12 +10,11 @@ #include "wotlk/violethold/VioletHoldStrategy.h" #include "wotlk/gundrak/GundrakStrategy.h" #include "wotlk/hallsofstone/HallsOfStoneStrategy.h" +#include "wotlk/hallsoflightning/HallsOfLightningStrategy.h" /* Full list/TODO: -Halls of Lightning - HoL -General Bjarngrim, Volkhan, Ionar, Loken The Oculus - Occ Drakos the Interrogator, Varos Cloudstrider, Mage-Lord Urom, Ley-Guardian Eregos Utgarde Pinnacle - UP @@ -76,8 +75,8 @@ class DungeonStrategyContext : public NamedObjectContext static Strategy* wotlk_vh(PlayerbotAI* botAI) { return new WotlkDungeonVHStrategy(botAI); } static Strategy* wotlk_gd(PlayerbotAI* botAI) { return new WotlkDungeonGDStrategy(botAI); } static Strategy* wotlk_hos(PlayerbotAI* botAI) { return new WotlkDungeonHoSStrategy(botAI); } + static Strategy* wotlk_hol(PlayerbotAI* botAI) { return new WotlkDungeonHoLStrategy(botAI); } - static Strategy* wotlk_hol(PlayerbotAI* botAI) { return new WotlkDungeonUKStrategy(botAI); } static Strategy* wotlk_occ(PlayerbotAI* botAI) { return new WotlkDungeonUKStrategy(botAI); } static Strategy* wotlk_up(PlayerbotAI* botAI) { return new WotlkDungeonUKStrategy(botAI); } static Strategy* wotlk_cos(PlayerbotAI* botAI) { return new WotlkDungeonUKStrategy(botAI); } diff --git a/src/strategy/dungeons/wotlk/WotlkDungeonActionContext.h b/src/strategy/dungeons/wotlk/WotlkDungeonActionContext.h index 73fbecb4..b1301868 100644 --- a/src/strategy/dungeons/wotlk/WotlkDungeonActionContext.h +++ b/src/strategy/dungeons/wotlk/WotlkDungeonActionContext.h @@ -9,7 +9,7 @@ #include "violethold/VioletHoldActionContext.h" #include "gundrak/GundrakActionContext.h" #include "hallsofstone/HallsOfStoneActionContext.h" -// #include "hallsoflightning/HallsOfLightningActionContext.h" +#include "hallsoflightning/HallsOfLightningActionContext.h" // #include "oculus/OculusActionContext.h" // #include "utgardepinnacle/UtgardePinnacleActionContext.h" // #include "cullingofstratholme/CullingOfStratholmeActionContext.h" diff --git a/src/strategy/dungeons/wotlk/WotlkDungeonTriggerContext.h b/src/strategy/dungeons/wotlk/WotlkDungeonTriggerContext.h index 7e966e2f..cb819430 100644 --- a/src/strategy/dungeons/wotlk/WotlkDungeonTriggerContext.h +++ b/src/strategy/dungeons/wotlk/WotlkDungeonTriggerContext.h @@ -9,7 +9,7 @@ #include "violethold/VioletHoldTriggerContext.h" #include "gundrak/GundrakTriggerContext.h" #include "hallsofstone/HallsOfStoneTriggerContext.h" -// #include "hallsoflightning/HallsOfLightningTriggerContext.h" +#include "hallsoflightning/HallsOfLightningTriggerContext.h" // #include "oculus/OculusTriggerContext.h" // #include "utgardepinnacle/UtgardePinnacleTriggerContext.h" // #include "cullingofstratholme/CullingOfStratholmeTriggerContext.h" diff --git a/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubMultipliers.cpp b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubMultipliers.cpp index 6c287591..38cb9e9a 100644 --- a/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubMultipliers.cpp +++ b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubMultipliers.cpp @@ -38,19 +38,15 @@ float KrikthirMultiplier::GetValue(Action* action) if (boss && watcher) { // Do not target swap - // TODO: Need to suppress AoE actions but unsure how to identify them - // TODO: TEST AOE Avoid - if (dynamic_cast(action) - || dynamic_cast(action)) + if (dynamic_cast(action)) + { + return 0.0f; + } + + if (action->getThreatType() == Action::ActionThreatType::Aoe) { return 0.0f; } - // Doesn't seem to work - // if (action->getThreatType() == Action::ActionThreatType::Aoe) - // { - // bot->Yell("Suppressed AoE", LANG_UNIVERSAL); - // return 0.0f; - // } } return 1.0f; } diff --git a/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningActionContext.h b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningActionContext.h new file mode 100644 index 00000000..c3073d49 --- /dev/null +++ b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningActionContext.h @@ -0,0 +1,34 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONHOLACTIONCONTEXT_H +#define _PLAYERBOT_WOTLKDUNGEONHOLACTIONCONTEXT_H + +#include "Action.h" +#include "NamedObjectContext.h" +#include "HallsOfLightningActions.h" + +class WotlkDungeonHoLActionContext : public NamedObjectContext +{ + public: + WotlkDungeonHoLActionContext() { + creators["bjarngrim target"] = &WotlkDungeonHoLActionContext::bjarngrim_target; + creators["avoid whirlwind"] = &WotlkDungeonHoLActionContext::avoid_whirlwind; + creators["volkhan target"] = &WotlkDungeonHoLActionContext::volkhan_target; + creators["static overload spread"] = &WotlkDungeonHoLActionContext::static_overload_spread; + creators["ball lightning spread"] = &WotlkDungeonHoLActionContext::ball_lightning_spread; + creators["ionar tank position"] = &WotlkDungeonHoLActionContext::ionar_tank_position; + creators["disperse position"] = &WotlkDungeonHoLActionContext::disperse_position; + creators["loken stack"] = &WotlkDungeonHoLActionContext::loken_stack; + creators["avoid lightning nova"] = &WotlkDungeonHoLActionContext::avoid_lightning_nova; + } + private: + static Action* bjarngrim_target(PlayerbotAI* ai) { return new BjarngrimTargetAction(ai); } + static Action* avoid_whirlwind(PlayerbotAI* ai) { return new AvoidWhirlwindAction(ai); } + static Action* volkhan_target(PlayerbotAI* ai) { return new VolkhanTargetAction(ai); } + static Action* static_overload_spread(PlayerbotAI* ai) { return new StaticOverloadSpreadAction(ai); } + static Action* ball_lightning_spread(PlayerbotAI* ai) { return new BallLightningSpreadAction(ai); } + static Action* ionar_tank_position(PlayerbotAI* ai) { return new IonarTankPositionAction(ai); } + static Action* disperse_position(PlayerbotAI* ai) { return new DispersePositionAction(ai); } + static Action* loken_stack(PlayerbotAI* ai) { return new LokenStackAction(ai); } + static Action* avoid_lightning_nova(PlayerbotAI* ai) { return new AvoidLightningNovaAction(ai); } +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningActions.cpp b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningActions.cpp new file mode 100644 index 00000000..ac469e95 --- /dev/null +++ b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningActions.cpp @@ -0,0 +1,170 @@ +#include "Playerbots.h" +#include "HallsOfLightningActions.h" +#include "HallsOfLightningStrategy.h" + +bool BjarngrimTargetAction::Execute(Event event) +{ + Unit* target = nullptr; + + // Target is not findable from threat table using AI_VALUE2(), + // therefore need to search manually for the unit name + GuidVector targets = AI_VALUE(GuidVector, "possible targets no los"); + + for (auto i = targets.begin(); i != targets.end(); ++i) + { + Unit* unit = botAI->GetUnit(*i); + if (unit && unit->GetEntry() == NPC_STORMFORGED_LIEUTENANT) + { + target = unit; + break; + } + } + + Unit* currentTarget = AI_VALUE(Unit*, "current target"); + // There are two, we don't want to ping-pong between them if we're attacking one already + if (target && currentTarget && currentTarget->GetEntry() == NPC_STORMFORGED_LIEUTENANT) + { + return false; + } + + if (AI_VALUE(Unit*, "current target") == target) + { + return false; + } + + return Attack(target); +} + +bool AvoidWhirlwindAction::Execute(Event event) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "general bjarngrim"); + if (!boss) { return false; } + + float distance = bot->GetExactDist2d(boss->GetPosition()); + float radius = 8.0f; + float distanceExtra = 2.0f; + + if (distance < radius + distanceExtra) + { + return MoveAway(boss, radius + distanceExtra - distance); + } + + return false; +} + +bool VolkhanTargetAction::Execute(Event event) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "volkhan"); + if (!boss || AI_VALUE(Unit*, "current target") == boss) + { + return false; + } + + return Attack(boss); +} + +bool StaticOverloadSpreadAction::Execute(Event event) +{ + float radius = 8.0f; + float distanceExtra = 2.0f; + + GuidVector members = AI_VALUE(GuidVector, "group members"); + for (auto& member : members) + { + if (bot->GetGUID() == member) + { + continue; + } + + Unit* unit = botAI->GetUnit(member); + if (unit && unit->HasAura(SPELL_STATIC_OVERLOAD) + && bot->GetExactDist2d(unit) < radius) + { + return MoveAway(unit, radius + distanceExtra - bot->GetExactDist2d(unit)); + } + } + return false; +} + +bool BallLightningSpreadAction::Execute(Event event) +{ + float radius = 6.0f; + float distanceExtra = 1.0f; + + GuidVector members = AI_VALUE(GuidVector, "group members"); + for (auto& member : members) + { + if (bot->GetGUID() == member) + { + continue; + } + Unit* unit = botAI->GetUnit(member); + if (unit && bot->GetExactDist2d(unit) < radius + distanceExtra) + { + return MoveAway(unit, radius + distanceExtra - bot->GetExactDist2d(unit)); + } + } + return false; +} + +bool IonarTankPositionAction::isUseful() { return bot->GetExactDist2d(IONAR_TANK_POSITION) > 10.0f; } +bool IonarTankPositionAction::Execute(Event event) +{ + return MoveTo(bot->GetMapId(), IONAR_TANK_POSITION.GetPositionX(), IONAR_TANK_POSITION.GetPositionY(), IONAR_TANK_POSITION.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_COMBAT); +} + +bool DispersePositionAction::isUseful() { return bot->GetExactDist2d(DISPERSE_POSITION) > 8.0f; } +bool DispersePositionAction::Execute(Event event) +{ + return MoveTo(bot->GetMapId(), DISPERSE_POSITION.GetPositionX(), DISPERSE_POSITION.GetPositionY(), DISPERSE_POSITION.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_COMBAT); +} + +bool LokenStackAction::isUseful() +{ + // Minimum hunter range is 5, but values too close to this seem to cause issues.. + // Hunter bots will try and melee in between ranged attacks, or just melee entirely at 5 as they are in range. + // 6.5 or 7.0 solves this. + if(bot->getClass() == CLASS_HUNTER) + { + return AI_VALUE2(float, "distance", "current target") > 6.5f; + } + // else + return AI_VALUE2(float, "distance", "current target") > 2.0f; +} +bool LokenStackAction::Execute(Event event) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "loken"); + if (!boss) { return false; } + + if (!boss->HasUnitState(UNIT_STATE_CASTING)) + { + if(bot->getClass() == CLASS_HUNTER) + { + return Move(bot->GetAngle(boss), fmin(bot->GetExactDist2d(boss) - 6.5f, 10.0f)); + } + // else + return Move(bot->GetAngle(boss), fmin(bot->GetExactDist2d(boss), 10.0f)); + } + + return false; +} + + +bool AvoidLightningNovaAction::Execute(Event event) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "loken"); + if (!boss) { return false; } + + float distance = bot->GetExactDist2d(boss->GetPosition()); + float radius = 20.0f; + float distanceExtra = 2.0f; + + if (distance < radius + distanceExtra) + { + return MoveAway(boss, radius + distanceExtra - distance); + } + + return false; +} diff --git a/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningActions.h b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningActions.h new file mode 100644 index 00000000..fe81aa44 --- /dev/null +++ b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningActions.h @@ -0,0 +1,79 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONHOLACTIONS_H +#define _PLAYERBOT_WOTLKDUNGEONHOLACTIONS_H + +#include "Action.h" +#include "AttackAction.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "HallsOfLightningTriggers.h" + +const Position IONAR_TANK_POSITION = Position(1078.860f, -261.928f, 61.226f); +const Position DISPERSE_POSITION = Position(1161.152f, -261.584f, 53.223f); + +class BjarngrimTargetAction : public AttackAction +{ +public: + BjarngrimTargetAction(PlayerbotAI* ai) : AttackAction(ai, "bjarngrim target") {} + bool Execute(Event event) override; +}; + +class AvoidWhirlwindAction : public MovementAction +{ +public: + AvoidWhirlwindAction(PlayerbotAI* ai) : MovementAction(ai, "avoid whirlwind") {} + bool Execute(Event event) override; +}; + +class VolkhanTargetAction : public AttackAction +{ +public: + VolkhanTargetAction(PlayerbotAI* ai) : AttackAction(ai, "volkhan target") {} + bool Execute(Event event) override; +}; + +class StaticOverloadSpreadAction : public MovementAction +{ +public: + StaticOverloadSpreadAction(PlayerbotAI* ai) : MovementAction(ai, "static overload spread") {} + bool Execute(Event event) override; +}; + +class BallLightningSpreadAction : public MovementAction +{ +public: + BallLightningSpreadAction(PlayerbotAI* ai) : MovementAction(ai, "ball lightning spread") {} + bool Execute(Event event) override; +}; + +class IonarTankPositionAction : public MovementAction +{ +public: + IonarTankPositionAction(PlayerbotAI* ai) : MovementAction(ai, "ionar tank position") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +class DispersePositionAction : public MovementAction +{ +public: + DispersePositionAction(PlayerbotAI* ai) : MovementAction(ai, "disperse position") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +class LokenStackAction : public MovementAction +{ +public: + LokenStackAction(PlayerbotAI* ai) : MovementAction(ai, "loken stack") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +class AvoidLightningNovaAction : public MovementAction +{ +public: + AvoidLightningNovaAction(PlayerbotAI* ai) : MovementAction(ai, "avoid lightning nova") {} + bool Execute(Event event) override; +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningMultipliers.cpp b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningMultipliers.cpp new file mode 100644 index 00000000..0ecb2e56 --- /dev/null +++ b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningMultipliers.cpp @@ -0,0 +1,103 @@ +#include "HallsOfLightningMultipliers.h" +#include "HallsOfLightningActions.h" +#include "GenericSpellActions.h" +#include "ChooseTargetActions.h" +#include "MovementActions.h" +#include "HallsOfLightningTriggers.h" +#include "Action.h" + +float BjarngrimMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "general bjarngrim"); + if (!boss || botAI->IsHeal(bot)) { return 1.0f; } + + if (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_WHIRLWIND_BJARNGRIM)) + { + if (dynamic_cast(action) && !dynamic_cast(action)) + { + return 0.0f; + } + } + + // Detect boss adds this way as sometimes they don't get added to threat table on dps bots, + // and some dps just stand at range and don't engage the boss at all as they can't find the adds + // Unit* boss_add = AI_VALUE2(Unit*, "find target", "stormforged lieutenant"); + Unit* boss_add = nullptr; + GuidVector targets = AI_VALUE(GuidVector, "possible targets no los"); + + for (auto i = targets.begin(); i != targets.end(); ++i) + { + Unit* unit = botAI->GetUnit(*i); + if (unit && unit->GetEntry() == NPC_STORMFORGED_LIEUTENANT) + { + boss_add = unit; + break; + } + } + + if (!boss_add || botAI->IsTank(bot)) { return 1.0f; } + + if (dynamic_cast(action)) + { + return 0.0f; + } + + if (action->getThreatType() == Action::ActionThreatType::Aoe) + { + return 0.0f; + } + + return 1.0f; +} + +float VolkhanMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "volkhan"); + if (!boss || botAI->IsTank(bot) || botAI->IsHeal(bot)) { return 1.0f; } + + if (dynamic_cast(action)) + { + return 0.0f; + } + + if (action->getThreatType() == Action::ActionThreatType::Aoe) + { + return 0.0f; + } + + return 1.0f; +} + +float IonarMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "ionar"); + if (!boss) { return 1.0f; } + + if(!bot->CanSeeOrDetect(boss)) + { + if (dynamic_cast(action) + && !dynamic_cast(action) + && !dynamic_cast(action)) + { + return 0.0f; + } + } + return 1.0f; +} + +float LokenMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "loken"); + if (!boss) { return 1.0f; } + + if (dynamic_cast(action)) { return 0.0f; } + + if (boss->FindCurrentSpellBySpellId(SPELL_LIGHTNING_NOVA) + && dynamic_cast(action) + && !dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} diff --git a/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningMultipliers.h b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningMultipliers.h new file mode 100644 index 00000000..8cc43967 --- /dev/null +++ b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningMultipliers.h @@ -0,0 +1,42 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONHOLMULTIPLIERS_H +#define _PLAYERBOT_WOTLKDUNGEONHOLMULTIPLIERS_H + +#include "Multiplier.h" + +class BjarngrimMultiplier : public Multiplier +{ + public: + BjarngrimMultiplier(PlayerbotAI* ai) : Multiplier(ai, "general bjarngrim") {} + + public: + virtual float GetValue(Action* action); +}; + +class VolkhanMultiplier : public Multiplier +{ + public: + VolkhanMultiplier(PlayerbotAI* ai) : Multiplier(ai, "volkhan") {} + + public: + virtual float GetValue(Action* action); +}; + +class IonarMultiplier : public Multiplier +{ + public: + IonarMultiplier(PlayerbotAI* ai) : Multiplier(ai, "ionar") {} + + public: + virtual float GetValue(Action* action); +}; + +class LokenMultiplier : public Multiplier +{ + public: + LokenMultiplier(PlayerbotAI* ai) : Multiplier(ai, "loken") {} + + public: + virtual float GetValue(Action* action); +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningStrategy.cpp b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningStrategy.cpp new file mode 100644 index 00000000..f438f17b --- /dev/null +++ b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningStrategy.cpp @@ -0,0 +1,41 @@ +#include "HallsOfLightningStrategy.h" +#include "HallsOfLightningMultipliers.h" + + +void WotlkDungeonHoLStrategy::InitTriggers(std::vector &triggers) +{ + // General Bjarngrim + triggers.push_back(new TriggerNode("stormforged lieutenant", + NextAction::array(0, new NextAction("bjarngrim target", ACTION_RAID + 5), nullptr))); + triggers.push_back(new TriggerNode("whirlwind", + NextAction::array(0, new NextAction("avoid whirlwind", ACTION_RAID + 4), nullptr))); + + // Volkhan + triggers.push_back(new TriggerNode("volkhan", + NextAction::array(0, new NextAction("volkhan target", ACTION_RAID + 5), nullptr))); + + // Ionar + triggers.push_back(new TriggerNode("ionar disperse", + NextAction::array(0, new NextAction("disperse position", ACTION_MOVE + 5), nullptr))); + triggers.push_back(new TriggerNode("ionar tank aggro", + NextAction::array(0, new NextAction("ionar tank position", ACTION_MOVE + 4), nullptr))); + triggers.push_back(new TriggerNode("static overload", + NextAction::array(0, new NextAction("static overload spread", ACTION_MOVE + 3), nullptr))); + // TODO: Targeted player can dodge the ball, but a single player soaking it isn't too bad to heal + triggers.push_back(new TriggerNode("ball lightning", + NextAction::array(0, new NextAction("ball lightning spread", ACTION_MOVE + 2), nullptr))); + + // Loken + triggers.push_back(new TriggerNode("lightning nova", + NextAction::array(0, new NextAction("avoid lightning nova", ACTION_MOVE + 5), nullptr))); + triggers.push_back(new TriggerNode("loken ranged", + NextAction::array(0, new NextAction("loken stack", ACTION_MOVE + 4), nullptr))); +} + +void WotlkDungeonHoLStrategy::InitMultipliers(std::vector &multipliers) +{ + multipliers.push_back(new BjarngrimMultiplier(botAI)); + multipliers.push_back(new VolkhanMultiplier(botAI)); + multipliers.push_back(new IonarMultiplier(botAI)); + multipliers.push_back(new LokenMultiplier(botAI)); +} diff --git a/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningStrategy.h b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningStrategy.h new file mode 100644 index 00000000..8ee265dd --- /dev/null +++ b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningStrategy.h @@ -0,0 +1,18 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONHOLSTRATEGY_H +#define _PLAYERBOT_WOTLKDUNGEONHOLSTRATEGY_H + +#include "Multiplier.h" +#include "AiObjectContext.h" +#include "Strategy.h" + + +class WotlkDungeonHoLStrategy : public Strategy +{ +public: + WotlkDungeonHoLStrategy(PlayerbotAI* ai) : Strategy(ai) {} + virtual std::string const getName() override { return "halls of lightning"; } + virtual void InitTriggers(std::vector &triggers) override; + virtual void InitMultipliers(std::vector &multipliers) override; +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningTriggerContext.h b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningTriggerContext.h new file mode 100644 index 00000000..5c8abc35 --- /dev/null +++ b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningTriggerContext.h @@ -0,0 +1,35 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONHOLTRIGGERCONTEXT_H +#define _PLAYERBOT_WOTLKDUNGEONHOLTRIGGERCONTEXT_H + +#include "NamedObjectContext.h" +#include "AiObjectContext.h" +#include "HallsOfLightningTriggers.h" + +class WotlkDungeonHoLTriggerContext : public NamedObjectContext +{ + public: + WotlkDungeonHoLTriggerContext() + { + creators["stormforged lieutenant"] = &WotlkDungeonHoLTriggerContext::stormforged_lieutenant; + creators["whirlwind"] = &WotlkDungeonHoLTriggerContext::bjarngrim_whirlwind; + creators["volkhan"] = &WotlkDungeonHoLTriggerContext::volkhan; + creators["static overload"] = &WotlkDungeonHoLTriggerContext::static_overload; + creators["ball lightning"] = &WotlkDungeonHoLTriggerContext::ball_lightning; + creators["ionar tank aggro"] = &WotlkDungeonHoLTriggerContext::ionar_tank_aggro; + creators["ionar disperse"] = &WotlkDungeonHoLTriggerContext::ionar_disperse; + creators["loken ranged"] = &WotlkDungeonHoLTriggerContext::loken_ranged; + creators["lightning nova"] = &WotlkDungeonHoLTriggerContext::lightning_nova; + } + private: + static Trigger* stormforged_lieutenant(PlayerbotAI* ai) { return new StormforgedLieutenantTrigger(ai); } + static Trigger* bjarngrim_whirlwind(PlayerbotAI* ai) { return new BjarngrimWhirlwindTrigger(ai); } + static Trigger* volkhan(PlayerbotAI* ai) { return new VolkhanTrigger(ai); } + static Trigger* static_overload(PlayerbotAI* ai) { return new IonarStaticOverloadTrigger(ai); } + static Trigger* ball_lightning(PlayerbotAI* ai) { return new IonarBallLightningTrigger(ai); } + static Trigger* ionar_tank_aggro(PlayerbotAI* ai) { return new IonarTankAggroTrigger(ai); } + static Trigger* ionar_disperse(PlayerbotAI* ai) { return new IonarDisperseTrigger(ai); } + static Trigger* loken_ranged(PlayerbotAI* ai) { return new LokenRangedTrigger(ai); } + static Trigger* lightning_nova(PlayerbotAI* ai) { return new LokenLightningNovaTrigger(ai); } +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningTriggers.cpp b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningTriggers.cpp new file mode 100644 index 00000000..6350acc5 --- /dev/null +++ b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningTriggers.cpp @@ -0,0 +1,92 @@ +#include "Playerbots.h" +#include "HallsOfLightningTriggers.h" +#include "AiObject.h" +#include "AiObjectContext.h" + +bool StormforgedLieutenantTrigger::IsActive() +{ + if (botAI->IsTank(bot) || botAI->IsHeal(bot)) { return false; } + + // Target is not findable from threat table using AI_VALUE2(), + // therefore need to search manually for the unit name + GuidVector targets = AI_VALUE(GuidVector, "possible targets no los"); + + for (auto i = targets.begin(); i != targets.end(); ++i) + { + Unit* unit = botAI->GetUnit(*i); + if (unit && unit->GetEntry() == NPC_STORMFORGED_LIEUTENANT) + { + return true; + } + } + return false; +} + +bool BjarngrimWhirlwindTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "general bjarngrim"); + if (!boss) { return false; } + + return boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_WHIRLWIND_BJARNGRIM); +} + +bool VolkhanTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "volkhan"); + return boss && !botAI->IsTank(bot) && !botAI->IsHeal(bot); +} + +bool IonarStaticOverloadTrigger::IsActive() +{ + GuidVector members = AI_VALUE(GuidVector, "group members"); + for (auto& member : members) + { + Unit* unit = botAI->GetUnit(member); + if (unit && unit->HasAura(SPELL_STATIC_OVERLOAD)) + { + return true; + } + } + return false; +} + +bool IonarBallLightningTrigger::IsActive() +{ + if (botAI->IsMelee(bot)) { return false; } + + Unit* boss = AI_VALUE2(Unit*, "find target", "ionar"); + if (!boss) { return false; } + + return boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_BALL_LIGHTNING); +} + +bool IonarTankAggroTrigger::IsActive() +{ + if (!botAI->IsTank(bot)) { return false; } + + Unit* boss = AI_VALUE2(Unit*, "find target", "ionar"); + if (!boss) { return false; } + + return AI_VALUE2(bool, "has aggro", "current target"); +} + +bool IonarDisperseTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "ionar"); + if (!boss) { return false; } + + return !bot->CanSeeOrDetect(boss) || boss->FindCurrentSpellBySpellId(SPELL_DISPERSE); +} + +bool LokenRangedTrigger::IsActive() +{ + return !botAI->IsMelee(bot) && AI_VALUE2(Unit*, "find target", "loken"); +} + +bool LokenLightningNovaTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "loken"); + if (!boss) { return false; } + + return boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_LIGHTNING_NOVA); +} diff --git a/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningTriggers.h b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningTriggers.h new file mode 100644 index 00000000..7dbbc461 --- /dev/null +++ b/src/strategy/dungeons/wotlk/hallsoflightning/HallsOfLightningTriggers.h @@ -0,0 +1,95 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONHOLTRIGGERS_H +#define _PLAYERBOT_WOTLKDUNGEONHOLTRIGGERS_H + +#include "Trigger.h" +#include "PlayerbotAIConfig.h" +#include "GenericTriggers.h" +#include "DungeonStrategyUtils.h" + +enum HallsOfLightningIDs +{ + // General Bjarngrim + NPC_STORMFORGED_LIEUTENANT = 29240, + SPELL_WHIRLWIND_BJARNGRIM = 52027, + + // Ionar + SPELL_STATIC_OVERLOAD_N = 52658, + SPELL_STATIC_OVERLOAD_H = 59795, + SPELL_BALL_LIGHTNING_N = 52780, + SPELL_BALL_LIGHTNING_H = 59800, + SPELL_DISPERSE = 52770, + NPC_SPARK_OF_IONAR = 28926, + + // Loken + SPELL_LIGHTNING_NOVA_N = 52960, + SPELL_LIGHTNING_NOVA_H = 59835, +}; + +#define SPELL_STATIC_OVERLOAD DUNGEON_MODE(bot, SPELL_STATIC_OVERLOAD_N, SPELL_STATIC_OVERLOAD_H) +#define SPELL_BALL_LIGHTNING DUNGEON_MODE(bot, SPELL_BALL_LIGHTNING_N, SPELL_BALL_LIGHTNING_H) +#define SPELL_LIGHTNING_NOVA DUNGEON_MODE(bot, SPELL_LIGHTNING_NOVA_N, SPELL_LIGHTNING_NOVA_H) + +class StormforgedLieutenantTrigger : public Trigger +{ +public: + StormforgedLieutenantTrigger(PlayerbotAI* ai) : Trigger(ai, "stormforged lieutenant") {} + bool IsActive() override; +}; + +class BjarngrimWhirlwindTrigger : public Trigger +{ +public: + BjarngrimWhirlwindTrigger(PlayerbotAI* ai) : Trigger(ai, "bjarngrim whirlwind") {} + bool IsActive() override; +}; + +class VolkhanTrigger : public Trigger +{ +public: + VolkhanTrigger(PlayerbotAI* ai) : Trigger(ai, "volkhan") {} + bool IsActive() override; +}; + +class IonarStaticOverloadTrigger : public Trigger +{ +public: + IonarStaticOverloadTrigger(PlayerbotAI* ai) : Trigger(ai, "ionar static overload") {} + bool IsActive() override; +}; + +class IonarBallLightningTrigger : public Trigger +{ +public: + IonarBallLightningTrigger(PlayerbotAI* ai) : Trigger(ai, "ionar ball lightning spread") {} + bool IsActive() override; +}; + +class IonarTankAggroTrigger : public Trigger +{ +public: + IonarTankAggroTrigger(PlayerbotAI* ai) : Trigger(ai, "ionar tank aggro") {} + bool IsActive() override; +}; + +class IonarDisperseTrigger : public Trigger +{ +public: + IonarDisperseTrigger(PlayerbotAI* ai) : Trigger(ai, "ionar disperse") {} + bool IsActive() override; +}; + +class LokenRangedTrigger : public Trigger +{ +public: + LokenRangedTrigger(PlayerbotAI* ai) : Trigger(ai, "loken ranged") {} + bool IsActive() override; +}; + +class LokenLightningNovaTrigger : public Trigger +{ +public: + LokenLightningNovaTrigger(PlayerbotAI* ai) : Trigger(ai, "lightning nova") {} + bool IsActive() override; +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/hallsoflightning/TODO b/src/strategy/dungeons/wotlk/hallsoflightning/TODO deleted file mode 100644 index e69de29b..00000000 diff --git a/src/strategy/dungeons/wotlk/nexus/NexusActions.cpp b/src/strategy/dungeons/wotlk/nexus/NexusActions.cpp index 1a918614..e494251e 100644 --- a/src/strategy/dungeons/wotlk/nexus/NexusActions.cpp +++ b/src/strategy/dungeons/wotlk/nexus/NexusActions.cpp @@ -54,9 +54,16 @@ bool FirebombSpreadAction::Execute(Event event) { continue; } - if (bot->GetExactDist2d(botAI->GetUnit(member)) < targetDist) + + Unit* unit = botAI->GetUnit(member); + if (!unit) { - return MoveAway(botAI->GetUnit(member), targetDist); + continue; + } + + if (bot->GetExactDist2d(unit) < targetDist) + { + return MoveAway(unit, targetDist); } } return false;