diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 88920a050..5ce5485d0 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -581,10 +581,10 @@ AiPlayerbot.AutoGearScoreLimit = 0 # Default: food, taxi, and raid are enabled AiPlayerbot.BotCheats = "food,taxi,raid" -# List of attunement quests (comma-separated list of quest IDs) that are automatically completed for all bots. +# List of attunement quests (comma-separated list of quest IDs) that are automatically completed for all bots. # While mod-playerbots does not restore removed attunement requirements, other mods, such as mod-individual-progression, may do so. # This is meant to exclude bots from such requirements. -# +# # Default: # Caverns of Time - Part 1 # - 10279, To The Master's Lair @@ -651,9 +651,14 @@ AiPlayerbot.BotTaxiGapJitterMs = 100 #################################################################################################### # PROFESSIONS -# Note: Random bots currently do not get professions # +# Percentage of randombots in each class bucket that receive a class-matching +# weighted profession combination. The remaining randombots use the weighted +# random sane-pair profession pool. +# Default: 30 +AiPlayerbot.ClassMatchingProfessionChance = 30 + # Automatically adds the 'master fishing' strategy to bots that have the fishing skill when the bots master fishes. # Default: 1 (Enabled) AiPlayerbot.EnableFishingWithMaster = 1 @@ -1319,7 +1324,7 @@ AiPlayerbot.DeleteRandomBotArenaTeams = 0 AiPlayerbot.PvpProhibitedZoneIds = "2255,656,2361,2362,2363,976,35,2268,3425,392,541,1446,3828,3712,3738,3565,3539,3623,4152,3988,4658,4284,4418,4436,4275,4323,4395,3703,4298,3951" # PvP Restricted Areas (bots don't pvp) -AiPlayerbot.PvpProhibitedAreaIds = "976,35,392,2268,4161,4010,4317,4312,3649,3887,3958,3724,4080,3938,3754,3786,3973" +AiPlayerbot.PvpProhibitedAreaIds = "976,35,392,2268,4161,4010,4317,4312,3649,3887,3958,3724,4080,3938,3754,3786,3973,4085,4086,4087,4088" # Improve reaction speeds in battlegrounds and arenas (may cause lag) AiPlayerbot.FastReactInBG = 1 @@ -1797,10 +1802,10 @@ AiPlayerbot.PremadeSpecLink.11.6.80 = 05320021--230033312031500531353013251 # Requires sending the command "nc +worldbuff" in chat to a bot (or a group of bots) to enable # Each entry in the matrix should be formatted as follows: Entry:FactionID,ClassID,SpecID,MinimumLevel,MaximumLevel:SpellID1,SpellID2,etc.; # FactionID may be set to 0 for the entry to apply buffs to bots of either faction -# The default entries create a cross-faction list of level 80 buffs for each implemented pve spec from the "Premade Specs" section +# The default entries create a cross-faction level 60-69 Vanilla buffs, level 70-79 TBC buffs, and level 80 buffs for each implemented pve spec from the "Premade Specs" section # The default entries may be deleted or modified, and new custom entries may be added -AiPlayerbot.WorldBuffMatrix = # WARRIOR ARMS 1:0,1,0,80,80:53760,57358; # WARRIOR FURY 2:0,1,1,80,80:53760,57358; # WARRIOR PROTECTION 3:0,1,2,80,80:53758,57356; # PALADIN HOLY 4:0,2,0,80,80:53749,57332,60347; # PALADIN PROTECTION 5:0,2,1,80,80:53758,57356; # PALADIN RETRIBUTION 6:0,2,2,80,80:53760,57371; # HUNTER BEAST 7:0,3,0,80,80:53760,57325; # HUNTER MARKSMANSHIP 8:0,3,1,80,80:53760,57358; # HUNTER SURVIVAL 9:0,3,2,80,80:53760,57367; # ROGUE ASSASSINATION 10:0,4,0,80,80:53760,57325; # ROGUE COMBAT 11:0,4,1,80,80:53760,57358; # ROGUE SUBTLETY 12:0,4,2,80,80:53760,57367; # PRIEST DISCIPLINE 13:0,5,0,80,80:53755,57327; # PRIEST HOLY 14:0,5,1,80,80:53755,57327; # PRIEST SHADOW 15:0,5,2,80,80:53755,57327; # DEATH KNIGHT BLOOD 16:0,6,0,80,80:53758,57356; # DEATH KNIGHT FROST 17:0,6,1,80,80:53760,57358; # DEATH KNIGHT UNHOLY 18:0,6,2,80,80:53760,57358; # DEATH KNIGHT BLOOD DPS 19:0,6,3,80,80:53760,57371; # SHAMAN ELEMENTAL 20:0,7,0,80,80:53755,57327; # SHAMAN ENHANCEMENT 21:0,7,1,80,80:53760,57325; # SHAMAN RESTORATION 22:0,7,2,80,80:53755,57327; # MAGE ARCANE 23:0,8,0,80,80:53755,57327; # MAGE FIRE 24:0,8,1,80,80:53755,57327; # MAGE FROST 25:0,8,2,80,80:53755,57327; # WARLOCK AFFLICTION 26:0,9,0,80,80:53755,57327; # WARLOCK DEMONOLOGY 27:0,9,1,80,80:53755,57327; # WARLOCK DESTRUCTION 28:0,9,2,80,80:53755,57327; # DRUID BALANCE 29:0,11,0,80,80:53755,57327; # DRUID FERAL BEAR 30:0,11,1,80,80:53749,53763,57367; # DRUID RESTORATION 31:0,11,2,80,80:54212,57334; # DRUID FERAL CAT 32:0,11,3,80,80:53760,57358 +AiPlayerbot.WorldBuffMatrix = # WARRIOR ARMS 1:0,1,0,80,80:53760,57358; # WARRIOR FURY 2:0,1,1,80,80:53760,57358; # WARRIOR PROTECTION 3:0,1,2,80,80:53758,57356; # PALADIN HOLY 4:0,2,0,80,80:53749,57332,60347; # PALADIN PROTECTION 5:0,2,1,80,80:53758,57356; # PALADIN RETRIBUTION 6:0,2,2,80,80:53760,57371; # HUNTER BEAST 7:0,3,0,80,80:53760,57325; # HUNTER MARKSMANSHIP 8:0,3,1,80,80:53760,57358; # HUNTER SURVIVAL 9:0,3,2,80,80:53760,57367; # ROGUE ASSASSINATION 10:0,4,0,80,80:53760,57325; # ROGUE COMBAT 11:0,4,1,80,80:53760,57358; # ROGUE SUBTLETY 12:0,4,2,80,80:53760,57367; # PRIEST DISCIPLINE 13:0,5,0,80,80:53755,57327; # PRIEST HOLY 14:0,5,1,80,80:53755,57327; # PRIEST SHADOW 15:0,5,2,80,80:53755,57327; # DEATH KNIGHT BLOOD 16:0,6,0,80,80:53758,57356; # DEATH KNIGHT FROST 17:0,6,1,80,80:53760,57358; # DEATH KNIGHT UNHOLY 18:0,6,2,80,80:53760,57358; # DEATH KNIGHT BLOOD DPS 19:0,6,3,80,80:53760,57371; # SHAMAN ELEMENTAL 20:0,7,0,80,80:53755,57327; # SHAMAN ENHANCEMENT 21:0,7,1,80,80:53760,57325; # SHAMAN RESTORATION 22:0,7,2,80,80:53755,57327; # MAGE ARCANE 23:0,8,0,80,80:53755,57327; # MAGE FIRE 24:0,8,1,80,80:53755,57327; # MAGE FROST 25:0,8,2,80,80:53755,57327; # WARLOCK AFFLICTION 26:0,9,0,80,80:53755,57327; # WARLOCK DEMONOLOGY 27:0,9,1,80,80:53755,57327; # WARLOCK DESTRUCTION 28:0,9,2,80,80:53755,57327; # DRUID BALANCE 29:0,11,0,80,80:53755,57327; # DRUID FERAL BEAR 30:0,11,1,80,80:53749,53763,57367; # DRUID RESTORATION 31:0,11,2,80,80:54212,57334; # DRUID FERAL CAT 32:0,11,3,80,80:53760,57358; # WARRIOR ARMS TBC 33:0,1,0,70,79:28520,33256; # WARRIOR FURY TBC 34:0,1,1,70,79:28520,33256; # WARRIOR PROTECTION TBC 35:0,1,2,70,79:28518,33257; # PALADIN HOLY TBC 36:0,2,0,70,79:28491,39627,33263; # PALADIN PROTECTION TBC 37:0,2,1,70,79:28518,33257; # PALADIN RETRIBUTION TBC 38:0,2,2,70,79:28520,33256; # HUNTER BEAST TBC 39:0,3,0,70,79:28520,33261; # HUNTER MARKSMANSHIP TBC 40:0,3,1,70,79:28520,33261; # HUNTER SURVIVAL TBC 41:0,3,2,70,79:28520,33261; # ROGUE ASSASSINATION TBC 42:0,4,0,70,79:28520,33261; # ROGUE COMBAT TBC 43:0,4,1,70,79:28520,33261; # ROGUE SUBTLETY TBC 44:0,4,2,70,79:28520,33261; # PRIEST DISCIPLINE TBC 45:0,5,0,70,79:28491,39627,33263; # PRIEST HOLY TBC 46:0,5,1,70,79:28491,39627,33263; # PRIEST SHADOW TBC 47:0,5,2,70,79:28540,33263; # SHAMAN ELEMENTAL TBC 48:0,7,0,70,79:28521,33263; # SHAMAN ENHANCEMENT TBC 49:0,7,1,70,79:28520,33261; # SHAMAN RESTORATION TBC 50:0,7,2,70,79:28491,39627,33263; # MAGE ARCANE TBC 51:0,8,0,70,79:28521,33263; # MAGE FIRE TBC 52:0,8,1,70,79:28540,33263; # MAGE FROST TBC 53:0,8,2,70,79:28540,33263; # WARLOCK AFFLICTION TBC 54:0,9,0,70,79:28540,33263; # WARLOCK DEMONOLOGY TBC 55:0,9,1,70,79:28540,33263; # WARLOCK DESTRUCTION TBC 56:0,9,2,70,79:28540,33263; # DRUID BALANCE TBC 57:0,11,0,70,79:28521,33263; # DRUID FERAL BEAR TBC 58:0,11,1,70,79:28518,33257; # DRUID RESTORATION TBC 59:0,11,2,70,79:28491,39627,33263; # DRUID FERAL CAT TBC 60:0,11,3,70,79:28520,33261; # WARRIOR ARMS VANILLA 61:0,1,0,60,69:17538,24799; # WARRIOR FURY VANILLA 62:0,1,1,60,69:17538,24799; # WARRIOR PROTECTION VANILLA 63:0,1,2,60,69:17626,25661; # PALADIN HOLY VANILLA 64:0,2,0,60,69:17627,18194; # PALADIN PROTECTION VANILLA 65:0,2,1,60,69:17626,25661; # PALADIN RETRIBUTION VANILLA 66:0,2,2,60,69:17628,24799; # HUNTER BEAST VANILLA 67:0,3,0,60,69:17538,18192; # HUNTER MARKSMANSHIP VANILLA 68:0,3,1,60,69:17538,18192; # HUNTER SURVIVAL VANILLA 69:0,3,2,60,69:17538,18192; # ROGUE ASSASSINATION VANILLA 70:0,4,0,60,69:17538,18192; # ROGUE COMBAT VANILLA 71:0,4,1,60,69:17538,18192; # ROGUE SUBTLETY VANILLA 72:0,4,2,60,69:17538,18192; # PRIEST DISCIPLINE VANILLA 73:0,5,0,60,69:17628,18194; # PRIEST HOLY VANILLA 74:0,5,1,60,69:17627,18194; # PRIEST SHADOW VANILLA 75:0,5,2,60,69:17628,18194; # SHAMAN ELEMENTAL VANILLA 76:0,7,0,60,69:17628,18194; # SHAMAN ENHANCEMENT VANILLA 77:0,7,1,60,69:17538,24799; # SHAMAN RESTORATION VANILLA 78:0,7,2,60,69:17627,18194; # MAGE ARCANE VANILLA 79:0,8,0,60,69:17628,18194; # MAGE FIRE VANILLA 80:0,8,1,60,69:17628,18194; # MAGE FROST VANILLA 81:0,8,2,60,69:17628,18194; # WARLOCK AFFLICTION VANILLA 82:0,9,0,60,69:17628,25661; # WARLOCK DEMONOLOGY VANILLA 83:0,9,1,60,69:17628,25661; # WARLOCK DESTRUCTION VANILLA 84:0,9,2,60,69:17628,25661; # DRUID BALANCE VANILLA 85:0,11,0,60,69:17628,18194; # DRUID FERAL BEAR VANILLA 86:0,11,1,60,69:17626,25661; # DRUID RESTORATION VANILLA 87:0,11,2,60,69:17627,18194; # DRUID FERAL CAT VANILLA 88:0,11,3,60,69:17538,24799 # # diff --git a/data/sql/playerbots/updates/2026_04_12_00_ai_playerbot_pull_texts.sql b/data/sql/playerbots/updates/2026_04_12_00_ai_playerbot_pull_texts.sql new file mode 100644 index 000000000..4e9c17583 --- /dev/null +++ b/data/sql/playerbots/updates/2026_04_12_00_ai_playerbot_pull_texts.sql @@ -0,0 +1,102 @@ +-- ######################################################### +-- Playerbots - Add pull command texts +-- Localized for all WotLK locales (koKR, frFR, deDE, zhCN, +-- zhTW, esES, esMX, ruRU) +-- ######################################################### + +DELETE FROM ai_playerbot_texts WHERE name IN ( + 'pull_no_target_error', + 'pull_target_too_far_error', + 'pull_invalid_target_error', + 'pull_action_unavailable_error' +); +DELETE FROM ai_playerbot_texts_chance WHERE name IN ( + 'pull_no_target_error', + 'pull_target_too_far_error', + 'pull_invalid_target_error', + 'pull_action_unavailable_error' +); + +-- pull_no_target_error +INSERT INTO `ai_playerbot_texts` + (`id`, `name`, `text`, `say_type`, `reply_type`, + `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, + `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) +VALUES ( + 1755, + 'pull_no_target_error', + 'You have no target', + 0, 0, + '대상이 없습니다', + 'Vous n''avez pas de cible', + 'Du hast kein Ziel', + '你没有目标', + '你沒有目標', + 'No tienes objetivo', + 'No tienes objetivo', + 'У вас нет цели'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('pull_no_target_error', 100); + +-- pull_target_too_far_error +INSERT INTO `ai_playerbot_texts` + (`id`, `name`, `text`, `say_type`, `reply_type`, + `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, + `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) +VALUES ( + 1756, + 'pull_target_too_far_error', + 'The target is too far away', + 0, 0, + '대상이 너무 멀리 있습니다', + 'La cible est trop loin', + 'Das Ziel ist zu weit entfernt', + '目标太远了', + '目標太遠了', + 'El objetivo está demasiado lejos', + 'El objetivo está demasiado lejos', + 'Цель слишком далеко'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('pull_target_too_far_error', 100); + +-- pull_invalid_target_error +INSERT INTO `ai_playerbot_texts` + (`id`, `name`, `text`, `say_type`, `reply_type`, + `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, + `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) +VALUES ( + 1757, + 'pull_invalid_target_error', + 'The target can''t be pulled', + 0, 0, + '해당 대상은 풀링할 수 없습니다', + 'La cible ne peut pas être attirée', + 'Das Ziel kann nicht gepullt werden', + '该目标无法被拉怪', + '該目標無法被拉怪', + 'No se puede hacer pull al objetivo', + 'No se puede hacer pull al objetivo', + 'Эту цель нельзя пуллить'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('pull_invalid_target_error', 100); + +-- pull_action_unavailable_error: %action_name is replaced with the configured pull action +INSERT INTO `ai_playerbot_texts` + (`id`, `name`, `text`, `say_type`, `reply_type`, + `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, + `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) +VALUES ( + 1758, + 'pull_action_unavailable_error', + 'Can''t perform pull action ''%action_name''', + 0, 0, + '''%action_name'' 풀 액션을 수행할 수 없습니다', + 'Impossible d''effectuer l''action d''engagement ''%action_name''', + 'Die Pull-Aktion ''%action_name'' kann nicht ausgeführt werden', + '无法执行拉怪动作“%action_name”', + '無法執行拉怪動作「%action_name」', + 'No se puede realizar la acción de pull ''%action_name''', + 'No se puede realizar la acción de pull ''%action_name''', + 'Невозможно выполнить действие пула ''%action_name'''); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('pull_action_unavailable_error', 100); diff --git a/src/Ai/Base/ActionContext.h b/src/Ai/Base/ActionContext.h index 79b5b4985..3026cfd50 100644 --- a/src/Ai/Base/ActionContext.h +++ b/src/Ai/Base/ActionContext.h @@ -45,6 +45,7 @@ #include "NonCombatActions.h" #include "OutfitAction.h" #include "PositionAction.h" +#include "PullActions.h" #include "DropQuestAction.h" #include "RandomBotUpdateAction.h" #include "ReachTargetActions.h" @@ -105,6 +106,13 @@ public: creators["shoot"] = &ActionContext::shoot; creators["lifeblood"] = &ActionContext::lifeblood; creators["arcane torrent"] = &ActionContext::arcane_torrent; + creators["pull my target"] = &ActionContext::pull_my_target; + creators["pull rti target"] = &ActionContext::pull_rti_target; + creators["pull start"] = &ActionContext::pull_start; + creators["pull action"] = &ActionContext::pull_action; + creators["pull end"] = &ActionContext::pull_end; + creators["return to pull position"] = &ActionContext::return_to_pull_position; + creators["reach pull"] = &ActionContext::reach_pull; creators["end pull"] = &ActionContext::end_pull; creators["healthstone"] = &ActionContext::healthstone; creators["healing potion"] = &ActionContext::healing_potion; @@ -313,6 +321,13 @@ private: static Action* gift_of_the_naaru(PlayerbotAI* botAI) { return new CastGiftOfTheNaaruAction(botAI); } static Action* lifeblood(PlayerbotAI* botAI) { return new CastLifeBloodAction(botAI); } static Action* arcane_torrent(PlayerbotAI* botAI) { return new CastArcaneTorrentAction(botAI); } + static Action* pull_my_target(PlayerbotAI* botAI) { return new PullMyTargetAction(botAI); } + static Action* pull_rti_target(PlayerbotAI* botAI) { return new PullRtiTargetAction(botAI); } + static Action* pull_start(PlayerbotAI* botAI) { return new PullStartAction(botAI); } + static Action* pull_action(PlayerbotAI* botAI) { return new PullAction(botAI); } + static Action* pull_end(PlayerbotAI* botAI) { return new PullEndAction(botAI); } + static Action* return_to_pull_position(PlayerbotAI* botAI) { return new ReturnToPullPositionAction(botAI); } + static Action* reach_pull(PlayerbotAI* botAI) { return new ReachPullAction(botAI); } static Action* mana_tap(PlayerbotAI* botAI) { return new CastManaTapAction(botAI); } static Action* end_pull(PlayerbotAI* botAI) { return new ChangeCombatStrategyAction(botAI, "-pull"); } static Action* cancel_channel(PlayerbotAI* botAI) { return new CancelChannelAction(botAI); } diff --git a/src/Ai/Base/Actions/AttackAction.cpp b/src/Ai/Base/Actions/AttackAction.cpp index 96bf5c4d3..af964f360 100644 --- a/src/Ai/Base/Actions/AttackAction.cpp +++ b/src/Ai/Base/Actions/AttackAction.cpp @@ -53,22 +53,6 @@ bool AttackMyTargetAction::Execute(Event /*event*/) bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) { - Unit* oldTarget = context->GetValue("current target")->Get(); - bool shouldMelee = bot->IsWithinMeleeRange(target) || botAI->IsMelee(bot); - - bool sameTarget = oldTarget == target && bot->GetVictim() == target; - bool inCombat = botAI->GetState() == BOT_STATE_COMBAT; - bool sameAttackMode = bot->HasUnitState(UNIT_STATE_MELEE_ATTACKING) == shouldMelee; - - if (bot->GetMotionMaster()->GetCurrentMovementGeneratorType() == FLIGHT_MOTION_TYPE || - bot->HasUnitState(UNIT_STATE_IN_FLIGHT)) - { - if (verbose) - botAI->TellError("I cannot attack in flight"); - - return false; - } - if (!target) { if (verbose) @@ -85,6 +69,15 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) return false; } + if (bot->GetMotionMaster()->GetCurrentMovementGeneratorType() == FLIGHT_MOTION_TYPE || + bot->HasUnitState(UNIT_STATE_IN_FLIGHT)) + { + if (verbose) + botAI->TellError("I cannot attack in flight"); + + return false; + } + // Check if bot OR target is in prohibited zone/area (skip for duels) if ((target->IsPlayer() || target->IsPet()) && (!bot->duel || bot->duel->Opponent != target) && @@ -121,6 +114,13 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) return false; } + Unit* oldTarget = context->GetValue("current target")->Get(); + bool shouldMelee = bot->IsWithinMeleeRange(target) || botAI->IsMelee(bot); + + bool sameTarget = oldTarget == target && bot->GetVictim() == target; + bool inCombat = botAI->GetState() == BOT_STATE_COMBAT; + bool sameAttackMode = bot->HasUnitState(UNIT_STATE_MELEE_ATTACKING) == shouldMelee; + if (sameTarget && inCombat && sameAttackMode) { if (verbose) @@ -146,8 +146,7 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) ObjectGuid guid = target->GetGUID(); bot->SetSelection(target->GetGUID()); - context->GetValue("old target")->Set(oldTarget); - + context->GetValue("old target")->Set(oldTarget); context->GetValue("current target")->Set(target); context->GetValue("available loot")->Get()->Add(guid); diff --git a/src/Ai/Base/Actions/GenericSpellActions.cpp b/src/Ai/Base/Actions/GenericSpellActions.cpp index 41911f62d..c81aca214 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.cpp +++ b/src/Ai/Base/Actions/GenericSpellActions.cpp @@ -273,7 +273,7 @@ bool BuffOnPartyAction::Execute(Event /*event*/) } // End greater buff fix -CastShootAction::CastShootAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "shoot") +CastShootAction::CastShootAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "shoot"), shootSpellId(0) { if (Item* const pItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED)) { @@ -283,17 +283,40 @@ CastShootAction::CastShootAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "s { case ITEM_SUBCLASS_WEAPON_GUN: spell += " gun"; + shootSpellId = 3018; break; case ITEM_SUBCLASS_WEAPON_BOW: spell += " bow"; + shootSpellId = 3018; break; case ITEM_SUBCLASS_WEAPON_CROSSBOW: spell += " crossbow"; + shootSpellId = 3018; + break; + case ITEM_SUBCLASS_WEAPON_THROWN: + spell = "throw"; + shootSpellId = 2764; break; } } } +bool CastShootAction::isPossible() +{ + if (shootSpellId) + return botAI->CanCastSpell(shootSpellId, GetTarget(), false); + + return CastSpellAction::isPossible(); +} + +bool CastShootAction::Execute(Event /*event*/) +{ + if (shootSpellId) + return botAI->CastSpell(shootSpellId, GetTarget()); + + return botAI->CastSpell(spell, GetTarget()); +} + Value* CastDebuffSpellOnAttackerAction::GetTargetValue() { return context->GetValue("attacker without aura", spell); diff --git a/src/Ai/Base/Actions/GenericSpellActions.h b/src/Ai/Base/Actions/GenericSpellActions.h index dc0785713..e9dacb7d2 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.h +++ b/src/Ai/Base/Actions/GenericSpellActions.h @@ -253,7 +253,12 @@ class CastShootAction : public CastSpellAction public: CastShootAction(PlayerbotAI* botAI); + bool isPossible() override; + bool Execute(Event event) override; ActionThreatType getThreatType() override { return ActionThreatType::None; } + +private: + uint32 shootSpellId; }; class CastLifeBloodAction : public CastHealingSpellAction diff --git a/src/Ai/Base/Actions/PullActions.cpp b/src/Ai/Base/Actions/PullActions.cpp new file mode 100644 index 000000000..805abd362 --- /dev/null +++ b/src/Ai/Base/Actions/PullActions.cpp @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "AttackersValue.h" +#include "CreatureAI.h" +#include "Playerbots.h" +#include "PlayerbotTextMgr.h" +#include "PositionValue.h" +#include "PullActions.h" +#include "PullStrategy.h" +#include "RtiTargetValue.h" +#include + +namespace +{ +float GetPullReachDistance(Player* bot, Unit* target, PullStrategy const* strategy) +{ + if (!bot || !target || !strategy) + return 0.0f; + + float const combatDistance = bot->GetCombatReach() + target->GetCombatReach(); + return std::max(0.0f, strategy->GetRange() - combatDistance); +} + +bool IsWithinPullRange(Player* bot, Unit* target, PullStrategy const* strategy) +{ + return bot && target && strategy && bot->GetExactDist(target) <= strategy->GetRange(); +} +} + +bool PullRequestAction::Execute(Event event) +{ + PullStrategy* strategy = PullStrategy::Get(botAI); + if (!strategy) + return false; + + if (!botAI->IsTank(bot)) + return false; + + Unit* target = GetPullTarget(event); + if (!target || !target->IsInWorld()) + { + std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "pull_no_target_error", "You have no target", {}); + botAI->TellError(text); + return false; + } + + float const maxPullDistance = sPlayerbotAIConfig.reactDistance * 3.0f; + if (target->GetMapId() != bot->GetMapId() || bot->GetDistance(target) > maxPullDistance) + { + std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "pull_target_too_far_error", "The target is too far away", {}); + botAI->TellError(text); + return false; + } + + if (!AttackersValue::IsPossibleTarget(target, bot)) + { + std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "pull_invalid_target_error", "The target can't be pulled", {}); + botAI->TellError(text); + return false; + } + + if (!strategy->CanDoPullAction(target)) + { + std::string const actionName = strategy->GetPullActionName(); + std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "pull_action_unavailable_error", + "Can't perform pull action '%action_name'", + {{"%action_name", actionName}}); + botAI->TellError(text); + return false; + } + + PositionMap& posMap = AI_VALUE(PositionMap&, "position"); + PositionInfo pullPosition = posMap["pull"]; + pullPosition.Set(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMapId()); + posMap["pull"] = pullPosition; + + strategy->RequestPull(target); + context->GetValue("current target")->Set(target); + botAI->ChangeEngine(BOT_STATE_COMBAT); + botAI->SetNextCheckDelay(sPlayerbotAIConfig.reactDelay); + return true; +} + +Unit* PullMyTargetAction::GetPullTarget(Event event) +{ + Player* requester = event.getOwner() ? event.getOwner() : GetMaster(); + if (event.GetSource() == "attack anything") + return botAI->GetCreature(event.getObject()); + + return requester ? requester->GetSelectedUnit() : nullptr; +} + +Unit* PullRtiTargetAction::GetPullTarget(Event /*event*/) +{ + Unit* rtiTarget = AI_VALUE(Unit*, "rti target"); + if (rtiTarget) + return rtiTarget; + + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + std::string const rti = AI_VALUE(std::string, "rti"); + int32 const index = RtiTargetValue::GetRtiIndex(rti); + if (index < 0) + return nullptr; + + ObjectGuid const guid = group->GetTargetIcon(index); + return guid.IsEmpty() ? nullptr : botAI->GetUnit(guid); +} + +bool PullStartAction::Execute(Event event) +{ + PullStrategy* strategy = PullStrategy::Get(botAI); + if (!strategy) + return false; + + Unit* target = strategy->GetTarget(); + if (!target) + return false; + + std::string const preActionName = strategy->GetPreActionName(); + if (!preActionName.empty() && !botAI->DoSpecificAction(preActionName, event, true)) + return false; + + if (Pet* pet = bot->GetPet()) + { + Creature* creature = pet->ToCreature(); + if (creature) + { + strategy->SetPetReactState(creature->GetReactState()); + creature->SetReactState(REACT_PASSIVE); + } + } + + strategy->OnPullStarted(); + return true; +} + +PullAction::PullAction(PlayerbotAI* botAI, std::string const name) : CastSpellAction(botAI, name) { InitPullAction(); } + +Unit* PullAction::GetTarget() +{ + PullStrategy* strategy = PullStrategy::Get(botAI); + if (!strategy) + return nullptr; + + return strategy->GetTarget(); +} + +std::vector PullAction::getPrerequisites() +{ + PullStrategy* strategy = PullStrategy::Get(botAI); + Unit* target = strategy ? strategy->GetTarget() : nullptr; + if (!strategy || !target) + return {}; + + return IsWithinPullRange(bot, target, strategy) ? std::vector{} + : std::vector{ NextAction("reach pull", ACTION_MOVE) }; +} + +bool PullAction::Execute(Event event) +{ + InitPullAction(); + + PullStrategy* strategy = PullStrategy::Get(botAI); + if (!strategy) + return false; + + Unit* target = strategy->GetTarget(); + if (!target || !target->IsInWorld()) + return false; + + if (target->IsInCombat()) + return false; + + if (!IsWithinPullRange(bot, target, strategy)) + { + strategy->RequestPull(target, false); + return false; + } + + if (bot->isMoving()) + { + bot->StopMoving(); + strategy->RequestPull(target, false); + return false; + } + + context->GetValue("current target")->Set(target); + if (!botAI->DoSpecificAction(strategy->GetPullActionName(), event, true)) + return false; + + return true; +} + +bool PullAction::isPossible() +{ + InitPullAction(); + + PullStrategy* strategy = PullStrategy::Get(botAI); + if (!strategy) + return false; + + Unit* target = strategy->GetTarget(); + std::string const spellName = strategy->GetSpellName(); + if (!target || !target->IsInWorld() || target->GetMapId() != bot->GetMapId() || spellName.empty()) + return false; + + return true; +} + +void PullAction::InitPullAction() +{ + PullStrategy* strategy = PullStrategy::Get(botAI); + if (!strategy) + return; + + std::string const spellName = strategy->GetSpellName(); + if (spellName.empty()) + return; + + spell = spellName; + + bool isShoot = (spellName == "shoot" || spellName == "shoot bow" || + spellName == "shoot gun" || spellName == "shoot crossbow" || + spellName == "throw"); + range = botAI->GetRange(isShoot ? "shoot" : "spell"); +} + +bool PullEndAction::Execute(Event /*event*/) +{ + PullStrategy* strategy = PullStrategy::Get(botAI); + if (!strategy) + return false; + + Unit* pullTarget = strategy->GetTarget(); + + if (!strategy->HasPullStarted() && !strategy->IsPullPendingToStart() && !strategy->HasTarget()) + return false; + + if (Pet* pet = bot->GetPet()) + { + Creature* creature = pet->ToCreature(); + if (creature) + creature->SetReactState(strategy->GetPetReactState()); + } + + PositionMap& posMap = AI_VALUE(PositionMap&, "position"); + PositionInfo pullPosition = posMap["pull"]; + if (pullPosition.isSet()) + posMap.erase("pull"); + + if (pullTarget && context->GetValue("current target")->Get() == pullTarget) + context->GetValue("current target")->Set(nullptr); + + strategy->OnPullEnded(); + return true; +} + +bool ReturnToPullPositionAction::Execute(Event /*event*/) +{ + PositionInfo pullPosition = AI_VALUE(PositionMap&, "position")["pull"]; + if (!pullPosition.isSet() || pullPosition.mapId != bot->GetMapId()) + return false; + + return MoveTo(pullPosition.mapId, pullPosition.x, pullPosition.y, pullPosition.z, + false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true); +} + +bool ReturnToPullPositionAction::isUseful() +{ + PullStrategy* strategy = PullStrategy::Get(botAI); + Unit* target = strategy ? strategy->GetTarget() : nullptr; + if (!strategy || !target || !target->IsInCombat()) + return false; + + PositionInfo pullPosition = AI_VALUE(PositionMap&, "position")["pull"]; + return pullPosition.isSet() && pullPosition.mapId == bot->GetMapId() && + bot->GetDistance(pullPosition.x, pullPosition.y, pullPosition.z) > sPlayerbotAIConfig.followDistance; +} + +bool ReachPullAction::Execute(Event /*event*/) +{ + Unit* target = GetTarget(); + PullStrategy* strategy = PullStrategy::Get(botAI); + if (!target || !strategy) + return false; + + float const reachDistance = GetPullReachDistance(bot, target, strategy); + return ReachCombatTo(target, reachDistance); +} + +bool ReachPullAction::isUseful() +{ + if (botAI->HasStrategy("stay", botAI->GetState())) + return false; + + if (bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL) != nullptr) + return false; + + PullStrategy* strategy = PullStrategy::Get(botAI); + Unit* target = strategy ? strategy->GetTarget() : nullptr; + return target && !IsWithinPullRange(bot, target, strategy); +} + +Unit* ReachPullAction::GetTarget() +{ + PullStrategy* strategy = PullStrategy::Get(botAI); + if (!strategy) + return nullptr; + + return strategy->GetTarget(); +} diff --git a/src/Ai/Base/Actions/PullActions.h b/src/Ai/Base/Actions/PullActions.h new file mode 100644 index 000000000..299dd4a1e --- /dev/null +++ b/src/Ai/Base/Actions/PullActions.h @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_PULLACTIONS_H +#define _PLAYERBOT_PULLACTIONS_H + +#include "GenericSpellActions.h" +#include "ReachTargetActions.h" + +class PullRequestAction : public Action +{ +public: + PullRequestAction(PlayerbotAI* botAI, std::string const name) : Action(botAI, name) {} + + bool Execute(Event event) override; + +protected: + virtual Unit* GetPullTarget(Event event) = 0; +}; + +class PullMyTargetAction : public PullRequestAction +{ +public: + PullMyTargetAction(PlayerbotAI* botAI) : PullRequestAction(botAI, "pull my target") {} + +private: + Unit* GetPullTarget(Event event) override; +}; + +class PullRtiTargetAction : public PullRequestAction +{ +public: + PullRtiTargetAction(PlayerbotAI* botAI) : PullRequestAction(botAI, "pull rti target") {} + +private: + Unit* GetPullTarget(Event event) override; +}; + +class PullStartAction : public Action +{ +public: + PullStartAction(PlayerbotAI* botAI, std::string const name = "pull start") : Action(botAI, name) {} + + bool Execute(Event event) override; +}; + +class PullAction : public CastSpellAction +{ +public: + PullAction(PlayerbotAI* botAI, std::string const name = "pull action"); + + bool Execute(Event event) override; + bool isPossible() override; + std::vector getPrerequisites() override; + Unit* GetTarget() override; + +private: + void InitPullAction(); +}; + +class PullEndAction : public Action +{ +public: + PullEndAction(PlayerbotAI* botAI, std::string const name = "pull end") : Action(botAI, name) {} + + bool Execute(Event event) override; +}; + +class ReachPullAction : public ReachTargetAction +{ +public: + ReachPullAction(PlayerbotAI* botAI) : ReachTargetAction(botAI, "reach pull", botAI->GetRange("spell")) {} + + bool Execute(Event event) override; + bool isUseful() override; + Unit* GetTarget() override; +}; + +class ReturnToPullPositionAction : public MovementAction +{ +public: + ReturnToPullPositionAction(PlayerbotAI* botAI) : MovementAction(botAI, "return to pull position") {} + + bool Execute(Event event) override; + bool isUseful() override; +}; + +#endif diff --git a/src/Ai/Base/ChatActionContext.h b/src/Ai/Base/ChatActionContext.h index 6f11fb33c..af51c23ae 100644 --- a/src/Ai/Base/ChatActionContext.h +++ b/src/Ai/Base/ChatActionContext.h @@ -43,6 +43,7 @@ #include "NewRpgAction.h" #include "PassLeadershipToMasterAction.h" #include "PositionAction.h" +#include "PullActions.h" #include "QueryItemUsageAction.h" #include "QueryQuestAction.h" #include "RangeAction.h" @@ -138,6 +139,8 @@ public: creators["autogear"] = &ChatActionContext::autogear; creators["equip upgrade"] = &ChatActionContext::equip_upgrade; creators["attack my target"] = &ChatActionContext::attack_my_target; + creators["pull my target"] = &ChatActionContext::pull_my_target; + creators["pull rti target"] = &ChatActionContext::pull_rti_target; creators["chat"] = &ChatActionContext::chat; creators["home"] = &ChatActionContext::home; creators["destroy"] = &ChatActionContext::destroy; @@ -250,6 +253,8 @@ private: static Action* home(PlayerbotAI* botAI) { return new SetHomeAction(botAI); } static Action* chat(PlayerbotAI* botAI) { return new ChangeChatAction(botAI); } static Action* attack_my_target(PlayerbotAI* botAI) { return new AttackMyTargetAction(botAI); } + static Action* pull_my_target(PlayerbotAI* botAI) { return new PullMyTargetAction(botAI); } + static Action* pull_rti_target(PlayerbotAI* botAI) { return new PullRtiTargetAction(botAI); } static Action* trainer(PlayerbotAI* botAI) { return new TrainerAction(botAI); } static Action* maintenance(PlayerbotAI* botAI) { return new MaintenanceAction(botAI); } static Action* remove_glyph(PlayerbotAI* botAI) { return new RemoveGlyphAction(botAI); } diff --git a/src/Ai/Base/ChatTriggerContext.h b/src/Ai/Base/ChatTriggerContext.h index 40316bd62..7742a9305 100644 --- a/src/Ai/Base/ChatTriggerContext.h +++ b/src/Ai/Base/ChatTriggerContext.h @@ -66,6 +66,9 @@ public: creators["autogear"] = &ChatTriggerContext::autogear; creators["equip upgrade"] = &ChatTriggerContext::equip_upgrade; creators["attack"] = &ChatTriggerContext::attack; + creators["pull"] = &ChatTriggerContext::pull; + creators["pull back"] = &ChatTriggerContext::pull_back; + creators["pull rti"] = &ChatTriggerContext::pull_rti; creators["chat"] = &ChatTriggerContext::chat; creators["accept"] = &ChatTriggerContext::accept; creators["home"] = &ChatTriggerContext::home; @@ -209,6 +212,9 @@ private: static Trigger* accept(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "accept"); } static Trigger* chat(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "chat"); } static Trigger* attack(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "attack"); } + static Trigger* pull(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "pull"); } + static Trigger* pull_back(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "pull back"); } + static Trigger* pull_rti(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "pull rti"); } static Trigger* trainer(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "trainer"); } static Trigger* maintenance(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "maintenance"); } static Trigger* remove_glyph(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "remove glyph"); } diff --git a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp index 77a8d9d0b..adcadb233 100644 --- a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp +++ b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp @@ -81,6 +81,12 @@ void ChatCommandHandlerStrategy::InitTriggers(std::vector& trigger new TriggerNode("attackers", { NextAction("tell attackers", relevance) })); triggers.push_back( new TriggerNode("target", { NextAction("tell target", relevance) })); + triggers.push_back( + new TriggerNode("pull", { NextAction("pull my target", relevance) })); + triggers.push_back( + new TriggerNode("pull back", { NextAction("pull my target", relevance) })); + triggers.push_back( + new TriggerNode("pull rti", { NextAction("pull rti target", relevance) })); triggers.push_back( new TriggerNode("ready", { NextAction("ready check", relevance) })); triggers.push_back( diff --git a/src/Ai/Base/Strategy/PullStrategy.cpp b/src/Ai/Base/Strategy/PullStrategy.cpp index d0c7c9eac..31351c571 100644 --- a/src/Ai/Base/Strategy/PullStrategy.cpp +++ b/src/Ai/Base/Strategy/PullStrategy.cpp @@ -5,8 +5,188 @@ #include "PullStrategy.h" +#include "AiObjectContext.h" #include "PassiveMultiplier.h" +#include "Player.h" +#include "PlayerbotAI.h" #include "Playerbots.h" +#include "SpellMgr.h" + +class PullStrategyActionNodeFactory : public NamedObjectFactory +{ +public: + PullStrategyActionNodeFactory() + { + creators["pull start"] = &pull_start; + } + +private: + static ActionNode* pull_start(PlayerbotAI* /*botAI*/) + { + return new ActionNode("pull start", {}, {}, { NextAction("pull action", ACTION_NORMAL) }); + } +}; + +PullStrategy::PullStrategy(PlayerbotAI* botAI, std::string const action, std::string const preAction) + : Strategy(botAI), action(action), preAction(preAction) +{ + actionNodeFactories.Add(new PullStrategyActionNodeFactory()); +} + +PullStrategy* PullStrategy::Get(PlayerbotAI* botAI) +{ + if (!botAI) + return nullptr; + + if (PullStrategy* strategy = dynamic_cast(botAI->GetStrategy("pull", BOT_STATE_NON_COMBAT))) + { + if (strategy->IsPullPendingToStart() || strategy->HasPullStarted() || strategy->HasTarget()) + return strategy; + } + + return dynamic_cast(botAI->GetStrategy("pull", BOT_STATE_COMBAT)); +} + +Unit* PullStrategy::GetTarget() const +{ + ObjectGuid const guid = botAI->GetAiObjectContext()->GetValue("pull target")->Get(); + if (guid.IsEmpty()) + return nullptr; + + Unit* target = botAI->GetUnit(guid); + Player* bot = botAI->GetBot(); + if (!bot || !target || !target->IsAlive() || !target->IsInWorld() || + target->GetMapId() != bot->GetMapId()) + return nullptr; + + return target; +} + +bool PullStrategy::HasTarget() const { return GetTarget() != nullptr; } + +void PullStrategy::SetTarget(Unit* target) +{ + botAI->GetAiObjectContext()->GetValue("pull target")->Set(target ? target->GetGUID() : ObjectGuid::Empty); +} + +std::string PullStrategy::GetPullActionName() const +{ + return action; +} + +std::string PullStrategy::GetSpellName() const +{ + Player* bot = botAI->GetBot(); + std::string spellName = GetPullActionName(); + if (!bot || spellName != "shoot") + return spellName; + + Item* equippedWeapon = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED); + if (!equippedWeapon) + return spellName; + + ItemTemplate const* itemTemplate = equippedWeapon->GetTemplate(); + if (!itemTemplate) + return spellName; + + switch (itemTemplate->SubClass) + { + case ITEM_SUBCLASS_WEAPON_THROWN: + return "throw"; + case ITEM_SUBCLASS_WEAPON_GUN: + return "shoot gun"; + case ITEM_SUBCLASS_WEAPON_BOW: + return "shoot bow"; + case ITEM_SUBCLASS_WEAPON_CROSSBOW: + return "shoot crossbow"; + default: + return spellName; + } +} + +float PullStrategy::GetRange() const +{ + Player* bot = botAI->GetBot(); + std::string const spellName = GetSpellName(); + if (bot && !spellName.empty()) + { + uint32 const spellId = botAI->GetAiObjectContext()->GetValue("spell id", spellName)->Get(); + if (SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId)) + return bot->GetSpellMaxRangeForTarget(GetTarget(), spellInfo) - CONTACT_DISTANCE; + } + + return (action == "shoot" ? botAI->GetRange("shoot") : botAI->GetRange("spell")) - CONTACT_DISTANCE; +} + +std::string PullStrategy::GetPreActionName() const +{ + return preAction; +} + +bool PullStrategy::CanDoPullAction(Unit* target) +{ + Player* bot = botAI->GetBot(); + if (!bot || !target) + return false; + + if (!target->IsInWorld() || target->GetMapId() != bot->GetMapId()) + return false; + + if (bot->getClass() != CLASS_DRUID && bot->getClass() != CLASS_PALADIN && + GetPullActionName() == "shoot" && !bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED)) + { + return false; + } + + std::string const spellName = GetSpellName(); + if (spellName.empty()) + return false; + + return true; +} + +void PullStrategy::RequestPull(Unit* target, bool resetTime) +{ + SetTarget(target); + pendingToStart = true; + if (resetTime) + pullStartTime = time(nullptr); +} + +void PullStrategy::OnPullStarted() { pendingToStart = false; } + +void PullStrategy::OnPullEnded() +{ + pullStartTime = 0; + pendingToStart = false; + SetTarget(nullptr); +} + +PullMultiplier::PullMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "pull") {} + +float PullMultiplier::GetValue(Action* action) +{ + PullStrategy const* strategy = PullStrategy::Get(botAI); + if (!strategy || !strategy->HasTarget() || !action) + return 1.0f; + + if (!strategy->IsPullPendingToStart() && !strategy->HasPullStarted()) + return 1.0f; + + std::string const actionName = action->getName(); + if (actionName == "pull my target" || + actionName == "pull rti target" || + actionName == "reach pull" || + actionName == "pull start" || + actionName == "pull action" || + actionName == "return to pull position" || + actionName == "pull end" || + actionName == "follow" || + actionName == "set facing") + return 1.0f; + + return 0.0f; +} class MagePullMultiplier : public PassiveMultiplier { @@ -24,8 +204,16 @@ float MagePullMultiplier::GetValue(Action* action) if (!action) return 1.0f; + PullStrategy const* strategy = PullStrategy::Get(botAI); + if (!strategy || !strategy->HasTarget()) + return 1.0f; + std::string const name = action->getName(); - if (actionName == name || name == "reach spell" || name == "change strategy") + if (actionName == name || name == "pull action" || name == "pull start" || name == "pull end" || + name == "pull my target" || name == "pull rti target" || + name == "reach spell" || name == "reach pull" || + name == "return to pull position" || name == "follow" || + name == "set facing" || name == "change strategy") return 1.0f; return PassiveMultiplier::GetValue(action); @@ -34,18 +222,32 @@ float MagePullMultiplier::GetValue(Action* action) std::vector PullStrategy::getDefaultActions() { return { - NextAction(action, 105.0f), - NextAction("follow", 104.0f), - NextAction("end pull", 103.0f), + NextAction("pull action", 105.0f), }; } -void PullStrategy::InitTriggers(std::vector& triggers) { CombatStrategy::InitTriggers(triggers); } +void PullStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back(new TriggerNode( + "pull start", + { + NextAction("pull start", 106.0f), + NextAction("pull action", ACTION_MOVE) + } + )); + + triggers.push_back(new TriggerNode( + "pull end", + { + NextAction("pull end", 107.0f) + } + )); +} void PullStrategy::InitMultipliers(std::vector& multipliers) { + multipliers.push_back(new PullMultiplier(botAI)); multipliers.push_back(new MagePullMultiplier(botAI, action)); - CombatStrategy::InitMultipliers(multipliers); } void PossibleAddsStrategy::InitTriggers(std::vector& triggers) @@ -61,3 +263,15 @@ void PossibleAddsStrategy::InitTriggers(std::vector& triggers) ) ); } + +void PullBackStrategy::InitTriggers(std::vector& triggers) +{ + Strategy::InitTriggers(triggers); + + triggers.push_back(new TriggerNode( + "return to pull position", + { + NextAction("return to pull position", ACTION_MOVE + 5.0f) + } + )); +} diff --git a/src/Ai/Base/Strategy/PullStrategy.h b/src/Ai/Base/Strategy/PullStrategy.h index bdd7332f3..428699c56 100644 --- a/src/Ai/Base/Strategy/PullStrategy.h +++ b/src/Ai/Base/Strategy/PullStrategy.h @@ -6,22 +6,65 @@ #ifndef _PLAYERBOT_PULLSTRATEGY_H #define _PLAYERBOT_PULLSTRATEGY_H -#include "CombatStrategy.h" +#include "Strategy.h" + +class Action; +class Multiplier; +class Unit; class PlayerbotAI; -class PullStrategy : public CombatStrategy +class PullStrategy : public Strategy { public: - PullStrategy(PlayerbotAI* botAI, std::string const action) : CombatStrategy(botAI), action(action) {} + PullStrategy(PlayerbotAI* botAI, std::string const action, std::string const preAction = ""); void InitTriggers(std::vector& triggers) override; void InitMultipliers(std::vector& multipliers) override; std::string const getName() override { return "pull"; } std::vector getDefaultActions() override; + uint32 GetType() const override { return STRATEGY_TYPE_COMBAT | STRATEGY_TYPE_NONCOMBAT; } + + static PullStrategy* Get(PlayerbotAI* botAI); + static uint8 GetMaxPullTime() { return 15; } + + time_t GetPullStartTime() const { return pullStartTime; } + bool IsPullPendingToStart() const { return pendingToStart; } + bool HasPullStarted() const { return pullStartTime > 0; } + + bool CanDoPullAction(Unit* target); + Unit* GetTarget() const; + bool HasTarget() const; + + virtual std::string GetPullActionName() const; + std::string GetSpellName() const; + float GetRange() const; + virtual std::string GetPreActionName() const; + + void RequestPull(Unit* target, bool resetTime = true); + void OnPullStarted(); + void OnPullEnded(); + + ReactStates GetPetReactState() const { return petReactState; } + void SetPetReactState(ReactStates reactState) { petReactState = reactState; } + +private: + void SetTarget(Unit* target); private: std::string const action; + std::string const preAction; + bool pendingToStart = false; + time_t pullStartTime = 0; + ReactStates petReactState = REACT_DEFENSIVE; +}; + +class PullMultiplier : public Multiplier +{ +public: + PullMultiplier(PlayerbotAI* botAI); + + float GetValue(Action* action) override; }; class PossibleAddsStrategy : public Strategy @@ -33,4 +76,13 @@ public: std::string const getName() override { return "adds"; } }; +class PullBackStrategy : public Strategy +{ +public: + PullBackStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "pull back"; } +}; + #endif diff --git a/src/Ai/Base/Strategy/WaitForAttackStrategy.cpp b/src/Ai/Base/Strategy/WaitForAttackStrategy.cpp index a38140512..21950f00c 100644 --- a/src/Ai/Base/Strategy/WaitForAttackStrategy.cpp +++ b/src/Ai/Base/Strategy/WaitForAttackStrategy.cpp @@ -82,6 +82,7 @@ float WaitForAttackMultiplier::GetValue(Action* action) actionName != "set facing" && actionName != "pull my target" && actionName != "pull rti target" && + actionName != "reach pull" && actionName != "pull start" && actionName != "pull action" && actionName != "pull end") diff --git a/src/Ai/Base/StrategyContext.h b/src/Ai/Base/StrategyContext.h index 5386e872b..8dab9c40d 100644 --- a/src/Ai/Base/StrategyContext.h +++ b/src/Ai/Base/StrategyContext.h @@ -95,6 +95,7 @@ public: creators["sit"] = &StrategyContext::sit; creators["mark rti"] = &StrategyContext::mark_rti; creators["adds"] = &StrategyContext::possible_adds; + creators["pull back"] = &StrategyContext::pull_back; creators["close"] = &StrategyContext::close; creators["ranged"] = &StrategyContext::ranged; creators["behind"] = &StrategyContext::behind; @@ -171,6 +172,7 @@ private: static Strategy* map_full(PlayerbotAI* botAI) { return new MapFullStrategy(botAI); } static Strategy* sit(PlayerbotAI* botAI) { return new SitStrategy(botAI); } static Strategy* possible_adds(PlayerbotAI* botAI) { return new PossibleAddsStrategy(botAI); } + static Strategy* pull_back(PlayerbotAI* botAI) { return new PullBackStrategy(botAI); } static Strategy* mount(PlayerbotAI* botAI) { return new MountStrategy(botAI); } static Strategy* bg(PlayerbotAI* botAI) { return new BGStrategy(botAI); } static Strategy* battleground(PlayerbotAI* botAI) { return new BattlegroundStrategy(botAI); } diff --git a/src/Ai/Base/Trigger/PullTriggers.cpp b/src/Ai/Base/Trigger/PullTriggers.cpp new file mode 100644 index 000000000..4d23b3896 --- /dev/null +++ b/src/Ai/Base/Trigger/PullTriggers.cpp @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "PullTriggers.h" + +#include "PositionValue.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "PullStrategy.h" + +bool PullStartTrigger::IsActive() +{ + PullStrategy const* strategy = PullStrategy::Get(botAI); + return strategy && strategy->IsPullPendingToStart(); +} + +bool PullEndTrigger::IsActive() +{ + PullStrategy const* strategy = PullStrategy::Get(botAI); + + if (!strategy || !strategy->HasPullStarted()) + return false; + + Unit* target = strategy->GetTarget(); + if (!target || !target->IsInWorld() || !target->IsAlive()) + return true; + + time_t const secondsSincePullStarted = time(nullptr) - strategy->GetPullStartTime(); + if (secondsSincePullStarted >= PullStrategy::GetMaxPullTime()) + return true; + + float distanceToPullTarget = bot->GetDistance(target); + if (distanceToPullTarget > ATTACK_DISTANCE && !target->IsNonMeleeSpellCast(false, false, true) && + (!botAI->IsRanged(bot) || distanceToPullTarget > botAI->GetRange("spell"))) + return false; + + if (!botAI->HasStrategy("pull back", BOT_STATE_COMBAT)) + return true; + + PositionInfo pullPosition = AI_VALUE(PositionMap&, "position")["pull"]; + if (!pullPosition.isSet() || pullPosition.mapId != bot->GetMapId()) + return true; + + return bot->GetDistance(pullPosition.x, pullPosition.y, pullPosition.z) <= botAI->GetRange("follow"); +} + +bool ReturnToPullPositionTrigger::IsActive() +{ + PullStrategy const* strategy = PullStrategy::Get(botAI); + + Unit* target = strategy ? strategy->GetTarget() : nullptr; + if (!strategy || !strategy->HasPullStarted() || !target || !target->IsInCombat() || + !botAI->HasStrategy("pull back", BOT_STATE_COMBAT)) + return false; + + PositionInfo pullPosition = AI_VALUE(PositionMap&, "position")["pull"]; + return pullPosition.isSet() && pullPosition.mapId == bot->GetMapId() && + bot->GetDistance(pullPosition.x, pullPosition.y, pullPosition.z) > sPlayerbotAIConfig.followDistance; +} diff --git a/src/Ai/Base/Trigger/PullTriggers.h b/src/Ai/Base/Trigger/PullTriggers.h new file mode 100644 index 000000000..d56036177 --- /dev/null +++ b/src/Ai/Base/Trigger/PullTriggers.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_PULLTRIGGERS_H +#define _PLAYERBOT_PULLTRIGGERS_H + +#include "Trigger.h" + +class PullStartTrigger : public Trigger +{ +public: + PullStartTrigger(PlayerbotAI* botAI, std::string const name = "pull start") : Trigger(botAI, name) {} + + bool IsActive() override; +}; + +class PullEndTrigger : public Trigger +{ +public: + PullEndTrigger(PlayerbotAI* botAI, std::string const name = "pull end") : Trigger(botAI, name) {} + + bool IsActive() override; +}; + +class ReturnToPullPositionTrigger : public Trigger +{ +public: + ReturnToPullPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "return to pull position") {} + + bool IsActive() override; +}; + +#endif diff --git a/src/Ai/Base/TriggerContext.h b/src/Ai/Base/TriggerContext.h index bfdddecf7..54edbb017 100644 --- a/src/Ai/Base/TriggerContext.h +++ b/src/Ai/Base/TriggerContext.h @@ -16,6 +16,7 @@ #include "NewRpgStrategy.h" #include "NewRpgTriggers.h" #include "PvpTriggers.h" +#include "PullTriggers.h" #include "RpgTriggers.h" #include "RtiTriggers.h" #include "StuckTriggers.h" @@ -129,6 +130,9 @@ public: creators["has attackers"] = &TriggerContext::has_attackers; creators["no possible targets"] = &TriggerContext::no_possible_targets; creators["possible adds"] = &TriggerContext::possible_adds; + creators["pull start"] = &TriggerContext::pull_start; + creators["pull end"] = &TriggerContext::pull_end; + creators["return to pull position"] = &TriggerContext::return_to_pull_position; creators["no drink"] = &TriggerContext::no_drink; creators["no food"] = &TriggerContext::no_food; @@ -280,6 +284,9 @@ private: static Trigger* swimming(PlayerbotAI* botAI) { return new IsSwimmingTrigger(botAI); } static Trigger* no_possible_targets(PlayerbotAI* botAI) { return new NoPossibleTargetsTrigger(botAI); } static Trigger* possible_adds(PlayerbotAI* botAI) { return new PossibleAddsTrigger(botAI); } + static Trigger* pull_start(PlayerbotAI* botAI) { return new PullStartTrigger(botAI); } + static Trigger* pull_end(PlayerbotAI* botAI) { return new PullEndTrigger(botAI); } + static Trigger* return_to_pull_position(PlayerbotAI* botAI) { return new ReturnToPullPositionTrigger(botAI); } static Trigger* can_loot(PlayerbotAI* botAI) { return new CanLootTrigger(botAI); } static Trigger* far_from_loot_target(PlayerbotAI* botAI) { return new FarFromCurrentLootTrigger(botAI); } static Trigger* far_from_master(PlayerbotAI* botAI) { return new FarFromMasterTrigger(botAI); } diff --git a/src/Ai/Class/Dk/DKAiObjectContext.cpp b/src/Ai/Class/Dk/DKAiObjectContext.cpp index 9a8271aa7..85f8bc8cd 100644 --- a/src/Ai/Class/Dk/DKAiObjectContext.cpp +++ b/src/Ai/Class/Dk/DKAiObjectContext.cpp @@ -8,11 +8,11 @@ #include "BloodDKStrategy.h" #include "DKActions.h" #include "DKTriggers.h" +#include "DeathKnightPullStrategy.h" #include "FrostDKStrategy.h" #include "GenericDKNonCombatStrategy.h" #include "GenericTriggers.h" #include "Playerbots.h" -#include "PullStrategy.h" #include "UnholyDKStrategy.h" class DeathKnightStrategyFactoryInternal : public NamedObjectContext @@ -28,7 +28,7 @@ public: private: static Strategy* nc(PlayerbotAI* botAI) { return new GenericDKNonCombatStrategy(botAI); } - static Strategy* pull(PlayerbotAI* botAI) { return new PullStrategy(botAI, "icy touch"); } + static Strategy* pull(PlayerbotAI* botAI) { return new DeathKnightPullStrategy(botAI); } static Strategy* frost_aoe(PlayerbotAI* botAI) { return new FrostDKAoeStrategy(botAI); } static Strategy* unholy_aoe(PlayerbotAI* botAI) { return new UnholyDKAoeStrategy(botAI); } }; diff --git a/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.cpp b/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.cpp new file mode 100644 index 000000000..be643b50c --- /dev/null +++ b/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "DeathKnightPullStrategy.h" + +#include "AiObjectContext.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" + +std::string DeathKnightPullStrategy::GetPullActionName() const +{ + Player* bot = botAI->GetBot(); + Unit* target = GetTarget(); + if (!bot || !target || + (!botAI->HasStrategy("blood", BOT_STATE_COMBAT) && !botAI->HasStrategy("blood", BOT_STATE_NON_COMBAT))) + { + return PullStrategy::GetPullActionName(); + } + + uint32 const deathGripSpellId = botAI->GetAiObjectContext()->GetValue("spell id", "death grip")->Get(); + if (deathGripSpellId && bot->HasSpell(deathGripSpellId) && + botAI->CanCastSpell(deathGripSpellId, target)) + { + return "death grip"; + } + + uint32 const icyTouchSpellId = botAI->GetAiObjectContext()->GetValue("spell id", "icy touch")->Get(); + if (!icyTouchSpellId || !bot->HasSpell(icyTouchSpellId) || + !botAI->CanCastSpell(icyTouchSpellId, target)) + { + uint32 const darkCommandSpellId = botAI->GetAiObjectContext()->GetValue("spell id", "dark command")->Get(); + if (darkCommandSpellId && bot->HasSpell(darkCommandSpellId) && + botAI->CanCastSpell(darkCommandSpellId, target)) + { + return "dark command"; + } + } + + return PullStrategy::GetPullActionName(); +} diff --git a/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.h b/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.h new file mode 100644 index 000000000..ce80c69f6 --- /dev/null +++ b/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_DEATH_KNIGHT_PULL_STRATEGY_H +#define _PLAYERBOT_DEATH_KNIGHT_PULL_STRATEGY_H + +#include "PullStrategy.h" + +class DeathKnightPullStrategy : public PullStrategy +{ +public: + DeathKnightPullStrategy(PlayerbotAI* botAI) : PullStrategy(botAI, "icy touch") {} + + std::string GetPullActionName() const override; +}; + +#endif diff --git a/src/Ai/Class/Druid/DruidAiObjectContext.cpp b/src/Ai/Class/Druid/DruidAiObjectContext.cpp index 3d9086cff..4d74d1db3 100644 --- a/src/Ai/Class/Druid/DruidAiObjectContext.cpp +++ b/src/Ai/Class/Druid/DruidAiObjectContext.cpp @@ -19,6 +19,7 @@ #include "MeleeDruidStrategy.h" #include "OffhealDruidCatStrategy.h" #include "Playerbots.h" +#include "DruidPullStrategy.h" class DruidStrategyFactoryInternal : public NamedObjectContext { @@ -26,6 +27,7 @@ public: DruidStrategyFactoryInternal() { creators["nc"] = &DruidStrategyFactoryInternal::nc; + creators["pull"] = &DruidStrategyFactoryInternal::pull; creators["cat aoe"] = &DruidStrategyFactoryInternal::cat_aoe; creators["caster aoe"] = &DruidStrategyFactoryInternal::caster_aoe; creators["caster debuff"] = &DruidStrategyFactoryInternal::caster_debuff; @@ -40,6 +42,7 @@ public: private: static Strategy* nc(PlayerbotAI* botAI) { return new GenericDruidNonCombatStrategy(botAI); } + static Strategy* pull(PlayerbotAI* botAI) { return new DruidPullStrategy(botAI); } static Strategy* cat_aoe(PlayerbotAI* botAI) { return new CatAoeDruidStrategy(botAI); } static Strategy* caster_aoe(PlayerbotAI* botAI) { return new CasterDruidAoeStrategy(botAI); } static Strategy* caster_debuff(PlayerbotAI* botAI) { return new CasterDruidDebuffStrategy(botAI); } diff --git a/src/Ai/Class/Druid/Strategy/DruidPullStrategy.cpp b/src/Ai/Class/Druid/Strategy/DruidPullStrategy.cpp new file mode 100644 index 000000000..dc72b9e82 --- /dev/null +++ b/src/Ai/Class/Druid/Strategy/DruidPullStrategy.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "DruidPullStrategy.h" + +#include "AiObjectContext.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" + +std::string DruidPullStrategy::GetPullActionName() const +{ + Player* bot = botAI->GetBot(); + std::string actionName = PullStrategy::GetPullActionName(); + if (!bot) + return actionName; + + uint32 const faerieFireFeralId = botAI->GetAiObjectContext()->GetValue("spell id", "faerie fire (feral)")->Get(); + if (faerieFireFeralId && bot->HasSpell(faerieFireFeralId) && + (botAI->HasStrategy("bear", BOT_STATE_COMBAT) || botAI->HasStrategy("cat", BOT_STATE_COMBAT))) + { + actionName = "faerie fire (feral)"; + } + + Unit* target = GetTarget(); + uint32 const faerieFireSpellId = botAI->GetAiObjectContext()->GetValue("spell id", actionName)->Get(); + if (target && (!faerieFireSpellId || !bot->HasSpell(faerieFireSpellId) || + !botAI->CanCastSpell(faerieFireSpellId, target))) + { + uint32 const growlSpellId = botAI->GetAiObjectContext()->GetValue("spell id", "growl")->Get(); + if (growlSpellId && bot->HasSpell(growlSpellId) && botAI->CanCastSpell(growlSpellId, target)) + return "growl"; + } + + return actionName; +} + +std::string DruidPullStrategy::GetPreActionName() const +{ + if (GetPullActionName() == "faerie fire") + return ""; + + return PullStrategy::GetPreActionName(); +} diff --git a/src/Ai/Class/Druid/Strategy/DruidPullStrategy.h b/src/Ai/Class/Druid/Strategy/DruidPullStrategy.h new file mode 100644 index 000000000..9a52f262a --- /dev/null +++ b/src/Ai/Class/Druid/Strategy/DruidPullStrategy.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_DRUID_PULL_STRATEGY_H +#define _PLAYERBOT_DRUID_PULL_STRATEGY_H + +#include "PullStrategy.h" + +class DruidPullStrategy : public PullStrategy +{ +public: + DruidPullStrategy(PlayerbotAI* botAI) : PullStrategy(botAI, "faerie fire", "dire bear form") {} + + std::string GetPullActionName() const override; + std::string GetPreActionName() const override; +}; + +#endif diff --git a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp index 7edbf5c8f..a58ede3f8 100644 --- a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp +++ b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp @@ -12,6 +12,7 @@ #include "OffhealRetPaladinStrategy.h" #include "PaladinActions.h" #include "PaladinBuffStrategies.h" +#include "PaladinPullStrategy.h" #include "PaladinTriggers.h" #include "Playerbots.h" #include "TankPaladinStrategy.h" @@ -22,6 +23,7 @@ public: PaladinStrategyFactoryInternal() { creators["nc"] = &PaladinStrategyFactoryInternal::nc; + creators["pull"] = &PaladinStrategyFactoryInternal::pull; creators["cure"] = &PaladinStrategyFactoryInternal::cure; creators["boost"] = &PaladinStrategyFactoryInternal::boost; creators["cc"] = &PaladinStrategyFactoryInternal::cc; @@ -31,6 +33,7 @@ public: private: static Strategy* nc(PlayerbotAI* botAI) { return new GenericPaladinNonCombatStrategy(botAI); } + static Strategy* pull(PlayerbotAI* botAI) { return new PaladinPullStrategy(botAI); } static Strategy* cure(PlayerbotAI* botAI) { return new PaladinCureStrategy(botAI); } static Strategy* boost(PlayerbotAI* botAI) { return new PaladinBoostStrategy(botAI); } static Strategy* cc(PlayerbotAI* botAI) { return new PaladinCcStrategy(botAI); } diff --git a/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.cpp b/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.cpp new file mode 100644 index 000000000..ba0381b5a --- /dev/null +++ b/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "PaladinPullStrategy.h" + +#include "AiObjectContext.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" + +std::string PaladinPullStrategy::GetPullActionName() const +{ + Player* bot = botAI->GetBot(); + Unit* target = GetTarget(); + if (!bot || !target || + (!botAI->HasStrategy("tank", BOT_STATE_COMBAT) && !botAI->HasStrategy("tank", BOT_STATE_NON_COMBAT))) + { + return PullStrategy::GetPullActionName(); + } + + uint32 const avengersShieldSpellId = botAI->GetAiObjectContext()->GetValue("spell id", "avenger's shield")->Get(); + if (avengersShieldSpellId && bot->HasSpell(avengersShieldSpellId) && + botAI->CanCastSpell(avengersShieldSpellId, target)) + { + return "avenger's shield"; + } + + uint32 const handOfReckoningSpellId = botAI->GetAiObjectContext()->GetValue("spell id", "hand of reckoning")->Get(); + if (handOfReckoningSpellId && bot->HasSpell(handOfReckoningSpellId) && + botAI->CanCastSpell(handOfReckoningSpellId, target)) + { + return "hand of reckoning"; + } + + return PullStrategy::GetPullActionName(); +} + +std::string PaladinPullStrategy::GetPreActionName() const +{ + if (botAI->HasStrategy("tank", BOT_STATE_COMBAT) || botAI->HasStrategy("tank", BOT_STATE_NON_COMBAT)) + return ""; + + return PullStrategy::GetPreActionName(); +} diff --git a/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.h b/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.h new file mode 100644 index 000000000..43d014ec7 --- /dev/null +++ b/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_PALADIN_PULL_STRATEGY_H +#define _PLAYERBOT_PALADIN_PULL_STRATEGY_H + +#include "PullStrategy.h" + +class PaladinPullStrategy : public PullStrategy +{ +public: + PaladinPullStrategy(PlayerbotAI* botAI) : PullStrategy(botAI, "judgement", "seal of righteousness") {} + + std::string GetPullActionName() const override; + std::string GetPreActionName() const override; +}; + +#endif diff --git a/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.cpp b/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.cpp new file mode 100644 index 000000000..cdae7e29c --- /dev/null +++ b/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.cpp @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "WarriorPullStrategy.h" + +#include "AiObjectContext.h" +#include "Player.h" +#include "PlayerbotAI.h" + +std::string WarriorPullStrategy::GetPullActionName() const +{ + Player* bot = botAI->GetBot(); + Unit* target = GetTarget(); + if (!bot || !target) + return PullStrategy::GetPullActionName(); + + uint32 const heroicThrowSpellId = botAI->GetAiObjectContext()->GetValue("spell id", "heroic throw")->Get(); + if (heroicThrowSpellId && bot->HasSpell(heroicThrowSpellId) && + botAI->CanCastSpell(heroicThrowSpellId, target)) + { + return "heroic throw"; + } + + return PullStrategy::GetPullActionName(); +} diff --git a/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.h b/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.h new file mode 100644 index 000000000..c63bb21ee --- /dev/null +++ b/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_WARRIOR_PULL_STRATEGY_H +#define _PLAYERBOT_WARRIOR_PULL_STRATEGY_H + +#include "PullStrategy.h" + +class WarriorPullStrategy : public PullStrategy +{ +public: + WarriorPullStrategy(PlayerbotAI* botAI) : PullStrategy(botAI, "shoot") {} + + std::string GetPullActionName() const override; +}; + +#endif diff --git a/src/Ai/Class/Warrior/WarriorAiObjectContext.cpp b/src/Ai/Class/Warrior/WarriorAiObjectContext.cpp index 22754beab..781307729 100644 --- a/src/Ai/Class/Warrior/WarriorAiObjectContext.cpp +++ b/src/Ai/Class/Warrior/WarriorAiObjectContext.cpp @@ -10,8 +10,8 @@ #include "GenericWarriorNonCombatStrategy.h" #include "NamedObjectContext.h" #include "Playerbots.h" -#include "PullStrategy.h" #include "TankWarriorStrategy.h" +#include "WarriorPullStrategy.h" #include "WarriorActions.h" #include "WarriorTriggers.h" @@ -28,7 +28,7 @@ public: private: static Strategy* nc(PlayerbotAI* botAI) { return new GenericWarriorNonCombatStrategy(botAI); } static Strategy* warrior_aoe(PlayerbotAI* botAI) { return new WarrirorAoeStrategy(botAI); } - static Strategy* pull(PlayerbotAI* botAI) { return new PullStrategy(botAI, "shoot"); } + static Strategy* pull(PlayerbotAI* botAI) { return new WarriorPullStrategy(botAI); } }; class WarriorCombatStrategyFactoryInternal : public NamedObjectContext diff --git a/src/Ai/Dungeon/AuchenaiCrypts/Action/AuchenaiCryptsActions.cpp b/src/Ai/Dungeon/AuchenaiCrypts/Action/AuchenaiCryptsActions.cpp new file mode 100644 index 000000000..2ec7907c7 --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/Action/AuchenaiCryptsActions.cpp @@ -0,0 +1,117 @@ +#include "Playerbots.h" +#include "AiFactory.h" +#include "AuchenaiCryptsTriggers.h" +#include "AuchenaiCryptsActions.h" + +// Shirrak the Dead Watcher + +static const Position SHIRRAK_RANGED_POSITION = { -21.777f, -162.700f, 26.062f }; +static const Position SHIRRAK_TANK_POSITION = { -65.171f, -162.920f, 26.504f }; + +// Tank will position Shirrak at the specified coordinates, further down the corridor past the stairs + +bool ShirrakTankPositionBossAction::Execute(Event /*event*/) +{ + Unit* shirrak = AI_VALUE2(Unit*, "find target", "shirrak the dead watcher"); + if (!shirrak) + return false; + + if (bot->GetVictim() != shirrak) + return Attack(shirrak); + + if (shirrak->GetVictim() == bot && bot->IsWithinMeleeRange(shirrak) && + bot->GetHealthPct()>30.0f) + { + const Position& position = SHIRRAK_TANK_POSITION; + float distToPosition = bot->GetExactDist2d(position.GetPositionX(), + position.GetPositionY()); + if (distToPosition > 6.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(2.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +// Flee from Shirrak's Focus Fire + +bool ShirrakFleeFocusFireAction::Execute(Event /*event*/) +{ + std::list creatureList; + bot->GetCreatureListWithEntryInGrid(creatureList, static_cast(AuchenaiCryptsIDs::NPC_FOCUS_FIRE), 20.0f); + + for (Creature* flare : creatureList) + { + if (flare && flare->IsAlive()) + { + float currentDistance = bot->GetDistance2d(flare); + constexpr float safeDistance = 12.0f; + constexpr float buffer = 5.0f; + + if (currentDistance < safeDistance) + { + bot->AttackStop(); + + float distanceToMove = safeDistance - currentDistance + buffer; + + return MoveAway(flare, distanceToMove); + } + } + } + return false; +} + +// Ranged should keep distance from Shirrak, staying at the edge of the stairs + +bool ShirrakRangedKeepDistanceAction::Execute(Event /*event*/) +{ + + std::vector rangedBots; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && botAI->IsRanged(member)) + rangedBots.push_back(member); + } + } + + auto findIt = std::find(rangedBots.begin(), rangedBots.end(), bot); + size_t botIndex = (findIt != rangedBots.end()) ? std::distance(rangedBots.begin(), findIt) : 0; + size_t count = rangedBots.size(); + + constexpr float arcSpan = M_PI / 2.0f; + float arcCenter = M_PI; + float arcStart = arcCenter - (arcSpan / 2.0f); + + float angle = (count <= 1) ? arcCenter : (arcStart + (arcSpan * (float)botIndex / (float)(count - 1))); + + constexpr float spreadRadius = 3.0f; + float targetX = SHIRRAK_RANGED_POSITION.GetPositionX() + cos(angle) * spreadRadius; + float targetY = SHIRRAK_RANGED_POSITION.GetPositionY() + sin(angle) * spreadRadius; + + float distToSpot = bot->GetExactDist2d(targetX, targetY); + + if (distToSpot > 4.0f) + { + float dX = targetX - bot->GetPositionX(); + float dY = targetY - bot->GetPositionY(); + + float moveDist = std::min(2.0f, distToSpot); + float moveX = bot->GetPositionX() + (dX / distToSpot) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToSpot) * moveDist; + + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + return false; +} diff --git a/src/Ai/Dungeon/AuchenaiCrypts/Action/AuchenaiCryptsActions.h b/src/Ai/Dungeon/AuchenaiCrypts/Action/AuchenaiCryptsActions.h new file mode 100644 index 000000000..4764efb65 --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/Action/AuchenaiCryptsActions.h @@ -0,0 +1,31 @@ +#ifndef _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSACTIONS_H +#define _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSACTIONS_H + +#include "AttackAction.h" +#include "MovementActions.h" +#include "AuchenaiCryptsTriggers.h" + +// Shirrak the Dead Watcher + +class ShirrakTankPositionBossAction : public AttackAction +{ +public: + ShirrakTankPositionBossAction(PlayerbotAI* botAI) : AttackAction(botAI, "shirrak tank position boss") {} + bool Execute(Event event) override; +}; + +class ShirrakFleeFocusFireAction : public MovementAction +{ +public: + ShirrakFleeFocusFireAction(PlayerbotAI* botAI) : MovementAction(botAI, "shirrak flee focus fire") {} + bool Execute(Event event) override; +}; + +class ShirrakRangedKeepDistanceAction : public MovementAction +{ +public: + ShirrakRangedKeepDistanceAction(PlayerbotAI* botAI) : MovementAction(botAI, "shirrak ranged keep distance") {} + bool Execute(Event event) override; +}; + +#endif diff --git a/src/Ai/Dungeon/AuchenaiCrypts/AuchenaiCryptsActionContext.h b/src/Ai/Dungeon/AuchenaiCrypts/AuchenaiCryptsActionContext.h new file mode 100644 index 000000000..4eea58716 --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/AuchenaiCryptsActionContext.h @@ -0,0 +1,34 @@ +#ifndef _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSACTIONCONTEXT_H +#define _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSACTIONCONTEXT_H + +#include "AiObjectContext.h" +#include "Action.h" +#include "AuchenaiCryptsActions.h" + +class TbcDungeonAuchenaiCryptsActionContext : public NamedObjectContext +{ +public: + TbcDungeonAuchenaiCryptsActionContext() : NamedObjectContext(false, true) + { + creators["shirrak tank position boss"] = + &TbcDungeonAuchenaiCryptsActionContext::shirrak_tank_position_boss; + + creators["shirrak flee focus fire"] = + &TbcDungeonAuchenaiCryptsActionContext::shirrak_flee_focus_fire; + + creators["shirrak ranged keep distance"] = + &TbcDungeonAuchenaiCryptsActionContext::shirrak_ranged_keep_distance; + } +private: + + static Action* shirrak_tank_position_boss( + PlayerbotAI* botAI) { return new ShirrakTankPositionBossAction(botAI); } + + static Action* shirrak_flee_focus_fire( + PlayerbotAI* botAI) { return new ShirrakFleeFocusFireAction(botAI); } + + static Action* shirrak_ranged_keep_distance( + PlayerbotAI* botAI) { return new ShirrakRangedKeepDistanceAction(botAI); } +}; + +#endif diff --git a/src/Ai/Dungeon/AuchenaiCrypts/AuchenaiCryptsTriggerContext.h b/src/Ai/Dungeon/AuchenaiCrypts/AuchenaiCryptsTriggerContext.h new file mode 100644 index 000000000..95f15f16a --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/AuchenaiCryptsTriggerContext.h @@ -0,0 +1,35 @@ +#ifndef _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSTRIGGERCONTEXT_H +#define _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSTRIGGERCONTEXT_H + +#include "AiObjectContext.h" +#include "TriggerContext.h" +#include "AuchenaiCryptsTriggers.h" + +class TbcDungeonAuchenaiCryptsTriggerContext : public NamedObjectContext +{ +public: + // Shirrak the Dead Watcher + TbcDungeonAuchenaiCryptsTriggerContext() + { + creators["shirrak tank position boss"] = + &TbcDungeonAuchenaiCryptsTriggerContext::shirrak_tank_position_boss; + + creators["shirrak flee focus fire"] = + &TbcDungeonAuchenaiCryptsTriggerContext::shirrak_flee_focus_fire; + + creators["shirrak ranged keep distance"] = + &TbcDungeonAuchenaiCryptsTriggerContext::shirrak_ranged_keep_distance; + } +private: + // Shirrak the Dead Watcher + static Trigger* shirrak_tank_position_boss( + PlayerbotAI* botAI) { return new ShirrakTankPositionBossTrigger(botAI); } + + static Trigger* shirrak_flee_focus_fire( + PlayerbotAI* botAI) { return new ShirrakFleeFocusFireTrigger(botAI); } + + static Trigger* shirrak_ranged_keep_distance( + PlayerbotAI* botAI) { return new ShirrakRangedKeepDistanceTrigger(botAI); } +}; + +#endif diff --git a/src/Ai/Dungeon/AuchenaiCrypts/Multiplier/AuchenaiCryptsMultipliers.cpp b/src/Ai/Dungeon/AuchenaiCrypts/Multiplier/AuchenaiCryptsMultipliers.cpp new file mode 100644 index 000000000..2f74a5a58 --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/Multiplier/AuchenaiCryptsMultipliers.cpp @@ -0,0 +1,45 @@ +#include "AuchenaiCryptsMultipliers.h" +#include "AuchenaiCryptsActions.h" +#include "AuchenaiCryptsTriggers.h" +#include "MovementActions.h" +#include "ReachTargetActions.h" +#include "FollowActions.h" +#include "AiObjectContext.h" +#include "Playerbots.h" + +// Shirrak the Dead Watcher + +// Flee from Focus Fire and dont run back in +float ShirrakFleeFocusFireMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "shirrak the dead watcher")) + return 1.0f; + + std::list creatureList; + bot->GetCreatureListWithEntryInGrid(creatureList, static_cast(AuchenaiCryptsIDs::NPC_FOCUS_FIRE), 20.0f); + + for (Creature* flare : creatureList) + { + if (flare && flare->IsAlive()) + { + if (dynamic_cast(action)) + return 0.0f; + + float currentDistance = bot->GetDistance2d(flare); + constexpr float safeDistance = 12.0f; + constexpr float buffer = 5.0f; + + if (currentDistance < safeDistance + buffer && ( + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action))) + { + return 0.0f; + } + } + } + return 1.0f; +} diff --git a/src/Ai/Dungeon/AuchenaiCrypts/Multiplier/AuchenaiCryptsMultipliers.h b/src/Ai/Dungeon/AuchenaiCrypts/Multiplier/AuchenaiCryptsMultipliers.h new file mode 100644 index 000000000..df5de2318 --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/Multiplier/AuchenaiCryptsMultipliers.h @@ -0,0 +1,13 @@ +#ifndef _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSMULTIPLIERS_H +#define _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSMULTIPLIERS_H + +#include "Multiplier.h" + +class ShirrakFleeFocusFireMultiplier : public Multiplier +{ +public: + ShirrakFleeFocusFireMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "shirrak flee focus fire") {} + float GetValue(Action* action) override; +}; + +#endif diff --git a/src/Ai/Dungeon/AuchenaiCrypts/Strategy/AuchenaiCryptsStrategy.cpp b/src/Ai/Dungeon/AuchenaiCrypts/Strategy/AuchenaiCryptsStrategy.cpp new file mode 100644 index 000000000..f975d46bb --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/Strategy/AuchenaiCryptsStrategy.cpp @@ -0,0 +1,21 @@ +#include "AuchenaiCryptsTriggers.h" +#include "AuchenaiCryptsStrategy.h" +#include "AuchenaiCryptsMultipliers.h" + +void TbcDungeonAuchenaiCryptsStrategy::InitTriggers(std::vector& triggers) +{ + // Shirrak The Dead Watcher + triggers.push_back(new TriggerNode("shirrak tank position boss", { + NextAction("shirrak tank position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("shirrak flee focus fire", { + NextAction("shirrak flee focus fire", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("shirrak ranged keep distance", { + NextAction("shirrak ranged keep distance", ACTION_RAID + 1) })); +} + +void TbcDungeonAuchenaiCryptsStrategy::InitMultipliers(std::vector& multipliers) +{ + multipliers.push_back(new ShirrakFleeFocusFireMultiplier(botAI)); +} diff --git a/src/Ai/Dungeon/AuchenaiCrypts/Strategy/AuchenaiCryptsStrategy.h b/src/Ai/Dungeon/AuchenaiCrypts/Strategy/AuchenaiCryptsStrategy.h new file mode 100644 index 000000000..ff82a0266 --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/Strategy/AuchenaiCryptsStrategy.h @@ -0,0 +1,19 @@ +#ifndef _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSSTRATEGY_H +#define _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSSTRATEGY_H + +#include "AiObjectContext.h" +#include "Strategy.h" +#include "Multiplier.h" + +class TbcDungeonAuchenaiCryptsStrategy : public Strategy +{ +public: + TbcDungeonAuchenaiCryptsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + virtual std::string const getName() override { return "tbc-ac"; } + + virtual void InitTriggers(std::vector &triggers) override; + virtual void InitMultipliers(std::vector &multipliers) override; +}; + +#endif diff --git a/src/Ai/Dungeon/AuchenaiCrypts/Trigger/AuchenaiCryptsTriggers.cpp b/src/Ai/Dungeon/AuchenaiCrypts/Trigger/AuchenaiCryptsTriggers.cpp new file mode 100644 index 000000000..372614d2f --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/Trigger/AuchenaiCryptsTriggers.cpp @@ -0,0 +1,34 @@ +#include "Playerbots.h" +#include "AuchenaiCryptsTriggers.h" +#include "AiObject.h" +#include "AiObjectContext.h" + +// Shirrak the Dead Watcher + +bool ShirrakTankPositionBossTrigger::IsActive() +{ + return botAI->IsTank(bot) && + AI_VALUE2(Unit*, "find target", "shirrak the dead watcher"); +} + +bool ShirrakFleeFocusFireTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "shirrak the dead watcher")) + return false; + + std::list creatureList; + bot->GetCreatureListWithEntryInGrid(creatureList, static_cast(AuchenaiCryptsIDs::NPC_FOCUS_FIRE), 20.0f); + + for (Creature* flare : creatureList) + { + if (flare && flare->IsAlive()) + return true; + } + return false; +} + +bool ShirrakRangedKeepDistanceTrigger::IsActive() +{ + return botAI->IsRanged(bot) && + AI_VALUE2(Unit*, "find target", "shirrak the dead watcher"); +} diff --git a/src/Ai/Dungeon/AuchenaiCrypts/Trigger/AuchenaiCryptsTriggers.h b/src/Ai/Dungeon/AuchenaiCrypts/Trigger/AuchenaiCryptsTriggers.h new file mode 100644 index 000000000..1d3144194 --- /dev/null +++ b/src/Ai/Dungeon/AuchenaiCrypts/Trigger/AuchenaiCryptsTriggers.h @@ -0,0 +1,38 @@ +#ifndef _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSTRIGGERS_H +#define _PLAYERBOT_TBCDUNGEONAUCHENAICRYPTSTRIGGERS_H + +#include "Trigger.h" +#include "GenericTriggers.h" +#include "DungeonStrategyUtils.h" + +enum class AuchenaiCryptsIDs : uint32 +{ + // Shirrak The Dead Watcher + NPC_FOCUS_FIRE = 18374, +}; + +class ShirrakTankPositionBossTrigger : public Trigger +{ +public: + ShirrakTankPositionBossTrigger(PlayerbotAI* botAI) : Trigger(botAI, "shirrak tank position boss") {} + + bool IsActive() override; +}; + +class ShirrakFleeFocusFireTrigger : public Trigger +{ +public: + ShirrakFleeFocusFireTrigger(PlayerbotAI* botAI) : Trigger(botAI, "shirrak flee focus fire") {} + + bool IsActive() override; +}; + +class ShirrakRangedKeepDistanceTrigger : public Trigger +{ +public: + ShirrakRangedKeepDistanceTrigger(PlayerbotAI* botAI) : Trigger(botAI, "shirrak ranged keep distance") {} + + bool IsActive() override; +}; + +#endif diff --git a/src/Ai/Dungeon/DungeonStrategyContext.h b/src/Ai/Dungeon/DungeonStrategyContext.h index 3311aeee2..07e5fe505 100644 --- a/src/Ai/Dungeon/DungeonStrategyContext.h +++ b/src/Ai/Dungeon/DungeonStrategyContext.h @@ -2,6 +2,7 @@ #define _PLAYERBOT_DUNGEONSTRATEGYCONTEXT_H #include "Strategy.h" +#include "AuchenaiCrypts/Strategy/AuchenaiCryptsStrategy.h" #include "UtgardeKeep/Strategy/UtgardeKeepStrategy.h" #include "Nexus/Strategy/NexusStrategy.h" #include "AzjolNerub/Strategy/AzjolNerubStrategy.h" @@ -44,7 +45,7 @@ class DungeonStrategyContext : public NamedObjectContext // ... // Burning Crusade - // ... + creators["tbc-ac"] = &DungeonStrategyContext::tbc_ac; // Auchindoun: Auchenai Crypts // Wrath of the Lich King creators["wotlk-uk"] = &DungeonStrategyContext::wotlk_uk; // Utgarde Keep @@ -65,6 +66,7 @@ class DungeonStrategyContext : public NamedObjectContext creators["wotlk-fos"] = &DungeonStrategyContext::wotlk_fos; // The Forge of Souls } private: + static Strategy* tbc_ac(PlayerbotAI* botAI) { return new TbcDungeonAuchenaiCryptsStrategy(botAI); } static Strategy* wotlk_uk(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); } diff --git a/src/Ai/Dungeon/TbcDungeonActionContext.h b/src/Ai/Dungeon/TbcDungeonActionContext.h new file mode 100644 index 000000000..8c3547224 --- /dev/null +++ b/src/Ai/Dungeon/TbcDungeonActionContext.h @@ -0,0 +1,6 @@ +#ifndef _PLAYERBOT_TBCDUNGEONACTIONCONTEXT_H +#define _PLAYERBOT_TBCDUNGEONACTIONCONTEXT_H + +#include "AuchenaiCrypts/AuchenaiCryptsActionContext.h" + +#endif diff --git a/src/Ai/Dungeon/TbcDungeonTriggerContext.h b/src/Ai/Dungeon/TbcDungeonTriggerContext.h new file mode 100644 index 000000000..9a680b7af --- /dev/null +++ b/src/Ai/Dungeon/TbcDungeonTriggerContext.h @@ -0,0 +1,6 @@ +#ifndef _PLAYERBOT_TBCDUNGEONTRIGGERCONTEXT_H +#define _PLAYERBOT_TBCDUNGEONTRIGGERCONTEXT_H + +#include "AuchenaiCrypts/AuchenaiCryptsTriggerContext.h" + +#endif diff --git a/src/Bot/Engine/BuildSharedActionContexts.cpp b/src/Bot/Engine/BuildSharedActionContexts.cpp index 8fbb6c135..7e243eadb 100644 --- a/src/Bot/Engine/BuildSharedActionContexts.cpp +++ b/src/Bot/Engine/BuildSharedActionContexts.cpp @@ -18,6 +18,7 @@ #include "Ai/Raid/Ulduar/RaidUlduarActionContext.h" #include "Ai/Raid/Onyxia/RaidOnyxiaActionContext.h" #include "Ai/Raid/Icecrown/RaidIccActionContext.h" +#include "Ai/Dungeon/TbcDungeonActionContext.h" #include "Ai/Dungeon/WotlkDungeonActionContext.h" void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList& actionContexts) @@ -41,6 +42,7 @@ void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList& triggerContexts) @@ -41,6 +42,7 @@ void AiObjectContext::BuildSharedTriggerContexts(SharedNamedObjectContextList::iterator i = strategies.find(name); + return i != strategies.end() ? i->second : nullptr; +} + void Engine::ProcessTriggers(bool minimal) { std::unordered_map fires; diff --git a/src/Bot/Engine/Engine.h b/src/Bot/Engine/Engine.h index 8a7c34189..976252cc5 100644 --- a/src/Bot/Engine/Engine.h +++ b/src/Bot/Engine/Engine.h @@ -70,6 +70,7 @@ public: void addStrategiesNoInit(std::string first, ...); bool removeStrategy(std::string const name, bool init = true); bool HasStrategy(std::string const name); + Strategy* GetStrategy(std::string const name); void removeAllStrategies(); void toggleStrategy(std::string const name); std::string const ListStrategies(); diff --git a/src/Bot/Factory/AiFactory.cpp b/src/Bot/Factory/AiFactory.cpp index a821886f9..6c638e807 100644 --- a/src/Bot/Factory/AiFactory.cpp +++ b/src/Bot/Factory/AiFactory.cpp @@ -315,7 +315,7 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa break; case CLASS_WARRIOR: if (tab == WARRIOR_TAB_PROTECTION) - engine->addStrategiesNoInit("tank", "tank assist", "aoe", nullptr); + engine->addStrategiesNoInit("tank", "tank assist", "pull", "pull back", "aoe", nullptr); else if (tab == WARRIOR_TAB_ARMS || !player->HasSpell(1680)) // Whirlwind engine->addStrategiesNoInit("arms", "aoe", "dps assist", nullptr); else @@ -333,7 +333,7 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa break; case CLASS_PALADIN: if (tab == PALADIN_TAB_PROTECTION) - engine->addStrategiesNoInit("tank", "tank assist", "bthreat", "barmor", "cure", nullptr); + engine->addStrategiesNoInit("tank", "tank assist", "pull", "pull back", "bthreat", "barmor", "cure", nullptr); else if (tab == PALADIN_TAB_HOLY) engine->addStrategiesNoInit("heal", "dps assist", "cure", "bcast", nullptr); else @@ -352,7 +352,7 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa if (player->HasSpell(768) /*cat form*/ && !player->HasAura(16931) /*thick hide*/) engine->addStrategiesNoInit("cat", "dps assist", nullptr); else - engine->addStrategiesNoInit("bear", "tank assist", nullptr); + engine->addStrategiesNoInit("bear", "tank assist", "pull", "pull back", nullptr); } break; case CLASS_HUNTER: @@ -383,7 +383,7 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa break; case CLASS_DEATH_KNIGHT: if (tab == DEATH_KNIGHT_TAB_BLOOD) - engine->addStrategiesNoInit("blood", "tank assist", nullptr); + engine->addStrategiesNoInit("blood", "tank assist", "pull", "pull back", nullptr); else if (tab == DEATH_KNIGHT_TAB_FROST) engine->addStrategiesNoInit("frost", "frost aoe", "dps assist", nullptr); else @@ -510,7 +510,7 @@ void AiFactory::AddDefaultNonCombatStrategies(Player* player, PlayerbotAI* const case CLASS_PALADIN: if (tab == PALADIN_TAB_PROTECTION) { - nonCombatEngine->addStrategiesNoInit("bthreat", "tank assist", "barmor", nullptr); + nonCombatEngine->addStrategiesNoInit("bthreat", "tank assist", "pull", "barmor", nullptr); if (player->GetLevel() >= 20) nonCombatEngine->addStrategy("bhealth", false); else @@ -548,14 +548,14 @@ void AiFactory::AddDefaultNonCombatStrategies(Player* player, PlayerbotAI* const if (player->GetLevel() >= 20 && !player->HasAura(16931) /*thick hide*/) nonCombatEngine->addStrategy("dps assist", false); else - nonCombatEngine->addStrategy("tank assist", false); + nonCombatEngine->addStrategiesNoInit("tank assist", "pull", nullptr); } else nonCombatEngine->addStrategiesNoInit("dps assist", "cure", nullptr); break; case CLASS_WARRIOR: if (tab == WARRIOR_TAB_PROTECTION) - nonCombatEngine->addStrategy("tank assist", false); + nonCombatEngine->addStrategiesNoInit("tank assist", "pull", nullptr); else nonCombatEngine->addStrategy("dps assist", false); break; @@ -571,7 +571,7 @@ void AiFactory::AddDefaultNonCombatStrategies(Player* player, PlayerbotAI* const break; case CLASS_DEATH_KNIGHT: if (tab == DEATH_KNIGHT_TAB_BLOOD) - nonCombatEngine->addStrategy("tank assist", false); + nonCombatEngine->addStrategiesNoInit("tank assist", "pull", nullptr); else nonCombatEngine->addStrategy("dps assist", false); break; diff --git a/src/Bot/Factory/PlayerbotFactory.cpp b/src/Bot/Factory/PlayerbotFactory.cpp index 11f301feb..9dfae8987 100644 --- a/src/Bot/Factory/PlayerbotFactory.cpp +++ b/src/Bot/Factory/PlayerbotFactory.cpp @@ -5,6 +5,7 @@ #include "PlayerbotFactory.h" +#include #include #include "AccountMgr.h" @@ -47,10 +48,11 @@ static std::vector initSlotsOrder = {EQUIPMENT_SLOT_TRINKET1, EQUIPMENT_ EQUIPMENT_SLOT_LEGS, EQUIPMENT_SLOT_HANDS, EQUIPMENT_SLOT_NECK, EQUIPMENT_SLOT_BODY, EQUIPMENT_SLOT_WAIST, EQUIPMENT_SLOT_FEET, EQUIPMENT_SLOT_WRISTS, EQUIPMENT_SLOT_FINGER1, EQUIPMENT_SLOT_FINGER2, EQUIPMENT_SLOT_BACK}; -uint32 PlayerbotFactory::tradeSkills[] = {SKILL_ALCHEMY, SKILL_ENCHANTING, SKILL_SKINNING, SKILL_TAILORING, - SKILL_LEATHERWORKING, SKILL_ENGINEERING, SKILL_HERBALISM, SKILL_MINING, - SKILL_BLACKSMITHING, SKILL_COOKING, SKILL_FIRST_AID, SKILL_FISHING, - SKILL_JEWELCRAFTING}; +uint32 PlayerbotFactory::tradeSkills[] = {SKILL_ALCHEMY, SKILL_ENCHANTING, SKILL_SKINNING, + SKILL_TAILORING, SKILL_LEATHERWORKING, SKILL_ENGINEERING, + SKILL_HERBALISM, SKILL_INSCRIPTION, SKILL_MINING, + SKILL_BLACKSMITHING, SKILL_COOKING, SKILL_FIRST_AID, + SKILL_FISHING, SKILL_JEWELCRAFTING}; std::list PlayerbotFactory::classQuestIds; std::list PlayerbotFactory::specialQuestIds; @@ -58,6 +60,264 @@ std::vector PlayerbotFactory::enchantSpellIdCache; std::vector PlayerbotFactory::enchantGemIdCache; std::unordered_map> PlayerbotFactory::trainerIdCache; +bool PlayerbotFactory::IsPrimaryTradeSkill(uint16 skillId) +{ + SkillLineEntry const* skillLine = sSkillLineStore.LookupEntry(skillId); + return skillLine && skillLine->categoryId == SKILL_CATEGORY_PROFESSION; +} + +bool PlayerbotFactory::IsGatheringTradeSkill(uint16 skillId) +{ + switch (skillId) + { + case SKILL_HERBALISM: + case SKILL_MINING: + case SKILL_SKINNING: + return true; + default: + return false; + } +} + +bool PlayerbotFactory::IsCraftingTradeSkill(uint16 skillId) +{ + return IsPrimaryTradeSkill(skillId) && !IsGatheringTradeSkill(skillId); +} + +uint32 PlayerbotFactory::GetProfessionStarterSpell(uint16 skillId) +{ + static constexpr std::array, 14> ProfessionStarterSpells = {{ + {SKILL_ALCHEMY, 2259}, + {SKILL_BLACKSMITHING, 2018}, + {SKILL_COOKING, 2550}, + {SKILL_ENCHANTING, 7411}, + {SKILL_ENGINEERING, 4036}, + {SKILL_FIRST_AID, 3273}, + {SKILL_FISHING, 7620}, + {SKILL_HERBALISM, 2366}, + {SKILL_INSCRIPTION, 45357}, + {SKILL_JEWELCRAFTING, 25229}, + {SKILL_LEATHERWORKING, 2108}, + {SKILL_MINING, 2575}, + {SKILL_SKINNING, 8613}, + {SKILL_TAILORING, 3908} + }}; + + for (auto const& [professionSkill, starterSpell] : ProfessionStarterSpells) + { + if (professionSkill == skillId) + return starterSpell; + } + + return 0; +} + +std::vector PlayerbotFactory::GetClassProfessionPairs(Player* bot) +{ + switch (bot->getClass()) + { + case CLASS_WARRIOR: + return {{SKILL_MINING, SKILL_BLACKSMITHING, 45}, + {SKILL_MINING, SKILL_ENGINEERING, 30}, + {SKILL_MINING, SKILL_JEWELCRAFTING, 15}, + {SKILL_HERBALISM, SKILL_ALCHEMY, 10}}; + case CLASS_PALADIN: + return {{SKILL_MINING, SKILL_BLACKSMITHING, 45}, + {SKILL_MINING, SKILL_JEWELCRAFTING, 30}, + {SKILL_MINING, SKILL_ENGINEERING, 15}, + {SKILL_HERBALISM, SKILL_ALCHEMY, 10}}; + case CLASS_DEATH_KNIGHT: + return {{SKILL_MINING, SKILL_BLACKSMITHING, 45}, + {SKILL_MINING, SKILL_ENGINEERING, 35}, + {SKILL_MINING, SKILL_JEWELCRAFTING, 20}}; + case CLASS_HUNTER: + return {{SKILL_SKINNING, SKILL_LEATHERWORKING, 45}, + {SKILL_MINING, SKILL_ENGINEERING, 35}, + {SKILL_HERBALISM, SKILL_ALCHEMY, 10}, + {SKILL_MINING, SKILL_JEWELCRAFTING, 10}}; + case CLASS_ROGUE: + return {{SKILL_SKINNING, SKILL_LEATHERWORKING, 35}, + {SKILL_HERBALISM, SKILL_ALCHEMY, 25}, + {SKILL_MINING, SKILL_ENGINEERING, 25}, + {SKILL_MINING, SKILL_JEWELCRAFTING, 10}, + {SKILL_HERBALISM, SKILL_INSCRIPTION, 5}}; + case CLASS_DRUID: + return {{SKILL_SKINNING, SKILL_LEATHERWORKING, 35}, + {SKILL_HERBALISM, SKILL_ALCHEMY, 35}, + {SKILL_HERBALISM, SKILL_INSCRIPTION, 20}, + {SKILL_MINING, SKILL_JEWELCRAFTING, 10}}; + case CLASS_SHAMAN: + return {{SKILL_HERBALISM, SKILL_ALCHEMY, 35}, + {SKILL_SKINNING, SKILL_LEATHERWORKING, 25}, + {SKILL_HERBALISM, SKILL_INSCRIPTION, 25}, + {SKILL_MINING, SKILL_JEWELCRAFTING, 15}}; + case CLASS_PRIEST: + return {{SKILL_TAILORING, SKILL_ENCHANTING, 45}, + {SKILL_HERBALISM, SKILL_INSCRIPTION, 30}, + {SKILL_HERBALISM, SKILL_ALCHEMY, 25}}; + case CLASS_MAGE: + return {{SKILL_TAILORING, SKILL_ENCHANTING, 50}, + {SKILL_HERBALISM, SKILL_ALCHEMY, 25}, + {SKILL_HERBALISM, SKILL_INSCRIPTION, 25}}; + case CLASS_WARLOCK: + default: + return {{SKILL_TAILORING, SKILL_ENCHANTING, 50}, + {SKILL_HERBALISM, SKILL_ALCHEMY, 25}, + {SKILL_HERBALISM, SKILL_INSCRIPTION, 25}}; + } +} + +std::vector PlayerbotFactory::GetRandomProfessionPairs() +{ + return {{SKILL_MINING, SKILL_BLACKSMITHING, 20}, + {SKILL_MINING, SKILL_ENGINEERING, 18}, + {SKILL_MINING, SKILL_JEWELCRAFTING, 16}, + {SKILL_SKINNING, SKILL_LEATHERWORKING, 18}, + {SKILL_HERBALISM, SKILL_ALCHEMY, 18}, + {SKILL_HERBALISM, SKILL_INSCRIPTION, 14}, + {SKILL_TAILORING, SKILL_ENCHANTING, 10}, + {SKILL_HERBALISM, SKILL_MINING, 6}, + {SKILL_HERBALISM, SKILL_SKINNING, 5}, + {SKILL_MINING, SKILL_SKINNING, 5}}; +} + +std::pair PlayerbotFactory::ChooseProfessionPair( + std::vector const& professionPairs) +{ + uint32 totalWeight = 0; + for (WeightedProfessionPair const& pair : professionPairs) + totalWeight += pair.weight; + + if (!totalWeight) + return {SKILL_HERBALISM, SKILL_ALCHEMY}; + + uint32 roll = urand(1, totalWeight); + for (WeightedProfessionPair const& pair : professionPairs) + { + if (roll <= pair.weight) + return {pair.firstSkill, pair.secondSkill}; + + roll -= pair.weight; + } + + WeightedProfessionPair const& fallback = professionPairs.back(); + return {fallback.firstSkill, fallback.secondSkill}; +} + +bool PlayerbotFactory::HasProfessionPair(std::vector const& professionPairs, + uint16 firstSkill, uint16 secondSkill) +{ + for (WeightedProfessionPair const& pair : professionPairs) + { + if (pair.firstSkill == firstSkill && pair.secondSkill == secondSkill) + return true; + } + + return false; +} + +uint16 PlayerbotFactory::ChooseSingleProfession(std::vector const& professionPairs) +{ + std::vector> gatheringSkills; + std::vector> craftingSkills; + + auto addWeightedSkill = [](std::vector>& skills, uint16 skillId, uint32 weight) + { + for (std::pair& skill : skills) + { + if (skill.first == skillId) + { + skill.second += weight; + return; + } + } + + skills.push_back({skillId, weight}); + }; + + for (WeightedProfessionPair const& pair : professionPairs) + { + if (IsGatheringTradeSkill(pair.firstSkill)) + addWeightedSkill(gatheringSkills, pair.firstSkill, pair.weight); + if (IsCraftingTradeSkill(pair.firstSkill)) + addWeightedSkill(craftingSkills, pair.firstSkill, pair.weight); + + if (IsGatheringTradeSkill(pair.secondSkill)) + addWeightedSkill(gatheringSkills, pair.secondSkill, pair.weight); + if (IsCraftingTradeSkill(pair.secondSkill)) + addWeightedSkill(craftingSkills, pair.secondSkill, pair.weight); + } + + std::vector>* selectedPool = nullptr; + if (!gatheringSkills.empty() && !craftingSkills.empty()) + selectedPool = urand(0, 1) == 0 ? &gatheringSkills : &craftingSkills; + else if (!gatheringSkills.empty()) + selectedPool = &gatheringSkills; + else if (!craftingSkills.empty()) + selectedPool = &craftingSkills; + + if (!selectedPool || selectedPool->empty()) + return SKILL_HERBALISM; + + uint32 totalWeight = 0; + for (std::pair const& skill : *selectedPool) + totalWeight += skill.second; + + if (!totalWeight) + return selectedPool->front().first; + + uint32 roll = urand(1, totalWeight); + for (std::pair const& skill : *selectedPool) + { + if (roll <= skill.second) + return skill.first; + + roll -= skill.second; + } + + return selectedPool->back().first; +} + +uint32 PlayerbotFactory::GetStoredOrRandomValue(Player* bot, + std::string const& key, + uint32 minValue, + uint32 maxValue) +{ + uint32 value = sRandomPlayerbotMgr.GetValue(bot, key); + if (value < minValue || value > maxValue) + { + value = urand(minValue, maxValue); + sRandomPlayerbotMgr.SetValue(bot, key, value); + } + + return value; +} + +bool PlayerbotFactory::HasAnySpell(Player* bot, std::vector const& spells) +{ + for (uint32 spellId : spells) + { + if (bot->HasSpell(spellId)) + return true; + } + + return false; +} + +bool PlayerbotFactory::LearnProfessionSpecialization(Player* bot, + ProfessionSpecializationSpell knownSpell, + ProfessionSpecializationSpell learnSpell) +{ + uint32 const knownSpellId = static_cast(knownSpell); + uint32 const learnSpellId = static_cast(learnSpell); + + if (bot->HasSpell(knownSpellId) || !sSpellMgr->GetSpellInfo(learnSpellId)) + return false; + + bot->CastSpell(bot, learnSpellId, true); + return bot->HasSpell(knownSpellId); +} + PlayerbotFactory::PlayerbotFactory(Player* bot, uint32 level, uint32 itemQuality, uint32 gearScoreLimit) : level(level), itemQuality(itemQuality), gearScoreLimit(gearScoreLimit), bot(bot) { @@ -2250,69 +2510,278 @@ bool PlayerbotFactory::CanEquipUnseenItem(uint8 slot, uint16& dest, uint32 item) void PlayerbotFactory::InitTradeSkills() { + if (!sRandomPlayerbotMgr.IsRandomBot(bot)) + return; + + uint32 const maxPrimaryTradeSkills = + std::min(2, sWorld->getIntConfig(CONFIG_MAX_PRIMARY_TRADE_SKILL)); + uint16 firstSkill = sRandomPlayerbotMgr.GetValue(bot, "firstSkill"); uint16 secondSkill = sRandomPlayerbotMgr.GetValue(bot, "secondSkill"); - if (!firstSkill || !secondSkill) + ProfessionRollType professionRollType = + static_cast(sRandomPlayerbotMgr.GetValue(bot, "professionRollType")); + + if (professionRollType != ProfessionRollType::Class && professionRollType != ProfessionRollType::Random) { - std::vector firstSkills; - std::vector secondSkills; + professionRollType = urand(1, 100) <= sPlayerbotAIConfig.classMatchingProfessionChance + ? ProfessionRollType::Class + : ProfessionRollType::Random; + sRandomPlayerbotMgr.SetValue(bot, "professionRollType", static_cast(professionRollType)); + } - switch (bot->getClass()) - { - case CLASS_WARRIOR: - case CLASS_PALADIN: - case CLASS_DEATH_KNIGHT: - firstSkills.push_back(SKILL_MINING); - secondSkills.push_back(SKILL_BLACKSMITHING); - secondSkills.push_back(SKILL_ENGINEERING); - secondSkills.push_back(SKILL_JEWELCRAFTING); - break; - case CLASS_SHAMAN: - case CLASS_DRUID: - case CLASS_HUNTER: - case CLASS_ROGUE: - firstSkills.push_back(SKILL_SKINNING); - secondSkills.push_back(SKILL_LEATHERWORKING); - break; - default: - firstSkills.push_back(SKILL_TAILORING); - secondSkills.push_back(SKILL_ENCHANTING); - } + std::vector professionPairs = professionRollType == ProfessionRollType::Class + ? GetClassProfessionPairs(bot) + : GetRandomProfessionPairs(); - switch (urand(0, 6)) + bool const hasStoredProfessionPair = firstSkill && secondSkill && firstSkill != secondSkill && + IsPrimaryTradeSkill(firstSkill) && IsPrimaryTradeSkill(secondSkill) && + HasProfessionPair(professionPairs, firstSkill, secondSkill); + bool const keepExistingProfessionPair = maxPrimaryTradeSkills < 2 && hasStoredProfessionPair; + + if (maxPrimaryTradeSkills == 1 && !keepExistingProfessionPair) + { + if (!IsPrimaryTradeSkill(firstSkill) || secondSkill != 0) { - case 0: - firstSkill = SKILL_HERBALISM; - secondSkill = SKILL_ALCHEMY; - break; - case 1: - firstSkill = SKILL_HERBALISM; - secondSkill = SKILL_MINING; - break; - case 2: - firstSkill = SKILL_MINING; - secondSkill = SKILL_SKINNING; - break; - case 3: - firstSkill = SKILL_HERBALISM; - secondSkill = SKILL_SKINNING; - break; - default: - firstSkill = firstSkills[urand(0, firstSkills.size() - 1)]; - secondSkill = secondSkills[urand(0, secondSkills.size() - 1)]; - break; + firstSkill = ChooseSingleProfession(professionPairs); + secondSkill = 0; + + sRandomPlayerbotMgr.SetValue(bot, "firstSkill", firstSkill); + sRandomPlayerbotMgr.SetValue(bot, "secondSkill", secondSkill); } + } + else if (maxPrimaryTradeSkills == 0 && !keepExistingProfessionPair) + { + firstSkill = 0; + secondSkill = 0; sRandomPlayerbotMgr.SetValue(bot, "firstSkill", firstSkill); sRandomPlayerbotMgr.SetValue(bot, "secondSkill", secondSkill); } + if (maxPrimaryTradeSkills >= 2 && + (!firstSkill || !secondSkill || firstSkill == secondSkill || !IsPrimaryTradeSkill(firstSkill) || + !IsPrimaryTradeSkill(secondSkill) || !HasProfessionPair(professionPairs, firstSkill, secondSkill))) + { + auto const& professionPair = ChooseProfessionPair(professionPairs); + firstSkill = professionPair.first; + secondSkill = professionPair.second; + + sRandomPlayerbotMgr.SetValue(bot, "firstSkill", firstSkill); + sRandomPlayerbotMgr.SetValue(bot, "secondSkill", secondSkill); + } + + std::vector primarySkills; + if (keepExistingProfessionPair) + { + primarySkills.push_back(firstSkill); + primarySkills.push_back(secondSkill); + } + else if (maxPrimaryTradeSkills > 0) + primarySkills.push_back(firstSkill); + if (!keepExistingProfessionPair && maxPrimaryTradeSkills > 1) + primarySkills.push_back(secondSkill); + SetRandomSkill(SKILL_FIRST_AID); SetRandomSkill(SKILL_FISHING); SetRandomSkill(SKILL_COOKING); - SetRandomSkill(firstSkill); - SetRandomSkill(secondSkill); + for (uint16 skillId : primarySkills) + SetRandomSkill(skillId); + + std::vector skillsToLearn = {SKILL_FIRST_AID, SKILL_FISHING, SKILL_COOKING}; + skillsToLearn.insert(skillsToLearn.end(), primarySkills.begin(), primarySkills.end()); + + for (uint16 skillId : skillsToLearn) + { + uint32 spellId = GetProfessionStarterSpell(skillId); + if (!spellId || bot->HasSpell(spellId)) + continue; + + if (IsPrimaryTradeSkill(skillId) && !bot->GetFreePrimaryProfessionPoints() && + !(keepExistingProfessionPair && bot->HasSkill(skillId))) + continue; + + bot->learnSpell(spellId, false); + } + + InitTradeSpecializations(); +} + +void PlayerbotFactory::InitTradeSpecializations() +{ + InitAlchemySpecialization(); + InitEngineeringSpecialization(); + InitLeatherworkingSpecialization(); + InitTailoringSpecialization(); + InitBlacksmithingSpecialization(); +} + +bool PlayerbotFactory::InitAlchemySpecialization() +{ + if (!bot->HasSkill(SKILL_ALCHEMY) || + bot->GetBaseSkillValue(SKILL_ALCHEMY) < 325 || + bot->GetLevel() <= 67) + return false; + + if (HasAnySpell(bot, {static_cast(ProfessionSpecializationSpell::Transmute), + static_cast(ProfessionSpecializationSpell::Elixir), + static_cast(ProfessionSpecializationSpell::Potion)})) + return false; + + switch (GetStoredOrRandomValue(bot, "alchemySpecialization", 1, 3)) + { + case 1: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Transmute, + ProfessionSpecializationSpell::LearnTransmute); + case 2: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Elixir, + ProfessionSpecializationSpell::LearnElixir); + case 3: + default: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Potion, + ProfessionSpecializationSpell::LearnPotion); + } +} + +bool PlayerbotFactory::InitEngineeringSpecialization() +{ + if (!bot->HasSkill(SKILL_ENGINEERING) || + bot->GetBaseSkillValue(SKILL_ENGINEERING) < 200 || + bot->GetLevel() < 30) + return false; + + if (HasAnySpell(bot, {static_cast(ProfessionSpecializationSpell::Goblin), + static_cast(ProfessionSpecializationSpell::Gnomish)})) + return false; + + switch (GetStoredOrRandomValue(bot, "engineeringSpecialization", 1, 2)) + { + case 1: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Goblin, + ProfessionSpecializationSpell::LearnGoblin); + case 2: + default: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Gnomish, + ProfessionSpecializationSpell::LearnGnomish); + } +} + +bool PlayerbotFactory::InitLeatherworkingSpecialization() +{ + if (!bot->HasSkill(SKILL_LEATHERWORKING) || + bot->GetBaseSkillValue(SKILL_LEATHERWORKING) < 225 || + bot->GetLevel() <= 40) + return false; + + if (HasAnySpell(bot, {static_cast(ProfessionSpecializationSpell::Dragon), + static_cast(ProfessionSpecializationSpell::Elemental), + static_cast(ProfessionSpecializationSpell::Tribal)})) + return false; + + switch (GetStoredOrRandomValue(bot, "leatherSpecialization", 1, 3)) + { + case 1: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Dragon, + ProfessionSpecializationSpell::LearnDragon); + case 2: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Elemental, + ProfessionSpecializationSpell::LearnElemental); + case 3: + default: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Tribal, + ProfessionSpecializationSpell::LearnTribal); + } +} + +bool PlayerbotFactory::InitTailoringSpecialization() +{ + if (!bot->HasSkill(SKILL_TAILORING) || + bot->GetBaseSkillValue(SKILL_TAILORING) < 350 || + bot->GetLevel() <= 59) + return false; + + if (HasAnySpell(bot, {static_cast(ProfessionSpecializationSpell::Spellfire), + static_cast(ProfessionSpecializationSpell::Mooncloth), + static_cast(ProfessionSpecializationSpell::Shadoweave)})) + return false; + + switch (GetStoredOrRandomValue(bot, "tailorSpecialization", 1, 3)) + { + case 1: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Spellfire, + ProfessionSpecializationSpell::LearnSpellfire); + case 2: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Mooncloth, + ProfessionSpecializationSpell::LearnMooncloth); + case 3: + default: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Shadoweave, + ProfessionSpecializationSpell::LearnShadoweave); + } +} + +bool PlayerbotFactory::InitBlacksmithingSpecialization() +{ + bool learnedSpecialization = false; + + if (!bot->HasSkill(SKILL_BLACKSMITHING) || + bot->GetBaseSkillValue(SKILL_BLACKSMITHING) < 225) + return false; + + if (!bot->HasSpell(static_cast(ProfessionSpecializationSpell::Armor)) && + !bot->HasSpell(static_cast(ProfessionSpecializationSpell::Weapon))) + { + switch (GetStoredOrRandomValue(bot, "blacksmithSpecialization", 1, 2)) + { + case 1: + learnedSpecialization = LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Armor, + ProfessionSpecializationSpell::LearnArmor); + break; + case 2: + default: + learnedSpecialization = LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Weapon, + ProfessionSpecializationSpell::LearnWeapon); + break; + } + } + + if (!bot->HasSpell(static_cast(ProfessionSpecializationSpell::Weapon)) || + bot->GetBaseSkillValue(SKILL_BLACKSMITHING) < 250 || + bot->GetLevel() <= 49 || + HasAnySpell(bot, {static_cast(ProfessionSpecializationSpell::Hammer), + static_cast(ProfessionSpecializationSpell::Axe), + static_cast(ProfessionSpecializationSpell::Sword)})) + return learnedSpecialization; + + switch (GetStoredOrRandomValue(bot, "blacksmithWeaponSpecialization", 1, 3)) + { + case 1: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Hammer, + ProfessionSpecializationSpell::LearnHammer); + case 2: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Axe, + ProfessionSpecializationSpell::LearnAxe); + case 3: + default: + return LearnProfessionSpecialization(bot, + ProfessionSpecializationSpell::Sword, + ProfessionSpecializationSpell::LearnSword); + } } void PlayerbotFactory::UpdateTradeSkills() @@ -2456,6 +2925,9 @@ void PlayerbotFactory::InitSkills() break; } + InitTradeSkills(); + InitInventorySkill(); + // switch (bot->getClass()) // { // case CLASS_WARRIOR: @@ -3804,30 +4276,21 @@ void PlayerbotFactory::InitInventory() void PlayerbotFactory::InitInventorySkill() { - if (bot->HasSkill(SKILL_MINING)) - { + if (bot->HasSkill(SKILL_MINING) && !bot->HasItemCount(2901, 1, true)) StoreItem(2901, 1); // Mining Pick - } - if (bot->HasSkill(SKILL_BLACKSMITHING) || bot->HasSkill(SKILL_ENGINEERING)) - { + if ((bot->HasSkill(SKILL_BLACKSMITHING) || bot->HasSkill(SKILL_ENGINEERING)) && + !bot->HasItemCount(5956, 1, true)) StoreItem(5956, 1); // Blacksmith Hammer - } - if (bot->HasSkill(SKILL_ENGINEERING)) - { + if (bot->HasSkill(SKILL_ENGINEERING) && !bot->HasItemCount(6219, 1, true)) StoreItem(6219, 1); // Arclight Spanner - } - if (bot->HasSkill(SKILL_ENCHANTING)) - { + if (bot->HasSkill(SKILL_ENCHANTING) && !bot->HasItemCount(16207, 1, true)) StoreItem(16207, 1); // Runed Arcanite Rod - } - if (bot->HasSkill(SKILL_SKINNING)) - { + if (bot->HasSkill(SKILL_SKINNING) && !bot->HasItemCount(7005, 1, true)) StoreItem(7005, 1); // Skinning Knife - } } Item* PlayerbotFactory::StoreItem(uint32 itemId, uint32 count) diff --git a/src/Bot/Factory/PlayerbotFactory.h b/src/Bot/Factory/PlayerbotFactory.h index d943463ea..1962c0428 100644 --- a/src/Bot/Factory/PlayerbotFactory.h +++ b/src/Bot/Factory/PlayerbotFactory.h @@ -6,6 +6,9 @@ #ifndef _PLAYERBOT_PLAYERBOTFACTORY_H #define _PLAYERBOT_PLAYERBOTFACTORY_H +#include +#include + #include "InventoryAction.h" #include "Player.h" #include "PlayerbotAI.h" @@ -87,12 +90,91 @@ public: void InitAttunementQuests(); private: + enum class ProfessionSpecializationSpell : uint32 + { + Weapon = 9787, + Armor = 9788, + Hammer = 17040, + Axe = 17041, + Sword = 17039, + + LearnWeapon = 9789, + LearnArmor = 9790, + LearnHammer = 39099, + LearnAxe = 39098, + LearnSword = 39097, + + Dragon = 10656, + Elemental = 10658, + Tribal = 10660, + + LearnDragon = 10657, + LearnElemental = 10659, + LearnTribal = 10661, + + Spellfire = 26797, + Mooncloth = 26798, + Shadoweave = 26801, + + Goblin = 20222, + Gnomish = 20219, + + LearnGoblin = 20221, + LearnGnomish = 20220, + + LearnSpellfire = 26796, + LearnMooncloth = 26799, + LearnShadoweave = 26800, + + Transmute = 28672, + Elixir = 28677, + Potion = 28675, + + LearnTransmute = 28674, + LearnElixir = 28678, + LearnPotion = 28676 + }; + + enum class ProfessionRollType : uint32 + { + Random = 1, + Class = 2 + }; + + struct WeightedProfessionPair + { + uint16 firstSkill; + uint16 secondSkill; + uint32 weight; + }; + void Prepare(); // void InitSecondEquipmentSet(); // void InitEquipmentNew(bool incremental); bool CanEquipItem(ItemTemplate const* proto); bool CanEquipUnseenItem(uint8 slot, uint16& dest, uint32 item); + static bool IsPrimaryTradeSkill(uint16 skillId); + static bool IsGatheringTradeSkill(uint16 skillId); + static bool IsCraftingTradeSkill(uint16 skillId); + static uint32 GetProfessionStarterSpell(uint16 skillId); + static std::vector GetClassProfessionPairs(Player* bot); + static std::vector GetRandomProfessionPairs(); + static std::pair ChooseProfessionPair(std::vector const& professionPairs); + static bool HasProfessionPair(std::vector const& professionPairs, + uint16 firstSkill, uint16 secondSkill); + static uint16 ChooseSingleProfession(std::vector const& professionPairs); + static uint32 GetStoredOrRandomValue(Player* bot, std::string const& key, uint32 minValue, uint32 maxValue); + static bool HasAnySpell(Player* bot, std::vector const& spells); + static bool LearnProfessionSpecialization(Player* bot, + ProfessionSpecializationSpell knownSpell, + ProfessionSpecializationSpell learnSpell); void InitTradeSkills(); + void InitTradeSpecializations(); + bool InitAlchemySpecialization(); + bool InitEngineeringSpecialization(); + bool InitLeatherworkingSpecialization(); + bool InitTailoringSpecialization(); + bool InitBlacksmithingSpecialization(); void UpdateTradeSkills(); void SetRandomSkill(uint16 id); void ClearSpells(); diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index e9de581de..7edc48aff 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -243,10 +243,22 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) nextAICheckDelay = 0; // Early return if bot is in invalid state - if (!bot || !bot->GetSession() || !bot->IsInWorld() || bot->IsBeingTeleported() || - bot->GetSession()->isLogingOut() || bot->IsDuringRemoveFromWorld()) + if (!bot || !bot->GetSession() || !bot->IsInWorld() || bot->IsBeingTeleported() || bot->IsDuringRemoveFromWorld()) return; + // During timed logout countdown, cancel if bot enters combat (this cancellation is handled client-side for real players). + if (bot->GetSession()->isLogingOut()) + { + bool canLogoutInCombat = bot->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING); + if (bot->IsInCombat() && !canLogoutInCombat) + { + WorldPackets::Character::LogoutCancel cancelData = WorldPacket(CMSG_LOGOUT_CANCEL); + bot->GetSession()->HandleLogoutCancelOpcode(cancelData); + } + else + return; + } + // Handle cheat options (set bot health and power if cheats are enabled) if (bot->IsAlive() && (static_cast(GetCheat()) > 0 || static_cast(sPlayerbotAIConfig.botCheatMask) > 0)) @@ -266,77 +278,104 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) if (!CanUpdateAI()) return; - // Handle the current spell + // Handle a spell that is still in its preparing phase (including channeled spells). Spell* currentSpell = bot->GetCurrentSpell(CURRENT_GENERIC_SPELL); if (!currentSpell) currentSpell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL); if (currentSpell) { - const SpellInfo* spellInfo = currentSpell->GetSpellInfo(); - if (spellInfo && currentSpell->getState() == SPELL_STATE_PREPARING) + if (currentSpell->getState() == SPELL_STATE_PREPARING) { - Unit* spellTarget = currentSpell->m_targets.GetUnitTarget(); - // Interrupt if target is dead or spell can't target dead units - if (spellTarget && !spellTarget->IsAlive() && !spellInfo->IsAllowingDeadTarget()) + // Allow external scripts to interrupt a cast in progress + if (spellInterruptRequested) { + spellInterruptRequested = false; InterruptSpell(); YieldThread(bot, GetReactDelay()); return; } - GameObject* goSpellTarget = currentSpell->m_targets.GetGOTarget(); - - if (goSpellTarget && !goSpellTarget->isSpawned()) + const SpellInfo* spellInfo = currentSpell->GetSpellInfo(); + if (spellInfo) { - InterruptSpell(); - YieldThread(bot, GetReactDelay()); - return; - } - - bool isHeal = false; - bool isSingleTarget = true; - - for (uint8 i = 0; i < 3; ++i) - { - if (!spellInfo->Effects[i].Effect) - continue; - - // Check if spell is a heal - if (spellInfo->Effects[i].Effect == SPELL_EFFECT_HEAL || - spellInfo->Effects[i].Effect == SPELL_EFFECT_HEAL_MAX_HEALTH || - spellInfo->Effects[i].Effect == SPELL_EFFECT_HEAL_MECHANICAL) - isHeal = true; - - // Check if spell is single-target - if ((spellInfo->Effects[i].TargetA.GetTarget() && - spellInfo->Effects[i].TargetA.GetTarget() != TARGET_UNIT_TARGET_ALLY) || - (spellInfo->Effects[i].TargetB.GetTarget() && - spellInfo->Effects[i].TargetB.GetTarget() != TARGET_UNIT_TARGET_ALLY)) + Unit* spellTarget = currentSpell->m_targets.GetUnitTarget(); + // Interrupt if target is dead or spell can't target dead units + if (spellTarget && !spellTarget->IsAlive() && !spellInfo->IsAllowingDeadTarget()) { - isSingleTarget = false; + InterruptSpell(); + YieldThread(bot, GetReactDelay()); + return; } - } - // Interrupt if target ally has full health (heal by other member) - if (isHeal && isSingleTarget && spellTarget && spellTarget->IsFullHealth()) - { - InterruptSpell(); + GameObject* goSpellTarget = currentSpell->m_targets.GetGOTarget(); + + if (goSpellTarget && !goSpellTarget->isSpawned()) + { + InterruptSpell(); + YieldThread(bot, GetReactDelay()); + return; + } + + bool isHeal = false; + bool isSingleTarget = true; + + for (uint8 i = 0; i < 3; ++i) + { + if (!spellInfo->Effects[i].Effect) + continue; + + // Check if spell is a heal + if (spellInfo->Effects[i].Effect == SPELL_EFFECT_HEAL || + spellInfo->Effects[i].Effect == SPELL_EFFECT_HEAL_MAX_HEALTH || + spellInfo->Effects[i].Effect == SPELL_EFFECT_HEAL_MECHANICAL) + isHeal = true; + + // Check if spell is single-target + if ((spellInfo->Effects[i].TargetA.GetTarget() && + spellInfo->Effects[i].TargetA.GetTarget() != TARGET_UNIT_TARGET_ALLY) || + (spellInfo->Effects[i].TargetB.GetTarget() && + spellInfo->Effects[i].TargetB.GetTarget() != TARGET_UNIT_TARGET_ALLY)) + { + isSingleTarget = false; + } + } + + // Interrupt if target ally has full health (heal by other member) + if (isHeal && isSingleTarget && spellTarget && spellTarget->IsFullHealth()) + { + InterruptSpell(); + YieldThread(bot, GetReactDelay()); + return; + } + + // Ensure bot is facing target if necessary + if (spellTarget && !bot->HasInArc(CAST_ANGLE_IN_FRONT, spellTarget) && + (spellInfo->FacingCasterFlags & SPELL_FACING_FLAG_INFRONT)) + { + ServerFacade::instance().SetFacingTo(bot, spellTarget); + } + + // Wait for spell cast YieldThread(bot, GetReactDelay()); return; } + } + } - // Ensure bot is facing target if necessary - if (spellTarget && !bot->HasInArc(CAST_ANGLE_IN_FRONT, spellTarget) && - (spellInfo->FacingCasterFlags & SPELL_FACING_FLAG_INFRONT)) - { - ServerFacade::instance().SetFacingTo(bot, spellTarget); - } - - // Wait for spell cast + if (spellInterruptRequested) + { + // At this point the preparing-cast branch above did not consume the request. + // Interrupt a current channel if one still exists; otherwise, clear the stale request. + if (bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL)) + { + spellInterruptRequested = false; + InterruptSpell(); YieldThread(bot, GetReactDelay()); return; } + + spellInterruptRequested = false; } // Handle transport check delay @@ -688,30 +727,9 @@ void PlayerbotAI::HandleCommand(uint32 type, const std::string& text, Player& fr Reset(true); } - // TODO: missing implementation to port - /*else if (filtered == "logout") - { - if (!(bot->IsStunnedByLogout() || bot->GetSession()->isLogingOut())) - { - if (type == CHAT_MSG_WHISPER) - TellPlayer(&fromPlayer, BOT_TEXT("logout_start")); - - if (master && master->GetPlayerbotMgr()) - SetShouldLogOut(true); - } - } - else if (filtered == "logout cancel") - { - if (bot->IsStunnedByLogout() || bot->GetSession()->isLogingOut()) - { - if (type == CHAT_MSG_WHISPER) - TellPlayer(&fromPlayer, BOT_TEXT("logout_cancel")); - - WorldPacket p; - bot->GetSession()->HandleLogoutCancelOpcode(p); - SetShouldLogOut(false); - } - } + // Commented-out logout commands blocks removed from here and implemented in HandleCommand. + // Remaining is a commented-out action delay command block. + /* else if ((filtered.size() > 5) && (filtered.substr(0, 5) == "wait ") && (filtered.find("wait for attack") == std::string::npos)) { @@ -1057,7 +1075,7 @@ void PlayerbotAI::HandleCommand(uint32 type, std::string const text, Player* fro TellMaster(message); } } - else if (filtered == "logout cancel") + else if (filtered == "cancel logout" || filtered == "logout cancel") { if (!bot->GetSession()->isLogingOut()) return; @@ -1073,9 +1091,7 @@ void PlayerbotAI::HandleCommand(uint32 type, std::string const text, Player* fro bot->GetSession()->HandleLogoutCancelOpcode(data); } else - { chatCommands.push_back(ChatCommandHolder(filtered, fromPlayer, type)); - } } void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet) @@ -1558,7 +1574,7 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) static const std::vector allInstanceStrategies = { "aq20", "bwl", "karazhan", "gruulslair", "icc", "magtheridon", "moltencore", - "naxx", "onyxia", "ssc", "tempestkeep", "ulduar", "voa", "wotlk-an", "wotlk-cos", + "naxx", "onyxia", "ssc", "tbc-ac", "tempestkeep", "ulduar", "voa", "wotlk-an", "wotlk-cos", "wotlk-dtk", "wotlk-eoe", "wotlk-fos", "wotlk-gd", "wotlk-hol", "wotlk-hor", "wotlk-hos", "wotlk-nex", "wotlk-occ", "wotlk-ok", "wotlk-os", "wotlk-pos", "wotlk-toc", "wotlk-uk", "wotlk-up", "wotlk-vh", "zulaman" @@ -1598,7 +1614,10 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) strategyName = "ssc"; // Serpentshrine Cavern break; case 550: - strategyName = "tempestkeep"; // Tempest Keep + strategyName = "tempestkeep"; // Tempest Keep: The Eye + break; + case 558: + strategyName = "tbc-ac"; // Auchindoun: Auchenai Crypts break; case 565: strategyName = "gruulslair"; // Gruul's Lair @@ -1776,6 +1795,11 @@ bool PlayerbotAI::ContainsStrategy(StrategyType type) bool PlayerbotAI::HasStrategy(std::string const name, BotState type) { return engines[type]->HasStrategy(name); } +Strategy* PlayerbotAI::GetStrategy(std::string const name, BotState type) +{ + return engines[type] ? engines[type]->GetStrategy(name) : nullptr; +} + void PlayerbotAI::ResetStrategies(bool load) { for (uint8 i = 0; i < BOT_STATE_MAX; i++) @@ -4189,6 +4213,19 @@ void PlayerbotAI::RemoveAura(std::string const name) bot->RemoveAurasDueToSpell(spellid); } +void PlayerbotAI::RequestSpellInterrupt() +{ + Spell* currentSpell = bot->GetCurrentSpell(CURRENT_GENERIC_SPELL); + if (currentSpell && currentSpell->getState() == SPELL_STATE_PREPARING) + { + spellInterruptRequested = true; + return; + } + + if (bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL)) + spellInterruptRequested = true; +} + bool PlayerbotAI::IsInterruptableSpellCasting(Unit* target, std::string const spell) { if (!IsValidUnit(target)) diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index cfa27ed4e..dc7770ed8 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -3,8 +3,8 @@ * and/or modify it under version 3 of the License, or (at your option), any later version. */ -#ifndef _PLAYERBOT_PLAYERbotAI_H -#define _PLAYERBOT_PLAYERbotAI_H +#ifndef _PLAYERBOT_PLAYERBOTAI_H +#define _PLAYERBOT_PLAYERBOTAI_H #include @@ -405,6 +405,7 @@ public: void ChangeStrategy(std::string const name, BotState type); void ClearStrategies(BotState type); std::vector GetStrategies(BotState type); + Strategy* GetStrategy(std::string const name, BotState type); void ApplyInstanceStrategies(uint32 mapId, bool tellMaster = false); void EvaluateHealerDpsStrategy(); bool ContainsStrategy(StrategyType type); @@ -471,6 +472,7 @@ public: void SpellInterrupted(uint32 spellid); int32 CalculateGlobalCooldown(uint32 spellid); void InterruptSpell(); + void RequestSpellInterrupt(); void RemoveAura(std::string const name); void RemoveShapeshift(); void WaitForSpellCast(Spell* spell); @@ -647,6 +649,7 @@ protected: BotCheatMask cheatMask = BotCheatMask::none; Position jumpDestination = Position(); uint32 nextTransportCheck = 0; + bool spellInterruptRequested = false; }; #endif diff --git a/src/Bot/PlayerbotMgr.cpp b/src/Bot/PlayerbotMgr.cpp index c3b614a98..fd205fe23 100644 --- a/src/Bot/PlayerbotMgr.cpp +++ b/src/Bot/PlayerbotMgr.cpp @@ -64,7 +64,7 @@ private: }; std::unordered_set BotInitGuard::botsBeingInitialized; -std::unordered_set PlayerbotHolder::botLoading; +std::unordered_map PlayerbotHolder::botLoading; PlayerbotHolder::PlayerbotHolder() : PlayerbotAIBase(false) {} class PlayerbotLoginQueryHolder : public LoginQueryHolder @@ -121,7 +121,13 @@ void PlayerbotHolder::AddPlayerBot(ObjectGuid playerGuid, uint32 masterAccountId LOG_DEBUG("playerbots", "PlayerbotMgr not found for master player with GUID: {}", masterPlayer->GetGUID().GetRawValue()); return; } - uint32 count = mgr->GetPlayerbotsCount() + botLoading.size(); + uint32 loadingForMaster = 0; + for (auto const& [guid, acctId] : botLoading) + { + if (acctId == masterAccountId) + ++loadingForMaster; + } + uint32 count = mgr->GetPlayerbotsCount() + loadingForMaster; if (count >= PlayerbotAIConfig::instance().maxAddedBots) { allowed = false; @@ -144,7 +150,7 @@ void PlayerbotHolder::AddPlayerBot(ObjectGuid playerGuid, uint32 masterAccountId return; } - botLoading.insert(playerGuid); + botLoading.emplace(playerGuid, masterAccountId); // Always login in with world session to avoid race condition sWorld->AddQueryHolderCallback(CharacterDatabase.DelayQueryHolder(holder)) @@ -293,6 +299,11 @@ void PlayerbotHolder::LogoutAllBots() if (!botAI || botAI->IsRealPlayer()) continue; + // If bot is mid-countdown, cancel the timer so LogoutPlayerBot proceeds immediately. + WorldSession* session = bot->GetSession(); + if (session && session->isLogingOut()) + session->SetLogoutStartTime(0); + LogoutPlayerBot(bot->GetGUID()); } } @@ -355,36 +366,50 @@ void PlayerbotHolder::LogoutPlayerBot(ObjectGuid guid) WorldSession* botWorldSessionPtr = bot->GetSession(); WorldSession* masterWorldSessionPtr = nullptr; + // If already in timed logout countdown, complete it once the 20-second timer expires. if (botWorldSessionPtr->isLogingOut()) + { + if (botWorldSessionPtr->ShouldLogOut(time(nullptr))) + { + std::string message = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "goodbye", "Goodbye!", {}); + botAI->TellMaster(message); + RemoveFromPlayerbotsMap(guid); + botWorldSessionPtr->LogoutPlayer(true); + delete botWorldSessionPtr; + } return; + } Player* master = botAI->GetMaster(); if (master) masterWorldSessionPtr = master->GetSession(); - // TODO: Review whether or not to implement timed logout. - // Unused block. Useful only for timed logout. -/* - // check for instant logout - bool logout = botWorldSessionPtr->ShouldLogOut(time(nullptr)); + // Instant logout checking: + bool logout = + bot->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) || + bot->HasUnitState(UNIT_STATE_IN_FLIGHT) || + (masterWorldSessionPtr && !masterWorldSessionPtr->GetPlayer()) || + // Master's socket is already gone (EXIT GAME -> EXIT NOW is the most typical cause). + // Force instant logout. Without this, the bot restarts its 20-second countdown and fires LogoutPlayer() 20 seconds + // after the master's Player object has been deleted, causing the bot's logout to crash on the now deleted master. + (masterWorldSessionPtr && masterWorldSessionPtr->IsSocketClosed()) || + (masterWorldSessionPtr && masterWorldSessionPtr->ShouldLogOut(time(nullptr))) || + // If the bot's master has security clearance for `InstantLogout` in worldserver.conf, so does the bot. + (master && + (master->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) || + master->HasUnitState(UNIT_STATE_IN_FLIGHT) || + (masterWorldSessionPtr && + masterWorldSessionPtr->GetSecurity() >= (AccountTypes)sWorld->getIntConfig(CONFIG_INSTANT_LOGOUT)))); - if (masterWorldSessionPtr && masterWorldSessionPtr->ShouldLogOut(time(nullptr))) - logout = true; + if (!logout) + { + // Start the 20-second logout countdown. CancelLogout() can interrupt this. + WorldPackets::Character::LogoutRequest data = WorldPacket(CMSG_LOGOUT_REQUEST); + botWorldSessionPtr->HandleLogoutRequestOpcode(data); + return; + } - if (masterWorldSessionPtr && !masterWorldSessionPtr->GetPlayer()) - logout = true; - - if (bot->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) || bot->HasUnitState(UNIT_STATE_IN_FLIGHT) || - botWorldSessionPtr->GetSecurity() >= (AccountTypes)sWorld->getIntConfig(CONFIG_INSTANT_LOGOUT)) - logout = true; - - if (master && - (master->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) || master->HasUnitState(UNIT_STATE_IN_FLIGHT) || - (masterWorldSessionPtr && - masterWorldSessionPtr->GetSecurity() >= (AccountTypes)sWorld->getIntConfig(CONFIG_INSTANT_LOGOUT)))) - logout = true; -*/ - // Instant logout (the only option right now) { std::string message = PlayerbotTextMgr::instance().GetBotTextOrDefault( "goodbye", "Goodbye!", {}); @@ -1472,6 +1497,15 @@ void PlayerbotMgr::UpdateAIInternal(uint32 elapsed, bool /*minimal*/) { SetNextCheckDelay(sPlayerbotAIConfig.reactDelay); CheckTellErrors(elapsed); + + // Complete timed logouts for added bots once the 20-second countdown has elapsed. + std::vector expiredLogouts; + for (auto const& [botGuid, bot] : playerBots) + if (bot && bot->GetSession() && bot->GetSession()->ShouldLogOut(time(nullptr))) + expiredLogouts.push_back(botGuid); + + for (ObjectGuid const& guid : expiredLogouts) + LogoutPlayerBot(guid); } void PlayerbotMgr::HandleCommand(uint32 type, std::string const text) diff --git a/src/Bot/PlayerbotMgr.h b/src/Bot/PlayerbotMgr.h index b80f6f236..316e34d47 100644 --- a/src/Bot/PlayerbotMgr.h +++ b/src/Bot/PlayerbotMgr.h @@ -57,7 +57,7 @@ protected: virtual void OnBotLoginInternal(Player* const bot) = 0; PlayerBotMap playerBots; - static std::unordered_set botLoading; + static std::unordered_map botLoading; }; class PlayerbotMgr : public PlayerbotHolder diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 8c8343db2..a8daf972a 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -167,11 +167,13 @@ bool PlayerbotAIConfig::Initialize() pvpProhibitedZoneIds); LoadList>( sConfigMgr->GetOption("AiPlayerbot.PvpProhibitedAreaIds", - "976,35,392,2268,4161,4010,4317,4312,3649,3887,3958,3724,4080,3938,3754,3786,3973"), + "976,35,392,2268,4161,4010,4317,4312,3649,3887,3958,3724,4080,3938,3754,3786," + "3973,4085,4086,4087,4088"), pvpProhibitedAreaIds); fastReactInBG = sConfigMgr->GetOption("AiPlayerbot.FastReactInBG", true); LoadList>( - sConfigMgr->GetOption("AiPlayerbot.RandomBotQuestIds", "3802,5505,6502,7761,7848,10277,10285,11492,13188,13189,24499,24511,24710,24712"), + sConfigMgr->GetOption("AiPlayerbot.RandomBotQuestIds", "3802,5505,6502,7761,7848,10277,10285,11492," + "13188,13189,24499,24511,24710,24712"), randomBotQuestIds); LoadSet>( @@ -181,7 +183,8 @@ bool PlayerbotAIConfig::Initialize() "165739,165738,175245,175970,176325,176327,123329,2560"), disallowedGameObjects); LoadSet>( - sConfigMgr->GetOption("AiPlayerbot.AttunementQuests", "10279,10277,10282,10283,10284,10285,10296,10297,10298,11481,11482,11488,11490,11492,10901,10888,10445,10985"), + sConfigMgr->GetOption("AiPlayerbot.AttunementQuests", "10279,10277,10282,10283,10284,10285,10296," + "10297,10298,11481,11482,11488,11490,11492,10901,10888,10445,10985"), attunementQuests); LoadSet>( @@ -233,6 +236,8 @@ bool PlayerbotAIConfig::Initialize() EnableICCBuffs = sConfigMgr->GetOption("AiPlayerbot.EnableICCBuffs", true); //////////////////////////// Professions + classMatchingProfessionChance = + std::min(100, sConfigMgr->GetOption("AiPlayerbot.ClassMatchingProfessionChance", 30)); fishingDistanceFromMaster = sConfigMgr->GetOption("AiPlayerbot.FishingDistanceFromMaster", 10.0f); endFishingWithMaster = sConfigMgr->GetOption("AiPlayerbot.EndFishingWithMaster", 30.0f); fishingDistance = sConfigMgr->GetOption("AiPlayerbot.FishingDistance", 40.0f); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 4e758e79e..877672678 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -152,6 +152,7 @@ public: // Professions bool enableFishingWithMaster; + uint32 classMatchingProfessionChance; float fishingDistanceFromMaster, fishingDistance, endFishingWithMaster; // chat