diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index d7d02059..c7d23eed 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -728,7 +728,7 @@ AiPlayerbot.FastReactInBG = 1 # # All In seconds -AiPlayerbot.RandomBotUpdateInterval = 10 +AiPlayerbot.RandomBotUpdateInterval = 20 AiPlayerbot.RandomBotCountChangeMinInterval = 1800 AiPlayerbot.RandomBotCountChangeMaxInterval = 7200 AiPlayerbot.MinRandomBotInWorldTime = 3600 @@ -1002,16 +1002,16 @@ AiPlayerbot.PremadeSpecLink.9.2.80 = -03310030003-05203205210331051335230351 AiPlayerbot.PremadeSpecName.11.0 = balance pve AiPlayerbot.PremadeSpecGlyph.11.0 = 40916,43331,40921,43335,44922,40919 -AiPlayerbot.PremadeSpecLink.11.0.60 = 5012203115331003213302301231 -AiPlayerbot.PremadeSpecLink.11.0.80 = 5012203125331103213305301231--205003212 +AiPlayerbot.PremadeSpecLink.11.0.60 = 5022203105331003213005301231 +AiPlayerbot.PremadeSpecLink.11.0.80 = 5032203105331303213305301231--205003012 AiPlayerbot.PremadeSpecName.11.1 = bear pve AiPlayerbot.PremadeSpecGlyph.11.1 = 40897,43331,46372,43335,43332,40899 AiPlayerbot.PremadeSpecLink.11.1.60 = -500232130322110353100301310501 AiPlayerbot.PremadeSpecLink.11.1.80 = -501232130322110353120303313511-20350001 AiPlayerbot.PremadeSpecName.11.2 = resto pve AiPlayerbot.PremadeSpecGlyph.11.2 = 40913,43331,40906,43335,44922,45602 -AiPlayerbot.PremadeSpecLink.11.2.60 = --230033312031501531050013051 -AiPlayerbot.PremadeSpecLink.11.2.80 = 05320001--230033312031512531153313051 +AiPlayerbot.PremadeSpecLink.11.2.60 = --230033312031500531050113051 +AiPlayerbot.PremadeSpecLink.11.2.80 = 05320031--230033312031501531053313051 AiPlayerbot.PremadeSpecName.11.3 = cat pve AiPlayerbot.PremadeSpecGlyph.11.3 = 40902,43331,40901,43335,44922,45604 AiPlayerbot.PremadeSpecLink.11.3.60 = -552202032322010053100030310501 @@ -1263,11 +1263,6 @@ Playerbots.Updates.EnableDatabases = 1 # Command server port, 0 - disabled AiPlayerbot.CommandServerPort = 8888 -# Diff with/without player in server. The server will tune bot activity to reach the desired server tick speed (in ms).# PLAYERBOT SYSTEM SETTINGS # -AiPlayerbot.EnablePrototypePerformanceDiff = 0 -AiPlayerbot.DiffWithPlayer = 100 -AiPlayerbot.DiffEmpty = 200 - # # # @@ -1462,9 +1457,16 @@ AiPlayerbot.RandombotsWalkingRPG.InDoors = 0 # The default is 10. With 10% of all bots going active or inactive each minute. AiPlayerbot.BotActiveAlone = 100 -# Specify 1 for enabled, 0 for disabled. -# The default is 1. Automatically adjusts 'BotActiveAlone' percentage based on server latency. -AiPlayerbot.botActiveAloneAutoScale = 1 +# Specify smart scaling is enabled or not. +# The default is 1. When enabled (smart) scales the 'BotActiveAlone' value. +AiPlayerbot.botActiveAloneSmartScale = 1 +# Only when botLevel is between WhenMinLevel and WhenMaxLevel. +AiPlayerbot.botActiveAloneSmartScaleWhenMinLevel = 1 +AiPlayerbot.botActiveAloneSmartScaleWhenMaxLevel = 80 +# The server will tune bot activity to reach the desired server tick speed (in ms) +# bots will only join battleground when there is no lag based on latency diffs below +AiPlayerbot.botActiveAloneSmartScaleDiffWithPlayer = 100 +AiPlayerbot.botActiveAloneSmartScaleDiffEmpty = 200 # Premade spell to avoid (undetected spells) # spellid-radius, ... diff --git a/src/AiFactory.cpp b/src/AiFactory.cpp index dadf327f..d3cd1ef3 100644 --- a/src/AiFactory.cpp +++ b/src/AiFactory.cpp @@ -277,10 +277,6 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa { engine->addStrategiesNoInit("racials", "chat", "default", "cast time", "duel", "boost", nullptr); } - if (sPlayerbotAIConfig->autoSaveMana) - { - engine->addStrategy("save mana", false); - } if (sPlayerbotAIConfig->autoAvoidAoe && facade->HasRealPlayerMaster()) { engine->addStrategy("avoid aoe", false); @@ -394,7 +390,12 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa if (PlayerbotAI::IsMelee(player, true) && PlayerbotAI::IsDps(player, true)) { engine->addStrategy("behind", false); } - + if (PlayerbotAI::IsHeal(player, true)) + { + if (sPlayerbotAIConfig->autoSaveMana) + engine->addStrategy("save mana", false); + engine->addStrategy("healer dps", false); + } if (facade->IsRealPlayer() || sRandomPlayerbotMgr->IsRandomBot(player)) { if (!player->GetGroup()) @@ -483,8 +484,8 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa engine->removeStrategy("threat", false); engine->addStrategy("boost", false); - if ((player->getClass() == CLASS_DRUID && tab == 2) || (player->getClass() == CLASS_SHAMAN && tab == 2)) - engine->addStrategiesNoInit("caster", "caster aoe", nullptr); + // if ((player->getClass() == CLASS_DRUID && tab == 2) || (player->getClass() == CLASS_SHAMAN && tab == 2)) + // engine->addStrategiesNoInit("caster", "caster aoe", nullptr); // if (player->getClass() == CLASS_DRUID && tab == 1) // engine->addStrategiesNoInit(/*"behind",*/ "dps", nullptr); diff --git a/src/PlayerbotAI.cpp b/src/PlayerbotAI.cpp index 4f79196f..e3fe0c4c 100644 --- a/src/PlayerbotAI.cpp +++ b/src/PlayerbotAI.cpp @@ -239,6 +239,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) { return; } + // if (!GetMaster() || !GetMaster()->IsInWorld() || !GetMaster()->GetSession() || // GetMaster()->GetSession()->isLogingOut()) { // return; @@ -301,6 +302,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) // bot->GetMotionMaster()->Clear(); // bot->GetMotionMaster()->MoveIdle(); // } + // cheat options if (bot->IsAlive() && ((uint32)GetCheat() > 0 || (uint32)sPlayerbotAIConfig->botCheatMask > 0)) { @@ -314,16 +316,20 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) AllowActivity(); + + if (!CanUpdateAI()) + return; + Spell* currentSpell = bot->GetCurrentSpell(CURRENT_GENERIC_SPELL); if (!currentSpell) currentSpell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL); - if (currentSpell && currentSpell->getState() == SPELL_STATE_PREPARING) + if (currentSpell && currentSpell->GetSpellInfo() && currentSpell->getState() == SPELL_STATE_PREPARING) { const SpellInfo* spellInfo = currentSpell->GetSpellInfo(); // interrupt if target is dead if (currentSpell->m_targets.GetUnitTarget() && !currentSpell->m_targets.GetUnitTarget()->IsAlive() && - spellInfo && !spellInfo->IsAllowingDeadTarget()) + !spellInfo->IsAllowingDeadTarget()) { InterruptSpell(); SetNextCheckDelay(sPlayerbotAIConfig->reactDelay); @@ -356,6 +362,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) } // wait for spell cast + YieldThread(GetReactDelay()); return; } @@ -382,9 +389,6 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) bot->StopMovingOnCurrentPos(); } } - - if (!CanUpdateAI()) - return; if (!bot->InBattleground() && !bot->inRandomLfgDungeon() && bot->GetGroup()) { @@ -703,8 +707,7 @@ void PlayerbotAI::HandleTeleportAck() p << (uint32)0; // supposed to be flags? not used currently p << (uint32)0; // time - not currently used bot->GetSession()->HandleMoveTeleportAck(p); - } - SetNextCheckDelay(urand(1000, 3000)); + }; } if (bot->IsBeingTeleportedFar()) { @@ -712,13 +715,13 @@ void PlayerbotAI::HandleTeleportAck() { bot->GetSession()->HandleMoveWorldportAck(); } - SetNextCheckDelay(urand(2000, 5000)); + // SetNextCheckDelay(urand(2000, 5000)); if (sPlayerbotAIConfig->applyInstanceStrategies) ApplyInstanceStrategies(bot->GetMapId(), true); + Reset(); } SetNextCheckDelay(sPlayerbotAIConfig->globalCoolDown); - Reset(); } void PlayerbotAI::Reset(bool full) @@ -1198,7 +1201,7 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet) void PlayerbotAI::SpellInterrupted(uint32 spellid) { - for (uint8 type = CURRENT_MELEE_SPELL; type < CURRENT_CHANNELED_SPELL; type++) + for (uint8 type = CURRENT_MELEE_SPELL; type <= CURRENT_CHANNELED_SPELL; type++) { Spell* spell = bot->GetCurrentSpell((CurrentSpellTypes)type); if (!spell) @@ -1206,8 +1209,8 @@ void PlayerbotAI::SpellInterrupted(uint32 spellid) if (spell->GetSpellInfo()->Id == spellid) bot->InterruptSpell((CurrentSpellTypes)type); } - LastSpellCast& lastSpell = aiObjectContext->GetValue("last spell cast")->Get(); - lastSpell.id = 0; + // LastSpellCast& lastSpell = aiObjectContext->GetValue("last spell cast")->Get(); + // lastSpell.id = 0; } int32 PlayerbotAI::CalculateGlobalCooldown(uint32 spellid) @@ -1516,7 +1519,7 @@ void PlayerbotAI::DoNextAction(bool min) void PlayerbotAI::ReInitCurrentEngine() { - InterruptSpell(); + // InterruptSpell(); currentEngine->Init(); } @@ -3339,11 +3342,11 @@ bool PlayerbotAI::CastSpell(uint32 spellId, Unit* target, Item* itemTarget) // } // } - WaitForSpellCast(spell); - if (spell->GetCastTime()) - aiObjectContext->GetValue("last spell cast") - ->Get() - .Set(spellId, target->GetGUID(), time(nullptr)); + // WaitForSpellCast(spell); + + aiObjectContext->GetValue("last spell cast") + ->Get() + .Set(spellId, target->GetGUID(), time(nullptr)); aiObjectContext->GetValue("position")->Get()["random"].Reset(); @@ -3473,7 +3476,7 @@ bool PlayerbotAI::CastSpell(uint32 spellId, float x, float y, float z, Item* ite } } - WaitForSpellCast(spell); + // WaitForSpellCast(spell); aiObjectContext->GetValue("last spell cast")->Get().Set(spellId, bot->GetGUID(), time(nullptr)); aiObjectContext->GetValue("position")->Get()["random"].Reset(); @@ -3688,7 +3691,7 @@ bool PlayerbotAI::CastVehicleSpell(uint32 spellId, Unit* target) return false; } - WaitForSpellCast(spell); + // WaitForSpellCast(spell); // aiObjectContext->GetValue("last spell cast")->Get().Set(spellId, target->GetGUID(), time(0)); // aiObjectContext->GetValue("position")->Get()["random"].Reset(); @@ -3745,23 +3748,15 @@ bool PlayerbotAI::IsInVehicle(bool canControl, bool canCast, bool canAttack, boo void PlayerbotAI::WaitForSpellCast(Spell* spell) { - return; SpellInfo const* spellInfo = spell->GetSpellInfo(); uint32 castTime = spell->GetCastTime(); - // float castTime = spell->GetCastTime(); - // if (spellInfo->IsChanneled()) - // { - // int32 duration = spellInfo->GetDuration(); - // bot->ApplySpellMod(spellInfo->Id, SPELLMOD_DURATION, duration); - // if (duration > 0) - // castTime += duration; - // } - - // castTime = ceil(castTime); - - // uint32 globalCooldown = CalculateGlobalCooldown(spellInfo->Id); - // if (castTime < globalCooldown) - // castTime = globalCooldown; + if (spellInfo->IsChanneled()) + { + int32 duration = spellInfo->GetDuration(); + bot->ApplySpellMod(spellInfo->Id, SPELLMOD_DURATION, duration); + if (duration > 0) + castTime += duration; + } SetNextCheckDelay(castTime + sPlayerbotAIConfig->reactDelay); } @@ -3933,7 +3928,10 @@ Player* PlayerbotAI::GetGroupMaster() uint32 PlayerbotAI::GetFixedBotNumer(BotTypeNumber typeNumber, uint32 maxNum, float cyclePerMin) { - uint32 randseed = rand32(); // Seed random number + //deterministic seed + uint8 seedNumber = uint8(typeNumber); + std::mt19937 rng(seedNumber); + uint32 randseed = rng(); // Seed random number uint32 randnum = bot->GetGUID().GetCounter() + randseed; // Semi-random but fixed number for each bot. if (cyclePerMin > 0) @@ -3943,8 +3941,7 @@ uint32 PlayerbotAI::GetFixedBotNumer(BotTypeNumber typeNumber, uint32 maxNum, fl randnum += cycle; // Make the random number cylce. } - randnum = - (randnum % (maxNum + 1)); // Loops the randomnumber at maxNum. Bassically removes all the numbers above 99. + randnum = (randnum % (maxNum + 1)); // Loops the randomnumber at maxNum. Bassically removes all the numbers above 99. return randnum; // Now we have a number unique for each bot between 0 and maxNum that increases by cyclePerMin. } @@ -4082,18 +4079,15 @@ inline bool HasRealPlayers(Map* map) return false; } -bool PlayerbotAI::AllowActive(ActivityType activityType) +ActivePiorityType PlayerbotAI::GetPriorityType(ActivityType activityType) { - // General exceptions - if (activityType == PACKET_ACTIVITY) - return true; + // First priority - priorities disabled or has player master. Always active. + if (HasRealPlayerMaster()) + return ActivePiorityType::HAS_REAL_PLAYER_MASTER; - if (GetMaster()) // Has player master. Always active. - { - PlayerbotAI* masterBotAI = GET_PLAYERBOT_AI(GetMaster()); - if (!masterBotAI || masterBotAI->IsRealPlayer()) - return true; - } + // Self bot in a group with a bot master. + if (IsRealPlayer()) + return ActivePiorityType::IS_REAL_PLAYER; Group* group = bot->GetGroup(); if (group) @@ -4109,19 +4103,40 @@ bool PlayerbotAI::AllowActive(ActivityType activityType) PlayerbotAI* memberBotAI = GET_PLAYERBOT_AI(member); if (!memberBotAI || memberBotAI->HasRealPlayerMaster()) - return true; + return ActivePiorityType::IN_GROUP_WITH_REAL_PLAYER; if (group->IsLeader(member->GetGUID())) + { if (!memberBotAI->AllowActivity(PARTY_ACTIVITY)) - return false; + return ActivePiorityType::IN_GROUP_WITH_REAL_PLAYER; + } } } - if (!WorldPosition(bot).isOverworld()) // bg, raid, dungeon - return true; + if (bot->IsBeingTeleported()) // Allow activity while teleportation. + return ActivePiorityType::IN_INSTANCE; - if (bot->InBattlegroundQueue()) // In bg queue. Speed up bg queue/join. - return true; + if (!WorldPosition(bot).isOverworld()) + return ActivePiorityType::IN_INSTANCE; + + if (HasPlayerNearby()) + return ActivePiorityType::VISIBLE_FOR_PLAYER; + + if (activityType != OUT_OF_PARTY_ACTIVITY && activityType != PACKET_ACTIVITY) + { + // Is in combat.Defend yourself. + if (bot->IsInCombat()) + return ActivePiorityType::IN_COMBAT; + } + + if (HasPlayerNearby(300.f)) + return ActivePiorityType::NEARBY_PLAYER; + + //if (sPlayerbotAIConfig->IsFreeAltBot(bot) || HasStrategy("travel once", BotState::BOT_STATE_NON_COMBAT)) + // return ActivePiorityType::IS_ALWAYS_ACTIVE; + + if (bot->InBattlegroundQueue()) + return ActivePiorityType::IN_BG_QUEUE; bool isLFG = false; if (group) @@ -4131,62 +4146,167 @@ bool PlayerbotAI::AllowActive(ActivityType activityType) isLFG = true; } } - if (sLFGMgr->GetState(bot->GetGUID()) != lfg::LFG_STATE_NONE) { isLFG = true; } - if (isLFG) - return true; + return ActivePiorityType::IN_LFG; - if (activityType != OUT_OF_PARTY_ACTIVITY && activityType != PACKET_ACTIVITY) // Is in combat. Defend yourself. - if (bot->IsInCombat()) - return true; - - if (HasPlayerNearby(300.f)) // Player is near. Always active. - return true; + // If has real players - slow down continents without player + // This means we first disable bots in a different continent/area. + if (sRandomPlayerbotMgr->GetPlayers().empty()) + return ActivePiorityType::IN_EMPTY_SERVER; // friends always active - - // HasFriend sometimes cause crash, disable - // for (auto& player : sRandomPlayerbotMgr->GetPlayers()) - // { - // if (!player || !player->IsInWorld()) - // continue; + // for (auto& player : sRandomPlayerbotMgr->GetPlayers()) + // { + // if (!player || !player->IsInWorld()) + // continue; // if (player->GetSocial()->HasFriend(bot->GetGUID())) - // return true; + // return ActivePiorityType::PLAYER_FRIEND; // } - if (activityType == OUT_OF_PARTY_ACTIVITY || - activityType == GRIND_ACTIVITY) // Many bots nearby. Do not do heavy area checks. - if (HasManyPlayersNearby()) - return false; + // real guild always active if member+ + if (IsInRealGuild()) + return ActivePiorityType::PLAYER_GUILD; - // Bots don't need to move using PathGenerator. - if (activityType == DETAILED_MOVE_ACTIVITY) - return false; + if (bot->IsBeingTeleported() || !bot->IsInWorld() || !HasRealPlayers(bot->GetMap())) + return ActivePiorityType::IN_INACTIVE_MAP; - // All exceptions are now done. - // Below is code to have a specified % of bots active at all times. - // The default is 10%. With 0.1% of all bots going active or inactive each minute. - if (sPlayerbotAIConfig->botActiveAlone <= 0) - return false; - - uint32 mod = sPlayerbotAIConfig->botActiveAlone > 100 ? 100 : sPlayerbotAIConfig->botActiveAlone; - if (sPlayerbotAIConfig->botActiveAloneAutoScale) + // IN_ACTIVE_AREA + if (activityType == OUT_OF_PARTY_ACTIVITY || activityType == GRIND_ACTIVITY) { - mod = AutoScaleActivity(mod); + // Many bots nearby. Do not do heavy area checks. + if (HasManyPlayersNearby()) + return ActivePiorityType::IN_ACTIVE_AREA; } - uint32 ActivityNumber = - GetFixedBotNumer(BotTypeNumber::ACTIVITY_TYPE_NUMBER, 100, - sPlayerbotAIConfig->botActiveAlone * static_cast(mod) / 100 * 0.01f); + return ActivePiorityType::IN_ACTIVE_AREA; +} - return ActivityNumber <= - (sPlayerbotAIConfig->botActiveAlone * mod) / - 100; // The given percentage of bots should be active and rotate 1% of those active bots each minute. +// Returns the lower and upper bracket for bots to be active. +// Ie. { 10, 20 } means all bots in this bracket will be inactive below 10% activityMod, +// and will be active above 20% activityMod and scale between those values. +std::pair PlayerbotAI::GetPriorityBracket(ActivePiorityType type) +{ + switch (type) + { + case ActivePiorityType::HAS_REAL_PLAYER_MASTER: + case ActivePiorityType::IS_REAL_PLAYER: + case ActivePiorityType::IN_GROUP_WITH_REAL_PLAYER: + case ActivePiorityType::IN_INSTANCE: + case ActivePiorityType::VISIBLE_FOR_PLAYER: + return {0, 0}; + case ActivePiorityType::IS_ALWAYS_ACTIVE: + case ActivePiorityType::IN_COMBAT: + return {0, 10}; + case ActivePiorityType::IN_BG_QUEUE: + return {0, 20}; + case ActivePiorityType::IN_LFG: + return {0, 30}; + case ActivePiorityType::NEARBY_PLAYER: + return {0, 40}; + case ActivePiorityType::PLAYER_FRIEND: + case ActivePiorityType::PLAYER_GUILD: + return {0, 50}; + case ActivePiorityType::IN_ACTIVE_AREA: + return {30, 100}; + case ActivePiorityType::IN_ACTIVE_MAP: + return {50, 100}; + case ActivePiorityType::IN_INACTIVE_MAP: + return {70, 100}; + case ActivePiorityType::IN_EMPTY_SERVER: + return {80, 100}; + default: + return {90, 100}; + } + + return {90, 100}; +} + +bool PlayerbotAI::AllowActive(ActivityType activityType) +{ + // General exceptions + if (activityType == PACKET_ACTIVITY) + return true; + + ActivePiorityType type = GetPriorityType(activityType); + if (activityType == DETAILED_MOVE_ACTIVITY) + { + switch (type) + { + case ActivePiorityType::HAS_REAL_PLAYER_MASTER: + case ActivePiorityType::IS_REAL_PLAYER: + case ActivePiorityType::IN_GROUP_WITH_REAL_PLAYER: + case ActivePiorityType::IN_INSTANCE: + case ActivePiorityType::VISIBLE_FOR_PLAYER: + case ActivePiorityType::IN_COMBAT: + case ActivePiorityType::NEARBY_PLAYER: + return true; + break; + case ActivePiorityType::IS_ALWAYS_ACTIVE: + case ActivePiorityType::IN_BG_QUEUE: + case ActivePiorityType::IN_LFG: + case ActivePiorityType::PLAYER_FRIEND: + case ActivePiorityType::PLAYER_GUILD: + case ActivePiorityType::IN_ACTIVE_AREA: + case ActivePiorityType::IN_EMPTY_SERVER: + case ActivePiorityType::IN_ACTIVE_MAP: + case ActivePiorityType::IN_INACTIVE_MAP: + default: + break; + } + } + else if (activityType == REACT_ACTIVITY) + { + switch (type) + { + case ActivePiorityType::HAS_REAL_PLAYER_MASTER: + case ActivePiorityType::IS_REAL_PLAYER: + case ActivePiorityType::IN_GROUP_WITH_REAL_PLAYER: + case ActivePiorityType::IN_INSTANCE: + case ActivePiorityType::VISIBLE_FOR_PLAYER: + case ActivePiorityType::IS_ALWAYS_ACTIVE: + case ActivePiorityType::IN_COMBAT: + return true; + break; + case ActivePiorityType::NEARBY_PLAYER: + case ActivePiorityType::IN_BG_QUEUE: + case ActivePiorityType::IN_LFG: + case ActivePiorityType::PLAYER_FRIEND: + case ActivePiorityType::PLAYER_GUILD: + case ActivePiorityType::IN_ACTIVE_AREA: + case ActivePiorityType::IN_EMPTY_SERVER: + case ActivePiorityType::IN_ACTIVE_MAP: + case ActivePiorityType::IN_INACTIVE_MAP: + default: + return false; + break; + } + } + + // GetPriorityBracket acitivity + float activePerc = 100; + if (sPlayerbotAIConfig->botActiveAloneSmartScale && + bot->GetLevel() >= sPlayerbotAIConfig->botActiveAloneSmartScaleWhenMinLevel && + bot->GetLevel() <= sPlayerbotAIConfig->botActiveAloneSmartScaleWhenMaxLevel) + { + std::pair priorityBracket = GetPriorityBracket(type); + if (!priorityBracket.second) return true; + float activityPercentage = sRandomPlayerbotMgr->getActivityPercentage(); + if (priorityBracket.first >= activityPercentage) return false; + if (priorityBracket.second <= activityPercentage && priorityBracket.second < 100) return true; + activePerc = (activityPercentage - priorityBracket.first) / (priorityBracket.second - priorityBracket.first); + activePerc *= (priorityBracket.second == 100) ? sPlayerbotAIConfig->botActiveAlone : 100; + } + + // The last number if the amount it cycles per min. Currently set to 1% of the active bots. + uint32 ActivityNumber = GetFixedBotNumer(BotTypeNumber::ACTIVITY_TYPE_NUMBER, 100, activePerc * 0.01f); + + // The given percentage of bots should be active and rotate 1% of those active bots each minute. + return ActivityNumber <= (activePerc); } bool PlayerbotAI::AllowActivity(ActivityType activityType, bool checkNow) @@ -4203,31 +4323,6 @@ bool PlayerbotAI::AllowActivity(ActivityType activityType, bool checkNow) return allowed; } -uint32 PlayerbotAI::AutoScaleActivity(uint32 mod) -{ - uint32 maxDiff = sWorldUpdateTime.GetAverageUpdateTime(); - - if (maxDiff > 500) return 0; - if (maxDiff > 250) - { - if (Map* map = bot->GetMap()) - { - if (map->GetEntry()->IsWorldMap() && - (!HasRealPlayers(map) || - !map->IsGridLoaded(bot->GetPositionX(), bot->GetPositionY()))) - return 0; - } - - return (mod * 1) / 10; - } - if (maxDiff > 200) return (mod * 3) / 10; - if (maxDiff > 150) return (mod * 5) / 10; - if (maxDiff > 100) return (mod * 6) / 10; - if (maxDiff > 80) return (mod * 9) / 10; - - return mod; -} - bool PlayerbotAI::IsOpposing(Player* player) { return IsOpposing(player->getRace(), bot->getRace()); } bool PlayerbotAI::IsOpposing(uint8 race1, uint8 race2) @@ -5335,15 +5430,29 @@ bool PlayerbotAI::CanMove() if (IsInVehicle() && !IsInVehicle(true)) return false; - if (bot->isFrozen() || bot->IsPolymorphed() || (bot->isDead() && !bot->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_GHOST)) || - bot->IsBeingTeleported() || bot->isInRoots() || bot->HasAuraType(SPELL_AURA_SPIRIT_OF_REDEMPTION) || - bot->HasAuraType(SPELL_AURA_MOD_CONFUSE) || bot->IsCharmed() || bot->HasAuraType(SPELL_AURA_MOD_STUN) || - bot->HasUnitState(UNIT_STATE_IN_FLIGHT) || bot->HasUnitState(UNIT_STATE_LOST_CONTROL)) + if (bot->isFrozen() || + bot->IsPolymorphed() || + (bot->isDead() && !bot->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_GHOST)) || + bot->IsBeingTeleported() || + bot->isInRoots() || + bot->HasAuraType(SPELL_AURA_SPIRIT_OF_REDEMPTION) || + bot->HasAuraType(SPELL_AURA_MOD_CONFUSE) || + bot->IsCharmed() || + bot->HasAuraType(SPELL_AURA_MOD_STUN) || + bot->HasUnitState(UNIT_STATE_IN_FLIGHT) || + bot->HasUnitState(UNIT_STATE_LOST_CONTROL)) + return false; return bot->GetMotionMaster()->GetCurrentMovementGeneratorType() != FLIGHT_MOTION_TYPE; } +bool PlayerbotAI::IsTaxiFlying() +{ + return bot->HasUnitMovementFlag(MOVEMENTFLAG_ONTRANSPORT) && + bot->HasUnitState(UNIT_STATE_IGNORE_PATHFINDING); +} + bool PlayerbotAI::IsInRealGuild() { if (!bot->GetGuildId()) diff --git a/src/PlayerbotAI.h b/src/PlayerbotAI.h index 6e82338b..d1a2f292 100644 --- a/src/PlayerbotAI.h +++ b/src/PlayerbotAI.h @@ -241,6 +241,27 @@ enum class GuilderType : uint8 VERY_LARGE = 250 }; +enum class ActivePiorityType : uint8 +{ + IS_REAL_PLAYER = 0, + HAS_REAL_PLAYER_MASTER = 1, + IN_GROUP_WITH_REAL_PLAYER = 2, + IN_INSTANCE = 3, + VISIBLE_FOR_PLAYER = 4, + IS_ALWAYS_ACTIVE = 5, + IN_COMBAT = 6, + IN_BG_QUEUE = 7, + IN_LFG = 8, + NEARBY_PLAYER = 9, + PLAYER_FRIEND = 10, + PLAYER_GUILD = 11, + IN_ACTIVE_AREA = 12, + IN_ACTIVE_MAP = 13, + IN_INACTIVE_MAP = 14, + IN_EMPTY_SERVER = 15, + MAX_TYPE +}; + enum ActivityType { GRIND_ACTIVITY = 1, @@ -250,8 +271,8 @@ enum ActivityType PACKET_ACTIVITY = 5, DETAILED_MOVE_ACTIVITY = 6, PARTY_ACTIVITY = 7, - ALL_ACTIVITY = 8, - + REACT_ACTIVITY = 8, + ALL_ACTIVITY = 9, MAX_ACTIVITY_TYPE }; @@ -525,9 +546,10 @@ public: bool HasPlayerNearby(WorldPosition* pos, float range = sPlayerbotAIConfig->reactDistance); bool HasPlayerNearby(float range = sPlayerbotAIConfig->reactDistance); bool HasManyPlayersNearby(uint32 trigerrValue = 20, float range = sPlayerbotAIConfig->sightDistance); + ActivePiorityType GetPriorityType(ActivityType activityType); + std::pair GetPriorityBracket(ActivePiorityType type); bool AllowActive(ActivityType activityType); bool AllowActivity(ActivityType activityType = ALL_ACTIVITY, bool checkNow = false); - uint32 AutoScaleActivity(uint32 mod); // Check if player is safe to use. bool IsSafe(Player* player); @@ -554,6 +576,7 @@ public: void ResetJumpDestination() { jumpDestination = Position(); } bool CanMove(); + bool IsTaxiFlying(); bool IsInRealGuild(); static std::vector dispel_whitelist; bool EqualLowercaseName(std::string s1, std::string s2); diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 84a8f405..816a345f 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -156,7 +156,7 @@ bool PlayerbotAIConfig::Initialize() randomBotAutologin = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutologin", true); minRandomBots = sConfigMgr->GetOption("AiPlayerbot.MinRandomBots", 50); maxRandomBots = sConfigMgr->GetOption("AiPlayerbot.MaxRandomBots", 200); - randomBotUpdateInterval = sConfigMgr->GetOption("AiPlayerbot.RandomBotUpdateInterval", 10); + randomBotUpdateInterval = sConfigMgr->GetOption("AiPlayerbot.RandomBotUpdateInterval", 20); randomBotCountChangeMinInterval = sConfigMgr->GetOption("AiPlayerbot.RandomBotCountChangeMinInterval", 30 * MINUTE); randomBotCountChangeMaxInterval = @@ -465,11 +465,15 @@ bool PlayerbotAIConfig::Initialize() playerbotsXPrate = sConfigMgr->GetOption("AiPlayerbot.KillXPRate", 1); disableDeathKnightLogin = sConfigMgr->GetOption("AiPlayerbot.DisableDeathKnightLogin", 0); botActiveAlone = sConfigMgr->GetOption("AiPlayerbot.BotActiveAlone", 100); - botActiveAloneAutoScale = sConfigMgr->GetOption("AiPlayerbot.botActiveAloneAutoScale", true); - - enablePrototypePerformanceDiff = sConfigMgr->GetOption("AiPlayerbot.EnablePrototypePerformanceDiff", false); - diffWithPlayer = sConfigMgr->GetOption("AiPlayerbot.DiffWithPlayer", 100); - diffEmpty = sConfigMgr->GetOption("AiPlayerbot.DiffEmpty", 200); + botActiveAloneSmartScale = sConfigMgr->GetOption("AiPlayerbot.botActiveAloneSmartScale", 1); + botActiveAloneSmartScaleWhenMinLevel = + sConfigMgr->GetOption("AiPlayerbot.botActiveAloneSmartScaleWhenMinLevel", 1); + botActiveAloneSmartScaleWhenMaxLevel = + sConfigMgr->GetOption("AiPlayerbot.botActiveAloneSmartScaleWhenMaxLevel", 80); + botActiveAloneSmartScaleDiffWithPlayer = + sConfigMgr->GetOption("AiPlayerbot.botActiveAloneSmartScaleDiffWithPlayer", 100); + botActiveAloneSmartScaleDiffEmpty = + sConfigMgr->GetOption("AiPlayerbot.botActiveAloneSmartScaleDiffEmpty", 200); randombotsWalkingRPG = sConfigMgr->GetOption("AiPlayerbot.RandombotsWalkingRPG", false); randombotsWalkingRPGInDoors = sConfigMgr->GetOption("AiPlayerbot.RandombotsWalkingRPG.InDoors", false); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index f8e3fd82..5ac3fa4a 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -263,11 +263,11 @@ public: uint32 playerbotsXPrate; bool disableDeathKnightLogin; uint32 botActiveAlone; - bool botActiveAloneAutoScale; - - uint32 enablePrototypePerformanceDiff; - uint32 diffWithPlayer; - uint32 diffEmpty; + bool botActiveAloneSmartScale; + uint32 botActiveAloneSmartScaleWhenMinLevel; + uint32 botActiveAloneSmartScaleWhenMaxLevel; + uint32 botActiveAloneSmartScaleDiffWithPlayer; + uint32 botActiveAloneSmartScaleDiffEmpty; bool freeMethodLoot; int32 lootRollLevel; diff --git a/src/RandomPlayerbotFactory.cpp b/src/RandomPlayerbotFactory.cpp index 8c350130..57b42bda 100644 --- a/src/RandomPlayerbotFactory.cpp +++ b/src/RandomPlayerbotFactory.cpp @@ -165,6 +165,13 @@ Player* RandomPlayerbotFactory::CreateRandomBot(WorldSession* session, uint8 cls raceOptions.push_back(race); } } + + if (raceOptions.empty()) + { + LOG_ERROR("playerbots", "No races available for class: {}", cls); + return nullptr; + } + uint8 race = raceOptions[urand(0, raceOptions.size() - 1)]; const auto raceAndGender = CombineRaceAndGender(gender, race); diff --git a/src/RandomPlayerbotMgr.cpp b/src/RandomPlayerbotMgr.cpp index 7d26a096..9991144a 100644 --- a/src/RandomPlayerbotMgr.cpp +++ b/src/RandomPlayerbotMgr.cpp @@ -292,12 +292,8 @@ void RandomPlayerbotMgr::UpdateAIInternal(uint32 elapsed, bool /*minimal*/) if (!sPlayerbotAIConfig->randomBotAutologin || !sPlayerbotAIConfig->enabled) return; - if (sPlayerbotAIConfig->enablePrototypePerformanceDiff) + if (sPlayerbotAIConfig->botActiveAloneSmartScale) { - LOG_INFO("playerbots", "---------------------------------------"); - LOG_INFO("playerbots", - "PROTOTYPE: Playerbot performance enhancements are active. Issues and instability may occur."); - LOG_INFO("playerbots", "---------------------------------------"); ScaleBotActivity(); } @@ -414,8 +410,9 @@ void RandomPlayerbotMgr::ScaleBotActivity() // max/min activity // % increase/decrease wanted diff , avg diff - float activityPercentageMod = pid.calculate( - sRandomPlayerbotMgr->GetPlayers().empty() ? sPlayerbotAIConfig->diffEmpty : sPlayerbotAIConfig->diffWithPlayer, + float activityPercentageMod = pid.calculate(sRandomPlayerbotMgr->GetPlayers().empty() ? + sPlayerbotAIConfig->botActiveAloneSmartScaleDiffEmpty : + sPlayerbotAIConfig->botActiveAloneSmartScaleDiffWithPlayer, sWorldUpdateTime.GetAverageUpdateTime()); activityPercentage = activityPercentageMod + 50; @@ -1108,6 +1105,9 @@ bool RandomPlayerbotMgr::ProcessBot(uint32 bot) bool RandomPlayerbotMgr::ProcessBot(Player* player) { + if (!player || !player->IsInWorld() || player->IsBeingTeleported() || player->GetSession()->isLogingOut()) + return false; + uint32 bot = player->GetGUID().GetCounter(); if (player->InBattleground()) diff --git a/src/strategy/AiObjectContext.cpp b/src/strategy/AiObjectContext.cpp index c6fcbc47..d8b603ff 100644 --- a/src/strategy/AiObjectContext.cpp +++ b/src/strategy/AiObjectContext.cpp @@ -51,6 +51,11 @@ AiObjectContext::AiObjectContext(PlayerbotAI* botAI) : PlayerbotAIAware(botAI) actionContexts.Add(new RaidUlduarActionContext()); actionContexts.Add(new RaidIccActionContext()); actionContexts.Add(new WotlkDungeonUKActionContext()); + actionContexts.Add(new WotlkDungeonNexActionContext()); + actionContexts.Add(new WotlkDungeonANActionContext()); + actionContexts.Add(new WotlkDungeonOKActionContext()); + actionContexts.Add(new WotlkDungeonDTKActionContext()); + actionContexts.Add(new WotlkDungeonVHActionContext()); triggerContexts.Add(new TriggerContext()); triggerContexts.Add(new ChatTriggerContext()); @@ -62,6 +67,11 @@ AiObjectContext::AiObjectContext(PlayerbotAI* botAI) : PlayerbotAIAware(botAI) triggerContexts.Add(new RaidUlduarTriggerContext()); triggerContexts.Add(new RaidIccTriggerContext()); triggerContexts.Add(new WotlkDungeonUKTriggerContext()); + triggerContexts.Add(new WotlkDungeonNexTriggerContext()); + triggerContexts.Add(new WotlkDungeonANTriggerContext()); + triggerContexts.Add(new WotlkDungeonOKTriggerContext()); + triggerContexts.Add(new WotlkDungeonDTKTriggerContext()); + triggerContexts.Add(new WotlkDungeonVHTriggerContext()); valueContexts.Add(new ValueContext()); diff --git a/src/strategy/AiObjectContext.h b/src/strategy/AiObjectContext.h index f7bd2694..07fa78a6 100644 --- a/src/strategy/AiObjectContext.h +++ b/src/strategy/AiObjectContext.h @@ -10,6 +10,7 @@ #include #include "Common.h" +#include "DynamicObject.h" #include "NamedObjectContext.h" #include "PlayerbotAIAware.h" #include "Strategy.h" diff --git a/src/strategy/Engine.cpp b/src/strategy/Engine.cpp index d298aaf5..a9569a20 100644 --- a/src/strategy/Engine.cpp +++ b/src/strategy/Engine.cpp @@ -202,9 +202,9 @@ bool Engine::DoNextAction(Unit* unit, uint32 depth, bool minimal) { LogAction("A:%s - PREREQ", action->getName().c_str()); - if (MultiplyAndPush(actionNode->getPrerequisites(), relevance + 0.02, false, event, "prereq")) + if (MultiplyAndPush(actionNode->getPrerequisites(), relevance + 0.002f, false, event, "prereq")) { - PushAgain(actionNode, relevance + 0.01, event); + PushAgain(actionNode, relevance + 0.001f, event); continue; } } @@ -226,7 +226,7 @@ bool Engine::DoNextAction(Unit* unit, uint32 depth, bool minimal) else { LogAction("A:%s - FAILED", action->getName().c_str()); - MultiplyAndPush(actionNode->getAlternatives(), relevance + 0.03, false, event, "alt"); + MultiplyAndPush(actionNode->getAlternatives(), relevance + 0.003f, false, event, "alt"); } } else @@ -246,7 +246,7 @@ bool Engine::DoNextAction(Unit* unit, uint32 depth, bool minimal) botAI->TellMasterNoFacing(out); } LogAction("A:%s - IMPOSSIBLE", action->getName().c_str()); - MultiplyAndPush(actionNode->getAlternatives(), relevance + 0.03, false, event, "alt"); + MultiplyAndPush(actionNode->getAlternatives(), relevance + 0.003f, false, event, "alt"); } } else diff --git a/src/strategy/actions/ActionContext.h b/src/strategy/actions/ActionContext.h index e195e2f9..c13f7162 100644 --- a/src/strategy/actions/ActionContext.h +++ b/src/strategy/actions/ActionContext.h @@ -94,6 +94,7 @@ public: creators["avoid aoe"] = &ActionContext::avoid_aoe; creators["combat formation move"] = &ActionContext::combat_formation_move; creators["tank face"] = &ActionContext::tank_face; + creators["rear flank"] = &ActionContext::rear_flank; creators["disperse set"] = &ActionContext::disperse_set; creators["gift of the naaru"] = &ActionContext::gift_of_the_naaru; creators["shoot"] = &ActionContext::shoot; @@ -278,6 +279,7 @@ private: static Action* avoid_aoe(PlayerbotAI* botAI) { return new AvoidAoeAction(botAI); } static Action* combat_formation_move(PlayerbotAI* botAI) { return new CombatFormationMoveAction(botAI); } static Action* tank_face(PlayerbotAI* botAI) { return new TankFaceAction(botAI); } + static Action* rear_flank(PlayerbotAI* botAI) { return new RearFlankAction(botAI); } static Action* disperse_set(PlayerbotAI* botAI) { return new DisperseSetAction(botAI); } static Action* gift_of_the_naaru(PlayerbotAI* botAI) { return new CastGiftOfTheNaaruAction(botAI); } static Action* lifeblood(PlayerbotAI* botAI) { return new CastLifeBloodAction(botAI); } diff --git a/src/strategy/actions/BattleGroundJoinAction.cpp b/src/strategy/actions/BattleGroundJoinAction.cpp index e6842b23..349ae410 100644 --- a/src/strategy/actions/BattleGroundJoinAction.cpp +++ b/src/strategy/actions/BattleGroundJoinAction.cpp @@ -234,16 +234,15 @@ bool BGJoinAction::shouldJoinBg(BattlegroundQueueTypeId queueTypeId, Battlegroun return false; TeamId teamId = bot->GetTeamId(); - bool noLag = sWorldUpdateTime.GetAverageUpdateTime() < (sRandomPlayerbotMgr->GetPlayers().empty() - ? sPlayerbotAIConfig->diffEmpty - : sPlayerbotAIConfig->diffWithPlayer) * - 1.1; + bool noLag = sWorldUpdateTime.GetAverageUpdateTime() < (sRandomPlayerbotMgr->GetPlayers().empty() ? + sPlayerbotAIConfig->botActiveAloneSmartScaleDiffEmpty : + sPlayerbotAIConfig->botActiveAloneSmartScaleDiffWithPlayer) * 1.1; uint32 BracketSize = bg->GetMaxPlayersPerTeam() * 2; uint32 TeamSize = bg->GetMaxPlayersPerTeam(); // If performance diff is enabled, only queue if there is no lag - if (sPlayerbotAIConfig->enablePrototypePerformanceDiff && !noLag) + if (sPlayerbotAIConfig->botActiveAloneSmartScale && !noLag) return false; // If the bot is in a group, only the leader can queue @@ -578,16 +577,15 @@ bool FreeBGJoinAction::shouldJoinBg(BattlegroundQueueTypeId queueTypeId, Battleg return false; TeamId teamId = bot->GetTeamId(); - bool noLag = sWorldUpdateTime.GetAverageUpdateTime() < (sRandomPlayerbotMgr->GetPlayers().empty() - ? sPlayerbotAIConfig->diffEmpty - : sPlayerbotAIConfig->diffWithPlayer) * - 1.1; + bool noLag = sWorldUpdateTime.GetAverageUpdateTime() < (sRandomPlayerbotMgr->GetPlayers().empty() ? + sPlayerbotAIConfig->botActiveAloneSmartScaleDiffEmpty : + sPlayerbotAIConfig->botActiveAloneSmartScaleDiffWithPlayer) * 1.1; uint32 BracketSize = bg->GetMaxPlayersPerTeam() * 2; uint32 TeamSize = bg->GetMaxPlayersPerTeam(); // If performance diff is enabled, only queue if there is no lag - if (sPlayerbotAIConfig->enablePrototypePerformanceDiff && !noLag) + if (sPlayerbotAIConfig->botActiveAloneSmartScale && !noLag) return false; // If the bot is in a group, only the leader can queue diff --git a/src/strategy/actions/ChooseTargetActions.cpp b/src/strategy/actions/ChooseTargetActions.cpp index dddb3627..c06f675d 100644 --- a/src/strategy/actions/ChooseTargetActions.cpp +++ b/src/strategy/actions/ChooseTargetActions.cpp @@ -82,7 +82,7 @@ bool DropTargetAction::Execute(Event event) bot->SetTarget(ObjectGuid::Empty); bot->SetSelection(ObjectGuid()); botAI->ChangeEngine(BOT_STATE_NON_COMBAT); - botAI->InterruptSpell(); + // botAI->InterruptSpell(); bot->AttackStop(); // if (Pet* pet = bot->GetPet()) diff --git a/src/strategy/actions/GenericActions.cpp b/src/strategy/actions/GenericActions.cpp index 9682d2ef..84997212 100644 --- a/src/strategy/actions/GenericActions.cpp +++ b/src/strategy/actions/GenericActions.cpp @@ -38,7 +38,7 @@ bool TogglePetSpellAutoCastAction::Execute(Event event) bool shouldApply = true; // imp's spell, felhunte's intelligence, cat stealth if (spellId == 4511 || spellId == 1742 || spellId == 54424 || spellId == 57564 || spellId == 57565 || - spellId == 57566 || spellId == 57567 || spellId == 24450 || spellId == 53477) + spellId == 57566 || spellId == 57567 || spellId == 24450) { shouldApply = false; } diff --git a/src/strategy/actions/GenericSpellActions.cpp b/src/strategy/actions/GenericSpellActions.cpp index 9285f20a..749bed14 100644 --- a/src/strategy/actions/GenericSpellActions.cpp +++ b/src/strategy/actions/GenericSpellActions.cpp @@ -196,8 +196,8 @@ bool CastEnchantItemAction::isPossible() } CastHealingSpellAction::CastHealingSpellAction(PlayerbotAI* botAI, std::string const spell, uint8 estAmount, - HealingManaEfficiency manaEfficiency) - : CastAuraSpellAction(botAI, spell, true), estAmount(estAmount), manaEfficiency(manaEfficiency) + HealingManaEfficiency manaEfficiency, bool isOwner) + : CastAuraSpellAction(botAI, spell, isOwner), estAmount(estAmount), manaEfficiency(manaEfficiency) { range = botAI->GetRange("heal"); } diff --git a/src/strategy/actions/GenericSpellActions.h b/src/strategy/actions/GenericSpellActions.h index 2ab49bb0..be1dc200 100644 --- a/src/strategy/actions/GenericSpellActions.h +++ b/src/strategy/actions/GenericSpellActions.h @@ -129,7 +129,7 @@ class CastHealingSpellAction : public CastAuraSpellAction { public: CastHealingSpellAction(PlayerbotAI* botAI, std::string const spell, uint8 estAmount = 15.0f, - HealingManaEfficiency manaEfficiency = HealingManaEfficiency::MEDIUM); + HealingManaEfficiency manaEfficiency = HealingManaEfficiency::MEDIUM, bool isOwner = true); std::string const GetTargetName() override { return "self target"; } bool isUseful() override; @@ -177,8 +177,8 @@ class HealPartyMemberAction : public CastHealingSpellAction, public PartyMemberA { public: HealPartyMemberAction(PlayerbotAI* botAI, std::string const spell, uint8 estAmount = 15.0f, - HealingManaEfficiency manaEfficiency = HealingManaEfficiency::MEDIUM) - : CastHealingSpellAction(botAI, spell, estAmount, manaEfficiency), PartyMemberActionNameSupport(spell) + HealingManaEfficiency manaEfficiency = HealingManaEfficiency::MEDIUM, bool isOwner = true) + : CastHealingSpellAction(botAI, spell, estAmount, manaEfficiency, isOwner), PartyMemberActionNameSupport(spell) { } diff --git a/src/strategy/actions/MovementActions.cpp b/src/strategy/actions/MovementActions.cpp index e07fc79a..791f0990 100644 --- a/src/strategy/actions/MovementActions.cpp +++ b/src/strategy/actions/MovementActions.cpp @@ -827,11 +827,12 @@ bool MovementAction::ReachCombatTo(Unit* target, float distance) float shortenTo = distance; // Avoid walking too far when moving towards each other - if (bot->GetDistance(tx, ty, tz) >= 10.0f) - shortenTo = std::max(distance, bot->GetDistance(tx, ty, tz) / 2); + float disToGo = bot->GetExactDist(tx, ty, tz) - distance; + if (disToGo >= 10.0f) + shortenTo = disToGo / 2 + distance; - if (bot->GetExactDist(tx, ty, tz) <= shortenTo) - return false; + // if (bot->GetExactDist(tx, ty, tz) <= shortenTo) + // return false; path.ShortenPathUntilDist(G3D::Vector3(tx, ty, tz), shortenTo); G3D::Vector3 endPos = path.GetPath().back(); @@ -2313,7 +2314,7 @@ bool TankFaceAction::Execute(Event event) if (!bot->GetGroup()) return false; - if (!bot->IsWithinMeleeRange(target)) + if (!bot->IsWithinMeleeRange(target) || target->isMoving()) return false; if (!AI_VALUE2(bool, "has aggro", "current target")) @@ -2371,6 +2372,46 @@ bool TankFaceAction::Execute(Event event) return MoveTo(bot->GetMapId(), nearest.GetPositionX(), nearest.GetPositionY(), nearest.GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_COMBAT); } +bool RearFlankAction::isUseful() +{ + Unit* target = AI_VALUE(Unit*, "current target"); + if (!target) { return false; } + + // Need to double the front angle check to account for mirrored angle. + bool inFront = target->HasInArc(2.f * minAngle, bot); + // Rear check does not need to double this angle as the logic is inverted + // and we are subtracting from 2pi. + bool inRear = !target->HasInArc((2.f * M_PI) - maxAngle, bot); + + return inFront || inRear; +} + +bool RearFlankAction::Execute(Event event) +{ + Unit* target = AI_VALUE(Unit*, "current target"); + if (!target) { return false; } + + float angle = frand(minAngle, maxAngle); + float baseDistance = bot->GetMeleeRange(target) * 0.5f; + Position leftFlank = target->GetPosition(); + Position rightFlank = target->GetPosition(); + Position* destination = nullptr; + leftFlank.RelocatePolarOffset(angle, baseDistance + distance); + rightFlank.RelocatePolarOffset(-angle, baseDistance + distance); + + if (bot->GetExactDist2d(leftFlank) < bot->GetExactDist2d(rightFlank)) + { + destination = &leftFlank; + } + else + { + destination = &rightFlank; + } + + return MoveTo(bot->GetMapId(), destination->GetPositionX(), destination->GetPositionY(), destination->GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_COMBAT); +} + bool DisperseSetAction::Execute(Event event) { std::string const text = event.getParam(); @@ -2513,7 +2554,7 @@ bool SetBehindTargetAction::Execute(Event event) if (target->GetVictim() == bot) return false; - if (!bot->IsWithinMeleeRange(target)) + if (!bot->IsWithinMeleeRange(target) || target->isMoving()) return false; float deltaAngle = Position::NormalizeOrientation(target->GetOrientation() - target->GetAngle(bot)); diff --git a/src/strategy/actions/MovementActions.h b/src/strategy/actions/MovementActions.h index 7b03f034..76929427 100644 --- a/src/strategy/actions/MovementActions.h +++ b/src/strategy/actions/MovementActions.h @@ -18,6 +18,9 @@ class Unit; class WorldObject; class Position; +#define ANGLE_45_DEG (static_cast(M_PI) / 4.f) +#define ANGLE_90_DEG M_PI_2 +#define ANGLE_120_DEG (2.f * static_cast(M_PI) / 3.f) class MovementAction : public Action { @@ -144,6 +147,27 @@ public: bool Execute(Event event) override; }; +class RearFlankAction : public MovementAction +{ +// 90 degree minimum angle prevents any frontal cleaves/breaths and avoids parry-hasting the boss. +// 120 degree maximum angle leaves a 120 degree symmetrical cone at the tail end which is usually enough to avoid tail swipes. +// Some dragons or mobs may have different danger zone angles, override if needed. +public: + RearFlankAction(PlayerbotAI* botAI, float distance = 0.0f, float minAngle = ANGLE_90_DEG, float maxAngle = ANGLE_120_DEG) + : MovementAction(botAI, "rear flank") + { + this->distance = distance; + this->minAngle = minAngle; + this->maxAngle = maxAngle; + } + + bool Execute(Event event) override; + bool isUseful() override; + +protected: + float distance, minAngle, maxAngle; +}; + class DisperseSetAction : public Action { public: @@ -268,4 +292,5 @@ public: bool Execute(Event event) override; }; + #endif diff --git a/src/strategy/druid/CasterDruidStrategy.cpp b/src/strategy/druid/CasterDruidStrategy.cpp index 1dfeadfa..27080efe 100644 --- a/src/strategy/druid/CasterDruidStrategy.cpp +++ b/src/strategy/druid/CasterDruidStrategy.cpp @@ -145,15 +145,12 @@ void CasterDruidStrategy::InitTriggers(std::vector& triggers) new TriggerNode("medium mana", NextAction::array(0, new NextAction("innervate", ACTION_HIGH + 9), NULL))); triggers.push_back(new TriggerNode("enemy too close for spell", NextAction::array(0, new NextAction("flee", ACTION_MOVE + 9), nullptr))); - triggers.push_back( - new TriggerNode("party member remove curse", - NextAction::array(0, new NextAction("remove curse on party", ACTION_DISPEL + 7), NULL))); } void CasterDruidAoeStrategy::InitTriggers(std::vector& triggers) { triggers.push_back( - new TriggerNode("high aoe", NextAction::array(0, new NextAction("hurricane", ACTION_HIGH + 1), nullptr))); + new TriggerNode("medium aoe", NextAction::array(0, new NextAction("hurricane", ACTION_HIGH + 1), nullptr))); triggers.push_back(new TriggerNode( "light aoe", NextAction::array(0, new NextAction("starfall", ACTION_NORMAL + 5), new NextAction("insect swarm on attacker", ACTION_NORMAL + 3), diff --git a/src/strategy/druid/DruidActions.cpp b/src/strategy/druid/DruidActions.cpp index 739ac941..1743a339 100644 --- a/src/strategy/druid/DruidActions.cpp +++ b/src/strategy/druid/DruidActions.cpp @@ -47,4 +47,35 @@ bool CastRebirthAction::isUseful() { return CastSpellAction::isUseful() && AI_VALUE2(float, "distance", GetTargetName()) <= sPlayerbotAIConfig->spellDistance; +} + +Unit* CastRejuvenationOnNotFullAction::GetTarget() +{ + Group* group = bot->GetGroup(); + MinValueCalculator calc(100); + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + Player* player = gref->GetSource(); + if (!player) + continue; + if (player->isDead() || player->IsFullHealth()) + { + continue; + } + if (player->GetDistance2d(bot) > sPlayerbotAIConfig->spellDistance) + { + continue; + } + if (botAI->HasAura("rejuvenation", player)) + { + continue; + } + calc.probe(player->GetHealthPct(), player); + } + return (Unit*)calc.param; +} + +bool CastRejuvenationOnNotFullAction::isUseful() +{ + return GetTarget(); } \ No newline at end of file diff --git a/src/strategy/druid/DruidActions.h b/src/strategy/druid/DruidActions.h index 749ba3ea..ee90788a 100644 --- a/src/strategy/druid/DruidActions.h +++ b/src/strategy/druid/DruidActions.h @@ -311,4 +311,16 @@ public: CastEnrageAction(PlayerbotAI* ai) : CastBuffSpellAction(ai, "enrage") {} }; + +class CastRejuvenationOnNotFullAction : public HealPartyMemberAction +{ +public: + CastRejuvenationOnNotFullAction(PlayerbotAI* ai) + : HealPartyMemberAction(ai, "rejuvenation", 5.0f, HealingManaEfficiency::VERY_HIGH) + { + } + bool isUseful() override; + Unit* GetTarget() override; +}; + #endif diff --git a/src/strategy/druid/DruidAiObjectContext.cpp b/src/strategy/druid/DruidAiObjectContext.cpp index 04652487..e0d8c7de 100644 --- a/src/strategy/druid/DruidAiObjectContext.cpp +++ b/src/strategy/druid/DruidAiObjectContext.cpp @@ -14,6 +14,7 @@ #include "DruidShapeshiftActions.h" #include "DruidTriggers.h" #include "GenericDruidNonCombatStrategy.h" +#include "GenericDruidStrategy.h" #include "HealDruidStrategy.h" #include "MeleeDruidStrategy.h" #include "Playerbots.h" @@ -33,6 +34,7 @@ public: creators["buff"] = &DruidStrategyFactoryInternal::buff; creators["boost"] = &DruidStrategyFactoryInternal::boost; creators["cc"] = &DruidStrategyFactoryInternal::cc; + creators["healer dps"] = &DruidStrategyFactoryInternal::healer_dps; } private: @@ -45,6 +47,7 @@ private: static Strategy* buff(PlayerbotAI* botAI) { return new GenericDruidBuffStrategy(botAI); } static Strategy* boost(PlayerbotAI* botAI) { return new DruidBoostStrategy(botAI); } static Strategy* cc(PlayerbotAI* botAI) { return new DruidCcStrategy(botAI); } + static Strategy* healer_dps(PlayerbotAI* botAI) { return new DruidHealerDpsStrategy(botAI); } }; class DruidDruidStrategyFactoryInternal : public NamedObjectContext @@ -161,6 +164,7 @@ public: creators["travel form"] = &DruidAiObjectContextInternal::travel_form; creators["aquatic form"] = &DruidAiObjectContextInternal::aquatic_form; creators["caster form"] = &DruidAiObjectContextInternal::caster_form; + creators["cancel tree form"] = &DruidAiObjectContextInternal::cancel_tree_form; creators["mangle (bear)"] = &DruidAiObjectContextInternal::mangle_bear; creators["maul"] = &DruidAiObjectContextInternal::maul; creators["bash"] = &DruidAiObjectContextInternal::bash; @@ -205,6 +209,7 @@ public: creators["healing touch"] = &DruidAiObjectContextInternal::healing_touch; creators["regrowth on party"] = &DruidAiObjectContextInternal::regrowth_on_party; creators["rejuvenation on party"] = &DruidAiObjectContextInternal::rejuvenation_on_party; + creators["rejuvenation on not full"] = &DruidAiObjectContextInternal::rejuvenation_on_not_full; creators["healing touch on party"] = &DruidAiObjectContextInternal::healing_touch_on_party; creators["rebirth"] = &DruidAiObjectContextInternal::rebirth; creators["revive"] = &DruidAiObjectContextInternal::revive; @@ -246,6 +251,7 @@ private: static Action* travel_form(PlayerbotAI* botAI) { return new CastTravelFormAction(botAI); } static Action* aquatic_form(PlayerbotAI* botAI) { return new CastAquaticFormAction(botAI); } static Action* caster_form(PlayerbotAI* botAI) { return new CastCasterFormAction(botAI); } + static Action* cancel_tree_form(PlayerbotAI* botAI) { return new CastCancelTreeFormAction(botAI); } static Action* mangle_bear(PlayerbotAI* botAI) { return new CastMangleBearAction(botAI); } static Action* maul(PlayerbotAI* botAI) { return new CastMaulAction(botAI); } static Action* bash(PlayerbotAI* botAI) { return new CastBashAction(botAI); } @@ -290,6 +296,7 @@ private: static Action* healing_touch(PlayerbotAI* botAI) { return new CastHealingTouchAction(botAI); } static Action* regrowth_on_party(PlayerbotAI* botAI) { return new CastRegrowthOnPartyAction(botAI); } static Action* rejuvenation_on_party(PlayerbotAI* botAI) { return new CastRejuvenationOnPartyAction(botAI); } + static Action* rejuvenation_on_not_full(PlayerbotAI* botAI) { return new CastRejuvenationOnNotFullAction(botAI); } static Action* healing_touch_on_party(PlayerbotAI* botAI) { return new CastHealingTouchOnPartyAction(botAI); } static Action* rebirth(PlayerbotAI* botAI) { return new CastRebirthAction(botAI); } static Action* revive(PlayerbotAI* botAI) { return new CastReviveAction(botAI); } diff --git a/src/strategy/druid/DruidShapeshiftActions.cpp b/src/strategy/druid/DruidShapeshiftActions.cpp index 9894b1f5..4354328e 100644 --- a/src/strategy/druid/DruidShapeshiftActions.cpp +++ b/src/strategy/druid/DruidShapeshiftActions.cpp @@ -45,6 +45,17 @@ bool CastCasterFormAction::Execute(Event event) return true; } +bool CastCancelTreeFormAction::isUseful() +{ + return botAI->HasAura(33891, bot); +} + +bool CastCancelTreeFormAction::Execute(Event event) +{ + botAI->RemoveAura("tree of life"); + return true; +} + bool CastTreeFormAction::isUseful() { return GetTarget() && CastSpellAction::isUseful() && !botAI->HasAura(33891, bot); diff --git a/src/strategy/druid/DruidShapeshiftActions.h b/src/strategy/druid/DruidShapeshiftActions.h index 1f8fc082..6b24208b 100644 --- a/src/strategy/druid/DruidShapeshiftActions.h +++ b/src/strategy/druid/DruidShapeshiftActions.h @@ -70,4 +70,14 @@ public: bool Execute(Event event) override; }; +class CastCancelTreeFormAction : public CastBuffSpellAction +{ +public: + CastCancelTreeFormAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "cancel tree form") {} + + bool isUseful() override; + bool isPossible() override { return true; } + bool Execute(Event event) override; +}; + #endif diff --git a/src/strategy/druid/GenericDruidNonCombatStrategy.cpp b/src/strategy/druid/GenericDruidNonCombatStrategy.cpp index 398ba274..be5e822f 100644 --- a/src/strategy/druid/GenericDruidNonCombatStrategy.cpp +++ b/src/strategy/druid/GenericDruidNonCombatStrategy.cpp @@ -125,26 +125,34 @@ void GenericDruidNonCombatStrategy::InitTriggers(std::vector& trig triggers.push_back( new TriggerNode("party member critical health", - NextAction::array(0, new NextAction("rejuvenation on party", ACTION_MEDIUM_HEAL + 5), - new NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 6), NULL))); + NextAction::array(0, + new NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 7), + new NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 6), + new NextAction("rejuvenation on party", ACTION_MEDIUM_HEAL + 5), + nullptr))); triggers.push_back( new TriggerNode("party member low health", - NextAction::array(0, new NextAction("rejuvenation on party", ACTION_MEDIUM_HEAL + 3), - new NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 4), NULL))); + NextAction::array(0, + new NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 5), + new NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 4), + new NextAction("rejuvenation on party", ACTION_MEDIUM_HEAL + 3), + nullptr))); triggers.push_back( new TriggerNode("party member medium health", - NextAction::array(0, new NextAction("rejuvenation on party", ACTION_MEDIUM_HEAL + 1), - new NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 2), NULL))); + NextAction::array(0, new NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 3), + new NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 2), + new NextAction("rejuvenation on party", ACTION_MEDIUM_HEAL + 1), + nullptr))); triggers.push_back( new TriggerNode("party member almost full health", - NextAction::array(0, new NextAction("rejuvenation on party", ACTION_LIGHT_HEAL + 2), NULL))); + NextAction::array(0, new NextAction("wild growth on party", ACTION_LIGHT_HEAL + 3), new NextAction("rejuvenation on party", ACTION_LIGHT_HEAL + 2), NULL))); triggers.push_back( new TriggerNode("party member remove curse", - NextAction::array(0, new NextAction("remove curse on party", ACTION_DISPEL + 7), NULL))); + NextAction::array(0, new NextAction("remove curse on party", ACTION_DISPEL + 7), nullptr))); } GenericDruidBuffStrategy::GenericDruidBuffStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) diff --git a/src/strategy/druid/GenericDruidStrategy.cpp b/src/strategy/druid/GenericDruidStrategy.cpp index c44668f7..d06a0e56 100644 --- a/src/strategy/druid/GenericDruidStrategy.cpp +++ b/src/strategy/druid/GenericDruidStrategy.cpp @@ -130,6 +130,11 @@ void DruidCureStrategy::InitTriggers(std::vector& triggers) triggers.push_back( new TriggerNode("party member cure poison", NextAction::array(0, new NextAction("abolish poison on party", ACTION_DISPEL + 1), nullptr))); + + triggers.push_back( + new TriggerNode("party member remove curse", + NextAction::array(0, new NextAction("remove curse on party", ACTION_DISPEL + 7), NULL))); + } void DruidBoostStrategy::InitTriggers(std::vector& triggers) @@ -147,3 +152,22 @@ void DruidCcStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode( "hibernate", NextAction::array(0, new NextAction("hibernate on cc", ACTION_HIGH + 3), nullptr))); } + +void DruidHealerDpsStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back( + new TriggerNode("healer should attack", + NextAction::array(0, + new NextAction("cancel tree form", ACTION_DEFAULT + 0.3f), + new NextAction("moonfire", ACTION_DEFAULT + 0.2f), + new NextAction("wrath", ACTION_DEFAULT + 0.1f), + new NextAction("starfire", ACTION_DEFAULT), + nullptr))); + + // long cast time + // triggers.push_back( + // new TriggerNode("medium aoe and healer should attack", + // NextAction::array(0, + // new NextAction("hurricane", ACTION_DEFAULT + 0.7f), + // nullptr))); +} diff --git a/src/strategy/druid/GenericDruidStrategy.h b/src/strategy/druid/GenericDruidStrategy.h index db6631b2..547b93b0 100644 --- a/src/strategy/druid/GenericDruidStrategy.h +++ b/src/strategy/druid/GenericDruidStrategy.h @@ -46,4 +46,13 @@ public: std::string const getName() override { return "cc"; } }; +class DruidHealerDpsStrategy : public Strategy +{ +public: + DruidHealerDpsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "healer dps"; } +}; + #endif diff --git a/src/strategy/druid/HealDruidStrategy.cpp b/src/strategy/druid/HealDruidStrategy.cpp index 0da13563..b1e2f125 100644 --- a/src/strategy/druid/HealDruidStrategy.cpp +++ b/src/strategy/druid/HealDruidStrategy.cpp @@ -10,7 +10,12 @@ class HealDruidStrategyActionNodeFactory : public NamedObjectFactory { public: - HealDruidStrategyActionNodeFactory() { creators["nourish on party"] = &nourtish_on_party; } + HealDruidStrategyActionNodeFactory() { + creators["nourish on party"] = &nourtish_on_party; + // creators["wild growth on party"] = &wild_growth_on_party; + // creators["rejuvenation on party"] = &rejuvenation_on_party; + // creators["regrowth on party"] = ®rowth_on_party; + } private: static ActionNode* nourtish_on_party([[maybe_unused]] PlayerbotAI* botAI) @@ -20,6 +25,27 @@ private: /*A*/ NextAction::array(0, new NextAction("healing touch on party"), nullptr), /*C*/ nullptr); } + // static ActionNode* wild_growth_on_party([[maybe_unused]] PlayerbotAI* botAI) + // { + // return new ActionNode("wild growth on party", + // /*P*/ NextAction::array(0, new NextAction("tree form"), nullptr), + // /*A*/ nullptr, + // /*C*/ nullptr); + // } + // static ActionNode* rejuvenation_on_party([[maybe_unused]] PlayerbotAI* botAI) + // { + // return new ActionNode("rejuvenation on party", + // /*P*/ NextAction::array(0, new NextAction("tree form"), nullptr), + // /*A*/ nullptr, + // /*C*/ nullptr); + // } + // static ActionNode* regrowth_on_party([[maybe_unused]] PlayerbotAI* botAI) + // { + // return new ActionNode("regrowth on party", + // /*P*/ NextAction::array(0, new NextAction("tree form"), nullptr), + // /*A*/ nullptr, + // /*C*/ nullptr); + // } }; HealDruidStrategy::HealDruidStrategy(PlayerbotAI* botAI) : GenericDruidStrategy(botAI) @@ -31,61 +57,72 @@ void HealDruidStrategy::InitTriggers(std::vector& triggers) { GenericDruidStrategy::InitTriggers(triggers); - // triggers.push_back(new TriggerNode("enemy out of spell", NextAction::array(0, new NextAction("reach spell", - // ACTION_NORMAL + 9), nullptr))); - triggers.push_back( - new TriggerNode("tree form", NextAction::array(0, new NextAction("tree form", ACTION_HIGH + 1), nullptr))); + // triggers.push_back( + // new TriggerNode("tree form", NextAction::array(0, new NextAction("tree form", ACTION_HIGH + 1), nullptr))); + triggers.push_back(new TriggerNode( "party member to heal out of spell range", NextAction::array(0, new NextAction("reach party member to heal", ACTION_CRITICAL_HEAL + 9), nullptr))); - triggers.push_back( - new TriggerNode("party member remove curse", - NextAction::array(0, new NextAction("remove curse on party", ACTION_DISPEL + 7), NULL))); + // CRITICAL triggers.push_back( new TriggerNode("party member critical health", - NextAction::array(0, new NextAction("swiftmend on party", ACTION_CRITICAL_HEAL + 4), - new NextAction("wild growth", ACTION_CRITICAL_HEAL + 3), - new NextAction("regrowth on party", ACTION_CRITICAL_HEAL + 2), + NextAction::array(0, + new NextAction("tree form", ACTION_CRITICAL_HEAL + 4.1f), + new NextAction("swiftmend on party", ACTION_CRITICAL_HEAL + 4), + new NextAction("regrowth on party", ACTION_CRITICAL_HEAL + 3), + new NextAction("wild growth on party", ACTION_CRITICAL_HEAL + 2), new NextAction("nourish on party", ACTION_CRITICAL_HEAL + 1), // new NextAction("healing touch on party", ACTION_CRITICAL_HEAL + 0), - NULL))); + nullptr))); triggers.push_back( new TriggerNode("party member critical health", - NextAction::array(0, new NextAction("nature's swiftness", ACTION_CRITICAL_HEAL + 4), NULL))); + NextAction::array(0, new NextAction("nature's swiftness", ACTION_CRITICAL_HEAL + 4), nullptr))); + + triggers.push_back(new TriggerNode( + "group heal setting", + NextAction::array(0, + new NextAction("tree form", ACTION_MEDIUM_HEAL + 2.3f), + new NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 2.2f), + new NextAction("rejuvenation on not full", ACTION_MEDIUM_HEAL + 2.1f), + nullptr))); triggers.push_back( - new TriggerNode("medium group heal occasion", - NextAction::array(0, new NextAction("tranquility", ACTION_CRITICAL_HEAL + 5), NULL))); + new TriggerNode("medium group heal setting", + NextAction::array(0, + new NextAction("tree form", ACTION_CRITICAL_HEAL + 0.6f), + new NextAction("tranquility", ACTION_CRITICAL_HEAL + 0.5f), nullptr))); // LOW triggers.push_back( new TriggerNode("party member low health", - NextAction::array(0, new NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 9), - new NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 8), - new NextAction("swiftmend on party", ACTION_MEDIUM_HEAL + 7), - new NextAction("nourish on party", ACTION_MEDIUM_HEAL + 6), - // new NextAction("healing touch on party", ACTION_MEDIUM_HEAL + 5), - NULL))); + NextAction::array(0, new NextAction("tree form", ACTION_MEDIUM_HEAL + 1.5f), + new NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 1.4f), + new NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 1.3f), + new NextAction("swiftmend on party", ACTION_MEDIUM_HEAL + 1.2), + new NextAction("nourish on party", ACTION_MEDIUM_HEAL + 1.1f), + nullptr))); // MEDIUM triggers.push_back( new TriggerNode("party member medium health", - NextAction::array(0, new NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 4), - new NextAction("rejuvenation on party", ACTION_MEDIUM_HEAL + 3), - new NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 2), - new NextAction("nourish on party", ACTION_MEDIUM_HEAL + 1), NULL))); + NextAction::array(0, + new NextAction("tree form", ACTION_MEDIUM_HEAL + 0.5f), + new NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 0.4f), + new NextAction("rejuvenation on party", ACTION_MEDIUM_HEAL + 0.3f), + new NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 0.2f), + new NextAction("nourish on party", ACTION_MEDIUM_HEAL + 0.1f), nullptr))); // almost full triggers.push_back( new TriggerNode("party member almost full health", - NextAction::array(0, new NextAction("wild growth on party", ACTION_LIGHT_HEAL + 3), - new NextAction("rejuvenation on party", ACTION_LIGHT_HEAL + 2), - new NextAction("regrowth on party", ACTION_LIGHT_HEAL + 1), NULL))); + NextAction::array(0, new NextAction("wild growth on party", ACTION_LIGHT_HEAL + 0.3f), + new NextAction("rejuvenation on party", ACTION_LIGHT_HEAL + 0.2f), + new NextAction("regrowth on party", ACTION_LIGHT_HEAL + 0.1f), nullptr))); triggers.push_back( - new TriggerNode("medium mana", NextAction::array(0, new NextAction("innervate", ACTION_HIGH + 5), NULL))); + new TriggerNode("medium mana", NextAction::array(0, new NextAction("innervate", ACTION_HIGH + 5), nullptr))); triggers.push_back(new TriggerNode("enemy too close for spell", NextAction::array(0, new NextAction("flee", ACTION_MOVE + 9), nullptr))); diff --git a/src/strategy/dungeons/DungeonStrategyContext.h b/src/strategy/dungeons/DungeonStrategyContext.h index f1d8e18f..8062e09b 100644 --- a/src/strategy/dungeons/DungeonStrategyContext.h +++ b/src/strategy/dungeons/DungeonStrategyContext.h @@ -1,22 +1,17 @@ -#ifndef _PLAYERBOT_DUNGEONSTRATEGYCONTEXT_H_ -#define _PLAYERBOT_DUNGEONSTRATEGYCONTEXT_H_ +#ifndef _PLAYERBOT_DUNGEONSTRATEGYCONTEXT_H +#define _PLAYERBOT_DUNGEONSTRATEGYCONTEXT_H #include "Strategy.h" #include "wotlk/utgardekeep/UtgardeKeepStrategy.h" +#include "wotlk/nexus/NexusStrategy.h" +#include "wotlk/azjolnerub/AzjolNerubStrategy.h" +#include "wotlk/oldkingdom/OldKingdomStrategy.h" +#include "wotlk/draktharonkeep/DrakTharonKeepStrategy.h" +#include "wotlk/violethold/VioletHoldStrategy.h" /* Full list/TODO: -The Nexus - Nex -Grand Magus Telestra, Anomalus, Ormorok the Tree-Shaper, Keristrasza, Commander Stoutbeard (Horde Heroic Only)/Commander Kolurg (Alliance Heroic Only) -Azjol-Nerub: Azjol-Nerub - AN -Krik'thir the Gatewatcher, Hadronox, Anub'arak -Ahn'kahet: The Old Kingdom - OK -Elder Nadox, Prince Taldaram, Jedoga Shadowseeker, Herald Volazj, Amanitar (Heroic Only) -Drak'Tharon Keep - DTK -Trollgore, Novos the Summoner, King Dred, The Prophet Tharon'ja -The Violet Hold - VH -Erekem, Moragg, Ichoron, Xevozz, Lavanthor, Zuramat the Obliterator, Cyanigosa Gundrak - GD Slad'ran, Drakkari Colossus, Moorabi, Gal'darah, Eck the Ferocious (Heroic only) Halls of Stone - HoS @@ -76,11 +71,12 @@ class DungeonStrategyContext : public NamedObjectContext } private: static Strategy* wotlk_uk(PlayerbotAI* botAI) { return new WotlkDungeonUKStrategy(botAI); } - static Strategy* wotlk_nex(PlayerbotAI* botAI) { return new WotlkDungeonUKStrategy(botAI); } - static Strategy* wotlk_an(PlayerbotAI* botAI) { return new WotlkDungeonUKStrategy(botAI); } - static Strategy* wotlk_ok(PlayerbotAI* botAI) { return new WotlkDungeonUKStrategy(botAI); } - static Strategy* wotlk_dtk(PlayerbotAI* botAI) { return new WotlkDungeonUKStrategy(botAI); } - static Strategy* wotlk_vh(PlayerbotAI* botAI) { return new WotlkDungeonUKStrategy(botAI); } + static Strategy* wotlk_nex(PlayerbotAI* botAI) { return new WotlkDungeonNexStrategy(botAI); } + static Strategy* wotlk_an(PlayerbotAI* botAI) { return new WotlkDungeonANStrategy(botAI); } + static Strategy* wotlk_ok(PlayerbotAI* botAI) { return new WotlkDungeonOKStrategy(botAI); } + static Strategy* wotlk_dtk(PlayerbotAI* botAI) { return new WotlkDungeonDTKStrategy(botAI); } + static Strategy* wotlk_vh(PlayerbotAI* botAI) { return new WotlkDungeonVHStrategy(botAI); } + static Strategy* wotlk_gd(PlayerbotAI* botAI) { return new WotlkDungeonUKStrategy(botAI); } static Strategy* wotlk_hos(PlayerbotAI* botAI) { return new WotlkDungeonUKStrategy(botAI); } static Strategy* wotlk_hol(PlayerbotAI* botAI) { return new WotlkDungeonUKStrategy(botAI); } diff --git a/src/strategy/dungeons/DungeonStrategyUtils.h b/src/strategy/dungeons/DungeonStrategyUtils.h index 6c014aba..2b738360 100644 --- a/src/strategy/dungeons/DungeonStrategyUtils.h +++ b/src/strategy/dungeons/DungeonStrategyUtils.h @@ -1,5 +1,5 @@ -#ifndef _PLAYERBOT_DUNGEONUTILS_H_ -#define _PLAYERBOT_DUNGEONUTILS_H_ +#ifndef _PLAYERBOT_DUNGEONUTILS_H +#define _PLAYERBOT_DUNGEONUTILS_H template inline diff --git a/src/strategy/dungeons/wotlk/WotlkDungeonActionContext.h b/src/strategy/dungeons/wotlk/WotlkDungeonActionContext.h index ea13c9b0..060a4e5c 100644 --- a/src/strategy/dungeons/wotlk/WotlkDungeonActionContext.h +++ b/src/strategy/dungeons/wotlk/WotlkDungeonActionContext.h @@ -1,12 +1,12 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONACTIONCONTEXT_H_ -#define _PLAYERBOT_WOTLKDUNGEONACTIONCONTEXT_H_ +#ifndef _PLAYERBOT_WOTLKDUNGEONACTIONCONTEXT_H +#define _PLAYERBOT_WOTLKDUNGEONACTIONCONTEXT_H #include "utgardekeep/UtgardeKeepActionContext.h" -// #include "nexus/NexusActionContext.h" -// #include "azjolnerub/AzjolNerubActionContext.h" -// #include "oldkingdom/OldKingdomActionContext.h" -// #include "draktharonkeep/DraktharonKeepActionContext.h" -// #include "violethold/VioletHoldActionContext.h" +#include "nexus/NexusActionContext.h" +#include "azjolnerub/AzjolNerubActionContext.h" +#include "oldkingdom/OldKingdomActionContext.h" +#include "draktharonkeep/DrakTharonKeepActionContext.h" +#include "violethold/VioletHoldActionContext.h" // #include "gundrak/GundrakActionContext.h" // #include "hallsofstone/HallsOfStoneActionContext.h" // #include "hallsoflightning/HallsOfLightningActionContext.h" diff --git a/src/strategy/dungeons/wotlk/WotlkDungeonTriggerContext.h b/src/strategy/dungeons/wotlk/WotlkDungeonTriggerContext.h index 12654700..11097b06 100644 --- a/src/strategy/dungeons/wotlk/WotlkDungeonTriggerContext.h +++ b/src/strategy/dungeons/wotlk/WotlkDungeonTriggerContext.h @@ -1,12 +1,12 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONTRIGGERCONTEXT_H_ -#define _PLAYERBOT_WOTLKDUNGEONTRIGGERCONTEXT_H_ +#ifndef _PLAYERBOT_WOTLKDUNGEONTRIGGERCONTEXT_H +#define _PLAYERBOT_WOTLKDUNGEONTRIGGERCONTEXT_H #include "utgardekeep/UtgardeKeepTriggerContext.h" -// #include "nexus/NexusTriggerContext.h" -// #include "azjolnerub/AzjolNerubTriggerContext.h" -// #include "oldkingdom/OldKingdomTriggerContext.h" -// #include "draktharonkeep/DraktharonKeepTriggerContext.h" -// #include "violethold/VioletHoldTriggerContext.h" +#include "nexus/NexusTriggerContext.h" +#include "azjolnerub/AzjolNerubTriggerContext.h" +#include "oldkingdom/OldKingdomTriggerContext.h" +#include "draktharonkeep/DrakTharonKeepTriggerContext.h" +#include "violethold/VioletHoldTriggerContext.h" // #include "gundrak/GundrakTriggerContext.h" // #include "hallsofstone/HallsOfStoneTriggerContext.h" // #include "hallsoflightning/HallsOfLightningTriggerContext.h" diff --git a/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubActionContext.h b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubActionContext.h new file mode 100644 index 00000000..6306643b --- /dev/null +++ b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubActionContext.h @@ -0,0 +1,22 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONANACTIONCONTEXT_H +#define _PLAYERBOT_WOTLKDUNGEONANACTIONCONTEXT_H + +#include "Action.h" +#include "NamedObjectContext.h" +#include "AzjolNerubActions.h" + +class WotlkDungeonANActionContext : public NamedObjectContext +{ + public: + WotlkDungeonANActionContext() { + creators["attack web wrap"] = &WotlkDungeonANActionContext::attack_web_wrap; + creators["krik'thir priority"] = &WotlkDungeonANActionContext::krikthir_priority; + creators["dodge pound"] = &WotlkDungeonANActionContext::dodge_pound; + } + private: + static Action* attack_web_wrap(PlayerbotAI* ai) { return new AttackWebWrapAction(ai); } + static Action* krikthir_priority(PlayerbotAI* ai) { return new WatchersTargetAction(ai); } + static Action* dodge_pound(PlayerbotAI* ai) { return new AnubarakDodgePoundAction(ai); } +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubActions.cpp b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubActions.cpp new file mode 100644 index 00000000..2bfe2723 --- /dev/null +++ b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubActions.cpp @@ -0,0 +1,110 @@ +#include "Playerbots.h" +#include "AzjolNerubActions.h" +#include "AzjolNerubStrategy.h" + + +bool AttackWebWrapAction::isUseful() { return !botAI->IsHeal(bot); } +bool AttackWebWrapAction::Execute(Event event) +{ + Unit* webWrap = 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_WEB_WRAP) + { + webWrap = unit; + break; + } + } + if (!webWrap || AI_VALUE(Unit*, "current target") == webWrap) + { + return false; + } + + return Attack(webWrap); +} + +bool WatchersTargetAction::isUseful() { return !botAI->IsHeal(bot); } +bool WatchersTargetAction::Execute(Event event) +{ + // Always prioritise web wraps + Unit* currTarget = AI_VALUE(Unit*, "current target"); + if (currTarget && currTarget->GetEntry() == NPC_WEB_WRAP) { return false; } + + // Do not search all units in range! + // There are many adds we don't want to aggro in close proximity, + // only check in-combat adds now. + GuidVector attackers = AI_VALUE(GuidVector, "attackers"); + Unit* priorityTargets[4] = {nullptr, nullptr, nullptr, nullptr}; + + for (auto& attacker : attackers) + { + Unit* npc = botAI->GetUnit(attacker); + if (!npc) + { + continue; + } + switch (npc->GetEntry()) + { + // Focus skirmishers first + case NPC_WATCHER_SKIRMISHER: + priorityTargets[0] = npc; + break; + // Then shadowcaster. This doesn't work so well for the shadowcaster + // + skirmisher pack - ideally we would kill the watcher second. + // But don't want to make this unnecessarily complex and rigid... + // Will revisit if this causes problems in heroic. + case NPC_WATCHER_SHADOWCASTER: + priorityTargets[1] = npc; + break; + // Named watcher next + case NPC_WATCHER_SILTHIK: + case NPC_WATCHER_GASHRA: + case NPC_WATCHER_NARJIL: + priorityTargets[2] = npc; + break; + // Warrior last + case NPC_WATCHER_WARRIOR: + priorityTargets[3] = npc; + break; + } + } + + for (Unit* target : priorityTargets) + { + // Attack the first valid split target in the priority list + if (target) + { + if (currTarget != target) + { + // bot->Yell("ATTACKING "+target->GetName(), LANG_UNIVERSAL); + return Attack(target); + } + // Don't continue loop here, the target exists so we don't + // want to move down the prio list. We just don't need to send attack + // command again, just return false and exit the loop that way + return false; + } + } + + return false; +} + +bool AnubarakDodgePoundAction::isUseful() { return !AI_VALUE2(bool, "behind", "current target"); } +bool AnubarakDodgePoundAction::Execute(Event event) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "anub'arak"); + if (!boss) { return false; } + + float distance = bot->GetExactDist2d(boss->GetPosition()); + // Extra units to move into the boss, instead of being just 1 pixel past his midpoint. + // Can be adjusted - this value tends to mirror how a human would play, + // and visibly ensures you won't get hit while not creating excessive movements. + float distanceExtra = 2.0f; + return Move(bot->GetAngle(boss), distance + distanceExtra); +} diff --git a/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubActions.h b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubActions.h new file mode 100644 index 00000000..ef388cbd --- /dev/null +++ b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubActions.h @@ -0,0 +1,34 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONANACTIONS_H +#define _PLAYERBOT_WOTLKDUNGEONANACTIONS_H + +#include "Action.h" +#include "AttackAction.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "AzjolNerubTriggers.h" + +class AttackWebWrapAction : public AttackAction +{ +public: + AttackWebWrapAction(PlayerbotAI* ai) : AttackAction(ai, "attack web wrap") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +class WatchersTargetAction : public AttackAction +{ +public: + WatchersTargetAction(PlayerbotAI* ai) : AttackAction(ai, "krik'thir priority") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +class AnubarakDodgePoundAction : public AttackAction +{ +public: + AnubarakDodgePoundAction(PlayerbotAI* ai) : AttackAction(ai, "anub'arak dodge pound") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubMultipliers.cpp b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubMultipliers.cpp new file mode 100644 index 00000000..6c287591 --- /dev/null +++ b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubMultipliers.cpp @@ -0,0 +1,56 @@ +#include "AzjolNerubMultipliers.h" +#include "AzjolNerubActions.h" +#include "GenericSpellActions.h" +#include "ChooseTargetActions.h" +#include "MovementActions.h" +#include "AzjolNerubTriggers.h" +#include "Action.h" + +float KrikthirMultiplier::GetValue(Action* action) +{ + // Target is not findable from threat table using AI_VALUE2(), + // therefore need to search manually for the unit name + Unit* boss = nullptr; + Unit* watcher = 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) { continue; } + + switch (unit->GetEntry()) + { + case NPC_KRIKTHIR: + boss = unit; + continue; + case NPC_WATCHER_SILTHIK: + case NPC_WATCHER_GASHRA: + case NPC_WATCHER_NARJIL: + case NPC_WATCHER_SKIRMISHER: + case NPC_WATCHER_SHADOWCASTER: + case NPC_WATCHER_WARRIOR: + watcher = unit; + continue; + } + } + + 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)) + { + 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/azjolnerub/AzjolNerubMultipliers.h b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubMultipliers.h new file mode 100644 index 00000000..bb01115e --- /dev/null +++ b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubMultipliers.h @@ -0,0 +1,15 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONANMULTIPLIERS_H +#define _PLAYERBOT_WOTLKDUNGEONANMULTIPLIERS_H + +#include "Multiplier.h" + +class KrikthirMultiplier : public Multiplier +{ + public: + KrikthirMultiplier(PlayerbotAI* ai) : Multiplier(ai, "krik'thir the gatewatcher") {} + + public: + virtual float GetValue(Action* action); +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubStrategy.cpp b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubStrategy.cpp new file mode 100644 index 00000000..1f0dd038 --- /dev/null +++ b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubStrategy.cpp @@ -0,0 +1,31 @@ +#include "AzjolNerubStrategy.h" +#include "AzjolNerubMultipliers.h" + + +void WotlkDungeonANStrategy::InitTriggers(std::vector &triggers) +{ + // Krik'thir the Gatewatcher + // TODO: Add CC trigger while web wraps are casting? + // TODO: Bring healer closer than ranged dps to avoid fixates? + triggers.push_back(new TriggerNode("krik'thir web wrap", + NextAction::array(0, new NextAction("attack web wrap", ACTION_RAID + 5), nullptr))); + triggers.push_back(new TriggerNode("krik'thir watchers", + NextAction::array(0, new NextAction("krik'thir priority", ACTION_RAID + 4), nullptr))); + + // Hadronox + // The core AC triggers are very buggy with this boss, but default strat seems to play correctly + + //Anub'arak + // TODO: No clear way to track these spikes. They don't seem to appear as gameobjects or triggers, + // and cast time is instant so no way to check currently casting location. + // May need to hook boss AI.. might be able to just heal through it for now. + // triggers.push_back(new TriggerNode("anub'arak impale", + // NextAction::array(0, new NextAction("TODO", ACTION_MOVE + 5), nullptr))); + triggers.push_back(new TriggerNode("anub'arak pound", + NextAction::array(0, new NextAction("dodge pound", ACTION_MOVE + 5), nullptr))); +} + +void WotlkDungeonANStrategy::InitMultipliers(std::vector &multipliers) +{ + multipliers.push_back(new KrikthirMultiplier(botAI)); +} diff --git a/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubStrategy.h b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubStrategy.h new file mode 100644 index 00000000..bf5a7917 --- /dev/null +++ b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubStrategy.h @@ -0,0 +1,18 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONANSTRATEGY_H +#define _PLAYERBOT_WOTLKDUNGEONANSTRATEGY_H + +#include "Multiplier.h" +#include "AiObjectContext.h" +#include "Strategy.h" + + +class WotlkDungeonANStrategy : public Strategy +{ +public: + WotlkDungeonANStrategy(PlayerbotAI* ai) : Strategy(ai) {} + virtual std::string const getName() override { return "azjol'nerub"; } + virtual void InitTriggers(std::vector &triggers) override; + virtual void InitMultipliers(std::vector &multipliers) override; +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubTriggerContext.h b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubTriggerContext.h new file mode 100644 index 00000000..d0e988c0 --- /dev/null +++ b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubTriggerContext.h @@ -0,0 +1,25 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONANTRIGGERCONTEXT_H +#define _PLAYERBOT_WOTLKDUNGEONANTRIGGERCONTEXT_H + +#include "NamedObjectContext.h" +#include "AiObjectContext.h" +#include "AzjolNerubTriggers.h" + +class WotlkDungeonANTriggerContext : public NamedObjectContext +{ + public: + WotlkDungeonANTriggerContext() + { + creators["krik'thir web wrap"] = &WotlkDungeonANTriggerContext::krikthir_web_wrap; + creators["krik'thir watchers"] = &WotlkDungeonANTriggerContext::krikthir_watchers; + // creators["anub'arak impale"] = &WotlkDungeonANTriggerContext::anubarak_impale; + creators["anub'arak pound"] = &WotlkDungeonANTriggerContext::anubarak_pound; + } + private: + static Trigger* krikthir_web_wrap(PlayerbotAI* ai) { return new KrikthirWebWrapTrigger(ai); } + static Trigger* krikthir_watchers(PlayerbotAI* ai) { return new KrikthirWatchersTrigger(ai); } + // static Trigger* anubarak_impale(PlayerbotAI* ai) { return new AnubarakImpaleTrigger(ai); } + static Trigger* anubarak_pound(PlayerbotAI* ai) { return new AnubarakPoundTrigger(ai); } +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubTriggers.cpp b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubTriggers.cpp new file mode 100644 index 00000000..584269da --- /dev/null +++ b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubTriggers.cpp @@ -0,0 +1,69 @@ +#include "Playerbots.h" +#include "AzjolNerubTriggers.h" +#include "AiObject.h" +#include "AiObjectContext.h" + + +bool KrikthirWebWrapTrigger::IsActive() +{ + if (!botAI->IsDps(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_WEB_WRAP) + { + return true; + } + } + + return false; +} + +bool KrikthirWatchersTrigger::IsActive() +{ + if (!botAI->IsDps(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_KRIKTHIR) + { + return true; + } + } + return false; +} + +// bool AnubarakImpaleTrigger::IsActive() +// { +// Unit* boss = AI_VALUE2(Unit*, "find target", "anub'arak"); +// if (!boss) { return false; } +// GuidVector triggers = AI_VALUE(GuidVector, "possible triggers"); +// for (auto i = triggers.begin(); i != triggers.end(); i++) +// { +// Unit* unit = botAI->GetUnit(*i); + +// if (unit) +// { +// bot->Yell("TRIGGER="+unit->GetName(), LANG_UNIVERSAL); +// } +// } +// return false; +// } + +bool AnubarakPoundTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "anub'arak"); + if (!boss) { return false; } + + return boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_POUND); +} diff --git a/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubTriggers.h b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubTriggers.h new file mode 100644 index 00000000..5d12b596 --- /dev/null +++ b/src/strategy/dungeons/wotlk/azjolnerub/AzjolNerubTriggers.h @@ -0,0 +1,61 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONANTRIGGERS_H +#define _PLAYERBOT_WOTLKDUNGEONANTRIGGERS_H + +#include "Trigger.h" +#include "PlayerbotAIConfig.h" +#include "GenericTriggers.h" +#include "DungeonStrategyUtils.h" + +enum AzjolNerubIDs +{ + // Krik'thir the Gatewatcher + NPC_KRIKTHIR = 28684, + NPC_WATCHER_SILTHIK = 28731, + NPC_WATCHER_GASHRA = 28730, + NPC_WATCHER_NARJIL = 28729, + NPC_WATCHER_SKIRMISHER = 28734, + NPC_WATCHER_SHADOWCASTER = 28733, + NPC_WATCHER_WARRIOR = 28732, + DEBUFF_WEB_WRAP = 52086, + NPC_WEB_WRAP = 28619, + + // Anub'arak + // Not sure how to track this - first one is cast as a buff on himself, + // which triggers periodic casts of the spikes spell. + SPELL_IMPALE_PERIODIC = 53456, + SPELL_IMPALE_SPIKES = 53457, + SPELL_POUND_N = 53472, + SPELL_POUND_H = 59433, +}; + +#define SPELL_POUND DUNGEON_MODE(bot, SPELL_POUND_N, SPELL_POUND_H) + +class KrikthirWebWrapTrigger : public Trigger +{ +public: + KrikthirWebWrapTrigger(PlayerbotAI* ai) : Trigger(ai, "krik'thir web wrap") {} + bool IsActive() override; +}; + +class KrikthirWatchersTrigger : public Trigger +{ +public: + KrikthirWatchersTrigger(PlayerbotAI* ai) : Trigger(ai, "krik'thir watchers") {} + bool IsActive() override; +}; + +// class AnubarakImpaleTrigger : public Trigger +// { +// public: +// AnubarakImpaleTrigger(PlayerbotAI* ai) : Trigger(ai, "anub'arak impale") {} +// bool IsActive() override; +// }; + +class AnubarakPoundTrigger : public Trigger +{ +public: + AnubarakPoundTrigger(PlayerbotAI* ai) : Trigger(ai, "anub'arak pound") {} + bool IsActive() override; +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/azjolnerub/TODO b/src/strategy/dungeons/wotlk/azjolnerub/TODO deleted file mode 100644 index e69de29b..00000000 diff --git a/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepActionContext.h b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepActionContext.h new file mode 100644 index 00000000..1435eedb --- /dev/null +++ b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepActionContext.h @@ -0,0 +1,32 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONDTKACTIONCONTEXT_H +#define _PLAYERBOT_WOTLKDUNGEONDTKACTIONCONTEXT_H + +#include "Action.h" +#include "NamedObjectContext.h" +#include "DrakTharonKeepActions.h" + +class WotlkDungeonDTKActionContext : public NamedObjectContext +{ + public: + WotlkDungeonDTKActionContext() { + creators["corpse explode spread"] = &WotlkDungeonDTKActionContext::corpse_explode_spread; + creators["avoid arcane field"] = &WotlkDungeonDTKActionContext::avoid_arcane_field; + creators["novos positioning"] = &WotlkDungeonDTKActionContext::novos_positioning; + creators["novos target priority"] = &WotlkDungeonDTKActionContext::novos_target_priority; + creators["slaying strike"] = &WotlkDungeonDTKActionContext::slaying_strike; + creators["tharonja taunt"] = &WotlkDungeonDTKActionContext::taunt; + creators["bone armor"] = &WotlkDungeonDTKActionContext::bone_armor; + creators["touch of life"] = &WotlkDungeonDTKActionContext::touch_of_life; + } + private: + static Action* corpse_explode_spread(PlayerbotAI* ai) { return new CorpseExplodeSpreadAction(ai); } + static Action* avoid_arcane_field(PlayerbotAI* ai) { return new AvoidArcaneFieldAction(ai); } + static Action* novos_positioning(PlayerbotAI* ai) { return new NovosDefaultPositionAction(ai); } + static Action* novos_target_priority(PlayerbotAI* ai) { return new NovosTargetPriorityAction(ai); } + static Action* slaying_strike(PlayerbotAI* ai) { return new CastSlayingStrikeAction(ai); } + static Action* taunt(PlayerbotAI* ai) { return new CastTauntAction(ai); } + static Action* bone_armor(PlayerbotAI* ai) { return new CastBoneArmorAction(ai); } + static Action* touch_of_life(PlayerbotAI* ai) { return new CastTouchOfLifeAction(ai); } +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepActions.cpp b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepActions.cpp new file mode 100644 index 00000000..e8863a12 --- /dev/null +++ b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepActions.cpp @@ -0,0 +1,174 @@ +#include "Playerbots.h" +#include "DrakTharonKeepActions.h" +#include "DrakTharonKeepStrategy.h" + + +bool CorpseExplodeSpreadAction::Execute(Event event) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "trollgore"); + if (!boss) { return false; } + + float distance = 6.0f; // 5 unit radius, 1 unit added as buffer + GuidVector corpses = AI_VALUE(GuidVector, "nearest corpses"); + for (auto i = corpses.begin(); i != corpses.end(); ++i) + { + Unit* unit = botAI->GetUnit(*i); + if (unit && unit->GetEntry() == NPC_DRAKKARI_INVADER) + { + if (bot->GetExactDist2d(unit) < distance) + { + return MoveAway(unit, distance - bot->GetExactDist2d(unit)); + } + } + } + return false; +} + +bool AvoidArcaneFieldAction::Execute(Event event) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "novos the summoner"); + if (!boss) { return false; } + + float distance = 12.0f; // 11 unit radius, 1 unit added as buffer + if (bot->GetExactDist2d(boss) < distance) + { + return MoveAway(boss, distance - bot->GetExactDist2d(boss)); + } + return false; +} + +bool NovosDefaultPositionAction::isUseful() +{ + // Distance to tether to centre of room + float threshold = 15.0f; + return bot->GetDistance(NOVOS_PARTY_POSITION) > threshold; +} +bool NovosDefaultPositionAction::Execute(Event event) +{ + float clusterDistance = 4.0f; + // Only reposition if we're not killing anything + if (!bot->GetTarget()) + { + return MoveNear(bot->GetMap()->GetId(), + NOVOS_PARTY_POSITION.GetPositionX(), + NOVOS_PARTY_POSITION.GetPositionY(), + NOVOS_PARTY_POSITION.GetPositionZ(), + clusterDistance, MovementPriority::MOVEMENT_NORMAL); + } + return false; +} + +bool NovosTargetPriorityAction::Execute(Event event) +{ + // TODO: This can be improved, some parts are still buggy. + // But it works for now and this fight is very easy + + // Designate a dps char to handle the stairs adds. + // This is probably better as a melee, so just pick the first + // melee dps in the party. If none exist, pick the first ranged. + + // TODO: Switch to botAI->Index instead, cleaner + Player* stairsDps = nullptr; + GuidVector members = AI_VALUE(GuidVector, "group members"); + for (auto& member : members) + { + Player* groupMember = botAI->GetPlayer(member); + if (!groupMember) { continue; } + + if (botAI->IsDps(groupMember)) + { + if (botAI->IsMelee(groupMember)) + { + // Found our first melee dps, grab handle and break + stairsDps = groupMember; + break; + } + else + { + // Ranged dps, only set if none already assigned. + // Don't break, we want to keep searching for a melee instead. + if (!stairsDps) + { + stairsDps = groupMember; + } + } + } + } + + Unit* selectedTargets[2] = {nullptr, 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) { continue; } + uint32 creatureId = unit->GetEntry(); + + // Tank priority: + // Hulking Corpse -> Crystal Handler + if (botAI->IsTank(bot)) + { + if (creatureId == NPC_HULKING_CORPSE) + { + selectedTargets[0] = unit; + } + else if (creatureId == NPC_CRYSTAL_HANDLER) + { + selectedTargets[1] = unit; + } + } + // Dedicated stairs dps is assigned. + // Priority: Risen Shadowcaster -> Fetid Troll Corpse + else if (stairsDps && bot == stairsDps) + { + if (creatureId == NPC_RISEN_SHADOWCASTER) + { + if (!selectedTargets[0] || bot->GetDistance(unit) < bot->GetDistance(selectedTargets[0]) - 5.0f) + { + selectedTargets[0] = unit; + } + + } + else if (creatureId == NPC_FETID_TROLL_CORPSE) + { + if (!selectedTargets[1] || bot->GetDistance(unit) < bot->GetDistance(selectedTargets[1]) - 5.0f) + { + selectedTargets[1] = unit; + } + } + } + // All other dps priority: + // Crystal Handler -> Hulking Corpse + else if (botAI->IsDps(bot)) + { + if (creatureId == NPC_CRYSTAL_HANDLER) + { + selectedTargets[0] = unit; + } + else if (creatureId == NPC_HULKING_CORPSE) + { + selectedTargets[1] = unit; + } + } + } + + for (Unit* primaryTarget : selectedTargets) + { + // Attack the first valid target in the priority list + if (primaryTarget) + { + if (AI_VALUE(Unit*, "current target") != primaryTarget) + { + // bot->Yell(primaryTarget->GetName(), LANG_UNIVERSAL); + return Attack(primaryTarget); + } + // Don't continue loop here, the target exists so we don't + // want to move down the prio list. We just don't need to send attack + // command again, just return false and exit the loop that way + return false; + } + } + return false; +} diff --git a/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepActions.h b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepActions.h new file mode 100644 index 00000000..8c59a7c2 --- /dev/null +++ b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepActions.h @@ -0,0 +1,67 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONDTKACTIONS_H +#define _PLAYERBOT_WOTLKDUNGEONDTKACTIONS_H + +#include "Action.h" +#include "AttackAction.h" +#include "GenericSpellActions.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "DrakTharonKeepTriggers.h" + +const Position NOVOS_PARTY_POSITION = Position(-378.852f, -760.349f, 28.587f); + +class CorpseExplodeSpreadAction : public MovementAction +{ +public: + CorpseExplodeSpreadAction(PlayerbotAI* ai) : MovementAction(ai, "corpse explode spread") {} + bool Execute(Event event) override; +}; + +class AvoidArcaneFieldAction : public MovementAction +{ +public: + AvoidArcaneFieldAction(PlayerbotAI* ai) : MovementAction(ai, "avoid arcane field") {} + bool Execute(Event event) override; +}; + +class NovosDefaultPositionAction : public MovementAction +{ +public: + NovosDefaultPositionAction(PlayerbotAI* ai) : MovementAction(ai, "novos default position") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +class NovosTargetPriorityAction : public AttackAction +{ +public: + NovosTargetPriorityAction(PlayerbotAI* ai) : AttackAction(ai, "novos target priority") {} + bool Execute(Event event) override; + // bool isUseful() override; +}; + +class CastSlayingStrikeAction : public CastMeleeSpellAction +{ +public: + CastSlayingStrikeAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "slaying strike") {} +}; + +class CastTauntAction : public CastSpellAction +{ +public: + CastTauntAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "taunt") {} +}; + +class CastBoneArmorAction : public CastSpellAction +{ +public: + CastBoneArmorAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "bone armor") {} +}; + +class CastTouchOfLifeAction : public CastSpellAction +{ +public: + CastTouchOfLifeAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "touch of life") {} +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepMultipliers.cpp b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepMultipliers.cpp new file mode 100644 index 00000000..bdb9d4d6 --- /dev/null +++ b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepMultipliers.cpp @@ -0,0 +1,62 @@ +#include "DrakTharonKeepMultipliers.h" +#include "DrakTharonKeepActions.h" +#include "GenericSpellActions.h" +#include "ChooseTargetActions.h" +#include "MovementActions.h" +#include "DrakTharonKeepTriggers.h" +#include "Action.h" + +float NovosMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "novos the summoner"); + if (!boss) { return 1.0f; } + + if (boss->FindCurrentSpellBySpellId(SPELL_ARCANE_FIELD) && bot->GetTarget()) + { + if (dynamic_cast(action) + || dynamic_cast(action)) + { + return 0.0f; + } + } + return 1.0f; +} + +float TharonjaMultiplier::GetValue(Action* action) +{ + if (!bot->HasAura(SPELL_GIFT_OF_THARONJA)) { return 1.0f; } + + // Suppress all skills that are not enabled in skeleton form. + // Still allow non-ability actions such as movement + if (dynamic_cast(action) + && !dynamic_cast(action) + && !dynamic_cast(action) + && !dynamic_cast(action) + && !dynamic_cast(action)) + { + return 0.0f; + } + // Also suppress FleeAction to prevent ranged characters from avoiding melee range + if (dynamic_cast(action)) + { + return 0.0f; + } + + // Tanks should only taunt, no slaying strike + if (botAI->IsTank(bot)) + { + if (dynamic_cast(action)) + { + return 0.0f; + } + } + // Dps & healer should not taunt + else + { + if (dynamic_cast(action)) + { + return 0.0f; + } + } + return 1.0f; +} diff --git a/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepMultipliers.h b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepMultipliers.h new file mode 100644 index 00000000..c8f3c813 --- /dev/null +++ b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepMultipliers.h @@ -0,0 +1,24 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONDTKMULTIPLIERS_H +#define _PLAYERBOT_WOTLKDUNGEONDTKMULTIPLIERS_H + +#include "Multiplier.h" + +class NovosMultiplier : public Multiplier +{ + public: + NovosMultiplier(PlayerbotAI* ai) : Multiplier(ai, "novos the summoner") {} + + public: + virtual float GetValue(Action* action); +}; + +class TharonjaMultiplier : public Multiplier +{ + public: + TharonjaMultiplier(PlayerbotAI* ai) : Multiplier(ai, "the prophet tharon'ja") {} + + public: + virtual float GetValue(Action* action); +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepStrategy.cpp b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepStrategy.cpp new file mode 100644 index 00000000..7685b3d5 --- /dev/null +++ b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepStrategy.cpp @@ -0,0 +1,41 @@ +#include "DrakTharonKeepStrategy.h" +#include "DrakTharonKeepMultipliers.h" + + +void WotlkDungeonDTKStrategy::InitTriggers(std::vector &triggers) +{ + // Trollgore + triggers.push_back(new TriggerNode("corpse explode", + NextAction::array(0, new NextAction("corpse explode spread", ACTION_MOVE + 5), nullptr))); + + // Novos the Summoner + // TODO: Can be improved - it's a pretty easy fight but complex to program, revisit if needed + triggers.push_back(new TriggerNode("arcane field", + NextAction::array(0, new NextAction("avoid arcane field", ACTION_MOVE + 5), nullptr))); + triggers.push_back(new TriggerNode("arcane field", + NextAction::array(0, new NextAction("novos positioning", ACTION_MOVE + 4), nullptr))); + triggers.push_back(new TriggerNode("arcane field", + NextAction::array(0, new NextAction("novos target priority", ACTION_NORMAL + 1), nullptr))); + + // King Dred + // TODO: Fear ward / tremor totem, or general anti-fear strat development + + //The Prophet Tharon'ja + triggers.push_back(new TriggerNode("gift of tharon'ja", + NextAction::array(0, new NextAction("touch of life", ACTION_NORMAL + 5), nullptr))); + triggers.push_back(new TriggerNode("gift of tharon'ja", + NextAction::array(0, new NextAction("bone armor", ACTION_NORMAL + 4), nullptr))); + // Run ranged chars (who would normally stand at range) into melee, to dps in skeleton form + triggers.push_back(new TriggerNode("tharon'ja out of melee", + NextAction::array(0, new NextAction("reach melee", ACTION_NORMAL + 3), nullptr))); + triggers.push_back(new TriggerNode("gift of tharon'ja", + NextAction::array(0, new NextAction("taunt", ACTION_NORMAL + 2), nullptr))); + triggers.push_back(new TriggerNode("gift of tharon'ja", + NextAction::array(0, new NextAction("slaying strike", ACTION_NORMAL + 2), nullptr))); +} + +void WotlkDungeonDTKStrategy::InitMultipliers(std::vector &multipliers) +{ + multipliers.push_back(new NovosMultiplier(botAI)); + multipliers.push_back(new TharonjaMultiplier(botAI)); +} diff --git a/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepStrategy.h b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepStrategy.h new file mode 100644 index 00000000..b819ad38 --- /dev/null +++ b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepStrategy.h @@ -0,0 +1,18 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONDTKSTRATEGY_H +#define _PLAYERBOT_WOTLKDUNGEONDTKSTRATEGY_H + +#include "Multiplier.h" +#include "AiObjectContext.h" +#include "Strategy.h" + + +class WotlkDungeonDTKStrategy : public Strategy +{ +public: + WotlkDungeonDTKStrategy(PlayerbotAI* ai) : Strategy(ai) {} + virtual std::string const getName() override { return "drak'tharon keep"; } + virtual void InitTriggers(std::vector &triggers) override; + virtual void InitMultipliers(std::vector &multipliers) override; +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepTriggerContext.h b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepTriggerContext.h new file mode 100644 index 00000000..e98f4372 --- /dev/null +++ b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepTriggerContext.h @@ -0,0 +1,28 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONDTKTRIGGERCONTEXT_H +#define _PLAYERBOT_WOTLKDUNGEONDTKTRIGGERCONTEXT_H + +#include "NamedObjectContext.h" +#include "AiObjectContext.h" +#include "DrakTharonKeepTriggers.h" + +class WotlkDungeonDTKTriggerContext : public NamedObjectContext +{ + public: + WotlkDungeonDTKTriggerContext() + { + creators["corpse explode"] = &WotlkDungeonDTKTriggerContext::corpse_explode; + creators["arcane field"] = &WotlkDungeonDTKTriggerContext::arcane_field; + // creators["crystal handler"] = &WotlkDungeonDTKTriggerContext::crystal_handler; + creators["gift of tharon'ja"] = &WotlkDungeonDTKTriggerContext::gift_of_tharonja; + creators["tharon'ja out of melee"] = &WotlkDungeonDTKTriggerContext::tharonja_out_of_melee; + + } + private: + static Trigger* corpse_explode(PlayerbotAI* ai) { return new CorpseExplodeTrigger(ai); } + static Trigger* arcane_field(PlayerbotAI* ai) { return new ArcaneFieldTrigger(ai); } + // static Trigger* crystal_handler(PlayerbotAI* ai) { return new CrystalHandlerTrigger(ai); } + static Trigger* gift_of_tharonja(PlayerbotAI* ai) { return new GiftOfTharonjaTrigger(ai); } + static Trigger* tharonja_out_of_melee(PlayerbotAI* ai) { return new TwoTriggers(ai, "gift of tharon'ja", "enemy out of melee"); } +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepTriggers.cpp b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepTriggers.cpp new file mode 100644 index 00000000..9ed4b191 --- /dev/null +++ b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepTriggers.cpp @@ -0,0 +1,61 @@ +#include "Playerbots.h" +#include "DrakTharonKeepTriggers.h" +#include "AiObject.h" +#include "AiObjectContext.h" + + +bool CorpseExplodeTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "trollgore"); + if (!boss) { return false; } + + float distance = 6.0f; // 5 unit radius, 1 unit added as buffer + GuidVector corpses = AI_VALUE(GuidVector, "nearest corpses"); + for (auto i = corpses.begin(); i != corpses.end(); ++i) + { + Unit* unit = botAI->GetUnit(*i); + if (unit && unit->GetEntry() == NPC_DRAKKARI_INVADER) + { + if (bot->GetExactDist2d(unit) < distance) + { + return true; + } + } + } + return false; +} + +bool ArcaneFieldTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "novos the summoner"); + if (boss) + { + return boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_ARCANE_FIELD); + } + return false; +} + +// bool CrystalHandlerTrigger::IsActive() +// { +// Unit* boss = AI_VALUE2(Unit*, "find target", "novos the summoner"); +// if (!boss) { 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_CRYSTAL_HANDLER) +// { +// return true; +// } +// } +// return false; +// } + +bool GiftOfTharonjaTrigger::IsActive() +{ + return bot->HasAura(SPELL_GIFT_OF_THARONJA); +} diff --git a/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepTriggers.h b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepTriggers.h new file mode 100644 index 00000000..627200e8 --- /dev/null +++ b/src/strategy/dungeons/wotlk/draktharonkeep/DrakTharonKeepTriggers.h @@ -0,0 +1,54 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONDTKTRIGGERS_H +#define _PLAYERBOT_WOTLKDUNGEONDTKTRIGGERS_H + +#include "Trigger.h" +#include "PlayerbotAIConfig.h" +#include "GenericTriggers.h" +#include "DungeonStrategyUtils.h" + +enum DrakTharonIDs +{ + // Trollgore + NPC_DRAKKARI_INVADER = 27709, + + // Novos the Summoner + NPC_NOVOS = 26631, + SPELL_ARCANE_FIELD = 47346, + NPC_CRYSTAL_HANDLER = 26627, + NPC_HULKING_CORPSE = 27597, + NPC_RISEN_SHADOWCASTER = 27600, + NPC_FETID_TROLL_CORPSE = 27598, + + // The Prophet Tharon'ja + SPELL_GIFT_OF_THARONJA = 52509, +}; + +class CorpseExplodeTrigger : public Trigger +{ +public: + CorpseExplodeTrigger(PlayerbotAI* ai) : Trigger(ai, "corpse explode") {} + bool IsActive() override; +}; + +class ArcaneFieldTrigger : public Trigger +{ +public: + ArcaneFieldTrigger(PlayerbotAI* ai) : Trigger(ai, "arcane field") {} + bool IsActive() override; +}; + +// class CrystalHandlerTrigger : public Trigger +// { +// public: +// CrystalHandlerTrigger(PlayerbotAI* ai) : Trigger(ai, "crystal handler") {} +// bool IsActive() override; +// }; + +class GiftOfTharonjaTrigger : public Trigger +{ +public: + GiftOfTharonjaTrigger(PlayerbotAI* ai) : Trigger(ai, "gift of tharon'ja") {} + bool IsActive() override; +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/draktharonkeep/TODO b/src/strategy/dungeons/wotlk/draktharonkeep/TODO deleted file mode 100644 index e69de29b..00000000 diff --git a/src/strategy/dungeons/wotlk/nexus/NexusActionContext.h b/src/strategy/dungeons/wotlk/nexus/NexusActionContext.h new file mode 100644 index 00000000..c0242262 --- /dev/null +++ b/src/strategy/dungeons/wotlk/nexus/NexusActionContext.h @@ -0,0 +1,28 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONNEXACTIONCONTEXT_H +#define _PLAYERBOT_WOTLKDUNGEONNEXACTIONCONTEXT_H + +#include "Action.h" +#include "NamedObjectContext.h" +#include "NexusActions.h" + +class WotlkDungeonNexActionContext : public NamedObjectContext +{ + public: + WotlkDungeonNexActionContext() { + creators["move from whirlwind"] = &WotlkDungeonNexActionContext::move_from_whirlwind; + creators["firebomb spread"] = &WotlkDungeonNexActionContext::firebomb_spread; + creators["telestra split target"] = &WotlkDungeonNexActionContext::telestra_split_target; + creators["chaotic rift target"] = &WotlkDungeonNexActionContext::chaotic_rift_target; + creators["dodge spikes"] = &WotlkDungeonNexActionContext::dodge_spikes; + creators["intense cold jump"] = &WotlkDungeonNexActionContext::intense_cold_jump; + } + private: + static Action* move_from_whirlwind(PlayerbotAI* ai) { return new MoveFromWhirlwindAction(ai); } + static Action* firebomb_spread(PlayerbotAI* ai) { return new FirebombSpreadAction(ai); } + static Action* telestra_split_target(PlayerbotAI* ai) { return new TelestraSplitTargetAction(ai); } + static Action* chaotic_rift_target(PlayerbotAI* ai) { return new ChaoticRiftTargetAction(ai); } + static Action* dodge_spikes(PlayerbotAI* ai) { return new DodgeSpikesAction(ai); } + static Action* intense_cold_jump(PlayerbotAI* ai) { return new IntenseColdJumpAction(ai); } +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/nexus/NexusActions.cpp b/src/strategy/dungeons/wotlk/nexus/NexusActions.cpp new file mode 100644 index 00000000..1a918614 --- /dev/null +++ b/src/strategy/dungeons/wotlk/nexus/NexusActions.cpp @@ -0,0 +1,159 @@ +#include "Playerbots.h" +#include "NexusActions.h" +#include "NexusStrategy.h" + +bool MoveFromWhirlwindAction::Execute(Event event) +{ + Unit* boss = nullptr; + uint8 faction = bot->GetTeamId(); + float targetDist = 10.0f; // Whirlwind has range of 8, add a couple for safety buffer + + switch (bot->GetMap()->GetDifficulty()) + { + case DUNGEON_DIFFICULTY_NORMAL: + if (faction == TEAM_ALLIANCE) + { + boss = AI_VALUE2(Unit*, "find target", "horde commander"); + } + else //if (faction == TEAM_HORDE) + { + boss = AI_VALUE2(Unit*, "find target", "alliance commander"); + } + break; + case DUNGEON_DIFFICULTY_HEROIC: + if (faction == TEAM_ALLIANCE) + { + boss = AI_VALUE2(Unit*, "find target", "commander kolurg"); + } + else //if (faction == TEAM_HORDE) + { + boss = AI_VALUE2(Unit*, "find target", "commander stoutbeard"); + } + break; + default: + break; + } + if (!boss || bot->GetExactDist2d(boss->GetPosition()) > targetDist) + { + return false; + } + return MoveAway(boss, targetDist - bot->GetExactDist2d(boss->GetPosition())); +} + +bool FirebombSpreadAction::Execute(Event event) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "grand magus telestra"); + float radius = 5.0f; + float targetDist = radius + 1.0f; + if (!boss) { return false; } + + GuidVector members = AI_VALUE(GuidVector, "group members"); + for (auto& member : members) + { + if (bot->GetGUID() == member) + { + continue; + } + if (bot->GetExactDist2d(botAI->GetUnit(member)) < targetDist) + { + return MoveAway(botAI->GetUnit(member), targetDist); + } + } + return false; +} + +bool TelestraSplitTargetAction::isUseful() { return !botAI->IsHeal(bot); } +bool TelestraSplitTargetAction::Execute(Event event) +{ + GuidVector attackers = AI_VALUE(GuidVector, "attackers"); + Unit* splitTargets[3] = {nullptr, nullptr, nullptr}; + + for (auto& attacker : attackers) + { + Unit* npc = botAI->GetUnit(attacker); + if (!npc) + { + continue; + } + switch (npc->GetEntry()) + { + // Focus arcane clone first + case NPC_ARCANE_MAGUS: + splitTargets[0] = npc; + break; + // Then the frost clone + case NPC_FROST_MAGUS: + splitTargets[1] = npc; + break; + // Fire clone last + case NPC_FIRE_MAGUS: + splitTargets[2] = npc; + break; + } + } + + for (Unit* target : splitTargets) + { + // Attack the first valid split target in the priority list + if (target) + { + if (AI_VALUE(Unit*, "current target") != target) + { + return Attack(target); + } + // Don't continue loop here, the target exists so we don't + // want to move down the prio list. We just don't need to send attack + // command again, just return false and exit the loop that way + return false; + } + } + + return false; +} + +bool ChaoticRiftTargetAction::isUseful() { return !botAI->IsHeal(bot); } +bool ChaoticRiftTargetAction::Execute(Event event) +{ + Unit* chaoticRift = 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->GetName() == "Chaotic Rift") + { + chaoticRift = unit; + break; + } + } + if (!chaoticRift || AI_VALUE(Unit*, "current target") == chaoticRift) + { + return false; + } + return Attack(chaoticRift); +} + +bool DodgeSpikesAction::isUseful() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "ormorok the tree-shaper"); + return bot->GetExactDist2d(boss) > 0.5f; +} +bool DodgeSpikesAction::Execute(Event event) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "ormorok the tree-shaper"); + return Move(bot->GetAngle(boss), bot->GetExactDist2d(boss) - 0.3f); +} + +bool IntenseColdJumpAction::Execute(Event event) +{ + // This needs improving but maybe it should be done in the playerbot core. + // Jump doesn't seem to support zero offset (eg. jump on the spot) so need to add a tiny delta. + // This does a tiny bunnyhop that takes a couple of ms, it doesn't do a natural jump. + // Adding extra Z offset causes floating, and appears to scale the jump speed based on Z difference. + // Probably best to revisit once bot movement is improved + return JumpTo(bot->GetMap()->GetId(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ() + 0.01f); + // bot->GetMotionMaster()->MoveFall(); +} diff --git a/src/strategy/dungeons/wotlk/nexus/NexusActions.h b/src/strategy/dungeons/wotlk/nexus/NexusActions.h new file mode 100644 index 00000000..145efba7 --- /dev/null +++ b/src/strategy/dungeons/wotlk/nexus/NexusActions.h @@ -0,0 +1,71 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONNEXACTIONS_H +#define _PLAYERBOT_WOTLKDUNGEONNEXACTIONS_H + +#include "Action.h" +#include "AttackAction.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "NexusTriggers.h" + +#define ANGLE_45_DEG (static_cast(M_PI) / 4.f) +#define ANGLE_90_DEG M_PI_2 +#define ANGLE_120_DEG (2.f * static_cast(M_PI) / 3.f) + +// Slice of the circle that we want melee dps to attack from. +// Measured from boss orientation, on one side. + +// Even though the breath cone is not the full 180 degrees, +// avoid melee dps from the front due to parry potential. +// Bots should learn good dps etiquette :) +#define DRAGON_MELEE_MIN_ANGLE ANGLE_90_DEG +// This leaves a danger zone of 60 degrees at the tail end on both sides. +// This is a total of 120 degrees tail arc that bots will avoid - +// number just happens to be the same in this case, but this is always measured from the front. +#define DRAGON_MELEE_MAX_ANGLE ANGLE_120_DEG + +class MoveFromWhirlwindAction : public MovementAction +{ +public: + MoveFromWhirlwindAction(PlayerbotAI* ai) : MovementAction(ai, "move from whirlwind") {} + bool Execute(Event event) override; +}; + +class FirebombSpreadAction : public MovementAction +{ +public: + FirebombSpreadAction(PlayerbotAI* ai) : MovementAction(ai, "firebomb spread") {} + bool Execute(Event event) override; +}; + +class TelestraSplitTargetAction : public AttackAction +{ +public: + TelestraSplitTargetAction(PlayerbotAI* ai) : AttackAction(ai, "telestra split target") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +class ChaoticRiftTargetAction : public AttackAction +{ +public: + ChaoticRiftTargetAction(PlayerbotAI* ai) : AttackAction(ai, "chaotic rift target") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +class DodgeSpikesAction : public MovementAction +{ +public: + DodgeSpikesAction(PlayerbotAI* ai) : MovementAction(ai, "dodge spikes") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +class IntenseColdJumpAction : public MovementAction +{ +public: + IntenseColdJumpAction(PlayerbotAI* ai) : MovementAction(ai, "intense cold jump") {} + bool Execute(Event event) override; +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/nexus/NexusMultipliers.cpp b/src/strategy/dungeons/wotlk/nexus/NexusMultipliers.cpp new file mode 100644 index 00000000..4b8ab97a --- /dev/null +++ b/src/strategy/dungeons/wotlk/nexus/NexusMultipliers.cpp @@ -0,0 +1,99 @@ +#include "NexusMultipliers.h" +#include "NexusActions.h" +#include "GenericSpellActions.h" +#include "ChooseTargetActions.h" +#include "MovementActions.h" +#include "NexusTriggers.h" + +float FactionCommanderMultiplier::GetValue(Action* action) +{ + Unit* boss = nullptr; + uint8 faction = bot->GetTeamId(); + + switch (bot->GetMap()->GetDifficulty()) + { + case DUNGEON_DIFFICULTY_NORMAL: + if (faction == TEAM_ALLIANCE) + { + boss = AI_VALUE2(Unit*, "find target", "horde commander"); + } + else //if (faction == TEAM_HORDE) + { + boss = AI_VALUE2(Unit*, "find target", "alliance commander"); + } + break; + case DUNGEON_DIFFICULTY_HEROIC: + if (faction == TEAM_ALLIANCE) + { + boss = AI_VALUE2(Unit*, "find target", "commander kolurg"); + } + else //if (faction == TEAM_HORDE) + { + boss = AI_VALUE2(Unit*, "find target", "commander stoutbeard"); + } + break; + default: + break; + } + if (boss && boss->HasUnitState(UNIT_STATE_CASTING) && + boss->FindCurrentSpellBySpellId(SPELL_WHIRLWIND)) + { + // Prevent movement actions other than flee during a whirlwind, to prevent running back in early. + if (dynamic_cast(action) && !dynamic_cast(action)) + { + return 0.0f; + } + } + return 1.0f; +} + +float TelestraMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "grand magus telestra"); + if (boss && boss->GetEntry() != NPC_TELESTRA) + { + // boss is split into clones, do not auto acquire target + if (dynamic_cast(action)) + { + return 0.0f; + } + } + return 1.0f; +} + +float AnomalusMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "anomalus"); + if (boss && boss->HasAura(BUFF_RIFT_SHIELD)) + { + if (dynamic_cast(action)) + { + return 0.0f; + } + } + return 1.0f; +} + +float OrmorokMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "ormorok the tree-shaper"); + if (!boss) + { + return 1.0f; + } + // These are used for auto ranged repositioning, need to suppress so ranged dps don't ping-pong + if (dynamic_cast(action)) + { + return 0.0f; + } + // This boss is annoying and shuffles around a lot. Don't let tank move once fight has started. + // Extra checks are to allow the tank to close distance and engage the boss initially + if (dynamic_cast(action) && !dynamic_cast(action) + && botAI->IsTank(bot) && bot->IsWithinMeleeRange(boss) + && AI_VALUE2(bool, "facing", "current target")) + { + return 0.0f; + } + return 1.0f; +} + diff --git a/src/strategy/dungeons/wotlk/nexus/NexusMultipliers.h b/src/strategy/dungeons/wotlk/nexus/NexusMultipliers.h new file mode 100644 index 00000000..c7321a40 --- /dev/null +++ b/src/strategy/dungeons/wotlk/nexus/NexusMultipliers.h @@ -0,0 +1,51 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONNEXMULTIPLIERS_H +#define _PLAYERBOT_WOTLKDUNGEONNEXMULTIPLIERS_H + +#include "Multiplier.h" + +class FactionCommanderMultiplier : public Multiplier +{ + public: + FactionCommanderMultiplier(PlayerbotAI* ai) : Multiplier(ai, "faction commander") {} + + public: + virtual float GetValue(Action* action); +}; + +class TelestraMultiplier : public Multiplier +{ + public: + TelestraMultiplier(PlayerbotAI* ai) : Multiplier(ai, "grand magus telestra") {} + + public: + virtual float GetValue(Action* action); +}; + +class AnomalusMultiplier : public Multiplier +{ + public: + AnomalusMultiplier(PlayerbotAI* ai) : Multiplier(ai, "anomalus") {} + + public: + virtual float GetValue(Action* action); +}; + +class OrmorokMultiplier : public Multiplier +{ + public: + OrmorokMultiplier(PlayerbotAI* ai) : Multiplier(ai, "ormorok the tree-shaper") {} + + public: + virtual float GetValue(Action* action); +}; + +class KeristraszaMultiplier : public Multiplier +{ + public: + KeristraszaMultiplier(PlayerbotAI* ai) : Multiplier(ai, "keristrasza") {} + + public: + virtual float GetValue(Action* action); +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/nexus/NexusStrategy.cpp b/src/strategy/dungeons/wotlk/nexus/NexusStrategy.cpp new file mode 100644 index 00000000..b86c1aa4 --- /dev/null +++ b/src/strategy/dungeons/wotlk/nexus/NexusStrategy.cpp @@ -0,0 +1,54 @@ +#include "NexusStrategy.h" +#include "NexusMultipliers.h" + + +void WotlkDungeonNexStrategy::InitTriggers(std::vector &triggers) +{ + // Horde Commander (Alliance N)/Commander Kolurg (Alliance H) + // or + // Alliance Commander (Horde N)/Commander Stoutbeard (Horde H) + triggers.push_back(new TriggerNode("faction commander whirlwind", + NextAction::array(0, new NextAction("move from whirlwind", ACTION_MOVE + 5), nullptr))); + // TODO: Handle fear? (tremor totems, fear ward etc.) + + // Grand Magus Telestra + triggers.push_back(new TriggerNode("telestra firebomb", + NextAction::array(0, new NextAction("firebomb spread", ACTION_MOVE + 5), nullptr))); + triggers.push_back(new TriggerNode("telestra split phase", + NextAction::array(0, new NextAction("telestra split target", ACTION_RAID + 1), nullptr))); + // TODO: Add priority interrupt on the frost split's Blizzard casts + + // Anomalus + triggers.push_back(new TriggerNode("chaotic rift", + NextAction::array(0, new NextAction("chaotic rift target", ACTION_RAID + 1), nullptr))); + + // Ormorok the Tree-Shaper + // Tank trigger to stack inside boss. Can also add return action to prevent boss repositioning + // if it becomes too much of a problem. He usually dies before he's up against a wall though + triggers.push_back(new TriggerNode("ormorok spikes", + NextAction::array(0, new NextAction("dodge spikes", ACTION_MOVE + 5), nullptr))); + // Non-tank trigger to stack. Avoiding the spikes at range is.. harder than it seems. + // TODO: This turns hunters into melee marshmallows, have not come up with a better solution yet + triggers.push_back(new TriggerNode("ormorok stack", + NextAction::array(0, new NextAction("dodge spikes", ACTION_MOVE + 5), nullptr))); + // TODO: Add handling for spell reflect... best to spam low level/weak spells but don't want + // to hardcode spells per class, might be difficult to dynamically generate this. + // Will revisit if I find my altbots killing themselves in heroic, just heal through it for now + + // Keristrasza + triggers.push_back(new TriggerNode("intense cold", + NextAction::array(0, new NextAction("intense cold jump", ACTION_MOVE + 5), nullptr))); + // Flank dragon positioning + triggers.push_back(new TriggerNode("keristrasza positioning", + NextAction::array(0, new NextAction("rear flank", ACTION_MOVE + 4), nullptr))); + // TODO: Add frost resist aura for paladins? +} + +void WotlkDungeonNexStrategy::InitMultipliers(std::vector &multipliers) +{ + multipliers.push_back(new FactionCommanderMultiplier(botAI)); + multipliers.push_back(new TelestraMultiplier(botAI)); + multipliers.push_back(new AnomalusMultiplier(botAI)); + multipliers.push_back(new OrmorokMultiplier(botAI)); + // multipliers.push_back(new KeristraszaMultiplier(botAI)); +} diff --git a/src/strategy/dungeons/wotlk/nexus/NexusStrategy.h b/src/strategy/dungeons/wotlk/nexus/NexusStrategy.h new file mode 100644 index 00000000..f9ecf426 --- /dev/null +++ b/src/strategy/dungeons/wotlk/nexus/NexusStrategy.h @@ -0,0 +1,18 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONNEXSTRATEGY_H +#define _PLAYERBOT_WOTLKDUNGEONNEXSTRATEGY_H + +#include "Multiplier.h" +#include "AiObjectContext.h" +#include "Strategy.h" + + +class WotlkDungeonNexStrategy : public Strategy +{ +public: + WotlkDungeonNexStrategy(PlayerbotAI* ai) : Strategy(ai) {} + virtual std::string const getName() override { return "nexus"; } + virtual void InitTriggers(std::vector &triggers) override; + virtual void InitMultipliers(std::vector &multipliers) override; +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/nexus/NexusTriggerContext.h b/src/strategy/dungeons/wotlk/nexus/NexusTriggerContext.h new file mode 100644 index 00000000..ca696ece --- /dev/null +++ b/src/strategy/dungeons/wotlk/nexus/NexusTriggerContext.h @@ -0,0 +1,33 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONNEXTRIGGERCONTEXT_H +#define _PLAYERBOT_WOTLKDUNGEONNEXTRIGGERCONTEXT_H + +#include "NamedObjectContext.h" +#include "AiObjectContext.h" +#include "NexusTriggers.h" + +class WotlkDungeonNexTriggerContext : public NamedObjectContext +{ + public: + WotlkDungeonNexTriggerContext() + { + creators["faction commander whirlwind"] = &WotlkDungeonNexTriggerContext::faction_commander_whirlwind; + creators["telestra firebomb"] = &WotlkDungeonNexTriggerContext::telestra_firebomb; + creators["telestra split phase"] = &WotlkDungeonNexTriggerContext::telestra_split_phase; + creators["chaotic rift"] = &WotlkDungeonNexTriggerContext::chaotic_rift; + creators["ormorok spikes"] = &WotlkDungeonNexTriggerContext::ormorok_spikes; + creators["ormorok stack"] = &WotlkDungeonNexTriggerContext::ormorok_stack; + creators["intense cold"] = &WotlkDungeonNexTriggerContext::intense_cold; + creators["keristrasza positioning"] = &WotlkDungeonNexTriggerContext::keristrasza_positioning; + } + private: + static Trigger* faction_commander_whirlwind(PlayerbotAI* ai) { return new FactionCommanderWhirlwindTrigger(ai); } + static Trigger* telestra_firebomb(PlayerbotAI* ai) { return new TelestraFirebombTrigger(ai); } + static Trigger* telestra_split_phase(PlayerbotAI* ai) { return new TelestraSplitPhaseTrigger(ai); } + static Trigger* chaotic_rift(PlayerbotAI* ai) { return new ChaoticRiftTrigger(ai); } + static Trigger* ormorok_spikes(PlayerbotAI* ai) { return new OrmorokSpikesTrigger(ai); } + static Trigger* ormorok_stack(PlayerbotAI* ai) { return new OrmorokStackTrigger(ai); } + static Trigger* intense_cold(PlayerbotAI* ai) { return new IntenseColdTrigger(ai); } + static Trigger* keristrasza_positioning(PlayerbotAI* ai) { return new KeristraszaPositioningTrigger(ai); } +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/nexus/NexusTriggers.cpp b/src/strategy/dungeons/wotlk/nexus/NexusTriggers.cpp new file mode 100644 index 00000000..a39e497d --- /dev/null +++ b/src/strategy/dungeons/wotlk/nexus/NexusTriggers.cpp @@ -0,0 +1,107 @@ +#include "Playerbots.h" +#include "NexusTriggers.h" +#include "AiObject.h" +#include "AiObjectContext.h" + +bool FactionCommanderWhirlwindTrigger::IsActive() +{ + Unit* boss = nullptr; + uint8 faction = bot->GetTeamId(); + + switch (bot->GetMap()->GetDifficulty()) + { + case DUNGEON_DIFFICULTY_NORMAL: + if (faction == TEAM_ALLIANCE) + { + boss = AI_VALUE2(Unit*, "find target", "horde commander"); + } + else //if (faction == TEAM_HORDE) + { + boss = AI_VALUE2(Unit*, "find target", "alliance commander"); + } + break; + case DUNGEON_DIFFICULTY_HEROIC: + if (faction == TEAM_ALLIANCE) + { + boss = AI_VALUE2(Unit*, "find target", "commander kolurg"); + } + else //if (faction == TEAM_HORDE) + { + boss = AI_VALUE2(Unit*, "find target", "commander stoutbeard"); + } + break; + default: + break; + } + + if (boss && boss->HasUnitState(UNIT_STATE_CASTING)) + { + if (boss->FindCurrentSpellBySpellId(SPELL_WHIRLWIND)) + { + return true; + } + } + return false; +} + +bool TelestraFirebombTrigger::IsActive() +{ + if (botAI->IsMelee(bot)) { return false; } + + Unit* boss = AI_VALUE2(Unit*, "find target", "grand magus telestra"); + // Avoid split phase with the fake Telestra units, only match the true boss id + return boss && boss->GetEntry() == NPC_TELESTRA; +} + +bool TelestraSplitPhaseTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "grand magus telestra"); + // Only match split phase with the fake Telestra units + return boss && boss->GetEntry() != NPC_TELESTRA; +} + +bool ChaoticRiftTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "anomalus"); + return boss && boss->HasAura(BUFF_RIFT_SHIELD); +} + +bool OrmorokSpikesTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "ormorok the tree-shaper"); + if (!boss || !botAI->IsTank(bot)) { return false; } + + GuidVector objects = AI_VALUE(GuidVector, "closest game objects"); + for (auto i = objects.begin(); i != objects.end(); ++i) + { + GameObject* go = botAI->GetGameObject(*i); + if (go && go->GetEntry() == GO_CRYSTAL_SPIKE) + { + return true; + } + } + return false; +} + +bool OrmorokStackTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "ormorok the tree-shaper"); + return (boss && !botAI->IsTank(bot)); +} + +bool IntenseColdTrigger::IsActive() +{ + // Adjust as needed - too much interrupting loses dps time, + // but too many stacks is deadly. Assuming 3-5 is a good number to clear + int stackThreshold = 5; + Unit* boss = AI_VALUE2(Unit*, "find target", "keristrasza"); + return boss && botAI->GetAura("intense cold", bot, false, false, stackThreshold); +} + +bool KeristraszaPositioningTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "keristrasza"); + // Include healers here for now, otherwise they stand in things + return boss && !botAI->IsTank(bot) && !botAI->IsRangedDps(bot); + // return boss && botAI->IsMelee(bot) && !botAI->IsTank(bot); +} diff --git a/src/strategy/dungeons/wotlk/nexus/NexusTriggers.h b/src/strategy/dungeons/wotlk/nexus/NexusTriggers.h new file mode 100644 index 00000000..ac5d88a4 --- /dev/null +++ b/src/strategy/dungeons/wotlk/nexus/NexusTriggers.h @@ -0,0 +1,89 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONNEXTRIGGERS_H +#define _PLAYERBOT_WOTLKDUNGEONNEXTRIGGERS_H + +#include "Trigger.h" +#include "PlayerbotAIConfig.h" +#include "GenericTriggers.h" +#include "DungeonStrategyUtils.h" + +enum NexusIDs +{ + // Faction Commander + NPC_ALLIANCE_COMMANDER = 27949, + NPC_HORDE_COMMANDER = 27947, + NPC_COMMANDER_STOUTBEARD = 26796, + NPC_COMMANDER_KOLURG = 26798, + // SPELL_FRIGHTENING_SHOUT = 19134, + SPELL_WHIRLWIND = 38618, + + // Grand Magus Telestra + NPC_TELESTRA = 26731, + NPC_FIRE_MAGUS = 26928, + NPC_FROST_MAGUS = 26930, + NPC_ARCANE_MAGUS = 26929, + + // Anomalus + BUFF_RIFT_SHIELD = 47748, + + // Ormorok the Tree Shaper + // NPC_CRYSTAL_SPIKE = 27099, + GO_CRYSTAL_SPIKE = 188537, +}; + +class FactionCommanderWhirlwindTrigger : public Trigger +{ +public: + FactionCommanderWhirlwindTrigger(PlayerbotAI* ai) : Trigger(ai, "faction commander whirlwind") {} + bool IsActive() override; +}; + +class TelestraFirebombTrigger : public Trigger +{ +public: + TelestraFirebombTrigger(PlayerbotAI* ai) : Trigger(ai, "telestra firebomb spread") {} + bool IsActive() override; +}; + +class TelestraSplitPhaseTrigger : public Trigger +{ +public: + TelestraSplitPhaseTrigger(PlayerbotAI* ai) : Trigger(ai, "telestra split phase") {} + bool IsActive() override; +}; + +class ChaoticRiftTrigger : public Trigger +{ +public: + ChaoticRiftTrigger(PlayerbotAI* ai) : Trigger(ai, "chaotic rift") {} + bool IsActive() override; +}; + +class OrmorokSpikesTrigger : public Trigger +{ +public: + OrmorokSpikesTrigger(PlayerbotAI* ai) : Trigger(ai, "ormorok spikes") {} + bool IsActive() override; +}; + +class OrmorokStackTrigger : public Trigger +{ +public: + OrmorokStackTrigger(PlayerbotAI* ai) : Trigger(ai, "ormorok stack") {} + bool IsActive() override; +}; + +class IntenseColdTrigger : public Trigger +{ +public: + IntenseColdTrigger(PlayerbotAI* ai) : Trigger(ai, "intense cold") {} + bool IsActive() override; +}; + +class KeristraszaPositioningTrigger : public Trigger +{ +public: + KeristraszaPositioningTrigger(PlayerbotAI* ai) : Trigger(ai, "keristrasza positioning") {} + bool IsActive() override; +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/nexus/TODO b/src/strategy/dungeons/wotlk/nexus/TODO deleted file mode 100644 index e69de29b..00000000 diff --git a/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomActionContext.h b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomActionContext.h new file mode 100644 index 00000000..19426c57 --- /dev/null +++ b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomActionContext.h @@ -0,0 +1,22 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONOKACTIONCONTEXT_H +#define _PLAYERBOT_WOTLKDUNGEONOKACTIONCONTEXT_H + +#include "Action.h" +#include "NamedObjectContext.h" +#include "OldKingdomActions.h" + +class WotlkDungeonOKActionContext : public NamedObjectContext +{ + public: + WotlkDungeonOKActionContext() { + creators["attack nadox guardian"] = &WotlkDungeonOKActionContext::attack_nadox_guardian; + creators["attack jedoga volunteer"] = &WotlkDungeonOKActionContext::attack_jedoga_volunteer; + creators["avoid shadow crash"] = &WotlkDungeonOKActionContext::avoid_shadow_crash; + } + private: + static Action* attack_nadox_guardian(PlayerbotAI* ai) { return new AttackNadoxGuardianAction(ai); } + static Action* attack_jedoga_volunteer(PlayerbotAI* ai) { return new AttackJedogaVolunteerAction(ai); } + static Action* avoid_shadow_crash(PlayerbotAI* ai) { return new AvoidShadowCrashAction(ai); } +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomActions.cpp b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomActions.cpp new file mode 100644 index 00000000..c5b27cca --- /dev/null +++ b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomActions.cpp @@ -0,0 +1,81 @@ +#include "Playerbots.h" +#include "OldKingdomActions.h" +#include "OldKingdomStrategy.h" + + +bool AttackNadoxGuardianAction::Execute(Event event) +{ + Unit* target = AI_VALUE2(Unit*, "find target", "ahn'kahar guardian"); + if (!target || AI_VALUE(Unit*, "current target") == target) + { + return false; + } + + return Attack(target); +} + +bool AttackJedogaVolunteerAction::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_TWILIGHT_VOLUNTEER) + { + target = unit; + break; + } + } + + if (!target || AI_VALUE(Unit*, "current target") == target) + { + return false; + } + return Attack(target); +} + +bool AvoidShadowCrashAction::Execute(Event event) +{ + // Could check all enemy units in range as it's possible to pull multiple of these mobs. + // They should really be killed 1 by 1, multipulls are messy so we just handle singles for now + Unit* npc = AI_VALUE2(Unit*, "find target", "forgotten one"); + Unit* victim = nullptr; + float radius = 10.0f; + float targetDist = radius + 2.0f; + if (!npc) { return false; } + + // Actively move if targeted by a shadow crash. + // Spell check not needed, they don't have any other non-instant casts + if (npc->HasUnitState(UNIT_STATE_CASTING)) // && npc->FindCurrentSpellBySpellId(SPELL_SHADOW_CRASH)) + { + // This doesn't seem to avoid casts very well, perhaps because this isn't checked while allies are casting. + // TODO: Revisit if this is an issue in heroics, otherwise ignore shadow crashes for the most part. + victim = botAI->GetUnit(npc->GetTarget()); + if (victim && bot->GetExactDist2d(victim) < radius) + { + return MoveAway(victim, targetDist - bot->GetExactDist2d(victim)); + } + } + + // Otherwise ranged members passively spread, to avoid AoE overlap + if (botAI->IsMelee(bot)) { return false; } + + GuidVector members = AI_VALUE(GuidVector, "group members"); + for (auto& member : members) + { + if (bot->GetGUID() == member) + { + continue; + } + float currentDist = bot->GetExactDist2d(botAI->GetUnit(member)); + if (currentDist < radius) + { + return MoveAway(botAI->GetUnit(member), targetDist - currentDist); + } + } + return false; +} diff --git a/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomActions.h b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomActions.h new file mode 100644 index 00000000..cba51878 --- /dev/null +++ b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomActions.h @@ -0,0 +1,31 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONOKACTIONS_H +#define _PLAYERBOT_WOTLKDUNGEONOKACTIONS_H + +#include "Action.h" +#include "AttackAction.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "OldKingdomTriggers.h" + +class AttackNadoxGuardianAction : public AttackAction +{ +public: + AttackNadoxGuardianAction(PlayerbotAI* ai) : AttackAction(ai, "attack nadox guardian") {} + bool Execute(Event event) override; +}; + +class AttackJedogaVolunteerAction : public AttackAction +{ +public: + AttackJedogaVolunteerAction(PlayerbotAI* ai) : AttackAction(ai, "attack jedoga volunteer") {} + bool Execute(Event event) override; +}; + +class AvoidShadowCrashAction : public MovementAction +{ +public: + AvoidShadowCrashAction(PlayerbotAI* ai) : MovementAction(ai, "avoid shadow crash") {} + bool Execute(Event event) override; +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomMultipliers.cpp b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomMultipliers.cpp new file mode 100644 index 00000000..9fc90802 --- /dev/null +++ b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomMultipliers.cpp @@ -0,0 +1,66 @@ +#include "OldKingdomMultipliers.h" +#include "OldKingdomActions.h" +#include "GenericSpellActions.h" +#include "ChooseTargetActions.h" +#include "MovementActions.h" +#include "OldKingdomTriggers.h" +#include "Action.h" + +float ElderNadoxMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "elder nadox"); + Unit* guardian = AI_VALUE2(Unit*, "find target", "ahn'kahar guardian"); + + if (boss && guardian) + { + if (dynamic_cast(action)) + { + return 0.0f; + } + } + return 1.0f; +} + +float JedogaShadowseekerMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "jedoga shadowseeker"); + // Unit* volunteer = AI_VALUE2(Unit*, "find target", "twilight volunteer"); + + Unit* volunteer = 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_TWILIGHT_VOLUNTEER) + { + volunteer = unit; + break; + } + } + + if (boss && volunteer) + { + if (dynamic_cast(action)) + { + return 0.0f; + } + } + return 1.0f; +} + +float ForgottenOneMultiplier::GetValue(Action* action) +{ + Unit* npc = AI_VALUE2(Unit*, "find target", "forgotten one"); + + if (npc && bot->isMoving()) + { + if (dynamic_cast(action)) + { + return 0.0f; + } + } + return 1.0f; +} diff --git a/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomMultipliers.h b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomMultipliers.h new file mode 100644 index 00000000..88ae2fa5 --- /dev/null +++ b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomMultipliers.h @@ -0,0 +1,33 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONOKMULTIPLIERS_H +#define _PLAYERBOT_WOTLKDUNGEONOKMULTIPLIERS_H + +#include "Multiplier.h" + +class ElderNadoxMultiplier : public Multiplier +{ + public: + ElderNadoxMultiplier(PlayerbotAI* ai) : Multiplier(ai, "elder nadox") {} + + public: + virtual float GetValue(Action* action); +}; + +class JedogaShadowseekerMultiplier : public Multiplier +{ + public: + JedogaShadowseekerMultiplier(PlayerbotAI* ai) : Multiplier(ai, "jedoga shadowseeker") {} + + public: + virtual float GetValue(Action* action); +}; + +class ForgottenOneMultiplier : public Multiplier +{ + public: + ForgottenOneMultiplier(PlayerbotAI* ai) : Multiplier(ai, "forgotten one") {} + + public: + virtual float GetValue(Action* action); +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomStrategy.cpp b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomStrategy.cpp new file mode 100644 index 00000000..b353a05d --- /dev/null +++ b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomStrategy.cpp @@ -0,0 +1,36 @@ +#include "OldKingdomStrategy.h" +#include "OldKingdomMultipliers.h" + + +void WotlkDungeonOKStrategy::InitTriggers(std::vector &triggers) +{ + // Elder Nadox + triggers.push_back(new TriggerNode("nadox guardian", + NextAction::array(0, new NextAction("attack nadox guardian", ACTION_RAID + 5), nullptr))); + + // Prince Taldaram + // Flame Orb spawns in melee, doesn't have a clear direction until it starts moving. + // Maybe not worth trying to avoid and just heal through. Only consideration is not to have ranged + // players anywhere near melee when it spawns + + // Jedoga Shadowseeker + triggers.push_back(new TriggerNode("jedoga volunteer", + NextAction::array(0, new NextAction("attack jedoga volunteer", ACTION_RAID + 5), nullptr))); + + // Herald Volazj + // Trash mobs before him have a big telegraphed shadow crash spell, + // this can be avoided and is intended to be dodged + triggers.push_back(new TriggerNode("shadow crash", + NextAction::array(0, new NextAction("avoid shadow crash", ACTION_MOVE + 5), nullptr))); + // Volazj is not implemented properly in AC, insanity phase does nothing. + + // Amanitar (Heroic Only) + // TODO: once I get to heroics +} + +void WotlkDungeonOKStrategy::InitMultipliers(std::vector &multipliers) +{ + multipliers.push_back(new ElderNadoxMultiplier(botAI)); + multipliers.push_back(new JedogaShadowseekerMultiplier(botAI)); + multipliers.push_back(new ForgottenOneMultiplier(botAI)); +} diff --git a/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomStrategy.h b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomStrategy.h new file mode 100644 index 00000000..03c5085e --- /dev/null +++ b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomStrategy.h @@ -0,0 +1,18 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONOKSTRATEGY_H +#define _PLAYERBOT_WOTLKDUNGEONOKSTRATEGY_H + +#include "Multiplier.h" +#include "AiObjectContext.h" +#include "Strategy.h" + + +class WotlkDungeonOKStrategy : public Strategy +{ +public: + WotlkDungeonOKStrategy(PlayerbotAI* ai) : Strategy(ai) {} + virtual std::string const getName() override { return "old kingdom"; } + virtual void InitTriggers(std::vector &triggers) override; + virtual void InitMultipliers(std::vector &multipliers) override; +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomTriggerContext.h b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomTriggerContext.h new file mode 100644 index 00000000..faca4976 --- /dev/null +++ b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomTriggerContext.h @@ -0,0 +1,23 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONOKTRIGGERCONTEXT_H +#define _PLAYERBOT_WOTLKDUNGEONOKTRIGGERCONTEXT_H + +#include "NamedObjectContext.h" +#include "AiObjectContext.h" +#include "OldKingdomTriggers.h" + +class WotlkDungeonOKTriggerContext : public NamedObjectContext +{ + public: + WotlkDungeonOKTriggerContext() + { + creators["nadox guardian"] = &WotlkDungeonOKTriggerContext::nadox_guardian; + creators["jedoga volunteer"] = &WotlkDungeonOKTriggerContext::jedoga_volunteer; + creators["shadow crash"] = &WotlkDungeonOKTriggerContext::shadow_crash; + } + private: + static Trigger* nadox_guardian(PlayerbotAI* ai) { return new NadoxGuardianTrigger(ai); } + static Trigger* jedoga_volunteer(PlayerbotAI* ai) { return new JedogaVolunteerTrigger(ai); } + static Trigger* shadow_crash(PlayerbotAI* ai) { return new ShadowCrashTrigger(ai); } +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomTriggers.cpp b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomTriggers.cpp new file mode 100644 index 00000000..951ab755 --- /dev/null +++ b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomTriggers.cpp @@ -0,0 +1,43 @@ +#include "Playerbots.h" +#include "OldKingdomTriggers.h" +#include "AiObject.h" +#include "AiObjectContext.h" + + +bool NadoxGuardianTrigger::IsActive() +{ + if (botAI->IsHeal(bot)) { return false; } + + Unit* boss = AI_VALUE2(Unit*, "find target", "elder nadox"); + Unit* guardian = AI_VALUE2(Unit*, "find target", "ahn'kahar guardian"); + + return boss && guardian; +} + +bool JedogaVolunteerTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "jedoga shadowseeker"); + // Unit* volunteer = AI_VALUE2(Unit*, "find target", "twilight volunteer"); + Unit* volunteer = 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_TWILIGHT_VOLUNTEER) + { + volunteer = unit; + break; + } + } + + return boss && volunteer; +} + +bool ShadowCrashTrigger::IsActive() +{ + if (botAI->IsMelee(bot)) { return false; } + return !botAI->IsMelee(bot) && AI_VALUE2(Unit*, "find target", "forgotten one"); +} diff --git a/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomTriggers.h b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomTriggers.h new file mode 100644 index 00000000..2aa77986 --- /dev/null +++ b/src/strategy/dungeons/wotlk/oldkingdom/OldKingdomTriggers.h @@ -0,0 +1,45 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONOKTRIGGERS_H +#define _PLAYERBOT_WOTLKDUNGEONOKTRIGGERS_H + +#include "Trigger.h" +#include "PlayerbotAIConfig.h" +#include "GenericTriggers.h" +#include "DungeonStrategyUtils.h" + +enum OldKingdomIDs +{ + // Elder Nadox + BUFF_GUARDIAN_AURA = 56153, + + // Jedoga Shadowseeker + NPC_TWILIGHT_VOLUNTEER = 30385, + + // Forgotten One(s) + SPELL_SHADOW_CRASH_N = 60833, + SPELL_SHADOW_CRASH_H = 60848, +}; + +#define SPELL_SHADOW_CRASH DUNGEON_MODE(bot, SPELL_SHADOW_CRASH_N, SPELL_SHADOW_CRASH_H) + +class NadoxGuardianTrigger : public Trigger +{ +public: + NadoxGuardianTrigger(PlayerbotAI* ai) : Trigger(ai, "elder nadox guardian") {} + bool IsActive() override; +}; + +class JedogaVolunteerTrigger : public Trigger +{ +public: + JedogaVolunteerTrigger(PlayerbotAI* ai) : Trigger(ai, "jedoga volunteer") {} + bool IsActive() override; +}; + +class ShadowCrashTrigger : public Trigger +{ +public: + ShadowCrashTrigger(PlayerbotAI* ai) : Trigger(ai, "shadow crash") {} + bool IsActive() override; +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/oldkingdom/TODO b/src/strategy/dungeons/wotlk/oldkingdom/TODO deleted file mode 100644 index e69de29b..00000000 diff --git a/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepActions.cpp b/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepActions.cpp index 2c17366d..f26823f4 100644 --- a/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepActions.cpp +++ b/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepActions.cpp @@ -2,6 +2,7 @@ #include "UtgardeKeepActions.h" #include "UtgardeKeepStrategy.h" +bool AttackFrostTombAction::isUseful() { return !botAI->IsHeal(bot); } bool AttackFrostTombAction::Execute(Event event) { Unit* frostTomb = nullptr; diff --git a/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepActions.h b/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepActions.h index 3774f374..c9c77d52 100644 --- a/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepActions.h +++ b/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepActions.h @@ -12,6 +12,7 @@ class AttackFrostTombAction : public AttackAction public: AttackFrostTombAction(PlayerbotAI* ai) : AttackAction(ai, "attack frost tomb") {} bool Execute(Event event) override; + bool isUseful() override; }; class AttackDalronnAction : public AttackAction @@ -32,16 +33,16 @@ class IngvarDodgeSmashAction : public MovementAction { public: IngvarDodgeSmashAction(PlayerbotAI* ai) : MovementAction(ai, "ingvar dodge smash") {} - bool isUseful() override; bool Execute(Event event) override; + bool isUseful() override; }; class IngvarSmashReturnAction : public MovementAction { public: IngvarSmashReturnAction(PlayerbotAI* ai) : MovementAction(ai, "ingvar smash return") {} - bool isUseful() override; bool Execute(Event event) override; + bool isUseful() override; }; #endif diff --git a/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepMultipliers.cpp b/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepMultipliers.cpp index 7d785e4c..31d310fa 100644 --- a/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepMultipliers.cpp +++ b/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepMultipliers.cpp @@ -6,7 +6,7 @@ float PrinceKelesethMultiplier::GetValue(Action* action) { - Unit* boss = AI_VALUE2(Unit*, "find target", "prince keleseth"); + Unit* boss = AI_VALUE2(Unit*, "find target", "prince keleseth"); if (!boss) { return 1.0f; @@ -15,8 +15,9 @@ float PrinceKelesethMultiplier::GetValue(Action* action) { return 0.0f; } - return 1.0f; + return 1.0f; } + float SkarvaldAndDalronnMultiplier::GetValue(Action* action) { // Unit* skarvald = AI_VALUE2(Unit*, "find target", "skarvald the constructor"); diff --git a/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepMultipliers.h b/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepMultipliers.h index e9e265be..e0b8e7af 100644 --- a/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepMultipliers.h +++ b/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepMultipliers.h @@ -1,5 +1,5 @@ -#ifndef _PLAYERRBOT_WOTLKDUNGEONUKMULTIPLIERS_H_ -#define _PLAYERRBOT_WOTLKDUNGEONUKMULTIPLIERS_H_ +#ifndef _PLAYERBOT_WOTLKDUNGEONUKMULTIPLIERS_H +#define _PLAYERBOT_WOTLKDUNGEONUKMULTIPLIERS_H #include "Multiplier.h" diff --git a/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepStrategy.cpp b/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepStrategy.cpp index c79b3dd3..cb3178ed 100644 --- a/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepStrategy.cpp +++ b/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepStrategy.cpp @@ -4,40 +4,40 @@ void WotlkDungeonUKStrategy::InitTriggers(std::vector &triggers) { - // Prince Keleseth - triggers.push_back(new TriggerNode("keleseth frost tomb", + // Prince Keleseth + triggers.push_back(new TriggerNode("keleseth frost tomb", NextAction::array(0, new NextAction("attack frost tomb", ACTION_RAID + 1), nullptr))); - - // Skarvald the Constructor & Dalronn the Controller - triggers.push_back(new TriggerNode("dalronn priority", + + // Skarvald the Constructor & Dalronn the Controller + triggers.push_back(new TriggerNode("dalronn priority", NextAction::array(0, new NextAction("attack dalronn", ACTION_RAID + 1), nullptr))); - - // Ingvar the Plunderer + + // Ingvar the Plunderer - // Doesn't work yet, this action doesn't get processed until the existing cast finishes - // triggers.push_back(new TriggerNode("ingvar staggering roar", + // Doesn't work yet, this action doesn't get processed until the existing cast finishes + // triggers.push_back(new TriggerNode("ingvar staggering roar", // NextAction::array(0, new NextAction("ingvar stop casting", ACTION_RAID + 1), nullptr))); - // No easy way to check LoS here, the pillars do not seem to count as gameobjects. - // Not implemented for now, unsure if this is needed as a good group can probably burst through the boss - // and just eat the debuff. - // triggers.push_back(new TriggerNode("ingvar dreadful roar", + // No easy way to check LoS here, the pillars do not seem to count as gameobjects. + // Not implemented for now, unsure if this is needed as a good group can probably burst through the boss + // and just eat the debuff. + // triggers.push_back(new TriggerNode("ingvar dreadful roar", // NextAction::array(0, new NextAction("ingvar hide los", ACTION_RAID + 1), nullptr))); - triggers.push_back(new TriggerNode("ingvar smash tank", + triggers.push_back(new TriggerNode("ingvar smash tank", NextAction::array(0, new NextAction("ingvar dodge smash", ACTION_MOVE + 5), nullptr))); - triggers.push_back(new TriggerNode("ingvar smash tank return", + triggers.push_back(new TriggerNode("ingvar smash tank return", NextAction::array(0, new NextAction("ingvar smash return", ACTION_MOVE + 5), nullptr))); - // Buggy... if not behind target, ai can get stuck running towards and away from target. - // I think for ranged chars, a custom action should be added that doesn't attempt to run into melee. - // This is a bandaid for now, needs to be improved. - triggers.push_back(new TriggerNode("not behind ingvar", + // Buggy... if not behind target, ai can get stuck running towards and away from target. + // I think for ranged chars, a custom action should be added that doesn't attempt to run into melee. + // This is a bandaid for now, needs to be improved. + triggers.push_back(new TriggerNode("not behind ingvar", NextAction::array(0, new NextAction("set behind", ACTION_MOVE + 1), nullptr))); } void WotlkDungeonUKStrategy::InitMultipliers(std::vector &multipliers) { - multipliers.push_back(new PrinceKelesethMultiplier(botAI)); - multipliers.push_back(new SkarvaldAndDalronnMultiplier(botAI)); - multipliers.push_back(new IngvarThePlundererMultiplier(botAI)); + multipliers.push_back(new PrinceKelesethMultiplier(botAI)); + multipliers.push_back(new SkarvaldAndDalronnMultiplier(botAI)); + multipliers.push_back(new IngvarThePlundererMultiplier(botAI)); } diff --git a/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepTriggers.h b/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepTriggers.h index e59ad8ca..db2032aa 100644 --- a/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepTriggers.h +++ b/src/strategy/dungeons/wotlk/utgardekeep/UtgardeKeepTriggers.h @@ -1,15 +1,12 @@ #ifndef _PLAYERBOT_WOTLKDUNGEONUKTRIGGERS_H #define _PLAYERBOT_WOTLKDUNGEONUKTRIGGERS_H -#include "EventMap.h" #include "Trigger.h" #include "PlayerbotAIConfig.h" #include "GenericTriggers.h" #include "DungeonStrategyUtils.h" -// Taken from: -// src/server/scripts/Northrend/UtgardeKeep/UtgardeKeep/boss_ingvar_the_plunderer.cpp -enum eSpells +enum UtgardeKeepIDs { SPELL_SUMMON_VALKYR = 42912, SPELL_RESURRECTION_BEAM = 42857, @@ -32,7 +29,6 @@ enum eSpells SPELL_DARK_SMASH = 42723, SPELL_SHADOW_AXE = 42749, - // Added DEBUFF_FROST_TOMB = 48400, }; diff --git a/src/strategy/dungeons/wotlk/violethold/TODO b/src/strategy/dungeons/wotlk/violethold/TODO deleted file mode 100644 index e69de29b..00000000 diff --git a/src/strategy/dungeons/wotlk/violethold/VioletHoldActionContext.h b/src/strategy/dungeons/wotlk/violethold/VioletHoldActionContext.h new file mode 100644 index 00000000..902332f1 --- /dev/null +++ b/src/strategy/dungeons/wotlk/violethold/VioletHoldActionContext.h @@ -0,0 +1,24 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONVHACTIONCONTEXT_H +#define _PLAYERBOT_WOTLKDUNGEONVHACTIONCONTEXT_H + +#include "Action.h" +#include "NamedObjectContext.h" +#include "VioletHoldActions.h" + +class WotlkDungeonVHActionContext : public NamedObjectContext +{ + public: + WotlkDungeonVHActionContext() { + creators["attack erekem"] = &WotlkDungeonVHActionContext::attack_erekem; + creators["attack ichor globule"] = &WotlkDungeonVHActionContext::attack_ichor_globule; + creators["attack void sentry"] = &WotlkDungeonVHActionContext::attack_void_sentry; + creators["stop attack"] = &WotlkDungeonVHActionContext::stop_attack; + } + private: + static Action* attack_erekem(PlayerbotAI* ai) { return new AttackErekemAction(ai); } + static Action* attack_ichor_globule(PlayerbotAI* ai) { return new AttackIchorGlobuleAction(ai); } + static Action* attack_void_sentry(PlayerbotAI* ai) { return new AttackVoidSentryAction(ai); } + static Action* stop_attack(PlayerbotAI* ai) { return new StopAttackAction(ai); } +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/violethold/VioletHoldActions.cpp b/src/strategy/dungeons/wotlk/violethold/VioletHoldActions.cpp new file mode 100644 index 00000000..a108b323 --- /dev/null +++ b/src/strategy/dungeons/wotlk/violethold/VioletHoldActions.cpp @@ -0,0 +1,99 @@ +#include "Playerbots.h" +#include "VioletHoldActions.h" +#include "VioletHoldStrategy.h" + + +bool AttackErekemAction::Execute(Event event) +{ + // Focus boss first, adds after + Unit* boss = AI_VALUE2(Unit*, "find target", "erekem"); + if (AI_VALUE(Unit*, "current target") != boss) + { + return Attack(boss); + } + return false; +} + +bool AttackIchorGlobuleAction::Execute(Event event) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "ichoron"); + if (!boss) { return false; } + + // Tank prioritise boss if it's up + if (botAI->IsTank(bot) && !boss->HasAura(SPELL_DRAINED)) + { + if (AI_VALUE(Unit*, "current target") != boss) + { + return Attack(boss); + } + 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"); + + for (auto i = targets.begin(); i != targets.end(); ++i) + { + Unit* unit = botAI->GetUnit(*i); + if (unit && unit->GetEntry() == NPC_ICHOR_GLOBULE) + { + Unit* currentTarget = AI_VALUE(Unit*, "current target"); + // Check IDs here, NOT Unit* pointers: + // Don't keep swapping between sentries. + // If we're already attacking one, don't retarget another + if (currentTarget && currentTarget->GetEntry() == NPC_ICHOR_GLOBULE) + { + return false; + } + return Attack(unit); + } + } + // No ichor globules left alive, fall back to targeting boss + if (AI_VALUE(Unit*, "current target") != boss) + { + return Attack(boss); + } + + return false; +} + +bool AttackVoidSentryAction::Execute(Event event) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "zuramat the obliterator"); + if (!boss) { 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"); + 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_VOID_SENTRY) + { + Unit* currentTarget = AI_VALUE(Unit*, "current target"); + // Check IDs here, NOT Unit* pointers: + // Don't keep swapping between sentries. + // If we're already attacking one, don't retarget another + if (currentTarget && currentTarget->GetEntry() == NPC_VOID_SENTRY) + { + return false; + } + return Attack(unit); + } + } + // No void sentries left alive, fall back to targeting boss + if (AI_VALUE(Unit*, "current target") != boss) + { + return Attack(boss); + } + + return false; +} + +bool StopAttackAction::Execute(Event event) +{ + return bot->AttackStop(); +} diff --git a/src/strategy/dungeons/wotlk/violethold/VioletHoldActions.h b/src/strategy/dungeons/wotlk/violethold/VioletHoldActions.h new file mode 100644 index 00000000..2d2dada0 --- /dev/null +++ b/src/strategy/dungeons/wotlk/violethold/VioletHoldActions.h @@ -0,0 +1,48 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONVHACTIONS_H +#define _PLAYERBOT_WOTLKDUNGEONVHACTIONS_H + +#include "Action.h" +#include "AttackAction.h" +#include "GenericSpellActions.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "VioletHoldTriggers.h" + +// const Position NOVOS_PARTY_POSITION = Position(-378.852f, -760.349f, 28.587f); + +class AttackErekemAction : public AttackAction +{ +public: + AttackErekemAction(PlayerbotAI* ai) : AttackAction(ai, "attack erekem") {} + bool Execute(Event event) override; +}; + +class AttackIchoronElementalsAction : public AttackAction +{ +public: + AttackIchoronElementalsAction(PlayerbotAI* ai) : AttackAction(ai, "attack ichoron elementals") {} + bool Execute(Event event) override; +}; + +class AttackIchorGlobuleAction : public AttackAction +{ +public: + AttackIchorGlobuleAction(PlayerbotAI* ai) : AttackAction(ai, "attack ichor globule") {} + bool Execute(Event event) override; +}; + +class AttackVoidSentryAction : public AttackAction +{ +public: + AttackVoidSentryAction(PlayerbotAI* ai) : AttackAction(ai, "attack void sentry") {} + bool Execute(Event event) override; +}; + +class StopAttackAction : public Action +{ +public: + StopAttackAction(PlayerbotAI* ai) : Action(ai, "stop attack") {} + bool Execute(Event event) override; +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/violethold/VioletHoldMultipliers.cpp b/src/strategy/dungeons/wotlk/violethold/VioletHoldMultipliers.cpp new file mode 100644 index 00000000..ff6cc6de --- /dev/null +++ b/src/strategy/dungeons/wotlk/violethold/VioletHoldMultipliers.cpp @@ -0,0 +1,57 @@ +#include "VioletHoldMultipliers.h" +#include "VioletHoldActions.h" +#include "GenericSpellActions.h" +#include "ChooseTargetActions.h" +#include "MovementActions.h" +#include "VioletHoldTriggers.h" +#include "Action.h" + +float ErekemMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "erekem"); + if (!boss || !botAI->IsDps(bot)) { return 1.0f; } + + if (dynamic_cast(action)) + { + return 0.0f; + } + if (action->getThreatType() == Action::ActionThreatType::Aoe) + { + return 0.0f; + } + return 1.0f; +} + +float IchoronMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "ichoron"); + if (!boss) { return 1.0f; } + + if (dynamic_cast(action) + || dynamic_cast(action) + || dynamic_cast(action)) + { + return 0.0f; + } + return 1.0f; +} + +float ZuramatMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "zuramat the obliterator"); + if (!boss) { return 1.0f; } + + if (bot->HasAura(SPELL_VOID_SHIFTED)) + { + if (dynamic_cast(action) || dynamic_cast(action)) + { + return 0.0f; + } + } + + if (boss->HasAura(SPELL_SHROUD_OF_DARKNESS) && dynamic_cast(action)) + { + return 0.0f; + } + return 1.0f; +} diff --git a/src/strategy/dungeons/wotlk/violethold/VioletHoldMultipliers.h b/src/strategy/dungeons/wotlk/violethold/VioletHoldMultipliers.h new file mode 100644 index 00000000..67952a16 --- /dev/null +++ b/src/strategy/dungeons/wotlk/violethold/VioletHoldMultipliers.h @@ -0,0 +1,33 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONVHMULTIPLIERS_H +#define _PLAYERBOT_WOTLKDUNGEONVHMULTIPLIERS_H + +#include "Multiplier.h" + +class ErekemMultiplier : public Multiplier +{ + public: + ErekemMultiplier(PlayerbotAI* ai) : Multiplier(ai, "erekem") {} + + public: + virtual float GetValue(Action* action); +}; + +class IchoronMultiplier : public Multiplier +{ + public: + IchoronMultiplier(PlayerbotAI* ai) : Multiplier(ai, "ichoron") {} + + public: + virtual float GetValue(Action* action); +}; + +class ZuramatMultiplier : public Multiplier +{ + public: + ZuramatMultiplier(PlayerbotAI* ai) : Multiplier(ai, "zuramat the obliterator") {} + + public: + virtual float GetValue(Action* action); +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/violethold/VioletHoldStrategy.cpp b/src/strategy/dungeons/wotlk/violethold/VioletHoldStrategy.cpp new file mode 100644 index 00000000..bcc27506 --- /dev/null +++ b/src/strategy/dungeons/wotlk/violethold/VioletHoldStrategy.cpp @@ -0,0 +1,41 @@ +#include "VioletHoldStrategy.h" +#include "VioletHoldMultipliers.h" + + +void WotlkDungeonVHStrategy::InitTriggers(std::vector &triggers) +{ + // Erekem + // This boss has many purgable buffs, purging/dispels could be merged into generic strats though + triggers.push_back(new TriggerNode("erekem target", + NextAction::array(0, new NextAction("attack erekem", ACTION_RAID + 1), nullptr))); + + // Moragg + // TODO: This guy has Optic Link which may require moving, add if needed + + // Ichoron + triggers.push_back(new TriggerNode("ichoron target", + NextAction::array(0, new NextAction("attack ichor globule", ACTION_RAID + 1), nullptr))); + + // Xevozz + // TODO: Revisit in heroics, waypoints back and forth on stairs. Need to test with double beacon spawn + + // Lavanthor + // Tank & spank + + // Zuramat the Obliterator + triggers.push_back(new TriggerNode("shroud of darkness", + NextAction::array(0, new NextAction("stop attack", ACTION_HIGH + 5), nullptr))); + triggers.push_back(new TriggerNode("void shift", + NextAction::array(0, new NextAction("attack void sentry", ACTION_RAID + 1), nullptr))); + + // Cyanigosa + triggers.push_back(new TriggerNode("cyanigosa positioning", + NextAction::array(0, new NextAction("rear flank", ACTION_MOVE + 5), nullptr))); +} + +void WotlkDungeonVHStrategy::InitMultipliers(std::vector &multipliers) +{ + multipliers.push_back(new ErekemMultiplier(botAI)); + multipliers.push_back(new IchoronMultiplier(botAI)); + multipliers.push_back(new ZuramatMultiplier(botAI)); +} diff --git a/src/strategy/dungeons/wotlk/violethold/VioletHoldStrategy.h b/src/strategy/dungeons/wotlk/violethold/VioletHoldStrategy.h new file mode 100644 index 00000000..16708871 --- /dev/null +++ b/src/strategy/dungeons/wotlk/violethold/VioletHoldStrategy.h @@ -0,0 +1,18 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONVHSTRATEGY_H +#define _PLAYERBOT_WOTLKDUNGEONVHSTRATEGY_H + +#include "Multiplier.h" +#include "AiObjectContext.h" +#include "Strategy.h" + + +class WotlkDungeonVHStrategy : public Strategy +{ +public: + WotlkDungeonVHStrategy(PlayerbotAI* ai) : Strategy(ai) {} + virtual std::string const getName() override { return "violet hold"; } + virtual void InitTriggers(std::vector &triggers) override; + virtual void InitMultipliers(std::vector &multipliers) override; +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/violethold/VioletHoldTriggerContext.h b/src/strategy/dungeons/wotlk/violethold/VioletHoldTriggerContext.h new file mode 100644 index 00000000..48a722f4 --- /dev/null +++ b/src/strategy/dungeons/wotlk/violethold/VioletHoldTriggerContext.h @@ -0,0 +1,27 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONVHTRIGGERCONTEXT_H +#define _PLAYERBOT_WOTLKDUNGEONVHTRIGGERCONTEXT_H + +#include "NamedObjectContext.h" +#include "AiObjectContext.h" +#include "VioletHoldTriggers.h" + +class WotlkDungeonVHTriggerContext : public NamedObjectContext +{ + public: + WotlkDungeonVHTriggerContext() + { + creators["erekem target"] = &WotlkDungeonVHTriggerContext::erekem_target; + creators["ichoron target"] = &WotlkDungeonVHTriggerContext::ichoron_target; + creators["void shift"] = &WotlkDungeonVHTriggerContext::void_shift; + creators["shroud of darkness"] = &WotlkDungeonVHTriggerContext::shroud_of_darkness; + creators["cyanigosa positioning"] = &WotlkDungeonVHTriggerContext::cyanigosa_positioning; + } + private: + static Trigger* erekem_target(PlayerbotAI* ai) { return new ErekemTargetTrigger(ai); } + static Trigger* ichoron_target(PlayerbotAI* ai) { return new IchoronTargetTrigger(ai); } + static Trigger* void_shift(PlayerbotAI* ai) { return new VoidShiftTrigger(ai); } + static Trigger* shroud_of_darkness(PlayerbotAI* ai) { return new ShroudOfDarknessTrigger(ai); } + static Trigger* cyanigosa_positioning(PlayerbotAI* ai) { return new CyanigosaPositioningTrigger(ai); } +}; + +#endif diff --git a/src/strategy/dungeons/wotlk/violethold/VioletHoldTriggers.cpp b/src/strategy/dungeons/wotlk/violethold/VioletHoldTriggers.cpp new file mode 100644 index 00000000..cf9e9ab1 --- /dev/null +++ b/src/strategy/dungeons/wotlk/violethold/VioletHoldTriggers.cpp @@ -0,0 +1,35 @@ +#include "Playerbots.h" +#include "VioletHoldTriggers.h" +#include "AiObject.h" +#include "AiObjectContext.h" + + +bool ErekemTargetTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "erekem") && botAI->IsDps(bot); +} + +bool IchoronTargetTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "ichoron") && !botAI->IsHeal(bot); +} + +bool VoidShiftTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "zuramat the obliterator"); + return boss && bot->HasAura(SPELL_VOID_SHIFTED) && !botAI->IsHeal(bot); +} + +bool ShroudOfDarknessTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "zuramat the obliterator"); + return boss && boss->HasAura(SPELL_SHROUD_OF_DARKNESS); +} + +bool CyanigosaPositioningTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "cyanigosa"); + // Include healers here for now, otherwise they stand in things + return boss && !botAI->IsTank(bot) && !botAI->IsRangedDps(bot); + // return boss && botAI->IsMelee(bot) && !botAI->IsTank(bot); +} diff --git a/src/strategy/dungeons/wotlk/violethold/VioletHoldTriggers.h b/src/strategy/dungeons/wotlk/violethold/VioletHoldTriggers.h new file mode 100644 index 00000000..b5a306d4 --- /dev/null +++ b/src/strategy/dungeons/wotlk/violethold/VioletHoldTriggers.h @@ -0,0 +1,59 @@ +#ifndef _PLAYERBOT_WOTLKDUNGEONVHTRIGGERS_H +#define _PLAYERBOT_WOTLKDUNGEONVHTRIGGERS_H + +#include "Trigger.h" +#include "PlayerbotAIConfig.h" +#include "GenericTriggers.h" +#include "DungeonStrategyUtils.h" + +enum VioletHoldIDs +{ + // Ichoron + SPELL_DRAINED = 59820, + NPC_ICHOR_GLOBULE = 29321, + + // Zuramat the Obliterator + SPELL_VOID_SHIFTED = 54343, + SPELL_SHROUD_OF_DARKNESS_N = 54524, + SPELL_SHROUD_OF_DARKNESS_H = 59745, + NPC_VOID_SENTRY = 29364, +}; + +#define SPELL_SHROUD_OF_DARKNESS DUNGEON_MODE(bot, SPELL_SHROUD_OF_DARKNESS_N, SPELL_SHROUD_OF_DARKNESS_H) + +class ErekemTargetTrigger : public Trigger +{ +public: + ErekemTargetTrigger(PlayerbotAI* ai) : Trigger(ai, "erekem target") {} + bool IsActive() override; +}; + +class IchoronTargetTrigger : public Trigger +{ +public: + IchoronTargetTrigger(PlayerbotAI* ai) : Trigger(ai, "ichoron target") {} + bool IsActive() override; +}; + +class VoidShiftTrigger : public Trigger +{ +public: + VoidShiftTrigger(PlayerbotAI* ai) : Trigger(ai, "void shift") {} + bool IsActive() override; +}; + +class ShroudOfDarknessTrigger : public Trigger +{ +public: + ShroudOfDarknessTrigger(PlayerbotAI* ai) : Trigger(ai, "shroud of darkness") {} + bool IsActive() override; +}; + +class CyanigosaPositioningTrigger : public Trigger +{ +public: + CyanigosaPositioningTrigger(PlayerbotAI* ai) : Trigger(ai, "cyanigosa positioning") {} + bool IsActive() override; +}; + +#endif diff --git a/src/strategy/mage/MageTriggers.cpp b/src/strategy/mage/MageTriggers.cpp index e935d51b..95c22e4d 100644 --- a/src/strategy/mage/MageTriggers.cpp +++ b/src/strategy/mage/MageTriggers.cpp @@ -29,7 +29,7 @@ bool FingersOfFrostSingleTrigger::IsActive() { // Fingers of Frost "stack" count is always 1. // The value is instead stored in the charges. - Aura* aura = botAI->GetAura(getName(), GetTarget(), false, true, -1); + Aura* aura = botAI->GetAura("fingers of frost", bot, false, true, -1); return (aura && aura->GetCharges() == 1); } diff --git a/src/strategy/paladin/GenericPaladinNonCombatStrategy.cpp b/src/strategy/paladin/GenericPaladinNonCombatStrategy.cpp index 80e23cd3..d19320bd 100644 --- a/src/strategy/paladin/GenericPaladinNonCombatStrategy.cpp +++ b/src/strategy/paladin/GenericPaladinNonCombatStrategy.cpp @@ -22,7 +22,7 @@ void GenericPaladinNonCombatStrategy::InitTriggers(std::vector& tr triggers.push_back(new TriggerNode("party member almost full health", NextAction::array(0, new NextAction("flash of light on party", 25.0f), NULL))); triggers.push_back(new TriggerNode("party member medium health", - NextAction::array(0, new NextAction("holy light on party", 26.0f), NULL))); + NextAction::array(0, new NextAction("flash of light on party", 26.0f), NULL))); triggers.push_back(new TriggerNode("party member low health", NextAction::array(0, new NextAction("holy light on party", 27.0f), NULL))); triggers.push_back(new TriggerNode("party member critical health", diff --git a/src/strategy/paladin/GenericPaladinStrategy.cpp b/src/strategy/paladin/GenericPaladinStrategy.cpp index d25a7518..b5b77f58 100644 --- a/src/strategy/paladin/GenericPaladinStrategy.cpp +++ b/src/strategy/paladin/GenericPaladinStrategy.cpp @@ -71,3 +71,17 @@ void PaladinCcStrategy::InitTriggers(std::vector& triggers) triggers.push_back( new TriggerNode("turn undead", NextAction::array(0, new NextAction("turn undead", ACTION_HIGH + 1), nullptr))); } + +void PaladinHealerDpsStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back( + new TriggerNode("healer should attack", + NextAction::array(0, + new NextAction("hammer of wrath", ACTION_DEFAULT + 0.6f), + new NextAction("holy shock", ACTION_DEFAULT + 0.5f), + new NextAction("shield of righteousness", ACTION_DEFAULT + 0.4f), + new NextAction("judgement of light", ACTION_DEFAULT + 0.3f), + new NextAction("consecration", ACTION_DEFAULT + 0.2f), + new NextAction("exorcism", ACTION_DEFAULT+ 0.1f), + nullptr))); +} diff --git a/src/strategy/paladin/GenericPaladinStrategy.h b/src/strategy/paladin/GenericPaladinStrategy.h index 4922a1bc..737048cc 100644 --- a/src/strategy/paladin/GenericPaladinStrategy.h +++ b/src/strategy/paladin/GenericPaladinStrategy.h @@ -46,4 +46,13 @@ public: std::string const getName() override { return "cc"; } }; +class PaladinHealerDpsStrategy : public Strategy +{ +public: + PaladinHealerDpsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "healer dps"; } +}; + #endif diff --git a/src/strategy/paladin/GenericPaladinStrategyActionNodeFactory.h b/src/strategy/paladin/GenericPaladinStrategyActionNodeFactory.h index 68617eba..3503d773 100644 --- a/src/strategy/paladin/GenericPaladinStrategyActionNodeFactory.h +++ b/src/strategy/paladin/GenericPaladinStrategyActionNodeFactory.h @@ -22,7 +22,7 @@ public: creators["cleanse magic"] = &cleanse_magic; creators["cleanse poison on party"] = &cleanse_poison_on_party; creators["cleanse disease on party"] = &cleanse_disease_on_party; - // creators["seal of wisdom"] = &seal_of_wisdom; + creators["seal of wisdom"] = &seal_of_wisdom; creators["seal of justice"] = &seal_of_justice; creators["hand of reckoning"] = &hand_of_reckoning; creators["judgement"] = &judgement; @@ -147,13 +147,13 @@ private: /*A*/ NextAction::array(0, new NextAction("purify disease on party"), nullptr), /*C*/ nullptr); } - // static ActionNode* seal_of_wisdom(PlayerbotAI* ai) - // { - // return new ActionNode ("seal of wisdom", - // /*P*/ NULL, - // /*A*/ NextAction::array(0, new NextAction("seal of justice"), NULL), - // /*C*/ NULL); - // } + static ActionNode* seal_of_wisdom(PlayerbotAI* ai) + { + return new ActionNode ("seal of wisdom", + /*P*/ NULL, + /*A*/ NextAction::array(0, new NextAction("seal of righteousness"), NULL), + /*C*/ NULL); + } static ActionNode* seal_of_justice(PlayerbotAI* ai) { return new ActionNode("seal of justice", diff --git a/src/strategy/paladin/HealPaladinStrategy.cpp b/src/strategy/paladin/HealPaladinStrategy.cpp index 70c461ce..5f1767dd 100644 --- a/src/strategy/paladin/HealPaladinStrategy.cpp +++ b/src/strategy/paladin/HealPaladinStrategy.cpp @@ -27,7 +27,7 @@ HealPaladinStrategy::HealPaladinStrategy(PlayerbotAI* botAI) : GenericPaladinStr NextAction** HealPaladinStrategy::getDefaultActions() { - return NextAction::array(0, new NextAction("judgement of light", ACTION_DEFAULT + 2), nullptr); + return NextAction::array(0, new NextAction("judgement of light", ACTION_DEFAULT), nullptr); } void HealPaladinStrategy::InitTriggers(std::vector& triggers) @@ -49,7 +49,7 @@ void HealPaladinStrategy::InitTriggers(std::vector& triggers) NextAction::array(0, new NextAction("reach party member to heal", ACTION_EMERGENCY + 3), nullptr))); triggers.push_back( - new TriggerNode("medium group heal occasion", + new TriggerNode("medium group heal setting", NextAction::array(0, new NextAction("divine sacrifice", ACTION_CRITICAL_HEAL + 5), new NextAction("avenging wrath", ACTION_HIGH + 4), nullptr))); diff --git a/src/strategy/paladin/PaladinAiObjectContext.cpp b/src/strategy/paladin/PaladinAiObjectContext.cpp index 094e2fb7..42e89a8a 100644 --- a/src/strategy/paladin/PaladinAiObjectContext.cpp +++ b/src/strategy/paladin/PaladinAiObjectContext.cpp @@ -25,6 +25,7 @@ public: creators["boost"] = &PaladinStrategyFactoryInternal::boost; creators["cc"] = &PaladinStrategyFactoryInternal::cc; creators["bthreat"] = &PaladinStrategyFactoryInternal::bthreat; + creators["healer dps"] = &PaladinStrategyFactoryInternal::healer_dps; } private: @@ -33,6 +34,7 @@ private: static Strategy* boost(PlayerbotAI* botAI) { return new PaladinBoostStrategy(botAI); } static Strategy* cc(PlayerbotAI* botAI) { return new PaladinCcStrategy(botAI); } static Strategy* bthreat(PlayerbotAI* botAI) { return new PaladinBuffThreatStrategy(botAI); } + static Strategy* healer_dps(PlayerbotAI* botAI) { return new PaladinHealerDpsStrategy(botAI); } }; class PaladinResistanceStrategyFactoryInternal : public NamedObjectContext diff --git a/src/strategy/paladin/TankPaladinStrategy.cpp b/src/strategy/paladin/TankPaladinStrategy.cpp index 553d2a7d..25e761b7 100644 --- a/src/strategy/paladin/TankPaladinStrategy.cpp +++ b/src/strategy/paladin/TankPaladinStrategy.cpp @@ -73,7 +73,7 @@ void TankPaladinStrategy::InitTriggers(std::vector& triggers) GenericPaladinStrategy::InitTriggers(triggers); triggers.push_back( - new TriggerNode("seal", NextAction::array(0, new NextAction("seal of command", ACTION_HIGH), nullptr))); + new TriggerNode("seal", NextAction::array(0, new NextAction("seal of corruption", ACTION_HIGH), nullptr))); triggers.push_back( new TriggerNode("low mana", NextAction::array(0, new NextAction("seal of wisdom", ACTION_HIGH + 9), nullptr))); // triggers.push_back(new TriggerNode("devotion aura", NextAction::array(0, new NextAction("devotion aura", 90.0f), @@ -106,7 +106,7 @@ void TankPaladinStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode( "righteous fury", NextAction::array(0, new NextAction("righteous fury", ACTION_HIGH + 8), nullptr))); triggers.push_back( - new TriggerNode("medium group heal occasion", + new TriggerNode("medium group heal setting", NextAction::array(0, new NextAction("divine sacrifice", ACTION_HIGH + 5), nullptr))); triggers.push_back(new TriggerNode( "enough mana", NextAction::array(0, new NextAction("consecration", ACTION_HIGH + 4), nullptr))); diff --git a/src/strategy/priest/GenericPriestStrategy.cpp b/src/strategy/priest/GenericPriestStrategy.cpp index 40bae42d..82ca65e9 100644 --- a/src/strategy/priest/GenericPriestStrategy.cpp +++ b/src/strategy/priest/GenericPriestStrategy.cpp @@ -87,3 +87,23 @@ void PriestCcStrategy::InitTriggers(std::vector& triggers) triggers.push_back( new TriggerNode("shackle undead", NextAction::array(0, new NextAction("shackle undead", 31.0f), nullptr))); } + +void PriestHealerDpsStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back( + new TriggerNode("healer should attack", + NextAction::array(0, + new NextAction("shadow word: pain", ACTION_DEFAULT + 0.5f), + new NextAction("holy fire", ACTION_DEFAULT + 0.4f), + new NextAction("smite", ACTION_DEFAULT + 0.3f), + new NextAction("mind blast", ACTION_DEFAULT + 0.2f), + new NextAction("shoot", ACTION_DEFAULT), + nullptr))); + + triggers.push_back( + new TriggerNode("medium aoe and healer should attack", + NextAction::array(0, + new NextAction("mind sear", ACTION_DEFAULT + 0.5f), + nullptr))); +} + diff --git a/src/strategy/priest/GenericPriestStrategy.h b/src/strategy/priest/GenericPriestStrategy.h index 7c6684af..2668723e 100644 --- a/src/strategy/priest/GenericPriestStrategy.h +++ b/src/strategy/priest/GenericPriestStrategy.h @@ -46,4 +46,13 @@ public: std::string const getName() override { return "cc"; } }; +class PriestHealerDpsStrategy : public Strategy +{ +public: + PriestHealerDpsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "healer dps"; } +}; + #endif diff --git a/src/strategy/priest/HealPriestStrategy.cpp b/src/strategy/priest/HealPriestStrategy.cpp index 25f8e8bd..64c747fd 100644 --- a/src/strategy/priest/HealPriestStrategy.cpp +++ b/src/strategy/priest/HealPriestStrategy.cpp @@ -29,51 +29,56 @@ void HealPriestStrategy::InitTriggers(std::vector& triggers) // triggers.push_back(new TriggerNode( // "medium aoe heal", // NextAction::array(0, - // new NextAction("circle of healing", ACTION_MEDIUM_HEAL + 8), + // new NextAction("circle of healing on party", ACTION_MEDIUM_HEAL + 8), // // new NextAction("power word: shield on almost full health below", ACTION_MEDIUM_HEAL + 7), // NULL))); triggers.push_back(new TriggerNode( - "group heal occasion", - NextAction::array(0, new NextAction("circle of healing", ACTION_MEDIUM_HEAL + 8), - new NextAction("power word: shield on almost full health below", ACTION_MEDIUM_HEAL + 7), - NULL))); + "group heal setting", + NextAction::array(0, + new NextAction("prayer of mending on party", ACTION_MEDIUM_HEAL + 8), + new NextAction("power word: shield on not full", ACTION_MEDIUM_HEAL + 7), + nullptr))); triggers.push_back(new TriggerNode( - "medium group heal occasion", - NextAction::array(0, new NextAction("divine hymn", ACTION_CRITICAL_HEAL + 6), - new NextAction("prayer of healing on party", ACTION_CRITICAL_HEAL + 5), nullptr))); + "medium group heal setting", + NextAction::array(0, new NextAction("divine hymn", ACTION_CRITICAL_HEAL + 7), + new NextAction("prayer of mending on party", ACTION_CRITICAL_HEAL + 6), + new NextAction("power word: shield on not full", ACTION_CRITICAL_HEAL + 5), + new NextAction("prayer of healing on party", ACTION_CRITICAL_HEAL + 4), + nullptr))); triggers.push_back(new TriggerNode( "party member critical health", - NextAction::array(0, new NextAction("power word: shield on party", ACTION_CRITICAL_HEAL + 6), + NextAction::array(0, new NextAction("power word: shield on party", ACTION_CRITICAL_HEAL + 5), new NextAction("penance on party", ACTION_CRITICAL_HEAL + 4), - new NextAction("flash heal on party", ACTION_CRITICAL_HEAL + 3), - new NextAction("prayer of mending on party", ACTION_CRITICAL_HEAL + 2), NULL))); + new NextAction("prayer of mending on party", ACTION_CRITICAL_HEAL + 3), + new NextAction("flash heal on party", ACTION_CRITICAL_HEAL + 2), + nullptr))); triggers.push_back( new TriggerNode("party member low health", NextAction::array(0, new NextAction("power word: shield on party", ACTION_MEDIUM_HEAL + 4), + new NextAction("prayer of mending on party", ACTION_MEDIUM_HEAL + 3), new NextAction("penance on party", ACTION_MEDIUM_HEAL + 2), - new NextAction("circle of healing", ACTION_MEDIUM_HEAL + 2), - new NextAction("prayer of mending on party", ACTION_MEDIUM_HEAL + 1), - new NextAction("flash heal on party", ACTION_MEDIUM_HEAL + 0), NULL))); + new NextAction("flash heal on party", ACTION_MEDIUM_HEAL + 0), nullptr))); triggers.push_back( new TriggerNode("party member medium health", NextAction::array(0, new NextAction("power word: shield on party", ACTION_LIGHT_HEAL + 9), - new NextAction("penance on party", ACTION_LIGHT_HEAL + 7), - new NextAction("circle of healing", ACTION_LIGHT_HEAL + 7), - new NextAction("prayer of mending on party", ACTION_LIGHT_HEAL + 6), + new NextAction("prayer of mending on party", ACTION_LIGHT_HEAL + 7), + new NextAction("penance on party", ACTION_LIGHT_HEAL + 6), new NextAction("flash heal on party", ACTION_LIGHT_HEAL + 5), // new NextAction("renew on party", ACTION_LIGHT_HEAL + 8), - NULL))); + nullptr))); triggers.push_back( new TriggerNode("party member almost full health", NextAction::array(0, // new NextAction("penance on party", ACTION_LIGHT_HEAL + 3), - new NextAction("renew on party", ACTION_LIGHT_HEAL + 2), NULL))); + new NextAction("prayer of mending on party", ACTION_LIGHT_HEAL + 2), + new NextAction("renew on party", ACTION_LIGHT_HEAL + 1), + nullptr))); // triggers.push_back(new TriggerNode("almost full health", NextAction::array(0, new NextAction("renew", 43.f), // nullptr))); triggers.push_back(new TriggerNode("party member almost full health", NextAction::array(0, new @@ -86,7 +91,7 @@ void HealPriestStrategy::InitTriggers(std::vector& triggers) NextAction::array(0, new NextAction("reach party member to heal", ACTION_CRITICAL_HEAL + 10), nullptr))); // triggers.push_back(new TriggerNode("medium aoe heal", NextAction::array(0, new NextAction("prayer of // mending", 49.0f), nullptr))); triggers.push_back(new TriggerNode("medium aoe heal", NextAction::array(0, new - // NextAction("circle of healing", 48.0f), nullptr))); triggers.push_back(new TriggerNode("binding heal", + // NextAction("circle of healing on party", 48.0f), nullptr))); triggers.push_back(new TriggerNode("binding heal", // NextAction::array(0, new NextAction("binding heal", 52.0f), nullptr))); triggers.push_back(new TriggerNode("low // mana", NextAction::array(0, new NextAction("shadowfiend", ACTION_HIGH), nullptr))); diff --git a/src/strategy/priest/HolyPriestStrategy.cpp b/src/strategy/priest/HolyPriestStrategy.cpp index 40f34a28..7c073e05 100644 --- a/src/strategy/priest/HolyPriestStrategy.cpp +++ b/src/strategy/priest/HolyPriestStrategy.cpp @@ -63,42 +63,50 @@ void HolyHealPriestStrategy::InitTriggers(std::vector& triggers) GenericPriestStrategy::InitTriggers(triggers); triggers.push_back( - new TriggerNode("group heal occasion", - NextAction::array(0, new NextAction("circle of healing", ACTION_MEDIUM_HEAL + 8), NULL))); + new TriggerNode("group heal setting", + NextAction::array(0, + new NextAction("prayer of mending on party", ACTION_MEDIUM_HEAL + 9), + new NextAction("circle of healing on party", ACTION_MEDIUM_HEAL + 8), nullptr))); triggers.push_back(new TriggerNode( - "medium group heal occasion", - NextAction::array(0, new NextAction("divine hymn", ACTION_CRITICAL_HEAL + 6), - new NextAction("prayer of healing on party", ACTION_CRITICAL_HEAL + 5), nullptr))); + "medium group heal setting", + NextAction::array(0, new NextAction("divine hymn", ACTION_CRITICAL_HEAL + 7), + new NextAction("prayer of mending on party", ACTION_CRITICAL_HEAL + 6), + new NextAction("circle of healing on party", ACTION_CRITICAL_HEAL + 5), + new NextAction("prayer of healing on party", ACTION_CRITICAL_HEAL + 4), nullptr))); triggers.push_back(new TriggerNode( "party member critical health", NextAction::array(0, new NextAction("guardian spirit on party", ACTION_CRITICAL_HEAL + 6), new NextAction("power word: shield on party", ACTION_CRITICAL_HEAL + 5), - new NextAction("flash heal on party", ACTION_CRITICAL_HEAL + 3), - new NextAction("prayer of mending on party", ACTION_CRITICAL_HEAL + 2), NULL))); + new NextAction("prayer of mending on party", ACTION_CRITICAL_HEAL + 3), + new NextAction("greater heal on party", ACTION_MEDIUM_HEAL + 2), + new NextAction("flash heal on party", ACTION_CRITICAL_HEAL + 1), + nullptr))); triggers.push_back( new TriggerNode("party member low health", - NextAction::array(0, new NextAction("circle of healing", ACTION_MEDIUM_HEAL + 4), - new NextAction("greater heal on party", ACTION_MEDIUM_HEAL + 3), - new NextAction("prayer of mending on party", ACTION_MEDIUM_HEAL + 2), - new NextAction("flash heal on party", ACTION_MEDIUM_HEAL + 1), NULL))); + NextAction::array(0, new NextAction("circle of healing on party", ACTION_MEDIUM_HEAL + 4), + new NextAction("prayer of mending on party", ACTION_MEDIUM_HEAL + 3), + new NextAction("greater heal on party", ACTION_MEDIUM_HEAL + 2), + new NextAction("flash heal on party", ACTION_MEDIUM_HEAL + 1), nullptr))); triggers.push_back( new TriggerNode("party member medium health", - NextAction::array(0, new NextAction("circle of healing", ACTION_LIGHT_HEAL + 7), + NextAction::array(0, new NextAction("circle of healing on party", ACTION_LIGHT_HEAL + 7), new NextAction("prayer of mending on party", ACTION_LIGHT_HEAL + 6), - new NextAction("flash heal on party", ACTION_LIGHT_HEAL + 5), + new NextAction("greater heal on party", ACTION_MEDIUM_HEAL + 5), + new NextAction("flash heal on party", ACTION_LIGHT_HEAL + 4), // new NextAction("renew on party", ACTION_LIGHT_HEAL + 8), - NULL))); + nullptr))); triggers.push_back( new TriggerNode("party member almost full health", - NextAction::array(0, new NextAction("renew on party", ACTION_LIGHT_HEAL + 2), - // new NextAction("flash heal on party", ACTION_LIGHT_HEAL + 1), - NULL))); + NextAction::array(0, + new NextAction("renew on party", ACTION_LIGHT_HEAL + 2), + new NextAction("prayer of mending on party", ACTION_LIGHT_HEAL + 1), + nullptr))); triggers.push_back(new TriggerNode( "party member to heal out of spell range", diff --git a/src/strategy/priest/PriestActions.cpp b/src/strategy/priest/PriestActions.cpp index 3a94dafc..2364a5e2 100644 --- a/src/strategy/priest/PriestActions.cpp +++ b/src/strategy/priest/PriestActions.cpp @@ -18,7 +18,7 @@ bool CastRemoveShadowformAction::Execute(Event event) return true; } -Unit* CastPowerWordShieldOnAlmostFullHealthBelow::GetTarget() +Unit* CastPowerWordShieldOnAlmostFullHealthBelowAction::GetTarget() { Group* group = bot->GetGroup(); for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) @@ -47,7 +47,7 @@ Unit* CastPowerWordShieldOnAlmostFullHealthBelow::GetTarget() return nullptr; } -bool CastPowerWordShieldOnAlmostFullHealthBelow::isUseful() +bool CastPowerWordShieldOnAlmostFullHealthBelowAction::isUseful() { Group* group = bot->GetGroup(); for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) @@ -74,4 +74,35 @@ bool CastPowerWordShieldOnAlmostFullHealthBelow::isUseful() return true; } return false; +} + +Unit* CastPowerWordShieldOnNotFullAction::GetTarget() +{ + Group* group = bot->GetGroup(); + MinValueCalculator calc(100); + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + Player* player = gref->GetSource(); + if (!player) + continue; + if (player->isDead() || player->IsFullHealth()) + { + continue; + } + if (player->GetDistance2d(bot) > sPlayerbotAIConfig->spellDistance) + { + continue; + } + if (botAI->HasAnyAuraOf(player, "weakened soul", "power word: shield", nullptr)) + { + continue; + } + calc.probe(player->GetHealthPct(), player); + } + return (Unit*)calc.param; +} + +bool CastPowerWordShieldOnNotFullAction::isUseful() +{ + return GetTarget(); } \ No newline at end of file diff --git a/src/strategy/priest/PriestActions.h b/src/strategy/priest/PriestActions.h index 468694c8..c84bfc7d 100644 --- a/src/strategy/priest/PriestActions.h +++ b/src/strategy/priest/PriestActions.h @@ -52,10 +52,23 @@ HEAL_PARTY_ACTION(CastGreaterHealOnPartyAction, "greater heal", 50.0f, HealingMa HEAL_PARTY_ACTION(CastPowerWordShieldOnPartyAction, "power word: shield", 15.0f, HealingManaEfficiency::VERY_HIGH); HEAL_PARTY_ACTION(CastFlashHealOnPartyAction, "flash heal", 15.0f, HealingManaEfficiency::LOW); HEAL_PARTY_ACTION(CastRenewOnPartyAction, "renew", 15.0f, HealingManaEfficiency::VERY_HIGH); -HEAL_PARTY_ACTION(CastPrayerOfMendingAction, "prayer of mending", 15.0f, HealingManaEfficiency::MEDIUM); +// HEAL_PARTY_ACTION(CastPrayerOfMendingAction, "prayer of mending", 10.0f, HealingManaEfficiency::HIGH); +class CastPrayerOfMendingAction : public HealPartyMemberAction +{ +public: + CastPrayerOfMendingAction(PlayerbotAI* botAI) : HealPartyMemberAction(botAI, "prayer of mending", 10.0f, HealingManaEfficiency::HIGH, false) {} +}; + HEAL_PARTY_ACTION(CastBindingHealAction, "binding heal", 15.0f, HealingManaEfficiency::MEDIUM); HEAL_PARTY_ACTION(CastPrayerOfHealingAction, "prayer of healing", 15.0f, HealingManaEfficiency::MEDIUM); -AOE_HEAL_ACTION(CastCircleOfHealingAction, "circle of healing", 15.0f, HealingManaEfficiency::HIGH); +// AOE_HEAL_ACTION(CastCircleOfHealingAction, "circle of healing", 15.0f, HealingManaEfficiency::HIGH); +class CastCircleOfHealingAction : public HealPartyMemberAction +{ +public: + CastCircleOfHealingAction(PlayerbotAI* ai) : HealPartyMemberAction(ai, "circle of healing", 15.0f, HealingManaEfficiency::HIGH) + { + } +}; AOE_HEAL_ACTION(CastLightwellAction, "lightwell", 15.0f, HealingManaEfficiency::MEDIUM); SPELL_ACTION(CastSmiteAction, "smite"); @@ -68,7 +81,13 @@ CURE_PARTY_ACTION(CastCureDiseaseOnPartyAction, "cure disease", DISPEL_DISEASE); CURE_ACTION(CastAbolishDiseaseAction, "abolish disease"); CURE_PARTY_ACTION(CastAbolishDiseaseOnPartyAction, "abolish disease", DISPEL_DISEASE); -DEBUFF_CHECKISOWNER_ACTION(CastHolyFireAction, "holy fire"); +// DEBUFF_CHECKISOWNER_ACTION(CastHolyFireAction, "holy fire"); +class CastHolyFireAction : public CastDebuffSpellAction +{ +public: + CastHolyFireAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "holy fire", true, 0.0f) {} +}; + // shadow 2.4.3 // BUFF_ACTION(CastShadowfiendAction, "shadowfiend"); SPELL_ACTION(CastShadowWordDeathAction, "shadow word: death"); @@ -156,10 +175,10 @@ public: virtual std::string const GetTargetName() { return "current target"; } }; -class CastPowerWordShieldOnAlmostFullHealthBelow : public HealPartyMemberAction +class CastPowerWordShieldOnAlmostFullHealthBelowAction : public HealPartyMemberAction { public: - CastPowerWordShieldOnAlmostFullHealthBelow(PlayerbotAI* ai) + CastPowerWordShieldOnAlmostFullHealthBelowAction(PlayerbotAI* ai) : HealPartyMemberAction(ai, "power word: shield", 15.0f, HealingManaEfficiency::HIGH) { } @@ -167,6 +186,17 @@ public: Unit* GetTarget() override; }; +class CastPowerWordShieldOnNotFullAction : public HealPartyMemberAction +{ +public: + CastPowerWordShieldOnNotFullAction(PlayerbotAI* ai) + : HealPartyMemberAction(ai, "power word: shield", 5.0f, HealingManaEfficiency::HIGH) + { + } + bool isUseful() override; + Unit* GetTarget() override; +}; + class CastMindSearAction : public CastSpellAction { public: diff --git a/src/strategy/priest/PriestAiObjectContext.cpp b/src/strategy/priest/PriestAiObjectContext.cpp index 0df50eec..4f057718 100644 --- a/src/strategy/priest/PriestAiObjectContext.cpp +++ b/src/strategy/priest/PriestAiObjectContext.cpp @@ -5,6 +5,7 @@ #include "PriestAiObjectContext.h" +#include "GenericPriestStrategy.h" #include "HolyPriestStrategy.h" #include "NamedObjectContext.h" #include "Playerbots.h" @@ -30,6 +31,7 @@ public: creators["boost"] = &PriestStrategyFactoryInternal::boost; creators["rshadow"] = &PriestStrategyFactoryInternal::rshadow; creators["cc"] = &PriestStrategyFactoryInternal::cc; + creators["healer dps"] = &PriestStrategyFactoryInternal::healer_dps; } private: @@ -42,6 +44,7 @@ private: static Strategy* pull(PlayerbotAI* botAI) { return new PullStrategy(botAI, "shoot"); } static Strategy* shadow_debuff(PlayerbotAI* botAI) { return new ShadowPriestDebuffStrategy(botAI); } static Strategy* cure(PlayerbotAI* botAI) { return new PriestCureStrategy(botAI); } + static Strategy* healer_dps(PlayerbotAI* botAI) { return new PriestHealerDpsStrategy(botAI); } }; class PriestCombatStrategyFactoryInternal : public NamedObjectContext @@ -172,6 +175,8 @@ public: creators["power word: shield on party"] = &PriestAiObjectContextInternal::power_word_shield_on_party; creators["power word: shield on almost full health below"] = &PriestAiObjectContextInternal::power_word_shield_on_almost_full_health_below; + creators["power word: shield on not full"] = + &PriestAiObjectContextInternal::power_word_shield_on_not_full; creators["renew"] = &PriestAiObjectContextInternal::renew; creators["renew on party"] = &PriestAiObjectContextInternal::renew_on_party; creators["greater heal"] = &PriestAiObjectContextInternal::greater_heal; @@ -192,7 +197,7 @@ public: creators["fade"] = &PriestAiObjectContextInternal::fade; creators["inner fire"] = &PriestAiObjectContextInternal::inner_fire; creators["resurrection"] = &PriestAiObjectContextInternal::resurrection; - creators["circle of healing"] = &PriestAiObjectContextInternal::circle_of_healing; + creators["circle of healing on party"] = &PriestAiObjectContextInternal::circle_of_healing; creators["psychic scream"] = &PriestAiObjectContextInternal::psychic_scream; creators["vampiric touch"] = &PriestAiObjectContextInternal::vampiric_touch; creators["vampiric touch on attacker"] = &PriestAiObjectContextInternal::vampiric_touch_on_attacker; @@ -282,7 +287,11 @@ private: } static Action* power_word_shield_on_almost_full_health_below(PlayerbotAI* ai) { - return new CastPowerWordShieldOnAlmostFullHealthBelow(ai); + return new CastPowerWordShieldOnAlmostFullHealthBelowAction(ai); + } + static Action* power_word_shield_on_not_full(PlayerbotAI* ai) + { + return new CastPowerWordShieldOnNotFullAction(ai); } static Action* renew(PlayerbotAI* botAI) { return new CastRenewAction(botAI); } static Action* renew_on_party(PlayerbotAI* botAI) { return new CastRenewOnPartyAction(botAI); } diff --git a/src/strategy/priest/PriestNonCombatStrategy.cpp b/src/strategy/priest/PriestNonCombatStrategy.cpp index 161ac361..be596054 100644 --- a/src/strategy/priest/PriestNonCombatStrategy.cpp +++ b/src/strategy/priest/PriestNonCombatStrategy.cpp @@ -55,7 +55,7 @@ void PriestNonCombatStrategy::InitTriggers(std::vector& triggers) NextAction::array(0, new NextAction("renew on party", ACTION_LIGHT_HEAL + 3), NULL))); triggers.push_back( - new TriggerNode("medium aoe heal", NextAction::array(0, new NextAction("circle of healing", 27.0f), NULL))); + new TriggerNode("group heal setting", NextAction::array(0, new NextAction("circle of healing on party", 27.0f), NULL))); } void PriestBuffStrategy::InitTriggers(std::vector& triggers) diff --git a/src/strategy/priest/PriestNonCombatStrategyActionNodeFactory.h b/src/strategy/priest/PriestNonCombatStrategyActionNodeFactory.h index 84318fcb..c1084610 100644 --- a/src/strategy/priest/PriestNonCombatStrategyActionNodeFactory.h +++ b/src/strategy/priest/PriestNonCombatStrategyActionNodeFactory.h @@ -29,7 +29,7 @@ public: creators["lesser heal on party"] = &lesser_heal_on_party; creators["flash heal"] = &flash_heal; creators["flash heal on party"] = &flash_heal_on_party; - creators["circle of healing"] = &circle_of_healing; + creators["circle of healing on party"] = &circle_of_healing; creators["prayer of fortitude on party"] = &prayer_of_fortitude_on_party; creators["prayer of spirit on party"] = &prayer_of_spirit_on_party; } @@ -128,7 +128,7 @@ private: } static ActionNode* circle_of_healing(PlayerbotAI* ai) { - return new ActionNode("circle of healing", + return new ActionNode("circle of healing on party", /*P*/ NextAction::array(0, new NextAction("remove shadowform"), NULL), // /*A*/ NextAction::array(0, new NextAction("flash heal on party"), NULL), /*A*/ NULL, diff --git a/src/strategy/shaman/CasterShamanStrategy.cpp b/src/strategy/shaman/CasterShamanStrategy.cpp index 746abf86..c11c523d 100644 --- a/src/strategy/shaman/CasterShamanStrategy.cpp +++ b/src/strategy/shaman/CasterShamanStrategy.cpp @@ -51,8 +51,8 @@ void CasterShamanStrategy::InitTriggers(std::vector& triggers) // triggers.push_back(new TriggerNode("enemy out of spell", NextAction::array(0, new NextAction("reach spell", // ACTION_NORMAL + 9), nullptr))); triggers.push_back(new TriggerNode("shaman weapon", NextAction::array(0, new // NextAction("flametongue weapon", 23.0f), nullptr))); - // triggers.push_back(new TriggerNode( - // "enough mana", NextAction::array(0, new NextAction("chain lightning", ACTION_DEFAULT + 0.1f), nullptr))); + triggers.push_back(new TriggerNode( + "enough mana", NextAction::array(0, new NextAction("chain lightning", ACTION_DEFAULT + 0.1f), nullptr))); triggers.push_back(new TriggerNode("main hand weapon no imbue", NextAction::array(0, new NextAction("flametongue weapon", 22.0f), nullptr))); diff --git a/src/strategy/shaman/GenericShamanStrategy.cpp b/src/strategy/shaman/GenericShamanStrategy.cpp index 7110036d..95ed14f2 100644 --- a/src/strategy/shaman/GenericShamanStrategy.cpp +++ b/src/strategy/shaman/GenericShamanStrategy.cpp @@ -3,6 +3,7 @@ * and/or modify it under version 2 of the License, or (at your option), any later version. */ +#include "GenericShamanStrategy.h" #include "HealShamanStrategy.h" #include "Playerbots.h" #include "Strategy.h" @@ -17,7 +18,7 @@ public: creators["windfury weapon"] = &windfury_weapon; creators["lesser healing wave"] = &lesser_healing_wave; creators["lesser healing wave on party"] = &lesser_healing_wave_on_party; - creators["chain heal"] = &chain_heal; + creators["chain heal on party"] = &chain_heal; creators["riptide"] = &riptide; creators["riptide on party"] = &riptide_on_party; creators["earth shock"] = &earth_shock; @@ -74,9 +75,9 @@ private: static ActionNode* chain_heal([[maybe_unused]] PlayerbotAI* botAI) { - return new ActionNode("chain heal", + return new ActionNode("chain heal on party", /*P*/ nullptr, - /*A*/ NextAction::array(0, new NextAction("lesser healing wave"), nullptr), + /*A*/ NextAction::array(0, new NextAction("lesser healing wave on party"), nullptr), /*C*/ nullptr); } @@ -116,7 +117,7 @@ void GenericShamanStrategy::InitTriggers(std::vector& triggers) // triggers.push_back(new TriggerNode("party member medium health", NextAction::array(0, new NextAction("lesser // healing wave on party", 25.0f), nullptr))); triggers.push_back(new TriggerNode("party member low health", // NextAction::array(0, new NextAction("riptide on party", 25.0f), nullptr))); triggers.push_back(new - // TriggerNode("medium aoe heal", NextAction::array(0, new NextAction("chain heal", 27.0f), nullptr))); + // TriggerNode("medium aoe heal", NextAction::array(0, new NextAction("chain heal on party", 27.0f), nullptr))); // triggers.push_back(new TriggerNode("medium health", NextAction::array(0, new NextAction("lesser healing // wave", 26.0f), nullptr))); triggers.push_back(new TriggerNode("low health", NextAction::array(0, new // NextAction("riptide", 26.0f), nullptr))); @@ -162,3 +163,18 @@ void ShamanCureStrategy::InitTriggers(std::vector& triggers) new TriggerNode("party member cleanse spirit curse", NextAction::array(0, new NextAction("cleanse spirit curse on party", 23.0f), nullptr))); } + +void ShamanHealerDpsStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back( + new TriggerNode("healer should attack", + NextAction::array(0, + new NextAction("flame shock", ACTION_DEFAULT + 0.2f), + new NextAction("lava burst", ACTION_DEFAULT + 0.1f), + new NextAction("lightning bolt", ACTION_DEFAULT), nullptr))); + + triggers.push_back( + new TriggerNode("medium aoe and healer should attack", + NextAction::array(0, + new NextAction("chain lightning", ACTION_DEFAULT + 0.3f), nullptr))); +} \ No newline at end of file diff --git a/src/strategy/shaman/GenericShamanStrategy.h b/src/strategy/shaman/GenericShamanStrategy.h index 49753e7b..5fa0ff83 100644 --- a/src/strategy/shaman/GenericShamanStrategy.h +++ b/src/strategy/shaman/GenericShamanStrategy.h @@ -45,4 +45,13 @@ public: std::string const getName() override { return "cure"; } }; +class ShamanHealerDpsStrategy : public Strategy +{ +public: + ShamanHealerDpsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "healer dps"; } +}; + #endif diff --git a/src/strategy/shaman/HealShamanStrategy.cpp b/src/strategy/shaman/HealShamanStrategy.cpp index ff305559..17d4ff44 100644 --- a/src/strategy/shaman/HealShamanStrategy.cpp +++ b/src/strategy/shaman/HealShamanStrategy.cpp @@ -49,8 +49,8 @@ void HealShamanStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("main hand weapon no imbue", NextAction::array(0, new NextAction("earthliving weapon", 22.0f), nullptr))); triggers.push_back(new TriggerNode( - "group heal occasion", - NextAction::array(0, new NextAction("riptide on party", 23.0f), new NextAction("chain heal", 22.0f), NULL))); + "group heal setting", + NextAction::array(0, new NextAction("riptide on party", 23.0f), new NextAction("chain heal on party", 22.0f), NULL))); triggers.push_back(new TriggerNode( "party member critical health", diff --git a/src/strategy/shaman/ShamanActions.h b/src/strategy/shaman/ShamanActions.h index c4f6ce38..2d110e51 100644 --- a/src/strategy/shaman/ShamanActions.h +++ b/src/strategy/shaman/ShamanActions.h @@ -42,11 +42,11 @@ public: } }; -class CastChainHealAction : public CastAoeHealSpellAction +class CastChainHealAction : public HealPartyMemberAction { public: CastChainHealAction(PlayerbotAI* botAI) - : CastAoeHealSpellAction(botAI, "chain heal", 15.0f, HealingManaEfficiency::HIGH) + : HealPartyMemberAction(botAI, "chain heal", 15.0f, HealingManaEfficiency::HIGH) { } }; @@ -335,7 +335,7 @@ public: class CastFlameShockAction : public CastDebuffSpellAction { public: - CastFlameShockAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "flame shock", true) {} + CastFlameShockAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "flame shock", true, 6.0f) {} }; class CastEarthShockAction : public CastSpellAction diff --git a/src/strategy/shaman/ShamanAiObjectContext.cpp b/src/strategy/shaman/ShamanAiObjectContext.cpp index e67f4230..9e79f792 100644 --- a/src/strategy/shaman/ShamanAiObjectContext.cpp +++ b/src/strategy/shaman/ShamanAiObjectContext.cpp @@ -6,6 +6,7 @@ #include "ShamanAiObjectContext.h" #include "CasterShamanStrategy.h" +#include "GenericShamanStrategy.h" #include "HealShamanStrategy.h" #include "MeleeShamanStrategy.h" #include "NamedObjectContext.h" @@ -25,6 +26,7 @@ public: creators["melee aoe"] = &ShamanStrategyFactoryInternal::melee_aoe; creators["caster aoe"] = &ShamanStrategyFactoryInternal::caster_aoe; creators["cure"] = &ShamanStrategyFactoryInternal::cure; + creators["healer dps"] = &ShamanStrategyFactoryInternal::healer_dps; } private: @@ -33,6 +35,7 @@ private: static Strategy* melee_aoe(PlayerbotAI* botAI) { return new MeleeAoeShamanStrategy(botAI); } static Strategy* caster_aoe(PlayerbotAI* botAI) { return new CasterAoeShamanStrategy(botAI); } static Strategy* cure(PlayerbotAI* botAI) { return new ShamanCureStrategy(botAI); } + static Strategy* healer_dps(PlayerbotAI* botAI) { return new ShamanHealerDpsStrategy(botAI); } }; class ShamanBuffStrategyFactoryInternal : public NamedObjectContext @@ -207,7 +210,7 @@ public: creators["lesser healing wave on party"] = &ShamanAiObjectContextInternal::lesser_healing_wave_on_party; creators["earth shield"] = &ShamanAiObjectContextInternal::earth_shield; creators["earth shield on party"] = &ShamanAiObjectContextInternal::earth_shield_on_party; - creators["chain heal"] = &ShamanAiObjectContextInternal::chain_heal; + creators["chain heal on party"] = &ShamanAiObjectContextInternal::chain_heal; creators["riptide"] = &ShamanAiObjectContextInternal::riptide; creators["riptide on party"] = &ShamanAiObjectContextInternal::riptide_on_party; creators["stormstrike"] = &ShamanAiObjectContextInternal::stormstrike; diff --git a/src/strategy/shaman/ShamanNonCombatStrategy.cpp b/src/strategy/shaman/ShamanNonCombatStrategy.cpp index 3d544419..ad4dcb82 100644 --- a/src/strategy/shaman/ShamanNonCombatStrategy.cpp +++ b/src/strategy/shaman/ShamanNonCombatStrategy.cpp @@ -39,7 +39,7 @@ void ShamanNonCombatStrategy::InitTriggers(std::vector& triggers) new NextAction("lesser healing wave on party", 24.0f), NULL))); triggers.push_back( - new TriggerNode("medium aoe heal", NextAction::array(0, new NextAction("chain heal", 27.0f), NULL))); + new TriggerNode("group heal setting", NextAction::array(0, new NextAction("chain heal on party", 27.0f), NULL))); triggers.push_back( new TriggerNode("cure poison", NextAction::array(0, new NextAction("cure poison", 21.0f), nullptr))); diff --git a/src/strategy/triggers/GenericTriggers.cpp b/src/strategy/triggers/GenericTriggers.cpp index f700e472..26572394 100644 --- a/src/strategy/triggers/GenericTriggers.cpp +++ b/src/strategy/triggers/GenericTriggers.cpp @@ -9,6 +9,8 @@ #include "BattlegroundWS.h" #include "CreatureAI.h" +#include "GameTime.h" +#include "LastSpellCastValue.h" #include "ObjectGuid.h" #include "PlayerbotAIConfig.h" #include "Playerbots.h" @@ -76,7 +78,7 @@ bool AlmostFullManaTrigger::IsActive() bool EnoughManaTrigger::IsActive() { - return AI_VALUE2(bool, "has mana", "self target") && AI_VALUE2(uint8, "mana", "self target") > 65; + return AI_VALUE2(bool, "has mana", "self target") && AI_VALUE2(uint8, "mana", "self target") > sPlayerbotAIConfig->highMana; } bool RageAvailable::IsActive() { return AI_VALUE2(uint8, "rage", "self target") >= amount; } @@ -366,6 +368,39 @@ bool GenericBoostTrigger::IsActive() return AI_VALUE(uint8, "balance") <= balance; } +bool HealerShouldAttackTrigger::IsActive() +{ + // nobody can help me + if (botAI->GetNearGroupMemberCount(sPlayerbotAIConfig->sightDistance) <= 1) + return true; + + if (AI_VALUE2(uint8, "health", "party member to heal") < sPlayerbotAIConfig->almostFullHealth) + return false; + + // special check for resto druid (dont remove tree of life frequently) + if (bot->GetAura(33891)) + { + LastSpellCast& lastSpell = botAI->GetAiObjectContext()->GetValue("last spell cast")->Get(); + if (lastSpell.timer + 5 > time(nullptr)) + return false; + } + + int manaThreshold; + int balance = AI_VALUE(uint8, "balance"); + // higher threshold in higher pressure + if (balance <= 50) + manaThreshold = 85; + else if (balance <= 100) + manaThreshold = sPlayerbotAIConfig->highMana; + else + manaThreshold = sPlayerbotAIConfig->mediumMana; + + if (AI_VALUE2(bool, "has mana", "self target") && AI_VALUE2(uint8, "mana", "self target") < manaThreshold) + return false; + + return true; +} + bool ItemCountTrigger::IsActive() { return AI_VALUE2(uint32, "item count", item) < count; } bool InterruptSpellTrigger::IsActive() diff --git a/src/strategy/triggers/GenericTriggers.h b/src/strategy/triggers/GenericTriggers.h index e9990c3e..f93cb156 100644 --- a/src/strategy/triggers/GenericTriggers.h +++ b/src/strategy/triggers/GenericTriggers.h @@ -459,6 +459,16 @@ protected: float balance; }; +class HealerShouldAttackTrigger : public Trigger +{ +public: + HealerShouldAttackTrigger(PlayerbotAI* botAI) + : Trigger(botAI, "healer should attack", 1) + { + } + + bool IsActive() override; +}; class RandomTrigger : public Trigger { diff --git a/src/strategy/triggers/HealthTriggers.cpp b/src/strategy/triggers/HealthTriggers.cpp index d4c971ac..3bc8d4f3 100644 --- a/src/strategy/triggers/HealthTriggers.cpp +++ b/src/strategy/triggers/HealthTriggers.cpp @@ -24,7 +24,18 @@ bool AoeHealTrigger::IsActive() { return AI_VALUE2(uint8, "aoe heal", type) >= c bool AoeInGroupTrigger::IsActive() { - Group* group = bot->GetGroup(); - return group && group->GetMembersCount() >= 5 && - AI_VALUE2(uint8, "aoe heal", type) >= (group->GetMembersCount() * ratio); + int32 member = botAI->GetNearGroupMemberCount(); + if (member < 5) + return false; + int threshold = member * 0.5; + if (member <= 5) + threshold = 3; + else if (member <= 10) + threshold = std::min(threshold, 5); + else if (member <= 25) + threshold = std::min(threshold, 10); + else + threshold = std::min(threshold, 15); + + return AI_VALUE2(uint8, "aoe heal", type) >= threshold; } \ No newline at end of file diff --git a/src/strategy/triggers/HealthTriggers.h b/src/strategy/triggers/HealthTriggers.h index d9f3253c..279bc3fe 100644 --- a/src/strategy/triggers/HealthTriggers.h +++ b/src/strategy/triggers/HealthTriggers.h @@ -186,14 +186,13 @@ protected: class AoeInGroupTrigger : public Trigger { public: - AoeInGroupTrigger(PlayerbotAI* ai, std::string name, std::string type, float ratio) - : Trigger(ai, name), ratio(ratio), type(type) + AoeInGroupTrigger(PlayerbotAI* ai, std::string name, std::string type) + : Trigger(ai, name), type(type) { } bool IsActive() override; protected: - float ratio; std::string type; }; diff --git a/src/strategy/triggers/TriggerContext.h b/src/strategy/triggers/TriggerContext.h index 860ee682..cbfeec6f 100644 --- a/src/strategy/triggers/TriggerContext.h +++ b/src/strategy/triggers/TriggerContext.h @@ -48,6 +48,7 @@ public: creators["almost full mana"] = &TriggerContext::AlmostFullMana; creators["enough mana"] = &TriggerContext::EnoughMana; + creators["party member critical health"] = &TriggerContext::PartyMemberCriticalHealth; creators["party member low health"] = &TriggerContext::PartyMemberLowHealth; creators["party member medium health"] = &TriggerContext::PartyMemberMediumHealth; @@ -83,6 +84,9 @@ public: creators["medium aoe"] = &TriggerContext::MediumAoe; creators["high aoe"] = &TriggerContext::HighAoe; + creators["healer should attack"] = &TriggerContext::healer_should_attack; + creators["medium aoe and healer should attack"] = &TriggerContext::medium_aoe_and_healer_should_attack; + creators["has area debuff"] = &TriggerContext::HasAreaDebuff; creators["enemy out of melee"] = &TriggerContext::EnemyOutOfMelee; @@ -135,8 +139,8 @@ public: creators["medium aoe heal"] = &TriggerContext::medium_aoe_heal; creators["almost full aoe heal"] = &TriggerContext::almost_full_aoe_heal; - creators["group heal occasion"] = &TriggerContext::group_heal_occasion; - creators["medium group heal occasion"] = &TriggerContext::medium_group_heal_occasion; + creators["group heal setting"] = &TriggerContext::group_heal_occasion; + creators["medium group heal setting"] = &TriggerContext::medium_group_heal_occasion; creators["invalid target"] = &TriggerContext::invalid_target; creators["lfg proposal active"] = &TriggerContext::lfg_proposal_active; @@ -242,11 +246,11 @@ private: } static Trigger* group_heal_occasion(PlayerbotAI* ai) { - return new AoeInGroupTrigger(ai, "group heal occasion", "almost full", 0.6); + return new AoeInGroupTrigger(ai, "group heal setting", "almost full"); } static Trigger* medium_group_heal_occasion(PlayerbotAI* ai) { - return new AoeInGroupTrigger(ai, "group heal occasion", "medium", 0.6); + return new AoeInGroupTrigger(ai, "medium group heal setting", "medium"); } static Trigger* target_changed(PlayerbotAI* botAI) { return new TargetChangedTrigger(botAI); } static Trigger* swimming(PlayerbotAI* botAI) { return new IsSwimmingTrigger(botAI); } @@ -265,6 +269,8 @@ private: static Trigger* LightAoe(PlayerbotAI* botAI) { return new LightAoeTrigger(botAI); } static Trigger* MediumAoe(PlayerbotAI* botAI) { return new MediumAoeTrigger(botAI); } static Trigger* HighAoe(PlayerbotAI* botAI) { return new HighAoeTrigger(botAI); } + static Trigger* healer_should_attack(PlayerbotAI* botAI) { return new HealerShouldAttackTrigger(botAI); } + static Trigger* medium_aoe_and_healer_should_attack(PlayerbotAI* botAI) { return new TwoTriggers(botAI, "medium aoe", "healer should attack"); } static Trigger* HasAreaDebuff(PlayerbotAI* botAI) { return new HasAreaDebuffTrigger(botAI); } static Trigger* LoseAggro(PlayerbotAI* botAI) { return new LoseAggroTrigger(botAI); } static Trigger* HasAggro(PlayerbotAI* botAI) { return new HasAggroTrigger(botAI); } diff --git a/src/strategy/values/AttackerCountValues.cpp b/src/strategy/values/AttackerCountValues.cpp index 59de8995..17ba4562 100644 --- a/src/strategy/values/AttackerCountValues.cpp +++ b/src/strategy/values/AttackerCountValues.cpp @@ -94,7 +94,7 @@ uint8 BalancePercentValue::Calculate() level *= 3; break; case CREATURE_ELITE_WORLDBOSS: - level *= 30; + level *= 20; break; } diff --git a/src/strategy/warlock/GenericWarlockNonCombatStrategy.cpp b/src/strategy/warlock/GenericWarlockNonCombatStrategy.cpp index 7ed900cf..a46346fe 100644 --- a/src/strategy/warlock/GenericWarlockNonCombatStrategy.cpp +++ b/src/strategy/warlock/GenericWarlockNonCombatStrategy.cpp @@ -55,7 +55,7 @@ private: { return new ActionNode("summon felhunter", /*P*/ nullptr, - /*A*/ NextAction::array(0, new NextAction("summon voidwalker"), nullptr), + /*A*/ NextAction::array(0, new NextAction("summon succubus"), nullptr), /*C*/ nullptr); } static ActionNode* summon_felguard([[maybe_unused]] PlayerbotAI* botAI)